从零开始的面试题
2023-02-28 20:13:09 7 举报
AI智能生成
P4+
作者其他创作
大纲/内容
Java
Java基础
面向对象三大特征
封装
把真实世界的某些物体包成对象,里面的信息不对外公开,只公开某些特定方法让别人使用,内部的属性与逻辑都隐藏起来,不让人直接使用,也不需要让别人直接使用,就是所谓的封装
继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
多态
多态:对于同一个行为,不同的子类对象具有不同的表现形式。多态存在的3个条件:1)继承;2)重写;3)父类引用指向子类对象。
数据类型
字符型char
数字型byte、short、int、long
布尔型boolean
浮点型float double
经典面试题
用最有效率的方法计算 2 乘以 8
直接进行位运算符,左移三位
switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上
从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
如上图
代码块1编译报错,错误原因是:不兼容的类型: 从int转换到short可能会有损失”。
代码块2正常编译和执行。
我们将代码块2进行编译,字节码如下:
代码块2正常编译和执行。
我们将代码块2进行编译,字节码如下:
可以看到字节码中包含了 i2s 指令,该指令用于将 int 转成 short。i2s 是 int to short 的缩写。
其实,s1 += 1 相当于 s1 = (short)(s1 + 1),有兴趣的可以自己编译下这两行代码的字节码,你会发现是一摸一样的。
== 和 equals 的区别是什么
==比较的是内存地址,Sting的equals进行了重写,会将String字符串转换成char[]数组 并进行逐一对比
两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?
如果a==b则一定相同,equals重写之后就不一定相同,但重写后最好保证equals为ture时,hashcode最好也为ture
为什么重写equals,必须要重写hashCode方法?
因为在Java底层,equals和hashCode是协同判断对象是否重复的,如果只重写了equals没有重写hashCode,在set集合中可能出现对象重复插入的情况
访访问修饰符的区别
访问修饰符final的作用?
修饰类:使类不可被继承,类中的所有方法都会成为final方法
子主题
Math.round是什么方法 有什么作用?
round 表示"四舍五入",算法为Math.floor(x+0.5) ,即将原来的数字加上 0.5 后再向下取整,所以 Math.round(11.5) 的结果为 12,Math.round(-11.5) 的结果为 -11。
IO流
多线程
基本概念
进程
进程是程序的基本执行实体
线程
线程是操作系统所能运行的最小单位
多线程的实现
继承Thread类
实现Runnable接口
实现Callable接口
容器
反射
对象拷贝
设计模式
网络模块
异常模块
Java Web模块
数据库 MySQL
SQL语句
索引
SQL优化
数据结构
JVM
JVM内存结构
程序计数器
记录JVM虚拟机需要执行的下一行JVM指令的内存地址
本地方法栈
本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用 native 关键字修饰的方法所存储的区域。
虚拟机栈
Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机栈中创建一个 栈帧(stack frame)。每个方法执行的过程就对应了一个入栈和出栈的过程。
方法中的子方法会在第一个栈帧上面插入,栈帧中包含了方法的局部变量,操作数栈,动态链接,返回地址等
堆空间
堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的对象实例都会分配在堆上。JDK 1.7后,字符串常量池从永久代中剥离出来,存放在堆中。
堆空间的空间分配
老年代 : 三分之二的堆空间
年轻代 : 三分之一的堆空间
eden 区: 8/10 的年轻代空间
survivor 0 : 1/10 的年轻代空间
survivor 1 : 1/10 的年轻代空间
方法区
方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。
垃圾回收
垃圾回收的两种机制
引用计数法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收(Python 在用,但主流虚拟机没有使用)。
优点:快,方便,实现简单。
缺陷:对象相互引用时(A.instance=B 同时 B.instance=A),很难判断对象是否该回收。
优点:快,方便,实现简单。
缺陷:对象相互引用时(A.instance=B 同时 B.instance=A),很难判断对象是否该回收。
可达性分析
来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
当前虚拟机栈中局部变量表中的引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
Java中的引用逻辑
强引用
demo 中尽管 o1已经被回收,但是 o2 强引用 o1,一直存在,所以不会被GC回收
软引用
软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。
弱引用
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存。
虚引用
虚引用随时都可能被回收,这种引用唯一存在的意义,大概就是能够在回收时发出一个通知
垃圾回收算法
标记 - 清除算法(Tracing Collector)
优点是当存活对象比较多的时候,性能比较高,因为该算法只需要处理待回收的对象,而不需要处理存活的对象。
缺点是内存会变得不连续
标记 - 整理算法(Compacting Collector)
该算法所带来的最大的优势便是使得内存上面不会再有碎片问题,并且新对象的分配只需要通过简单的指针碰撞便可完成。
但是对于性能的要求相对较高
复制算法(Copying Collector)
复制算法的优势是:① 不会产生内存碎片;② 标记和复制可以同时进行;③ 复制时也只需要移动栈顶指针即可,按顺序分配内存,简单高效;④ 每次只需要回收一块内存区域即可,而不用回收整块内存区域,所以性能会相对高效一点。
可用的内存减小了一半,存在内存浪费的情况。
垃圾回收器:主流使用是PS+PO
性能优化
子主题
Java类的加载机制
类加载机制一共有五个步骤,分别是加载、链接、初始化、使用和卸载阶段,这五个阶段的顺序是确定的。其中链接阶段会细分成三个阶段,分别是验证、准备、解析阶段,这三个阶段的顺序是不确定的,这三个阶段通常交互进行。解析阶段通常会在初始化之后再开始,这是为了支持 Java 语言的运行时绑定特性(也被称为动态绑定)。
缓存 Redis
Redis数据类型
Redis数据类型:常用的是String,List,ZSet,Set,Hash,不过无论是哪种数据类型Redis都不会直接将它放在内存中存储,而是转而内部使用RedisObject来存储以及表示所有类型的key-value,RedisObject最主要的信息如上图所示:type表示一个value对象具体是何种数据类型,encoding是不同数据类型在Redis内部的存储方式。比如:type=string表示value存储的是一个普通字符串,那么encoding可以是raw或者int
String(字符串)
二进制安全
计数器、分布式锁、字符缓存、分布式ID生成、session共享、秒杀token、IP限流等
Hash
键值对存储,类似于Map集合
适合存储对象,可以将对象属性一个个存储,更新时也可以更新单个属性,操作某一个字段
LIst
双向链表
增删快,适合做栈、队列、有限集合、消息队列、消息推送、阻塞队列等
Set
元素不能重复,每次获取无序
添加、删除、查找的复杂度都是O(1),提供了求交集、并集、差集的操作,可以做抽奖活动、朋友圈点赞、用户(微博好友)关注、相关关注、共同关注、好友推荐(可能认识的人)等
ZSet/Sorted set
与Set一样不允许元素重复,但会进行有序排列
基于分数进行排序,如果分数相等,以key值的 ascii 值进行排序,可做商品评价,排行榜等
Bitmaps
Bitmaps是一个字节由 8 个二进制位组成
在字符串类型上面定义的位操作,可以做签到,在线人数统计等
Hyperloglogs
在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。在 Redis 里面,每个HyperLogLog键只需要花费12 KB内存,就可以计算接近2^64个不同元素的基数。
可以做基数统计,即使面对海量数据也只需占用12k内存
Geospatial(地理信息)
以将用户给定的地理位置信息储存起来, 并对这些信息进行操作
地理位置计算
Streams(消息)
支持发布订阅,一对多消费
消息队列
子主题
Redis为什么快?
Redis完全基于内存
Redis整个结构类似于HashMap,查找和操作复杂度为O(1),不需要和MySQL查找数据一样需要产生随机磁盘IO或者全表
Redis对于客户端的处理是单线程的,采用单线程处理所有客户端请求,避免了多线程的上下文切换和线程竞争造成的开销
持久化
自动开启的RDB
以快照形式将内存中的数据存储到硬盘中,默认是有一个触发机制,在多少秒内数据修改量到达指定级别就会进行快照
工作原理:当 Redis 需要做持久化时,Redis 会 fork 一个子进程,子进程将数据写到磁盘上一个临时 RDB 文件中。当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处就是可以 copy-on-write。
优点
这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适用于灾难恢复(disaster recovery)。
适合版本回退
缺点
如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
可能导致数据丢失
手动开启的AOF
在子线程中将存储的命令直接存入硬盘,每一条命令都会同步的写入处理,数据保存相对RDB会完整些,
工作原理:就是每次都在aof文件后面追加命令。他与主进程收到请求、处理请求是串行化的,而非异步并行的。图示如下
优点
持久化的速度快,因为每次都只是追加,rdb每次都全量持久化
数据相对更可靠,丢失少,因可以配置每秒持久化、每个命令执行完就持久化
缺点
灾难性恢复的时候过慢,因为aof每次都只追加原命令,导致aof文件过大,但是后面会rewrite,但是相对于rdb也是慢的。
会对主进程对外提供请求的效率造成影响,接收请求、处理请求、写aof文件这三步是串行原子执行的。而非异步多线程执行的。Redis单线程!
会对主进程对外提供请求的效率造成影响,接收请求、处理请求、写aof文件这三步是串行原子执行的。而非异步多线程执行的。Redis单线程!
Redis常用命令
keys *
返回所有键(keys还能用来搜索,比如keys h*:搜索所有以h开头的键)
dbsize
返回键数量
exists key
检查键是否存在,存在返回 1,不存在返回 0
del key
删除键,返回删除键个数,删除不存在键返回 0
ttl key
查看键存活时间,返回键剩余过期时间,不存在返回-1
缓存
穿透
访问了一个Redis和数据库都没有的数据,通常是恶意访问,比如id=-1
当有这样的访问请求时,可以设置一个value为null的值存入Redis,这样后续的请求就会直接到达Redis然后结束
进行权限校验,或访问次数限制
布隆过滤器
击穿
某一个数据刚好过期时,大量的访问请求同时到达,这个时候请求会同时到达数据库,造成数据库宕机
在对应的请求上增加互斥锁,这样同时之后一个访问到到达数据库
热点的Key可以不设置过期时间
雪崩
大量的key同时过期,并且有大量的访问请求到达,同步进入到数据库,直接打垮数据库
设置Key的过期时间分批,例如随机1-5分钟
如果真的发生了缓存雪崩,如何处理?
设置熔断机制,当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
提高数据库的容灾能力,可以使用分库分表,读写分离的策略。
为了防止Redis宕机导致缓存雪崩的问题,可以搭建Redis集群,提高Redis的容灾性。
一致性
一致性的三种实现级别
强一致性
这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
弱一致性
这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
最终一致性
最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
三种实现模式
Cache-Aside Pattern(旁路缓存模式)
读的时候,先读缓存,缓存命中的话,直接返回数据,缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
Read-Through/Write-Through(读写穿透)
从缓存读取数据,读到直接返回,如果读取不到的话,从数据库加载,写入缓存后,再返回响应。
客户端发送请求到缓存层(Cache-Provider),缓存层同时对缓存和数据源进行写入操作
Write behind (异步缓存写入)
这种策略仅涉及写入请求,会优先更新缓存,并在最终批量异步的更新数据库,高一致性的系统慎用
常见问题
Cache-Aside在写入请求的时候,为什么是删除缓存而不是更新缓存呢?
如果出现第一条数据更新缓存的同时,第二条数据读取了未更改的数据进行了更新,就出现了脏读
如果你写入的缓存值,是经过复杂计算才得到的话。更新缓存频率高的话,会浪费性能
在写数据库场景多,读数据场景少的情况下,数据很多时候还没被读取到,又被更新了,这也浪费了性能
如果发生双写的情况,为什么先写入数据库而不是先写入缓存?
缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。因此,Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存。
什么是缓存双删?
缓存双删可以有效的保证数据一致性,但如果出现二次删除失败的情况,则可能出现脏读,因此建议引入缓存失败重试机制,发送异步消息进行确认
Redis八种淘汰策略与三种删除策略
淘汰策略 配置参数
maxmemory-policy:参数配置淘汰策略。maxmemory:限制内存大小。
maxmemory-policy:参数配置淘汰策略。maxmemory:限制内存大小。
volatile-lru
从已设置过期时间的数据集中挑选最近最少使用的数据淘汰,没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
volatile-ttl
从已设置过期时间的数据集中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。
volatile-random
从已设置过期时间的数据集中任意选择数据淘汰
volatile-lfu
从已设置过期时间的数据集挑选使用频率最低的数据淘汰
allkeys-lru
从数据集中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合(应用最广泛的策略)。
allkeys-lfu
从数据集中挑选使用频率最低的数据淘汰
allkeys-random
从数据集中任意选择数据淘汰
no-enviction(驱逐)
禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。
删除策略
定期删除
key过期后任然留在内存中不做处理,当有请求操作这个key的时候,会检查这个key是否过期,如果过期则删除,否则返回key对应的数据信息。(惰性删除对CPU是友好的,因为只有在读取的时候检测到过期了才会将其删除。但对内存是不友好,如果过期键后续不被访问,那么这些过期键将积累在缓存中,对内存消耗是比较大的。)
惰性删除
Redis数据库默认每隔100ms就会进行随机抽取一些设置过期时间的key进行检测,过期则删除。(定期删除是定时删除和惰性删除的一个折中方案。可以根据实际场景自定义这个间隔时间,在CPU资源和内存资源上作出权衡。)
定时删除
在设置键的过期时间的同时,设置一个定时器,当键过期了,定时器马上把该键删除。(定时删除对内存来说是友好的,因为它可以及时清理过期键;但对CPU是不友好的,如果过期键太多,删除操作会消耗过多的资源。)
Redis工作模式
主从
什么是主从复制?
主从复制可以将数据同步到多台不同机器,也能够保证在主节点宕机时任然对外提供服务,还可以做到通过读写分离的形式提升整体缓存业务群吞吐量。一般在线上环境时我们去搭建主从环境时,为了保证数据一致性,从节点是不允许写的,而是通过复制主节点数据的形式保障数据同步。所以在整个Redis节点群中只能同时运行存在一台主,其他的全为从节点,示意图如下(读的QPS可以通过对从节点的线性扩容来提升)
主从复制的常见概念
runId
每个Redis节点启动都会生成唯一的uuid,每次Redis重启后,runId都会发生变化
offset
主节点和从节点都各自维护自己的主从复制偏移量offset,当主节点有写入命令时,offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的offset,并把自己的offset发送给主节点。这样,主节点同时保存自己的offset和从节点的offset,通过对比offset来判断主从节点数据是否一致
repl_back_buffer
复制缓冲区,用来存储增量数据命令
全量复制
从服务器首次启动或重启时,需要进行全量复制
不满足增量复制的条件时,会进行全量复制
增量复制(部分复制)
当掉线重连后的offset满足增量复制条件
主从复制的发起方式
psync[runId][offset]命令
主从的优缺点
优点
能够为后续的高可用机制打下基础
在持久化的基础上能够将数据同步到其他机器,在极端情况下做到灾备的效果
能够通过主写从读的形式实现读写分离提升Redis整体吞吐,并且读的性能可以通过对从节点进行线性扩容无限提升
缺点
全量数据同步时如果数据量比较大,在之前会导致线上短暂性的卡顿
一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预
写入的QPS性能受到主节点限制,虽然主从复制能够通过读写分离来提升整体性能,但是只有从节点能够做到线性扩容升吞吐,写入的性能还是受到主节点限制
木桶效应,整个Redis节点群能够存储的数据容量受到所有节点中内存最小的那台限制,比如一主两从架构:master=32GB、slave1=32GB、slave2=16GB,那么整个Redis节点群能够存储的最大容量为16GB
哨兵
哨兵机制解决了什么问题?
哨兵能够自主的监视主服务器是否正常工作,当被监控的某个Redis服务器出现问题,哨兵会通过API脚本向管理员或者其他应用程序发出通知,当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样就不需要人工干预进行主从切换
哨兵+主从的常规配置方式:一主两从三哨兵
集群
代理集群
分片集群
分布式锁(SetNX)的原理
SETNX 命令的作用是:如果指定的 key 不存在,则创建并为其设置值,然后返回状态码 1;如果指定的 key 存在,则直接返回 0。如果返回值为 1,代表获得该锁;此时其他进程再次尝试创建时,由于 key 已经存在,则都会返回 0 ,代表锁已经被占用。
当获得锁的进程处理完成业务后,再通过 del 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。
通常为了避免死锁,我们会为锁设置一个超时时间,在 Redis 中可以通过 expire 命令来进行实现
当获得锁的进程处理完成业务后,再通过 del 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。
通常为了避免死锁,我们会为锁设置一个超时时间,在 Redis 中可以通过 expire 命令来进行实现
主从+哨兵模式下的分布式锁
由于主从之间的复制操作是异步的,当主节点上创建好锁后,此时从节点上的锁可能尚未创建。而如果此时主节点发生了宕机,从节点上将不会创建该分布式锁;
从节点晋升为主节点后,其他进程(或线程)仍然可以在该新主节点创建分布式锁,此时就存在多个进程(或线程)同时进入了临界区,分布式锁就失效了。
从节点晋升为主节点后,其他进程(或线程)仍然可以在该新主节点创建分布式锁,此时就存在多个进程(或线程)同时进入了临界区,分布式锁就失效了。
集群模式与分布式锁
以毫秒为单位记录当前的时间,作为开始时间;
接着采用和单机版相同的方式,依次尝试在每个实例上创建锁。为了避免客户端长时间与某个故障的 Redis 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为 10 秒,则访问每个 Redis 实例的超时时间可能在 5 到 50 毫秒之间,如果在这个时间内还没有建立通信,则尝试连接下一个实例;
如果在至少 N/2+1 个实例上都成功创建了锁。并且 当前时间 - 开始时间 < 锁的超时时间 ,则认为已经获取了锁,锁的有效时间等于 超时时间 - 花费时间(如果考虑不同 Redis 实例所在服务器的时钟漂移,则还需要减去时钟漂移);
如果少于 N/2+1 个实例,则认为创建分布式锁失败,此时需要删除这些实例上已创建的锁,以便其他客户端进行创建。
该客户端在失败后,可以等待一个随机的时间后,再次进行重试。
接着采用和单机版相同的方式,依次尝试在每个实例上创建锁。为了避免客户端长时间与某个故障的 Redis 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为 10 秒,则访问每个 Redis 实例的超时时间可能在 5 到 50 毫秒之间,如果在这个时间内还没有建立通信,则尝试连接下一个实例;
如果在至少 N/2+1 个实例上都成功创建了锁。并且 当前时间 - 开始时间 < 锁的超时时间 ,则认为已经获取了锁,锁的有效时间等于 超时时间 - 花费时间(如果考虑不同 Redis 实例所在服务器的时钟漂移,则还需要减去时钟漂移);
如果少于 N/2+1 个实例,则认为创建分布式锁失败,此时需要删除这些实例上已创建的锁,以便其他客户端进行创建。
该客户端在失败后,可以等待一个随机的时间后,再次进行重试。
消息中间件 RabbitMQ
RabbitMQ基础知识
RabbitMQ是什么?
消息中间件,通过生产者、队列、消费者提供的通讯服务
RabbitMQ的工作原理是什么?
消息被生产者发到指定的交换机根据路由规则路由到绑定的队列,然后推送给消费者。
RabbitMQ消息的可靠性解决方案
消息顺序消费
一个队列对应多个消费者,导致未按顺序同步消费
解决方案
一个队列对应一个消费者,但是消费者多线程消费,导致未按顺序消费
解决方案
consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
确保一个队列对应一个消费者
消息重复消费(幂等性)
我们解决消息重复消费有两种角度,第一种就是不让消费端执行两次,第二种是让它重复消费了,但是不会对我的业务数据造成影响就行了。
确保消费端只执行一次
一般来说消息重复消费都是在短暂的一瞬间消费多次,我们可以使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识。举个例子,订单使用优惠券后,要通知优惠券系统,增加使用流水。这里可以用订单号 + 优惠券 id 做唯一标识。业务开始先判断 redis 是否已经存在这个标识,如果已经存在代表处理过了。不存在就放进 redis 设置过期时间,执行业务。
允许消费端执行多次,保证数据不受影响
数据库唯一键约束
如果消费端业务是新增操作,我们可以利用数据库的唯一键约束,比如优惠券流水表的优惠券编号,如果重复消费将会插入两条相同的优惠券编号记录,数据库会给我们报错,可以保证数据库数据不会插入两条。
数据库乐观锁思想
如果消费端业务是更新操作,可以给业务表加一个 version 字段,每次更新把 version 作为条件,更新之后 version + 1。由于 MySQL 的 innoDB 是行锁,当其中一个请求成功更新之后,另一个请求才能进来,由于版本号 version 已经变成 2,必定更新的 SQL 语句影响行数为 0,不会影响数据库数据。
防止消息丢失
生产者宕机
交换机异常
RabbitMQ宕机
相当于RabbitMQ造成消息丢失
消费者未正确消费消息
消息堆积
消息的消费速度低于生产速度
未成功ack确认的消息被反复的返回队列
RabbitMQ常用场景
应用解耦
例如订单系统如果做同步操作,客户端发起下单请求,服务器进行订单生成和库存扣减操作
短期来看没有问题,但如果订单系统需要引入积分,或其他新的功能模块,或者订单系统异常,则会出现大量问题需要后期维护
短期来看没有问题,但如果订单系统需要引入积分,或其他新的功能模块,或者订单系统异常,则会出现大量问题需要后期维护
此时我们引入RabbitMQ做异步通信,客户端发起下单请求后,发送消息到消息队列,由订单系统和库存系统做监听,这样后续如果添加其他模块也可以直接添加,无需在原有的代码上进行修改
异步处理
异步处理最常用的就是短信验证码,我们假设如果注册信息入库是30ms,发短信也是30ms,两个动作 串行执行 的话,会比较耗时,响应60ms
如果采用并行执行的方式,可以减少响应时间。注册信息入库后,同时异步发短信和邮件。如何实现异步呢,用消息队列即可,就是说,注册信息入库成功后,写入到消息队列(这个一般比较快,如只需要3ms),然后异步读取发邮件和短信。
流量削峰
秒杀场景下,可能会出现高并发的情况,此时如果让大量的请求直接到达服务器,一旦服务器没有能力同时处理过多请求,就有被打垮宕机的风险,此时我们可以引入消息队列来进行处理
通过消息队列,将秒杀活动中的下单请求放入消息队列中,并设置订单系统监听此队列,并逐一进行处理,就可以有效控制流量,达到流量削峰的效果
消息通讯
消息队列内置了高效的通信机制,可用于消息通讯。如实现点对点消息队列、聊天室等。
延迟队列
RabbitMQ本身是没有延迟队列的,但可以通过设置消息的过期时间和死信交换机,来实现消息的延迟消费
消息过期后会交给死信交换机处理,可以把消息发送到阻塞队列中,消息在阻塞队列中无法完成消费,等待过期后自动进入死信交换机,进行处理,最经典的案例就是订单的超时自动取消功能
RabbitMQ持久化
持久化的目的
默认情况下,exchange、queue、message 等数据都是存储在内存中的,这意味着如果 RabbitMQ 重启、关闭、宕机时所有的信息都将丢失。
RabbitMQ 提供了持久化来解决这个问题,持久化后,如果 RabbitMQ 发送 重启、关闭、宕机,下次起到时 RabbitMQ 会从硬盘中恢复exchange、queue、message 等数据。
如何持久化
交换机持久化
队列持久化
RabbitMQ的补偿机制
`
据说可以处理事务?现在脑子是昏的,等想明白了来这里改
框架
Spring
Springboot
Mybatis
分布式
设计理念
组件
插件的使用
事务
锁
海量数据处理
elasticSearch
前端
vue
thymleaf
0 条评论
下一页