这是一个环形结构的缓冲区,如下图:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

主节点有一个写偏移量 master_repl_offset,从节点也有一个偏移量 slave_repl_offset。

优先选择 slave_repl_offset 最接近 master_repl_offset 的从节点作为新的主节点。

所以,上图中偏移量为 114 的从节点优先被选为新的主节点。

ID 编号:优先级和参数都一样的情况下,ID 编号小的从节点优先被选为新主节点。

[](

)3.选举哨兵 Leader


第一个判断主节点下线的哨兵节点收到其他节点的回复并确定主节点下线后,就会给其他哨兵发送命令申请成为哨兵 Leader。

成为 Leader 的条件如下:

  • 收到赞成票必须大于等 quorum 值

  • 必须拿到半数以上的赞成票

如果集群配置了 5 个哨兵,quorum 的值设置为 3,其中一个哨兵节点挂了,很有可能会判断到主节点下线,但是因为选举不出哨兵 Leader 而不能切换。

如果集群有 2 个哨兵,其中一个挂了,那必定选不出哨兵 Leader。

下面的图展示了哨兵一成功当选 Leader 的过程:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

[](

)4.主节点切换


选出新主节点和哨兵 Leader 后,哨兵 Leader 会执行主从切换的操作。

完成后会做一些事件通知:

  • 通知其他哨兵新主节点地址

  • 通知所有从节点新的主节点地址,从节点收到后向新主节点请求主从同步

  • 通知客户端连接新主节点

[](

)5.主从切换过程中请求处理


如果客户端的读请求会发送到从节点,可以正常处理。在客户端收到新主节点地址通知前写请求会失败。客户端可以采取一些应急措施应对主节点下线,比如缓存写请求。

为了能够及时获取到新主节点信息,客户端可以订阅哨兵的主节点下线事件和新主节点变更事件。

[](

)Redis 为什么变慢了

===============================================================================

Redis 变慢了的原因有很多,总结一下有 11 个,见下图:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

从图中看出,Redis 变慢原因主要有两类:阻塞主线程和操作系统限制。

[](

)1.主线程阻塞


AOF 重写和 RDB 快照:前面已经讲过了,Redis 在 AOF 重写时,主线程会 Fork 出一个 bgrewriteaof 子进程。Redis 进行 RDB 快照时主线程会 Fork 出一个 bgsave 子进程。

这两个操作表面上看不阻塞主线程,但 Fork 子进程的这个过程是在主线程完成的。

Fork 子进程时 Redis 需要拷贝内存页表,如果 Redis 实例很大,这个拷贝会耗费大量的 CPU 资源,阻塞主线程的时间也会变长。

内存大页:Redis 默认支持内存大页是 2MB,使用内存大页,一定程度上可以减少 Redis 的内存分配次数,但是对数据持久化会有一定影响。

Redis 在 AOF 重写和 RDB 快照过程中,如果主线程收到新的写请求,就需要 CopyOnWrite。

使用了内存大页,即使 Redis 只修改其中一个大小是 1kb 的 key,也需要拷贝一整页的数据,即 2MB。在写入量较多时,大量拷贝就会导致 Redis 性能下降。

命令复杂度高:执行复杂度高的命令是造成 Redis 阻塞的常见原因。比如对一个 set 或者 list 数据类型执行 SORT 操作,复杂度是 O(N+M*log(M))。

bigkey 操作:如果一个 key 的 value 非常大,创建的时候分配内存会很耗时,删除的时候释放内存也很耗时。

Redis 4.0 以后引入了 layfree 机制,可以使用子进程异步删除,从而不影响主线程执行。用 UNLINK 命令替代 DEL 命令,就可以使用子进程异步删除。

Redis 6.0 增加了配置项 lazyfree-lazy-user-del,配置成 yes 后,del 命令也可以用子进程异步删除。

如果 lazyfree-lazy-user-del 不设置为 yes,那 Redis 是否采用异步删除,是要看删除的时机的。

对于 String 类型和底层采用整数数组和压缩列表的数据类型,Redis 是不会采用异步删除的。

从节点全量同步:从节点全量同步过程中,需要先清除内存中的数据,然后再加载 RDB 文件,这个过程中是阻塞的,如果有读请求到来,只能等到加载 RDB 文件完成后才能处理请求,所以响应会很慢。

另外,如果 Redis 实例很大,也会造成 RDB 文件太大,从库加载时间长。所以尽量保持 Redis 实例不要太大,比如单个实例限制 4G,如果超出就采用切片集群。

AOF 同步写盘:appendfsync 策略有 3 种:always、everysec、no,如果采用 always,每个命令都会同步写盘,这个过程是阻塞的,等写盘成功后才能处理下一条命令。

除非是严格不能丢数据的场景,否则尽量不要选择 always 策略,推荐尽量选择 everysec 策略,如果对丢失数据不敏感,可以采用 no。

内存达到 maxmemory:需要使用淘汰策略来淘汰部分 key。即使采用 lazyfree 异步删除,选择 key 的过程也是阻塞的。

可以选择较快的淘汰策略,比如用随机淘汰来替换 LRU 和 LFU 算法淘汰。也可以扩大切片数量来减轻淘汰 key 的时间消耗。

[](

)2.操作系统限制


使用了 swap:使用 swap 的原因是操作系统不能给 Redis 分配足够大的内存,如果操作其他开启了 swap,内存数据就需要不停地跟 swap 换入和换出,对性能影响非常大。

操作系统没有能力分配内存的原因也可能是其他进程使用了大量的内存。

网络问题:如果网卡负载很大,对 Redis 性能影响会很大。这一方面有可能 Redis 的访问量确实很高,另一方面也可能是有其他流量大的程序占用了带宽。

这个最好从运维层面进行监控。

线程上下文切换:Redis 虽然是单线程的,但是在多核 CPU 的情况下,也可能会发生上下文切换。

如果主线程从一个物理核切换到了另一个物理核,那就不能使用 CPU 高效的一级缓存和二级缓存了。

如下图所示:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

为防止这种情况,可以把 Redis 绑定到一个 CPU 物理核。

磁盘性能低:对于 AOF 同步写盘的使用场景,如果磁盘性能低,也会影响 Redis 的响应。可以优先采用性能更好的 SSD 硬盘。

[](

)设计排行榜功能

==========================================================================

Redis 的 zset 类型保存了分数值,可以方便的实现排行榜的功能。

比如要统计 10 篇文章的排行榜,可以先建立一个存放 10 篇文章的 zset,每当有读者阅读一篇文章时,就用 ZINCRBY 命令给这篇文章的分数加 1,最后可以用 range 命令统计排行榜前几位的文章。

[](

)Redis 实现分布式锁

===============================================================================

[](

)1.Redis 单节点的分布式锁


如下图,一个服务部署了 2 个客户端,获取分布式锁时一个成功,另一个就失败了。

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

Redis 一般使用 setnx 实现分布式锁,命令如下:

SETNX KEY_NAME VALUE

设置成功返回 1,设置失败返回 0。使用单节点分布式锁存在一些问题。

客户端 1 获取锁后发生了故障:结果锁就不能释放了,其他客户端永远获取不到锁。

解决方法是用下面命令对 key 设置过期时间:

SET key value [EX seconds] [PX milliseconds] NX

客户端 2 误删除了锁:解决方法是对 key 设置 value 时加入一个客户端表示,比如在客户端 1 设置 key 时在 value 前拼接一个字符串 application1,删除的时候做一下判断。

[](

)2.Redis 红锁


Redis 单节点会有可靠性问题,节点故障后锁操作就会失败。Redis 为了应对单点故障的问题,设计了多节点的分布式锁,也叫红锁。

主要思想是客户端跟多个 Redis 实例请求加锁,只有超过半数的实例加锁成功,才认为成功获取了分布式锁。

如下图,客户端分别跟 3 个实例请求加锁,有 2 个实例加锁成功,所以获取分布式锁成功:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

[](

)缓存雪崩、击穿、穿透

=============================================================================

[](

)1.缓存雪崩


Redis 做缓存时,如果同一时间大量缓存数据失效,客户端请求会大量发送到数据库,导致数据库压力激增。

如下图:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

应对方法主要有 3 个:

  • 给 key 设置过期时间时加一个小的随机数

  • 限流

  • 服务降级

[](

)2.缓存击穿


某个热点 key,突然过期了,大量请求发送到了数据库。解决方案是给热点 key 不设置过期时间。

[](

)3.缓存穿透


某个热点 key,查询缓存和查询数据库都没有,就发生了缓存穿透。

如下图:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

应对方法主要有 2 个:

  • 缓存热点的空值和缺省值

  • 查询数据库之前先查询布隆过滤器

[](

)数据倾斜

=======================================================================

什么是数据倾斜?看下面这个面试题:如果 Redis 有一个热点 key,QPS 能达到 100w,该如何存储?

如果这个热点 key 被放到一个 Redis 实例上,这个实例面临的访问压力会非常大。

如下图,redis3 这个实例保存了 foo 这个热点 key,访问压力会很大:

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

解决方法主要有两个:

1.使用客户端本地缓存来缓存 key。

这样改造会有两个问题:

  • 客户端缓存的热点 key 可能消耗大量内存。

  • 客户端需要保证本地缓存和 Redis 缓存的一致性。

2.给热点 key 加一个随机前缀,让它保存到不同的 Redis 实例上。

这样也会存在两个问题:

  • 客户端在访问的时候需要给这个 key 加前缀

  • 客户端在删除的时候需要根据所有前缀来删除不同实例上保存的这个 key

[](

)Bitmap 使用

============================================================================

有一道经典的面试题,10 亿整数怎么在内存中去重排序?

我们先算一下 10 亿整数占的内存,Java 一个整数类型占四字节,占用内存大小约:

10亿 * 4 / 1024 / 1024 = 3.7G

占得内存太大了,如果内存不够,怎么办呢?

[](

)1.Bitmap 介绍


Bitmap 类型使用的数据结构是 String,底层存储格式是二进制的 bit 数组。假如我们有 1、4、6、9 四个数,保存在 bit 数组中如下图:

总目录展示

该笔记共八个节点(由浅入深),分为三大模块。

高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。该笔记将从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这4个方面重点介绍。

一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,将用一个节点来专门讲解如何设计秒杀减库存方案。

高可用。 虽然介绍了很多极致的优化思路,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。笔记的最后,将带你思考可以从哪些环节来设计兜底方案。


篇幅有限,无法一个模块一个模块详细的展示(这些要点都收集在了这份《高并发秒杀顶级教程》里),麻烦各位转发一下(可以帮助更多的人看到哟!)

CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

我画了19张图,彻底帮你搞定Redis,2021Java开发面试解答

由于内容太多,这里只截取部分的内容。