Redis八股文
Redis为什么这么快?
- 基于内存
- io 多路复用:一个线程可以管理多个IO网络请求,内置多个API可以监听多个IO请求
- 单线程架构
为什么是单线程而不是多线程?
- Redis是内存数据库,性能瓶颈是在内存与网络IO延迟
- 用单线程极大减少了多线程的上下文切换和锁的竞争
- 持久化机制
- 数据结构优化
Redis穿透&击穿&雪崩
穿透
当客户端请求的 key 在 缓存里找不到,而且 数据库里也没有,就会导致每次请求都绕过 Redis 直击数据库
出现的场景:
- 恶意/异常请求:爬虫、攻击脚本随机构造ID = 99999,但是Redis缓存中没有,导致数据库被频繁打醒
- 用户传参异常:用户手滑将Id = -1传入,没有击中缓存和数据库
防护措施:
- 布隆过滤器把所有合法 ID 的集合(或 hash)先“塞”进布隆过滤器;当请求进来先问布隆:“这个 key 可能存在吗?”
优点:大幅削减不可能存在的请求
缺点:有极小误判率,需要定期同步全量数据
- 缓存空值/占位符
数据库里也没有?那就把 “没有” 这个事实也缓存起来
1 | // 查库后发现null |
- 参数校验 & 黑/白名单
在到达缓存前就把“离谱”参数拦下来;或者为热点接口设置白名单
productId <= 0 直接 400;超出最大合法 ID 直接拒
击穿
数据在数据库中确实存在,海量并发来袭 + redis缓存瞬间失效 = 击穿
出现的场景:
- 热点Key正常过期 + 此时超高并发:缓存忽然下线,所有请求瞬间朝向数据库
- 热点Key被手动/程序删缓存 + 此时超高并发:误删就相当于提前过期,加上高并发来袭
- 缓存节点重启,故障:热门key失效,请求朝向数据库
- 统一的TTL导致"同秒过期",多个热点设置同一TTL
防护措施:
- 互斥锁:很多请求都想读 同一个热点数据,只让第一个线程去查库 & 回写缓存,其余等待
- 随机TTL(加随机偏移):给热点 key 的 TTL 再乘个 1 + rand(0, 0.2);失效时间“较散”
- 热点预热/主动刷新:在快过期前异步刷新缓存;或部署定时任务
雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过大挂掉
防护措施:
- 随机TTL:让过期时间分布在区间内,60s + random(0, 15s)
- 热点预热/定时续期
缓存降级
当Redis命中率降低,节点宕机,网络抖动时,系统会主动切换到“简化版”读取兜底数据
- 正常时候:Redis击中$\Longrightarrow$完整实时数据
- 轻度故障:Redis命中率低,但数据库还可以抗住$\Longrightarrow$少查表,比如要两张表联合查询,只会查其中一张表的数据
- 严重故障:数据库不能抗了$\Longrightarrow$返回兜底数据,比如默认文案【请稍后再试】
Redis支持哪几种数据类型(数据结构)?
- String
它的底层数据结构是sds
1 | // 数据结构 |
- List:消息队列,任务堆栈
- Hash:存储对象属性
- Set:标签去重
- zSet:排行榜,排序,带权队列
- Bitmap:二位图,只有两种状态;是布隆过滤器的基石
- HyperLogLog:统计“去重后的元素个数”的概率算法
- Stream:日志收集、消息队列
- GEO:地理位置
LUA脚本
Lua就是将多条指令打包成单次往返的工具,还能自带原子性
【例子】限制接口访问频率
比如某个接口 /login 最多允许 10 次/60 秒。如果在这 60 秒里被疯狂点 100 下,就要拦下来,否则服务器吃不消。
为什么要用Lua脚本?
- 原本Redis让key对应的数字值 + 1,给key设置存活时间,两步操作
- 两条命令分开发,在高并发下有可能“只加数没设过期” $\Rightarrow$失效。
- 把两步写进 Lua,Redis 会一次性执行,绝对原子(要么全成功,要么全不做),还少一次网络来回
具体步骤
首次访问的时候,设置窗口过期,同时访问次数加一
再次访问的时候判断有没有过期
- 过期:重新设置一个新的窗口,访问次数加一
- 没过期,检查是否超过次数:若超过了,则返回错误提示;若没超过,则返回数据
为什么Redis过期了,内存没释放?
- 业务层原因
过期时间被意外覆盖:键原本设了 TTL,但代码又执行了修改/覆盖操作,把 TTL 改掉或清空 → 看似“过期”,但是内存没有释放,实则计时器被重置。
- Redis内部淘汰策略原因
惰性删除 + 定期删除 机制导致延迟
惰性删除:只有客户端再次访问该键时,Redis 才检查 TTL 并真正删除;没人访问就一直躺在内存里。(对CPU友好、对内存不友好)
定期删除:Redis 每隔 100 ms 抽样扫描一部分键批量清理 → 如果抽样没命中就继续保留,直到下次轮询命中才释放。(对内存友好,对CPU不友好)
Redis大Key怎么处理?
什么是大key?
- String:value > 5MB,一次GET/SET就要搬5MB,阻塞时间循环、卡慢查询
- 集合类型:元素个数 > 1万
解决方案:
【String】
- 序列化:将对象转化为一个可存储格式如JSON,但序列化和反序列化会带来额外时间消耗
- 压缩:在序列化基础上对数据进行压缩,减少存储空间如gzip,压缩和解压缩需要计算资源
- 拆分大key:将一个大数据分割成多个小的部分,并为每个部分创建一个独立的键
【集合】
- 将数据分散存储在多个节点上,减少单个节点的瓶颈
- 可以基于范围、哈希函数或者用户信息(ID、姓名…)来进行分片
Redis事务实现原理
- 通过Multi开始一个事务
- 使用exec提交
当出错的时候不会回滚事务,单这种简单的机制已经能满足需求了
Redis怎么实现延时队列
使用ZADD

Redis怎么实现消息队列
- 业务线程 RPUSH ——> 队列累积任务
- 多个Worker线程/实例 BLPOP ——> 拿到任务干活
- 队列空时,BLPOP自动阻塞,不需要手动sleep
Redis和MySQL数据如何保证一致性?
- 先更新数据库,然后删除缓存
1 | public void update() { |
- 基于MQ实现异步删除逻辑

- 异步方案之canal
canal的原理是基于MySQL的主从同步来实现,原理如下
- master将变更数据写入bin log
- slave将master的bin log拉取到中继日志(relay log)
- slave中执行中继日志里的sql

总结
- 先更新数据库 + 再更新缓存
- 延迟双删
写库成功后立即删除缓存,再延时一小段时间再删一次缓存
解决的问题:在并发场景下,线程 A 在写库并删除缓存,线程 B 在删除前读了缓存,发现 miss,去 MySQL 中读旧数据,结果缓存里又出现脏数据。
缺点:
- 增加代码复杂度
- 时间难以精准控制
- 多余删除操作,在低并发场景下
- Canal
Redis突然变慢,原因有哪些?
- 存在bigkey:如果Redis实例中存储了 bigkey,那么在淘汰删除 bigkey 释放内存时,也会耗时比较久。应该避免存储 bigkey,降低释放内存的耗时。
- 设置了内存上限maxmemory:当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据
- 开启了内存大页:做备份时 BGSAVE fork 子进程;主进程写 1 KB 数据却需 复制整块 2 MB 大页,申请内存卡顿
- 使用了swap:操作系统将部分Redis数据换到磁盘,磁盘IO慢
- 网络带宽过载:双十一突发流量,单机带宽上限 1 Gbps 被打满
- 频繁短连接:某微服务每次操作 Redis 都 connect → set → close
Redis中pipeline的作用
redis客户端执行一条命令过程:发送命令、命令排队、命令执行、返回结果
pineline作用是将多条命令一次性发出、一次性收回结果 → 避免反复 TCP 往返
局限:
- 非原子:中间报错不会滚,已经执行的命令照样生效 —> 可以使用lua脚本解决
- 过大包会阻塞:堆太多指令会撑爆内存 / 阻塞网络 → 建议拆成多批小 Pipeline。
Redis过期删除策略
为什么要有过期删除策略?
- 节省内存
- 实现缓存自动失效
- 避免脏数据长期驻留
【策略】
- 定时删除:当设置了过期时间,Redis会在过期时间点,自动安排删除任务
- 惰性删除:当通过GET key 或访问某个 key 时,如果发现该 key 已经过期,这时候才删除它
- 定期删除:每隔一段时间,Redis会随机抽取一部分设置了TTL的key检查是否过期,若过期则删除
Redis内存淘汰策略(Redis内存满了怎么办)
(8种淘汰策略)
当内存达到 max memeory 上限时,Redis会用什么规则去“踢掉”已有的数据给新数据腾位置
- 不淘汰类(默认)
- 不删除任何数据,直接返回错误
- 基于过期时间(设置了TTL的键里挑)
- volatile-lru(Least Rencently Used):淘汰最久未使用的
- volatile-lfu(Least Frequently Used):淘汰使用次数最少的
- volatile-ttl:淘汰生存时间最短的
- volatile-random:随机淘汰
- 针对所有键
- allkeys-lru:淘汰最久未使用的
- allkeys-lfu:淘汰使用次数最少的
- allkeys-random:随机淘汰
Redis持久化
如果Redis宕机重启,通过加载缓存的数据文件,缓存的数据就可以恢复,有两种持久化的方法:①AOF日志 ②RDB快照(默认)
AOF和RDB区别
- AOF 文件的内容是操作命令
- RDB 文件的内容是二进制数据
AOF日志

将记录命令顺序追加到日志中,注意只会记录写操作命令,读操作命令不会被记录
【Redis先执行操作,再写入日志的好处】
- 避免额外的检查开销
- 不会阻塞当前写操作命令的执行
【存在的风险】
- 如果Redis没来得及存入AOF日志时宕机,会产生数据丢失的风险
- 可能会阻塞后续客户端发送给 Redis 的 写命令
【三种写回磁盘(刷盘)的策略】
- Always:每次操作命令执行完毕后,同步AOF日志写回磁盘
- Everysec:每隔一秒将缓冲区内容写入磁盘
- No:由操作系统决定何时将缓冲区数据写回磁盘
【AOF重写机制】
AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大;Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
使用重写机制后,会读取最新的name,只会存储一条数据

AOF后台重写
在出发AOF重写时,如果AOF文件大于64MB,这时要读取完所有键值对,为每一个键值对生成一条命令,再写入新的AOF文件,写完后,将原来的AOF文件替换掉,过程耗时间
所以,Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么做可以达到两个好处
- 子进程重写期间,主进程可以继续处理命令,避免阻塞
- 子进程有主进程的数据副本,不需要加锁来保证数据的安全
RDB快照
RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据
【生成方式】
- 【save】:在主线程生成RDB文件,如果写入RDB文件时间太长,会阻塞主进程
- 【bgsave】:创建一个子进程生成RDB文件,可以避免主线程的阻塞
【执行快照时,数据能被修改吗?】
- 执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的
- 关键的技术就在于写时复制技术(Copy-On-Write, COW)
- 执行bgsave时,会通过fork()创建子进程,它和父进程共享一片内存数据,当主进程修改共享数据中某一块数据时,内存页会发生复制,主进程修改复制后的内存,子进程继续把原来的数据写入RDB文件。发生了写时复制后,RDB 快照保存的是原本的内存数据
AOF、RDB的优缺点
AOF:更强持久性、更安全;但是IO开销大、恢复略慢
RDB:更快恢复、更小文件;但会丢最近一段时间的数据
Redis主从复制&哨兵
主从复制
主从服务器采用读写分离

【同步过程】

- 第一阶段:建立连接、协商同步
执行replicaof命令后,从服务器会给主服务器发送 psync 命令,包含两个参数【主服务器的runID、复制进度offset】
主服务器接收到psync命令后,会用FULLRESYNC作为响应命令返回给对方
- 第二阶段:主服务器同步数据给从服务器(全量复制)
主服务器会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。
从服务器收到 RDB 文件后,会先清空当前的数据,然后载入 RDB 文件
- 第三阶段:主服务器发送新写操作命令给从服务器
主服务器RDB文件发送完后,从服务器收到文件,丢弃所有旧数据,将RDB数据载入到内存
完成载入后,会回复一个确认消息给主服务器
主服务器将replication buffer缓冲区的命令发送给从服务器,从服务器执行命令,此时主从服务器数据就一致了
分摊主服务器的压力
主服务器耗时操作,生成RDB文件和传输RDB文件,如果有多个从服务器,主服务器会忙于使用fork()创建子进程,多个子进程可能会导致Redis无法处理请求,因此可以采取主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器。
设置某一个从服务器作为代理

增量复制
主从服务器之间网络不稳定,导致断开,此时客户端可能从【从服务器】读到旧的数据,Redis 2.8之前使用的全量复制,2.8之后使用增量复制

从服务器恢复网络后,会发送psync命令给主服务器,命令里的offset参数不是-1;主服务器收到后,用CONTINUE响应命令告诉从服务器,接下来要用增量复制;主服务将断线期间,所执行的写命令发送给从服务器,从服务器执行
哨兵
它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。
主要负责三件事
- 监控
- 选主
- 通知
①如何判断主节点故障
-
主观下线:哨兵每隔1秒会给所有主从节点发送PING命令,主从节点接收到后,会发送一个响应给哨兵,若主从节点没有发送响应就是主观下线
-
客观下线
当一个哨兵认为节点 挂掉 时,它会向其他哨兵询问:
“你们也觉得这个节点挂了吗?”
假设有3个哨兵,quorum配置是2,当2个哨兵认为下线了,就是客观下线

在哨兵集群中至少有3个哨兵节点,用于判断客观下线
② 选出哨兵leader
某个哨兵判断主节点客观下线后,会发起投票,告诉其他哨兵想成为leader
需要满足两个条件
- 拿到半数以上的赞同票
- 拿到的票数同时还需要 ≥ 哨兵配置文件中的 quorum 值
③ 由哨兵leader进行主从故障转移
- 从已下线的主节点,属下所有【从节点】中选出一个主节点,要求
过滤已经离线的从节点
过滤历史网络连接状态不好的从节点
从剩下结点中,按照优先级、复制进度、ID号进行考察选出新主节点
- 让已下线的主节点,属下所有【从节点】修改复制目标为【新主节点】
- 将【新主节点】的IP地址,信息通过通知给客户端
- 继续监视【旧主节点】,当它重新上线时,设置为【新主节点】的从节点
Redis集群模式有哪些?
- 主从复制
优点:
- 实现简单:部署和配置成本低,易于快速搭建
- 读写分离:主节点负责写,从节点负责读,提升性能
- 数据备份:从节点可作为数据备份,提高数据安全性
缺点:
- 主节点有写入压力:所有写操作在主节点
- 故障转移无法自动切换主节点
- 延迟:主从同步是异步的,可能导致数据不一致
- 哨兵模式
优点:
- 高可用:主节点宕机时,哨兵可自动选举出新的主节点
- 监控与通知:持续监控Redis节点状态
- 自动故障转移:无需手动干预,减少服务中断时间
缺点:
- 整体架构稍微复杂:比主从复制多了哨兵节点,部署维护成本高
- 可能出现脑裂现象:网络分区情况下可能出现两个主节点,导致数据不一致
- Cluster模式(数据分片)
优点:
- 横向扩展:数据分片存储在多个主节点
- 支持高并发:读写可以分配到不同节点,提升吞吐量
缺点:
- 配置和维护相对复杂:需要规划分片和槽位(哈希槽)
- 网络开销增加
Redis分布式锁
如果两个线程同时访问redis进行增删改,可能会造成数据不同步,因此需要锁
- 方案一:对使用Redis的方法加锁,可以锁住同一服务下,但是有两个服务同时对redis数据进行操作,就不行了
- 方案二:全局锁,将使用到Redis的地方全部抽成一个服务,只在这一个服务中操作Redis,使用Redis分布式锁
一个优秀的分布式锁
- 互斥性:只让一个竞争者持有锁
- 安全性:避免死锁,竞争者持有期间,若意外崩溃锁没有主动释放,持有的锁也能自动释放,后续的竞争者也可以加锁
- 对称性:同一个锁,加锁、解锁必须是同一个竞争者
- 可靠性:具有一定异常处理能力、容灾能力
实现方式
【最简版本】
通过 setnx key value 设置
set not exist:只有当键不存在的时候才能设置
setnx lock xx ; 加锁之后其他服务无法加锁
【支持过期时间版本】
防止获取到锁的服务挂断,导致无法释放锁
通过 set key value nx ex seconds 增加过期时间;nx=不存在才能设置,ex=过期时间
谁申请谁释放:给锁加一个“身份证(归属标识)”,只有锁的“主人”才有权释放。
如果任务提前完成了,需要手动释放锁,为了保证原子性操作,引入LUA

解决了互斥性、安全性、对称性
【Redisson】
API简单:提供了类似Java原生Lock的API(RLock,ReadWriteLock)
安全性高:加锁时会写入UUID标识,解锁前会检查标识是否一致,防止误删;引入看门狗机制
多种锁类型
- 公平锁
- 读写锁
- 支持RedLock
RedLock红锁
由Redis官方提出的分布式锁方法
特性:
- 安全特性:互斥访问,永远只有一个client能拿到锁
- 避免死锁:如果client挂掉,client都会释放锁,不会出现死锁情况
- 容错性:只要大部分 Redis 节点存活就可以正常提供服务
实现流程
- 客户端向多个Redis节点尝试获取锁
- 客户端获取到大多数节点上的锁(大多数指一半以上的节点)就认为获取锁成功
脑裂问题

防护措施:配合哨兵模式,保证节点主从唯一性。
网络分区:原本在一个整体网络里的多台机器,因为网络故障,被分成了几个互相不通的区域,每个区域内部还能正常通信,但区域之间互相看不见了,这就叫网络分区
保证可靠性
【主从容灾】
Redis主库实时把数据同步到从库们
哨兵24h 监控主库的心跳,发现主库down,就自动把某个从库升级为新主库,其他从库切到它
好处
- 读写不长时间中断,锁数据也能跟着存活
- 避免 “主节点宕机 —> 锁直接蒸发”的风险
【多级部署】
把同一把锁同时写进多个独立Redis
如RedLock,多个机器,通常是奇数个,达到一半以上同一加锁才算加锁成功
【看门狗机制】
设置过期时间:加锁时就带 EX/ PX,先给锁一条生命线(比如 30 s)。
**为什么要续期?**任务可能比 30 s 长 → 锁要是到点就失效,其他线程就进来了。
看门狗工作流程:
- 线程持锁成功 → 启动 看门狗定时任务。
- 每隔 TTL / 3 去 Lua 校验 + 续期,把过期时间往后推。
- 任务正常结束 ⇒ 手动 DEL 锁并停掉看门狗。
崩溃场景:若线程挂了 / 宕机 ⟹ 看门狗也停止 ⇒ 锁无法续期,过期后自动释放,下一位可以加锁。
Redis常见性能问题和解决方案?
- Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
- 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
- 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
- 尽量避免在压力较大的主库上增加从库。
- Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
- 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master ← Slave1 ← Slave2 ← Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
为什么 Redis 集群的最大槽数是 16384 个?
Redis节点发送心跳数据包时,需要将所有槽放入心跳包,如果采用16384个插槽,占用空间位2KB(16384/8);如果采用65536个插槽,占用8KB(65536/8)。8KB心跳数据有点大
Redis Cluster不太可能扩展到1000个节点,太多会导致网络堵塞。选择16384,可以确保每个主节点有足够的槽
Redis除了做缓存还可以干什么?
- 分布式锁
- 延时队列
- 限流
【例子】配合Lua限制接口访问频率
详情见【LUA脚本】
【例子】令牌桶
一个控速模型:系统按固定速率往桶里“加令牌”(比如 100 个/秒),每个请求来就拿走 N 个令牌(默认 1 个),拿得到就放行,拿不到就限流/等待。
关键参数
- rate:令牌生成速率
- capacity:桶容量
- cost:每次请求要消耗几个令牌
- now:当前时间(用Redis TIME)
流程
- 准备一个桶,在Redis中就是一个key,比如 rate:api:user,里面装令牌,最多装 capacity 个
- 每隔一段时间自动往桶里加令牌,加的速度是rate 个/秒
- 每个请求来时拿令牌,如果数量不够就拒绝
- 记录两个状态在Redis中:当前令牌数 tokens;上次加令牌的时间 lastRefillTime
- 每次请求都会先补桶再扣桶





