本文最后更新于:5 天前
常见考点

- 缓存:
- 穿透、击穿、雪崩
- 双写一致、持久化
- 数据过期、淘汰策略
- 分布式锁:
- setnx、redisson
- 计数器
- 保存 token:常用 String 类型
- 消息队列:常用 list 类型
- 延迟队列:常用 zset 类型
- 集群:
- 主从
- 哨兵
- 集群
- 事务
- 单线程但很快的原因
缓存穿透
概念:查询一个不存在的数据,mysql 查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
解决方案:
1. 缓存空数据:查询返回的数据为空,仍把这个空结果进行缓存
1. 优点:简单
2. 缺点:消耗内存,可能会发生不一致的问题(新增数据存在旧缓存)
2. 布隆过滤器:放在缓存层之前,利用位图快速判断元素是否存在(基于 Redisson 或 Guava 实现,缓存预热同时预热过滤器)
1. 优点:内存占用较少,无多余 key
2. 缺点:存在交叉哈希碰撞导致误判(过滤器数组大小决定误判概率,一般误判率控制在 5%);数组较大消耗内存
缓存击穿
概念:给某一个 key 设置了过期时间,当 key 过期的时候,恰好这时间点对这个 key 有大量的并发请求过来,这些并发的请求可能会瞬间把 DB 压垮
解决方案:
1. 互斥锁:加锁使首个请求更新缓存,迫使其他并发请求等待缓存更新
1. 优点:数据强一致性
2. 缺点:性能较差
2. 逻辑过期:value 中添加逻辑过期字段,并发请求检测到逻辑过期则异步更新缓存,但直接返回旧值,保证后续请求取缓存
1. 优点:高可用、性能优
2. 缺点:不保证数据绝对一致
缓存雪崩
概念:同一时段大量的缓存 key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
1. 不同 key 设置不同 TTL:给不同的 Key 的 TTL 添加随机值
2. 构建高可用 Redis 集群:搭建哨兵模式、集群模式
3. 添加降级限流策略:给缓存业务添加降级限流策略,降级作为保底策略,适用于穿透、击穿、雪崩。
4. 添加多级缓存:添加 Guava 或 Caffeine 作为一级缓存
速记顺口溜
穿透无中生有 key,布隆过滤 null 隔离。
缓存击穿过期 key,锁与非期解难题。
雪崩大量过期 key,过期时间要随机。
面试必考三兄弟,可用限流来保底。
双写一致性
当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
根据业务需要,选择合适的方案。
延迟双删(最终一致性)
读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间
写操作:延迟双删(双删为了较大程度上控制了脏数据的风险,延迟是为了等待数据库主从同步,保证最终一致性)
仅删除一次缓存,始终存在脏数据问题。
强一致性
Redisson 分布式锁/读写锁:共享读,独占写保证数据强一致性,但性能差(Java 对应类 RReadWriteLock)
共享锁:读锁 readLock,加锁之后,其他线程可以共享读操作
排他锁:也叫独占锁 writeLock,加锁之后,阻塞其他线程读写操作(底层使用 setnx,保证同时只能有一个线程操作加锁的方法)
允许延迟一致
异步通知保证数据的最终一致性
- 消息队列:MQ 发送写数据通知,Redis 监听消息同步更新。

- Canal 中间件:基于 Canal 的异步通知,非侵入式,伪装为 MySQL 的一个从节点,监听 binlog 通知数据变更情况更新缓存。
二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言) 语句,但不包括数据查询(SELECT、SHOW)语句。

持久化
RDB
RDB 全称 Redis Database Backup file(Redis 数据备份文件),也被叫做 Redis 数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据。
- 主动备份
1 | |
- 定时备份(配置文件配置备份频率)
1 | |
执行原理
bgsave 开始时会 fork 主进程得到子进程,子进程共享主进程的内存数据。完成 fork 后读取内存数据并写入 RDB 文件。
fork 采用的是 copy-on-write 技术:
- 当主进程执行读操作时,访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

AOF
AOF 全称为 Append Only File(追加文件)。Redis 处理的每一个写命令都会记录在 AOF 文件,可以看做是命令日志文件
开启方式:AOF 默认是关闭的,需要修改 redis.conf 配置文件来开启 AOF
1 | |
AOF 的命令记录的频率也可以通过 redis.conf 文件来配:常用每秒刷盘
1 | |

因为是记录命令,AOF 文件会比 RDB 文件大的多。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才有意义。通过执行 bgrewriteaof 命令,可以让 AOF 文件执行重写功能,用最少的命令达到相同效果。

Redis 也会在触发阀值时自动去重写 AOF 文件。國值也可以在 redis.conf 中配置:
1 | |
RDB 与 AOF 对比
RDB 和 AOF 各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。

数据过期策略
概念:Redis 对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。
Redis 数据删除策略-惰性删除
惰性删除:设置该 key 过期时间后,我们不去管它,当需要该 key 时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该 key
例子
1 | |
- 优点:对 CPU 友好,只会在使用该 key 时才会进行过期检查,对于很多用不到的 key 不用浪费时间进行过期检查
- 缺点:对内存不友好,如果一个 key 已经过期,但是一直没有使用,那么该 key 就会一直存在内存中,内存永远不会释放
Redis 数据删除策略-定期删除
定期删除:每隔一段时间,我们就对一些 key 进行检查,删除里面过期的 key(从一定数量的数据库中取出一定数量的随机 key 进行检查,并删除其中的过期 key)。
定期清理有两种模式:
- SLOW 模式是定时任务,执行频率默认为 10hz,每次不超过 25ms,以通过修改配置文件 redis.conf 的 hz 选项来调整这个次数
- FAST 模式执行频率不固定,但两次间隔不低于 2ms,每次耗时不超过 1ms
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
Redis 的过期删除策略:情性删除+定期删除 两种策略进行配合使用
数据淘汰策略
概念:当 Redis 中的内存不够用时,此时在向 Redis 中添加新的 key,那么 Redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
Redis 支持 8 种不同策略来选择要删除的 key:
- noeviction:不淘汰任何 key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl: 对设置了 TTL 的 key,比较 key 的剩余 TTL 值,TTL 越小越先被淘汰。
- allkeys-random:对全体 key,随机进行淘汰。
- volatile-random:对设置了 TTL 的 key,随机进行淘汰。
- allkeys-lru:对全体 key,基于 LRU 算法进行淘汰。
- volatile-Iru: 对设置了 TTL 的 key,基于 LRU 算法进行淘汰。
- allkeys-lfu: 对全体 key,基于 LFU 算法进行淘汰。
- volatile-Ifu:对设置了 TTL 的 key,基于 LFU 算法进行淘汰。
LRU(Least Recently Used) 最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU(Least Frequently Used) 最少频率使用。会统计每个 key 的访问频率,值越小淘汰优先级越高。
数据淘汰策略-使用建议
- 优先使用 allkeys-Iru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
- 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰。
- 如果业务中有置顶的需求,可以使用volatile-Iru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
- 如果业务中有短时高频访问的数据,可以使用 allkeys-Ifu 或 volatile-lfu策略。
分布式锁
Redis 实现分布式锁主要利用 Redis 的 setnx 命令。setnx 是 SET if not exists(如果不存在,则 SET)的简写。
使用场景:定时任务、抢卷、幂等(涉及到共享数据的一切分布式/多线程操作)
1 | |

Redisson 实现的分布式锁-执行流程

- 看门狗对持有锁的线程进行以 1/3 过期时间的周期进行续期
- 未持有锁的线程重试等待获取锁
- 锁的获取、设置过期时间、释放等都基于 lua 脚本,保证执行的原子性
Redisson 实现的分布式锁-可重入

redisson 分布式锁可重入,redis 锁不可重入,基于线程 id 做标识。
Redisson 实现的分布式锁-主从一致性
RedLock(红锁):不能只在一个 redis 实例上创建锁,应该是在多个 redis 实例上创建锁 (n / 2 + 1) (一半节点),避免在一个 redis 实例上加锁。
实际很少使用红锁,缺点比较多,redis 重在 AP 思想数据最终一致性,zookeeper 的 CP 思想强调强一致性。

集群方案
在 Redis 中提供的集群方案总共有三种
- 主从复制
- 哨兵模式
- 分片集群
主从复制
单节点 Redis 的并发能力是有上限的,要进一步提高 Redis 的并发能力,就需要搭建主从集群,实现读写分离。

主从数据同步原理
主从全量同步:
ReplicationId:简称 replid,是数据集的标记,id 一致则说明是同一数据集。每一个 master 都有唯一的 replid,slave 则会继承 master 节点的 replid
offset:偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。slave 完成同步时也会记录当前同步的 offset。如果 slave 的 offset 小于 master 的 offset,说明 slave 数据落后于 master,需要更新。

主从增量同步(slave 重启或后期数据变化)

哨兵的作用
Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:
监控:Sentinel 会不断检查您的 master 和 slave 是否按预期工作自动故障恢复:如果 master 故障,Sentinel 会将一个 slave 提升为 master。当故障实例恢复后也以新的 master 为主通知:Sentinel 充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端

服务状态监控
Sentinel 基于心跳机制监测服务状态,每隔 1 秒向集群的每个实例发送 ping 命令:主观下线:如果某 sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线。客观下线:若超过指定数量(quorum)的 sentinel 都认为该实例主观下线,则该实例客观下线。quorum 值最好超过 Sentinel 实例数量的一半。

哨兵选主规则
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
- 然后判断从节点的 slave-priority 值,越小优先级越高
- 如果 slave-prority 一样,则判断 slave 节点的 offset 值,越大优先级越高
- 最后是判断 slave 节点的运行 id 大小,越小优先级越高。
Redis 集群(哨兵模式)脑裂
集群脑裂是由于主节点和从节点和 sentinel 处于不同的网络分区,使得 sentinel 没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个 master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel 会将老的主节点降为从节点,这时再从新 master 同步数据,就会导致数据丢失

通过以下配置减小数据丢失:
我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求就可以避免大量的数据丢失
1 | |

分片集群结构
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有多个master,每个master保存不同数据每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

分片集群结构-数据读写
Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽,分配到不同的实例,每个key的有效部分(如果key前面有大括号,则大括号里的就是有效部分,没有大括号key就是有效部分) 通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

Redis 单线程-快的原因
- Redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO
I/O多路复用模型
Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求
- 用户空间和内核空间
- 常见的IO模型
- 阻塞IO (Blocking IO)
- 非阻塞lO (Nonblocking IO)
- IO多路复用(IO Multiplexing)
- Redis网络模型
用户空间和内核空间
Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间
- 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源必须通过内核提供的接口来访问
- 内核空间可以执行特权命令(RingO),调用一切系统资源
Linux系统为了提高lO效率,会在用户空间和内核空间都加入缓冲区: - 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

阻塞IO

非阻塞IO

IO多路复用
是指利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的l/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
Redis网络模型
Redis通过 IO多路复用 + 事件派发 来提高网络性能,支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库
使用IO多路复用结合事件的处理器来应对多个Socket请求连接应答处理器
- 命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件
- 命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
多线程优化部分:
- 接受请求参数转换为命令
- 响应数据处理
