⽹络
虾⽪ Java ⾯试
虾⽪(Shopee )这⼏天都开奖了,之前有同学谈薪的时候,喊 28k ,hr 姐姐 劝他⼤胆⼀点,看来
虾⽪还是很舍得给钱。
拿到 sp offer 以上的同学,⼤部分都觉得虾⽪诚意确实有的,可惜开的⽐较晚,先签了其他⼤⼚。
25 届虾⽪的后端开发岗位的校招情况如下,虾⽪办公地点主要在⼀线城市,上海/北京/深圳

32k * 15+5w 签字费,同学 bg 硕⼠ 985 ,base 上海
30k * 15+5w 签字费,同学 bg 未知,base 深圳
29k * 15+3w 签字费,同学 bg 本科 985 ,base 深圳
23.5k * 15 ,同学 bg 硕⼠ 985 ,base 深圳
年终平均 3 个⽉,公积⾦ 10% ,15 天年假,14 天病假,这带薪假期是真的多,不愧是外企。
那么虾⽪的⾯试难度如何?
今天就给⼤家拆解虾⽪后端⾯经,跟⼤⼚流程⼀样,技术⼋股+项⽬+算法,这次的⾯经考察后端
组件的原理⽐较多,对语⾔的⼋股考 察较少。

这次⾯经考察的知识,我给⼤家罗列⼀下:
⽹络:HTTPS 和HTTP 、限流算法
Java :线程池
Redis :内存淘汰算法、过期删除策略
MySQL :隔离级别、索引结构、MVCC 、SQL 优化、redolog 和 binlog ⽇志
RocektMQ :分布式事务、消息有序、消息积压
算法:轮转 数组
⽹络
HTTP 和HTTPS 区别是什么 ?
HTTP 是超⽂本传输协议,信息是明 ⽂传输,存在安全⻛险的问题。HTTPS 则解决 HTTP 不安全
的缺陷,在 TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
HTTP 连接建⽴相对简单, TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。⽽ HTTPS 在 TCP 三
次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
两者的默认端⼝不⼀样,HTTP 默认端⼝号是 80 ,HTTPS 默认端⼝号是 443 。
HTTPS 协议需要向 CA (证书权威机构 )申请数字证书,来保证服务器的⾝份是可信的。
限流了解么?
限流是当⾼并发或者瞬时⾼并发时,为了 保证系统的稳定性、可⽤性,对超出服务处理能⼒之外
的请求进⾏拦截 ,对访问服务的流量进⾏限制。
常⻅的限流算法有四种:固定窗⼝限流算法、滑动窗⼝限流算法、漏桶限流算法和令牌桶限流算
法。
固定窗⼝限流算法实现简单,容易理解,但是流量曲线可能不够平滑,有“突刺现象”,在窗⼝
切换时可能会产⽣两倍于阈值流量的请求。
滑动窗⼝限流算法是对固定窗⼝限流算法的改进,有效解决了窗⼝切换时可能会产⽣两倍于阈
值流量请求的问题。
漏桶限流算法能够对流量起到整流的作⽤,让随机不稳定的流量以固定的速率流出,但是不能
解决流量突发的问题。
令牌桶算法作为漏⽃算法的⼀种改进,除了能够起到平滑流量的作⽤,还允许⼀定程度的流量
突发。
固定窗⼝限流算法
固定窗⼝限流算法就是对⼀段固定时间窗⼝内的请求进⾏计数,如果请求数超过了阈值,则舍弃
该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当时间窗⼝结束时,重置计数器
为0。

固定窗⼝限流优点是实现简单,但是会有“流量吐刺”的问题,假设窗⼝⼤⼩为1s ,限流⼤⼩为
100 ,然后恰好在某个窗⼝的第999ms 来了100 个请求,窗⼝前期没有请求,所以这100 个请求都会通过。
再恰好,下⼀个窗⼝的第1ms 有来 了100 个请求,也全部通 过了,那也就是在2ms 之内通过了200 个请求,⽽我们设定的阈值是100 ,通过的请求达到了阈值的两倍,这样可能会给系统造成巨⼤的负载压⼒。

滑动窗⼝限流算法
改进固定窗⼝缺陷的⽅法是采⽤滑动窗⼝限流算法,滑动窗⼝就是将限流窗⼝内部切分 成⼀些更
⼩的时间⽚,然后在时间轴上滑动,每次 滑动,滑过⼀个⼩时间⽚,就形成⼀个新的限流窗⼝,
即滑动窗⼝。
然后在这个滑动窗⼝内执⾏固定窗⼝算法即可。滑动窗⼝可以避免固定窗⼝出现的放过两倍请求
的问题,因为⼀个短时间内出现的所有请求必然在⼀个滑动窗⼝内,所以⼀定会被滑动窗⼝限 流。

漏桶限流算法
漏桶限流算法是模拟⽔流过⼀个有漏洞的桶进⽽限流的思路。
⽔⻰头的⽔先流⼊漏桶,再通过漏桶底部的孔流出。如果流⼊的⽔量太⼤,底部的孔来不及流
出,就会导致⽔桶太满溢 出去。从系统的⻆度来看,我们不知道什么 时候会有请求来,也不 知道
请求会以 多⼤的速率来,这就给系统的安全性埋下了 隐患。
但是如果加了⼀层漏⽃算法限流之后,就能够保证请求以恒定的速率流出。在系统看来,请求永
远是以平滑的传输速率过来,从⽽起到了保护系统的作⽤。使⽤漏桶限流算法,缺点有两个 :
即使系统资源很空闲,多个请求同时到达时,漏桶也是慢慢 地⼀个接⼀个地去处理请求,这其
实并不符合⼈们的期望 ,因为这样就是在浪费计算资源。
不能解决流量突发的问题,假设漏⽃速率是2个/秒,然后突然来了10 个请求,受限于漏⽃的容
量,只有5个请求被接受,另外5个被拒绝。你可能会说,漏⽃速率是2个/秒,然后瞬间接受了5
个请求,这不就解决了流量突发的问题吗?不,这5个请求只是被接受了,但是没有⻢上被处
理,处理的速度仍然是我们设定的2个/秒,所以没有解决流量突发的问题
令牌桶限流算法
令牌桶是另⼀种桶限流算法,模拟⼀个特定⼤⼩的桶,然后向 桶中以特定的速度放⼊令牌
(token ),请求到达后,必须从桶中取出⼀个令牌才能继续处理。
如果桶中已经没有令牌了,那么当前请求就被限流。如果桶中的令牌放满了,令牌桶也会溢出。
放令牌的动作是持续不断进⾏的,如果桶中令牌数达到上限,则丢弃令牌,因此桶中可能⼀直持
有⼤量的可⽤令牌。此时请求进来可以直接拿 到令牌执⾏。
⽐如设置 qps 为 100 ,那么限流器初始化完成 1 秒后,桶中就已经有 100 个令牌了,如果此前还
没有请求过来,这时突然来了 100 个请求,该限流器可以抵挡瞬时的 100 个请求。
由此可⻅,只有桶中没有令牌时,请求才会进⾏等待,最终表现的效果即为以⼀定的速率执⾏。
令牌桶的⽰意图如下:

令牌桶限流算法综合效果⽐较好,能在最⼤程度利⽤系统资源处理请求的基础上,实现限流的⽬
标,建议通常场景中优先使⽤该算法。
Java
为什么 要⽤线程池?
线程池是为了 减少频繁的创建线程和销毁线程带来的性能损耗。
线程池分为核⼼线程池,线程池的最⼤容量,还有等待任务的队列,提交⼀个任务,如果核⼼线
程没有满,就创建⼀个线程,如果满了,就是会加⼊等待队列,如果等待队列满了,就会增加线
程,如果达到最⼤线程数量,如果都达到最⼤线程数量,就会按照⼀些丢 弃的策略进⾏处理。

线程池的构造函数有7个参数:

corePoolSize :线程池核⼼线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize ,那么即使这些线程处于空闲状态,那也不 会被销毁。maximumPoolSize :线程池中最多可容纳的线程数量。限制了线程池能创建的最⼤线程总数
(包括核⼼线程和⾮核⼼线程),当 corePoolSize 已满 并且 尝试将新任务加 ⼊阻塞队列失败
(即队列已满)并且 当前线程数 < maximumPoolSize ,就会创建新线程执⾏此任务,但是当
满 并且 队列满 并且 线程数已达 maximumPoolSize 并且 ⼜有新任务提交时,就会触发拒绝策略。
keepAliveTime :当线程池中线程的数量⼤于corePoolSize ,并且某个线程的空闲时间超过了
keepAliveTime ,那么这个线程就会被销毁。
unit :就是keepAliveTime 时间的单位。
workQueue :⼯作队列。当没有空闲的线程执⾏新任务时,该任务就会被放⼊⼯作队列中,等
待执⾏。
threadFactory :线程⼯⼚。可以⽤来给线 程取名字等等
handler :拒绝策略。当⼀个新任务交给线 程池,如果此时线程池中有空闲的线程,就会直接执
⾏,如果没有空闲的线程,就会将该任务加 ⼊到阻塞队列中,如果阻塞队列满了,就会创建⼀
个新线程,从阻塞队列头部取出⼀个任务来执⾏,并将新任务加 ⼊到阻塞队列末尾。如果当前
线程池中线程的数量等于maximumPoolSize ,就不会创建新线程,就会去执⾏拒绝策略。
有哪些线程池?
ScheduledThreadPool :可以设置定期的执⾏任务,它⽀持定时或周期性执⾏任务,⽐如每隔
10 秒钟执⾏⼀次任务,我通过这 个实现类设置定期执⾏任务的策略。
FixedThreadPool :它的核⼼线程数和最⼤线程数是⼀样的,所以可以把它看作是固定线程数的
线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就
是固定的,就算任务数超过线程数,线程池也不 会再创建更多的线程来处理任务,⽽是会把超
出线程处理能⼒的任务放到任务队列中进⾏等待。⽽且就算任务队列满了,到了本该继续增加
线程数的时候,由于它的最⼤线程数和核⼼线程数是⼀样的,所以也⽆法再增加新的线程了。
CachedThreadPool :可以称作可缓存线程池,它的特点在于线程数是⼏乎可以⽆限增加的(实
际最⼤可以达到 Integer.MAX_VALUE ,为 2^31-1 ,这个数⾮常⼤,所以基本不可能达到),⽽当线程闲置时还可以对线程进⾏回收。也就是说该线程池的线程数量不是固定不变的,当然它
也有⼀个⽤于存储提交任务的队列,但这个队列是 SynchronousQueue ,队列的容量为0,实际
不存储任何任 务,它只负责 对任务进⾏中转和传递,所以效率⽐较⾼。
SingleThreadExecutor :它会使 ⽤唯⼀的线程去执⾏任务,原理和 FixedThreadPool 是⼀样的,
只不过这 ⾥线程只有⼀个,如果线程在执⾏任务的过程中发⽣异常,线程池也会重新创建⼀个
线程来执⾏后续的任务。这种线程池由于只有⼀个线程,所以⾮常适合⽤于所有任务都需要按
被提交的顺序依次执⾏的场景,⽽前⼏种线程池不⼀定能够保障任务的执⾏顺序等于被提交的
顺序,因为它们是多线程并⾏执⾏的。
SingleThreadScheduledExecutor :它实 际和 ScheduledThreadPool 线程池⾮常相似,它只是
ScheduledThreadPool 的⼀个特例,内部只有⼀个线程。
Redis
Redis 内存淘汰策略有哪些?
在配置⽂件 redis.conf 中,可以通过参数 maxmemory 来设定最⼤运⾏内存,只有在 Redis 的运⾏
内存达到了我们设置的最⼤运⾏内存,才会触发内存淘汰策略。不同位数的操作系统,
maxmemory 的默认值是不同的:
在 64 位操作系统中,maxmemory 的默认值是 0,表⽰没有内存⼤⼩限制,那么不 管⽤⼾存放
多少数据到 Redis 中,Redis 也不 会对可⽤内存进⾏检查,直到 Redis 实例因内存不⾜⽽崩溃也
⽆作为。
在 32 位操作系统中,maxmemory 的默认值是 3G ,因为 32 位的机器最⼤只⽀持 4GB 的内
存,⽽系统本⾝就需要⼀定的内存资源来⽀持运⾏,所以 32 位操作系统限制最⼤ 3 GB 的可⽤
内存是⾮常合理的,这样可以避免因为内存不⾜⽽导致 Redis 实例崩溃。
Redis 内存淘汰策略共有⼋种,这⼋种策略⼤体分为「不进⾏数据淘汰」和「进⾏数据淘汰」两类
策略。

1、不进⾏数据淘汰的策略
noeviction (Redis3.0 之后,默认的内存淘汰策略) :它表⽰当运⾏内存超过最⼤设置内存时,不
淘汰任何 数据,这时如果有 新的数据写⼊,会报错通知禁⽌写⼊,不淘汰任何 数据,但是如果没
⽤数据写⼊的话,只是单纯的查询或者删除操作的话,还是可以正常⼯作。
2、进⾏数据淘汰的策略
针对「进⾏数据淘汰」这⼀类策略,⼜可以细分为「在设置了过期时间的数据中进⾏淘汰」和
「在所有数据范围内进⾏淘汰」这两类策略。在设置了过期时间的数据中进⾏淘汰:
volatile-random :随机淘汰设置了过期时间的任意键值;
volatile-ttl :优先淘汰更早过期的键值。
volatile-lru (Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久
未使⽤的键值;
volatile-lfu (Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使⽤
的键值;
在所有数据范围内进⾏淘汰:
allkeys-random :随机淘汰任意键值;
allkeys-lru :淘汰整个键值中最久未使⽤的键值;
allkeys-lfu (Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使⽤的键值。
Redis 过期删除策略是什么 ?
Redis 选择「惰性删除+定期删除」这两种策略配和使⽤,以求在合理使⽤ CPU 时间和避免内 存浪
费之间取得平衡。
Redis 是怎么实现惰性删除的?
Redis 的惰性删除策略由 db.c ⽂件中的 expireIfNeeded 函数实现,代码如下:
int expireIfNeeded(redisDb *db, robj *key) {
// 判断 key 是否过期
if (!keyIsExpired(db,key)) return 0;
..../* 删除过期键 */
....
// 如果 server.lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除;
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}Redis 在访问或者修改 key 之前,都会调⽤ expireIfNeeded 函数对其进⾏检查,检查 key 是否过
期:
如果过期,则删 除该 key ,⾄于选择异步删除,还是选择同步删除,根据 lazyfree_lazy_expire
参数配置决定(Redis 4.0 版本开始提供参数),然后返回 null 客⼾端;如果没有过期,不做任何 处理,然后返回正常的键值对给客⼾端;

Redis 是怎么实现定期删除的?
再回忆⼀下,定期删除策略的做法:每隔⼀段时间「随机」从数据库中取出⼀定数量的 key 进⾏
检查,并删除其中的过期key 。
1、这个间隔 检查的时间是多⻓呢?
在 Redis 中,默认每秒进⾏ 10 次过期检查⼀次数据库,此配置可通过 Redis 的配置⽂件
redis.conf 进⾏配置,配置键为 hz 它的默认值是 hz 10 。特别强调下,每次 检查数据库并 不是遍历
过期字典中的所有 key ,⽽是从数据库中随机抽取⼀定数量的 key 进⾏过期检查。
2、随机抽查的数量是多少呢?
我查了下 源码,定期删除的实现在 expire.c ⽂件下的 activeExpireCycle 函数中,其中随机抽查的
数量由 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 定义的,它是写死在代码中的,数值是 20 。
也就是说,数据库每轮抽查时,会随机选择 20 个 key 判断是否过期。接下来,详细说说 Redis 的
定期删除的流程:
从过期字典中随机抽取 20 个 key ;
检查这 20 个 key 是否过期,并删除已过期的 key ;
如果本 轮检查的已过期 key 的数量,超过 5 个(20/4 ),也就是「已过期 key 的数量」占⽐「随机抽取 key 的数量」⼤于 25% ,则继续重复步骤 1;如果已过期的 key ⽐例⼩于 25% ,则停⽌继续删除过期 key ,然后等待下⼀轮再检查。
可以看到,定期删除是⼀个循环的流程。那 Redis 为了 保证定期删除不会出现循环过度,导致线程
卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms 。针对定期删除的流
程,我写了个 伪代 码:
do {
//已过期的数量
expired = 0;
//随机抽取的数量
num = 20;
while (num--) {
//1. 从过期字典中随机抽取 1 个 key
//2. 判断该 key 是否过期,如果已过期则进行删除,同时对 expired++
// 超过时间限制则退出
if (timelimit_exit) return;/* 如果本轮检查的已过期 key 的数量,超过 25%,则继续随机抽查,否则退出本轮检查 */
} while (expired > 20/4);定期删除的流程如下:
}// 超过时间限制则退出MySQL
MySQL 隔离级别有哪些?
读未提交,指⼀个事 务还没提交时,它做的变更就能被其他事 务看到;
读提交,指⼀个事 务提交之 后,它做的变更才能被其他事 务看到;
可重复读,指⼀个事 务执⾏过程中看到的数据,⼀直跟这个事 务启动时看到的数据是⼀致的,
MySQL InnoDB 引擎的默认隔离级别;
串⾏化;会对记录加上读写锁,在多个事 务对这条记录进⾏读写操作时,如果发⽣了读写冲 突
的时候,后访问的事务必须等前⼀个事 务执⾏完成,才能继续执⾏;
按隔离⽔平⾼低排序如下:

针对不同的隔离级别,并发事务时可能发⽣的现象也会不同。

MySQL 索引结构是什么 ?
可以按照四个⻆度来分类索引:
按「数据结构」分类:B+tree 索引、Hash 索引、Full-text 索引。
按「物理存储」分类:聚簇索引(主键索引)、⼆级索引(辅助索引)。
按「字段特性」分类:主键索引、唯⼀索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
从数据结构的⻆度来看,MySQL 常⻅索引有 B+Tree 索引、HASH 索引、Full-Text 索引。每⼀种存
储引擎⽀持的索引类型不⼀定相同,我在表中总结了 MySQL 常⻅的存储引擎 InnoDB 、MyISAM
和 Memory 分别 ⽀持的索引类型。

InnoDB 是在 MySQL 5.5 之后成为默认的 MySQL 存储引擎,B+Tree 索引类型也是 MySQL 存储引
擎采⽤最多的索引类型。
为什么 索引⽤B+ 树?
B+Tree vs B Tree :B+Tree 只在叶⼦节点存储数据,⽽ B 树 的⾮叶⼦节点也要存储数据,所以
B+Tree 的单个节点的数据量更⼩,在相同的磁盘 I/O 次数下,就能查询更多的节点。另外,
B+Tree 叶⼦节点采⽤的是双链表连接,适合 MySQL 中常⻅的基于范围的顺序查找,⽽ B 树⽆
法做到这⼀点。
**B+Tree vs ⼆叉树:** 对于有 N 个叶⼦节点的 B+Tree ,其搜索复杂度为O(logdN) ,其中 d 表⽰节点允许的最⼤⼦节点个数为 d 个。在实际的应⽤当中, d 值是⼤于100 的,这样就保证
了,即使数据达到千万级别时,B+Tree 的⾼度依然维持在 3~4 层左右,也就是说⼀次数据查询
操作只需要做 3~4 次的磁盘 I/O 操作就能查询到⽬标数据。⽽⼆叉树的每个⽗节点的⼉⼦节点
个数只能是 2 个,意味着其搜索复杂度为 O(logN) ,这已经⽐ B+Tree ⾼出不少,因此⼆叉树检索到⽬标数据所经历的磁盘 I/O 次数要更多。
B+Tree vs Hash :Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1) 。但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要⽐ Hash 表索引有着更⼴泛的适
⽤场景的原因。
怎么实现可重复读?MVCC 怎么保证可重复读?
可重复读隔离级别是启动事务时⽣成⼀个 Read View ,然后整个事 务期间都在⽤这个 Read View ,
这样就保证了在事务期间读到的数据都是事务启动前 的记录。

实现是通过「事务的 Read View ⾥的字段」和「记录中的两个 隐藏列」的⽐对,来控制并发事务
访问同⼀个记录时的⾏为,这就叫 MVCC (多版本并发控制)。

在创建 Read View 后,我们可以将记录中的 trx_id 划分 这三种情况:

⼀个事 务去访问记录的时候,除了⾃⼰的更新记录总是可⻅之外,还有这⼏种情况:
如果记录的 trx_id 值⼩于 Read View 中的 min_trx_id 值,表⽰这个版本的记录是在创建 Read View 前已经提交的事务⽣成的,所以该版本的记录对当前事务可⻅。
如果记录的 trx_id 值⼤于等于 Read View 中的 max_trx_id 值,表⽰这个版本的记录是在创建Read View 后才启动的事务⽣成的,所以该版本的记录对当前事务不可⻅。
如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在
m_ids 列表中:
如果记录的 trx_id 在 m_ids 列表中,表⽰⽣成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可⻅。
如果记录的 trx_id 不在 m_ids 列表中,表⽰⽣成该版本记录的活跃事务已经被提交,所以该
版本的记录对当前事务可⻅。
慢查询怎么解决?
通过 explain 执⾏结果,查看 sql 是否⾛索引,如果不⾛索引,考虑增加索引。
可以通过建⽴联合索引,实现覆盖索引优化,减少回表,使⽤联合索引符合最左匹配原则,不
然会索引失效
避免索引失效,⽐如不要⽤左模糊匹配、函数计算、表达式计算等等 。
联表查询最好要以⼩表驱动⼤表,并且被驱动表的字段要有索引,当然最好通过冗余字段的设计,避免联表查询。
针对 limit n,y 深分⻚的查询优化,可以把Limit 查询转换成某个位置的查询:select * from tb_sku where id>20000 limit 10 ,该⽅案适⽤于主 键⾃增的表,将字段多的表分解成多个表,有些字段使⽤频率⾼,有些低,数据量⼤时,会由于使⽤频率低
的存在⽽变慢,可以考虑分开
redolog 和binlog 区别?
这两个 ⽇志有四个区别。1、适⽤对象不同:
binlog 是 MySQL 的 Server 层实现的⽇志,所有存储引擎都可以使 ⽤;
redo log 是 Innodb 存储引擎实现的⽇志;
2、⽂件格式不同:
binlog 有 3 种格式类型,分别 是 STATEMENT (默认格式)、ROW 、 MIXED ,区别如下:
STATEMENT :每⼀条修改数 据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所
以针对这种格式, binlog 可以称为逻辑⽇志),主从 复制中 slave 端再根据 SQL 语句重现。
但 STATEMENT 有动态函数的问题,⽐如你⽤了 uuid 或者 now 这些函数,你在主库上执⾏
的结果并不是你在从库执⾏的结果,这种随时在变的函数会导致复制的数据不⼀致;
ROW :记录⾏数据最终被修改成什么 样了(这种格式的⽇志,就不能称为逻辑⽇志了),不
会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每⾏数据的变化 结果都会被记
录,⽐如执⾏批量 update 语句,更新多少⾏数据就会产⽣多少条记录,使 binlog ⽂件过
⼤,⽽在 STATEMENT 格式下只会记录⼀个 update 语句⽽已;
MIXED :包含了 STATEMENT 和 ROW 模式,它会根据不同的情况⾃动使⽤ ROW 模式和
STATEMENT 模式;
redo log 是物理⽇志,记录的是在某个数据⻚做了什么 修改,⽐如对 XXX 表空间中的 YYY 数据
⻚ ZZZ 偏移量的地⽅做了AAA 更新;
3、写⼊⽅式不同:
binlog 是追加写,写满⼀个⽂件,就创建⼀个新的⽂件继续写,不会覆盖以前的⽇志,保存的
是全量的⽇志。
redo log 是循环写,⽇志空间⼤⼩是固定,全部写满就从头开始,保存未被刷⼊磁盘的 脏⻚⽇
志。
4、⽤途不同:
binlog ⽤于备份恢复、主从 复制;
redo log ⽤于掉电等故障恢复。
消息队列
RocektMQ 怎么处理分布式事务?
RocketMQ 是⼀种最终⼀致性的分布式事务,就是说它保证的是消息最终⼀致性,⽽不是像2PC 、
3PC 、TCC 那样强⼀致分布式事务
假设 A 给 B 转 100 块钱,同时它们不是同⼀个服务上,现在⽬标是就是 A 减100 块钱,B 加100 块
钱。实际情况可能有四种:
1)就是A账⼾减100 (成功),B账⼾加100 (成功)
2)就是A账⼾减100 (失败),B账⼾加100 (失败)
3)就是A账⼾减100 (成功),B账⼾加100 (失败)
4)就是A账⼾减100 (失败),B账⼾加100 (成功)
这⾥ 第1和第2 种情况是能够保证事务的⼀致性的,但是 第3和第4 是⽆法保证事务的⼀致性的。
那我们来看下RocketMQ 是如何来保证事务的⼀致性的。

分布式事务的流程如上图:
1、A服务先发送个Half Message (
是指暂不能被Consumer
消费的消息。Producer 已经把消息
成功发送到了Broker 端,但此消息被标记为暂不能投递状态,处于该种状态下的消息称为半消
息。需要 Producer 对消息的⼆次确认后,Consumer 才能去消费它_)给Brock 端,消息中携带 B
服务 即将要+100 元的信息。
2、当A服务知道Half Message 发送成功后,那么开始第3步执⾏本地事务。
3、执⾏本地事务(会有三种情况1、执⾏成功。2、执⾏失败。3、⽹络等原因导致没有响应)
4.1) 、如果本 地事务成功,那么Product 像Brock 服务器发送Commit, 这样B服务就可以消费该message 。
4.2) 、如果本 地事务失败,那么Product 像Brock 服务器发送Rollback, 那么就会直接删除上⾯这条半消息。
4.3) 、如果因为⽹络等原因迟迟 没有返回失败还是成功,那么会执⾏RocketMQ 的回调接⼝,来进⾏事务的回查。
从上 ⾯流程可以得知 只有A服务本地事务执⾏成功 ,B服务才能消费该message 。
那么 A账⼾减100 (成功),B账⼾加100 (失败),这时候B服务失败怎么办?
如果B最终执⾏失败,⼏乎可以断定就是代码有问题所以才引起的异常,因为消费端RocketMQ 有
重试机制,如果不是代码问题⼀般重试⼏次就能成功。
如果是代码的原因引起多次重试失败后,也没有关系,将该异常记录下来,由⼈⼯处理,⼈⼯兜
底处理后,就可以让事务达到最终的⼀致性。
RocketMQ 消息顺序怎么保证?
消息的有序性是指消息的消费顺序能够严格保存与消息的发送顺序⼀致。例如,⼀个订单产⽣了3
条消息,分别 是订单创建、订单付款和订单完成。在消息消费时,同⼀条订单要严格按照这个顺
序进⾏消费,否则业务会发⽣混乱。同时,不同订单之间的消息⼜是可以并发消费的,⽐如可以
先执⾏第三个 订单的付款,再执⾏第⼆个订单的创建。
RocketMQ 采⽤了局部顺序⼀致性的机制,实现了单个队列中的消息严格有序。也就是说,如果想
要保证顺序消费,必须将⼀组消息发送到同⼀个队列中,然后再由消费者进⾏注意消费。
RocketMQ 推荐的顺序消费解决⽅案是:安装业务划分 不同的队列,然后将需要顺序消费的消息发
往同⼀队列中即可,不同业务之间的消息仍采⽤并发消费。这种⽅式在满⾜顺序消费的同时提⾼
了消息的处理速度,在⼀定程度上避免了消息堆积问题
RocketMQ 顺序消息的原理是:
在 Producer (⽣产者) 把⼀批需要保证顺序的消息发送到同⼀个 MessageQueue
Consumer (消费者) 则通过加锁的机制来保证消息消费的顺序性,Broker 端通过对
MessageQueue 进⾏加锁,保证同⼀个 MessageQueue 只能被同⼀个 Consumer 进⾏消费。
RocketMQ 消息积压了,怎么办?
导致消息积压突然增加,最粗粒 度的原因,只有两种:要么是发送变快了,要么是消费变慢了。
要解 决积压的问题,可以通过扩容消费端的实例数来提升总体的消费能⼒。
如果短时间内没有⾜够的服务器资源进⾏扩容,没办法的办法是,将系统降级,通过关闭⼀些不
重要的业务,减少发送⽅发送的数据量,最低限度让系统还能正常运转 ,服务⼀些重要业务。
算法
轮转 数组
