7217 字
36 分钟
Redis 八股学习日记

Redis 八股学习日记#

记录日期:2026-08-31 学习方式:先按知识框架过一遍原理,每节末尾附「可能的考法」,最后用自测清单检验掌握程度。

Redis 是基于内存的高性能键值存储系统,核心竞争力是”内存操作 + 单线程事件循环 + IO 多路复用 + 贴合场景的数据结构”。本篇沿着 数据结构 → 持久化 → 高可用 → 缓存问题 → 事务/Lua → 应用场景 的主线展开。

1. 开胃菜:Redis 为什么快(总纲)#

面试官很喜欢拿这个问题开场,答案是全局性的,先记住四点:

  1. 纯内存操作:读写都在内存中完成,这是数量级上的优势。
  2. IO 多路复用 + 单线程事件循环:基于 epoll(Linux),单线程处理成千上万连接,没有线程上下文切换和锁竞争开销。
  3. 高效的数据结构:SDS、跳表、listpack/ziplist、intset 等,都是为内存和特定操作优化的结构。
  4. 单线程指”命令执行”单线程:Redis 6.0 引入多线程,但只用于网络 IO 读写和协议解析,命令本身仍然串行执行,所以不需要考虑并发竞争。

追问点:单线程为什么还这么快?→ 内存 + epoll + 无锁。单线程有什么好处?→ 实现简单、可维护、天然无并发问题。


2. 数据结构与底层实现#

Redis 对外提供五种基本类型(String、List、Hash、Set、ZSet),每种类型会根据数据量大小自动选择内部编码,小数据用省内存的紧凑结构,大数据用保性能的常规结构——本质是时间与空间的动态平衡。

先上一张总表(这张表本身就是考点,建议默画):

类型小数据编码大数据编码典型场景
Stringint / embstrraw缓存对象、计数器、分布式锁
Listquicklist(内部节点为 ziplist,7.0 起为 listpack)同左消息队列、最新列表
Hashziplist(7.0 起 listpack)hashtable对象属性、购物车
Setintsethashtable标签、去重、共同关注
ZSetziplist(7.0 起 listpack)skiplist + dict排行榜、延时队列

2.1 String#

底层实现是 SDS(简单动态字符串),相对 C 原生字符串的优势:

  • O(1) 取长度:SDS header 里记录了 len,STRLEN 不用遍历。
  • 二进制安全:以 len 判断结尾而不是 \0,可以存图片、序列化对象等任意二进制。
  • 空间预分配 + 惰性释放:扩容时多分配一些空间(新长度 < 1MB 时翻倍,≥ 1MB 时每次多给 1MB),减少连续修改时的内存重分配;缩短时不立即回收,留待下次用。

三种编码:

  • int:可以用 long 表示的整数直接存值。
  • embstr:短字符串(≤ 44 字节),redisObject 对象头和 SDS 一次分配、连续存放,对缓存友好;但只读,修改后会转成 raw。
  • raw:长字符串,对象头和 SDS 两次分配。

44 怎么来的:jemalloc 按 64 字节为一个分配单元,减去 16 字节对象头和 SDS 头部,剩下的空间恰好装 44 字节字符串。

应用:缓存 JSON 对象、INCR 系列计数器、SET NX 分布式锁、全局唯一 ID。

2.2 List#

底层实现是 quicklist(Redis 3.2 引入):一个双向链表,每个节点是一个 ziplist(Redis 7.0 起节点改为 listpack)。

设计动机是取两者之长:

  • 双向链表:两端插入删除 O(1),但每个元素带前后指针,内存开销大、缓存不友好。
  • ziplist:连续内存、极其紧凑,但太大时插入删除要整体挪动,还有”连锁更新”风险。
  • quicklist:把大 ziplist 切成多个小节点串起来,单节点大小由 list-max-ziplist-size 控制,内存利用率和访问效率折中。

特点与应用:支持双端操作(LPUSH/RPOP/LPUSH+BRPOP 阻塞队列),适合简单消息队列、朋友圈时间线、最新 N 条列表(LPUSH + LTRIM 截断)。

2.3 Hash#

底层实现按阈值二选一:

  • 字段少且值短时用 ziplist(7.0 起 listpack):连续内存,按 field-value 紧凑排列,省空间。
  • 超过阈值转为 hashtable:真正的散列表,O(1) 读写。

阈值(⚠️ 易错点,别背混):

  • hash-max-ziplist-entries 128:字段个数超过 128 转表(不是 512,512 是 Set 的 intset 阈值)。
  • hash-max-ziplist-value 64:任一 value 超过 64 字节转表。

加分点:hashtable 的 rehash 是渐进式的——字典持有 ht[0]、ht[1] 两张表,扩容/缩容时新数据写 ht[1],每次增删改顺带迁移一小批旧桶,避免一次性 rehash 卡顿;期间查询两张表都查。

应用:购物车(用户id → {商品id: 数量})、对象属性存储。与”String 存整个 JSON”相比,Hash 可以局部读写某个字段,不用整个取出反序列化。

2.4 Set#

底层实现:

  • 元素全为整数且个数 ≤ set-max-intset-entries(默认 512)时用 intset:有序整数数组,二分查找,内存极省;数组只升级(int16→int32→int64)不降级。
  • 否则用 hashtable(value 全为 NULL 的字典)。

特点:无序、去重,支持交并差集合运算——共同关注(SINTER)、推荐(SDIFF)、抽奖去重(SRANDMEMBER/SPOP)、点赞/投票去重。

2.5 ZSet(有序集合)#

底层实现:

  • 数据量小时用 ziplist(7.0 起 listpack):member 和 score 紧凑存放,按 score 有序。
  • 超过阈值(zset-max-ziplist-entries 128、zset-max-ziplist-value 64)转为 skiplist + dict 双结构:
    • skiplist 负责”有序”:按 score 组织,范围查询(ZRANGE/ZRANGEBYSCORE)平均 O(logN)。
    • dict 负责”单点查询”:member → score 映射,ZSCORE O(1)。
    • 两个结构通过指针共享同一份数据,不重复存储,是典型的空间换时间。

跳跃表原理:多层有序链表,每个节点插入时随机决定层高(1~32 层,晋升概率 0.25),高层是抽出来的索引。查找从最高层往右走、走不动就下沉,平均 O(logN);插入删除只改指针,不用像平衡树那样旋转。

经典追问:为什么用跳表不用红黑树/AVL?

  1. 实现简单得多,代码量小、好维护(antirez 的原话风格理由)。
  2. 范围查询天然友好:定位到起点后沿底层链表顺序遍历即可;平衡树需要中序遍历回旋。
  3. 插入删除只调整前后指针,无需旋转再平衡。
  4. 内存可控:平均每节点约 1.33 个指针,并不算贵。

应用:排行榜、延时队列(score 存到期时间戳,轮询 ZRANGEBYSCORE 取到期任务)。

2.6 其他高级结构#

  • HyperLogLog:基数统计,固定约 12KB 内存,标准误差约 0.81%,命令 PFADD/PFCOUNT/PFMERGE。原理是概率估计(伯努利试验 + 分桶调和平均),适合”网页 UV”这种允许小误差的海量去重,比 Set 动辄几 MB 省太多。
  • Geo:地理坐标存储,底层基于 ZSet(geohash 编码成 52 位整数当 score),支持 GEOADD、GEOSEARCH 查附近、算距离。附近的人/附近门店用它。
  • Bitmaps:String 上的位操作(SETBIT/BITCOUNT),365 天签到记录只占约 46 字节,还常用于在线状态、日活统计(按天分 key 或按用户分 key 两种建模)。
  • Stream:Redis 5.0 引入的持久化消息队列,支持消费者组、ACK 确认、消息回溯,详见 8.2。

补充一个编码演进的考点:Redis 7.0 用 listpack 全面替换 ziplist(Hash/ZSet/quicklist 节点)。ziplist 每个 entry 记录”前一个 entry 的长度”,前项变长就可能引发后面所有 entry 级联更新(连锁更新),最坏 O(N²);listpack 每个 entry 只记录自己的长度,从根上消除了连锁更新。回答编码问题时带上这句很加分。

📌 第 2 节可能的考法

  • 五种数据类型分别适用什么场景?底层数据结构是什么?(总表)
  • SDS 和 C 字符串的区别?embstr 和 raw 的分界线?为什么是 44?
  • ziplist 和 hashtable 的转换阈值?什么是渐进式 rehash?
  • 跳表的查找过程?为什么 ZSet 用跳表不用红黑树?
  • ziplist 有什么缺陷?listpack 做了什么改进?(7.0 加分点)
  • HyperLogLog 为什么 12KB 就能统计亿级 UV?

3. 持久化#

Redis 数据在内存里,持久化解决的是”重启后数据还在”。三种方案:RDB、AOF、混合。

3.1 RDB(快照)#

原理:主进程 fork 一个子进程,子进程把fork 那一刻的内存数据写成紧凑的二进制 RDB 文件,写完后原子替换旧文件。关键在写时复制(COW):fork 后父子进程共享物理内存页,主进程继续接写请求,只有被修改的页才会复制一份——所以子进程看到的永远是 fork 瞬间的快照,且主进程大部分时间不被阻塞。

触发方式:

  • 手动:SAVE(主进程执行,会阻塞所有请求,生产禁用)/ BGSAVE(fork 子进程,后台执行)。
  • 自动:配置 save 900 1(900 秒内至少 1 次修改)这类条件触发。
  • 其他场景:主从全量复制时主库也会 BGSAVE;正常 shutdown 时执行。

优点:文件紧凑、恢复速度快(直接加载二进制),适合定时备份容灾。 缺点:两次快照之间的数据会丢;fork 虽快但内存大/页表大时仍可能造成百毫秒级停顿;COW 极端情况下(快照期间所有页都被改写)内存占用接近翻倍。

3.2 AOF(追加日志)#

原理:把每条写命令追加到 AOF 缓冲区,按策略刷入 AOF 文件;重启时重放命令恢复数据。

三种刷盘策略(appendfsync):

策略行为数据安全性能
always每条命令都 fsync最多丢 1 条命令最差
everysec(默认)后台线程每秒 fsync最多丢 1 秒折中,推荐
no交给操作系统(约 30 秒)不可控最好

重写机制:AOF 记录的是命令,INCR 100 次 会留下 100 条记录,文件无限膨胀。BGREWRITEAOF(或配置 auto-aof-rewrite-percentage/-min-size 自动触发)同样 fork 子进程,根据当前内存状态生成等价的最小命令集写入新文件;重写期间的新命令会同时写入 AOF 缓冲和重写缓冲,最后追加到新文件。Redis 7.0 起改为 Multi-Part AOF:manifest 文件 + 多个 base/incr 分片,重写只追加增量,不再需要大缓冲(加分点)。

3.3 混合持久化(Redis 4.0+)#

aof-use-rdb-preamble yes(5.0 起默认开启)。AOF 重写时,子进程先把当前数据以 RDB 格式写入 AOF 文件开头,之后的增量继续用 AOF 命令格式追加。重启恢复时:先快速加载 RDB 前缀(快),再重放少量增量命令(全),兼顾恢复速度与数据完整性,是生产环境的常见选择。

RDB vs AOF 对比:

维度RDBAOF
文件内容二进制紧凑文本命令,体积大
恢复速度快慢(逐条重放)
数据安全性丢两次快照间的数据最多丢 1 秒(everysec)
性能影响fork 瞬时开销always 影响大,everysec 小
加载优先级低(两者都有时先加载 AOF,因为更完整)高

📌 第 3 节可能的考法

  • RDB 和 AOF 的原理、优缺点?生产怎么选?→ 一般混合持久化。
  • fork 之后子进程写的是哪个时刻的数据?写时复制过程讲一遍。
  • SAVE 和 BGSAVE 的区别?什么命令会隐式触发 BGSAVE?(主从全量复制)
  • AOF 三种刷盘策略怎么选?为什么默认 everysec?
  • AOF 重写为什么能省空间?重写期间的新写入会不会丢?
  • 实例重启时先加载 RDB 还是 AOF?AOF 文件损坏怎么办?(redis-check-aof —fix)

4. 高可用与分布式#

4.1 主从复制#

一主多从,从库默认只读,实现读写分离 + 数据热备。

  • 全量复制(首次连接):从库发 PSYNC → 主库 BGSAVE 生成 RDB 发给从库(从库清空旧数据后加载)→ 同步期间主库把新写命令记入 replication buffer → RDB 加载完后补发增量。(加分:repl-diskless-sync yes 开启无盘复制,RDB 不落盘直接走网络发给从库,适合磁盘慢/带宽好的场景。)
  • 增量复制(网络闪断重连):从库带着 replid + offset 请求 PSYNC;主库维护 replication backlog(环形缓冲区,默认 1MB),若 offset 还在缓冲区范围内就只补发差量,避免全量;否则退化为全量。
  • 注意:命令传播是异步的——主库写完不等从库确认,主库宕机可能丢最新数据(可用 min-replicas-to-write 缓解)。级联复制(从库再挂从库)可分摊主库压力。

4.2 哨兵(Sentinel)#

主从解决了备份但不解决”主库挂了没人管”。哨兵本身不存数据,职责是监控、通知、自动故障转移、配置中心(客户端通过哨兵拿当前主库地址)。

工作流程:

  1. 主观下线(SDOWN):单个哨兵 ping 主库,超过 down-after-milliseconds 无响应,该哨兵”认为”主库下线。
  2. 客观下线(ODOWN):认定主观下线的哨兵数 ≥ quorum,才判定主库真的下线——防止个别哨兵网络抖动误判。
  3. 选举 leader:哨兵之间按 Raft 思想选举(先到先得拉票、需多数派同意),由 leader 执行故障转移。
  4. 故障转移:挑选新主(依次比较 replica-priority 优先级 → 复制 offset 最大 → runid 最小)→ 其余从库改挂新主 → 通过发布订阅通知客户端新主地址 → 旧主恢复后降级为从。

追问:哨兵为什么至少部署 3 个且为奇数?→ leader 选举需要多数派,2 个哨兵挂 1 个就无法选举;奇数节省资源。quorum 和多数派不是一个东西:quorum 只用于判定客观下线,选举必须过半。

4.3 集群(Cluster)#

数据量/写入量超过单机上限时用 Cluster 做分片。

  • 数据分片:预设 16384 个哈希槽,slot = CRC16(key) % 16384,每个主节点负责一段槽。key 按槽分布在不同节点,容量和写吞吐水平扩展。
  • 为什么是 16384(高频追问):节点间心跳要携带自己负责的槽位图,16384 个槽只需 2KB bitmap,65536 就要 8KB,太重;且官方建议集群规模不超过 1000 节点,16384 个槽绰绰有余。
  • hash tag:{user1000}:follow 和 {user1000}:fans 只对 {} 内内容算槽,强制落在同一节点——这是集群下多 key 操作的前提。
  • 通信:Gossip 协议(PING/PONG/MEET)节点间扩散状态,无中心架构,去中心化自组织。
  • 请求路由:客户端可请求任意节点,key 不归它管时返回 MOVED 重定向(槽已固定归属,smart client 会本地缓存槽位表);迁移中则返回 ASK 临时重定向。
  • 故障转移:每个主节点挂从节点;某主被过半主节点判定下线后,其从节点发起选举(Raft 思想),胜者升为新主。
  • 限制:多 key 命令/事务/Lua 要求所有 key 在同一槽;只支持 db0。

三种方案对比:

维度主从哨兵集群
解决什么热备、读写分离主从 + 自动故障转移分片容量扩展 + 高可用
数据分片✗✗✓(16384 槽)
自动故障转移✗(手工)✓(哨兵执行)✓(节点自选)
客户端复杂度低需先问哨兵需支持 MOVED/ASK、缓存槽表

📌 第 4 节可能的考法

  • 全量复制和增量复制的触发条件与流程?replication backlog 的作用?
  • 主从复制是同步还是异步?会丢数据吗?
  • 主观下线和客观下线的区别?quorum 是干嘛的?
  • 哨兵怎么选出 leader?新主是怎么挑的?
  • 为什么槽是 16384 个?key 怎么定位到节点?
  • MOVED 和 ASK 重定向的区别?hash tag 用在什么场景?

5. 缓存问题#

缓存三兄弟先对比着记(考试就考”现象一句话 + 方案三条”):

问题触发条件一句话本质
穿透查询不存在的数据缓存和 DB 都没有,缓存永远无法生效
击穿单个热点 key 过期瞬间大量并发同时打到 DB 重建缓存
雪崩大量 key 同时过期或 Redis 宕机缓存整体失效,流量洪峰直灌 DB

5.1 缓存穿透#

方案:

  1. 布隆过滤器:缓存前置一层,“说不存在就一定不存在,说存在可能误判”,把对不存在 key 的请求直接拦掉。原理:bit 数组 + k 个哈希函数,插入置位、查询看位是否全 1。缺陷:有误判率、不支持删除(要删得用计数布隆/布谷鸟过滤器)。
  2. 缓存空对象:查 DB 确认不存在后也写入缓存(value 为 null,TTL 设短如 60s),挡住后续重复查询。
  3. 兜底:接口层参数合法性校验(id ≤ 0 直接拒)、限流、风控黑名单。

5.2 缓存击穿#

方案(二选一,考对比):

  1. 互斥锁(SETNX):缓存失效时只允许一个线程抢到锁去查 DB 重建缓存,其余线程等待重试。保证一致性但牺牲吞吐,有死锁风险要设锁超时。
  2. 逻辑过期:key 不设物理 TTL,把过期时间存在 value 里;读到已”逻辑过期”的数据时,由异步线程去重建,其他线程先返回旧值。不等待、可用性好,但牺牲短暂一致性。
  3. 也可以热点 key 设置不过期 + 后台定时刷新。

5.3 缓存雪崩#

方案(按两种成因对症下药):

  • 大量 key 同时过期 → TTL 加随机值打散失效时间。
  • Redis 宕机 → 哨兵/集群保证高可用,避免单点。
  • 无论如何 → 熔断、限流、降级保护 DB;再加多级缓存(本地 Caffeine + Redis)削峰。

5.4 缓存与数据库一致性(高频!)#

结论先行:常用方案是 Cache Aside(旁路缓存)——读:命中返回,未命中查 DB 回填;写:先更新 DB,再删除缓存,只能做到最终一致。

要点:

  • 为什么”删缓存”而不是”更新缓存”?并发写时更新缓存的先后顺序无法保证,容易把旧值写回;且缓存值往往是懒计算的,删除后按需重建更省。
  • 为什么”先更 DB 再删缓存”?反过来(先删缓存再更 DB)在”删除后、更新前”这个窗口,读请求会把旧值查出来回填缓存,脏数据驻留概率更大。先更 DB 再删缓存只剩一个极小概率的并发坏case(读请求先查缓存未命中、读旧 DB,回填前写请求已完成更新+删除——需要读比写还慢才发生)。
  • 延迟双删:写请求先删缓存 → 更新 DB → 休眠几百毫秒再删一次,应对”主从延迟导致读从库旧值回填”的场景。
  • 更彻底的做法:订阅 binlog(如 Canal)异步删缓存,业务代码零侵入,一致性和可用性都更好。

📌 第 5 节可能的考法

  • 穿透/击穿/雪崩分别怎么发生、怎么解决?(先答对比表再展开)
  • 布隆过滤器的原理?误判是怎么回事?为什么不能删除元素?
  • 互斥锁方案和逻辑过期方案的取舍?
  • 为什么删缓存而不是更新缓存?为什么先操作数据库?
  • 延迟双删解决什么问题?能做到强一致吗?(不能,强一致要加锁或别用缓存)

6. 过期删除与内存淘汰(易漏,必背!)#

这一节内容不多,但出现频率极高,很多人只背了缓存三问却漏了它。

6.1 过期 key 怎么删#

两种策略配合使用:

  • 惰性删除:访问 key 时检查过期才删。对 CPU 友好,但冷 key 过期后一直占内存。
  • 定期删除:默认每秒 10 次(由 hz 控制),每次从带 TTL 的过期字典里随机抽 20 个 key,删除其中已过期的;若过期比例超过 25% 就重复一轮,直到比例降下来或时间用完。是前者的兜底。

6.2 内存满了怎么淘汰(maxmemory-policy)#

内存达到 maxmemory 后,写命令触发淘汰,共 8 种策略:

  • noeviction(默认):不淘汰,写请求报错。
  • allkeys-lru / volatile-lru:按 LRU 淘汰全部 key / 仅淘汰设置了过期时间的 key。
  • allkeys-lfu / volatile-lfu(4.0+):按 LFU(访问频率)淘汰。
  • allkeys-random / volatile-random:随机淘汰。
  • volatile-ttl:剩余存活时间越短越先淘汰。

关键加分点:

  • Redis 的 LRU 是近似 LRU:随机采样(默认 maxmemory-samples 5)淘汰其中最久未访问的,而不是维护全局双向链表——省掉每 key 的链表指针内存和移动开销。
  • LFU 用对数计数器 + 时间衰减(lfu-log-factor、lfu-decay-time),避免”历史热点”永远赖着不走,比 LRU 更适合缓存场景。

📌 第 6 节可能的考法

  • 过期 key 是到点就被删吗?(惰性 + 定期)
  • 定期删除每次删多少?25% 这个数字的意义?
  • 8 种淘汰策略背得出来吗?allkeys 和 volatile 前缀的区别?
  • Redis 的 LRU 和标准 LRU 有什么不同?为什么要近似?

7. 事务与 Lua#

7.1 事务(MULTI/EXEC)#

流程:MULTI 开启 → 后续命令依次入队(返回 QUEUED)→ EXEC 一次性顺序执行,DISCARD 放弃。

三个特性要分清:

  • 不保证原子性:入队阶段的语法错误会导致整个事务拒绝执行;但 EXEC 后的运行时错误(比如对 String 执行 LPUSH)只让那一条失败,其余命令照常执行,没有回滚。
  • 不保证持久性(事务层面):是否落盘取决于持久化配置。
  • 保证隔离性:命令执行是单线程的,EXEC 执行期间不会插入其他客户端的命令。

追问:为什么 Redis 事务不支持回滚?官方回答:错误只应来自编程错误(语法/类型),生产环境不该出现;支持回滚不会带来好处,反而让简单高效的实现变复杂。

WATCH 乐观锁:EXEC 前若 WATCH 的 key 被其他客户端修改,事务返回 nil 拒绝执行,实现 CAS 语义,典型用法是”读-改-写”(如扣减余额:WATCH balance → 读 → MULTI 入队新值 → EXEC,失败则重试)。

7.2 Lua 脚本#

  • 原子性:Redis 把整个脚本当作一条命令执行,执行期间不会插入其他命令,天然避免竞争条件。
  • 减少网络往返:一段逻辑一次发过去,比多次请求快。
  • 典型应用:分布式锁释放(GET 比对 + DEL 必须原子)、限流、库存扣减这类”读-判-写”复合操作。
  • 注意事项:脚本执行期间阻塞整个 Redis,必须短小高效;Cluster 下脚本涉及的所有 key 必须在同槽(hash tag)。

📌 第 7 节可能的考法

  • Redis 事务和 MySQL 事务的 ACID 对比?(原子性、持久性不保证)
  • 事务中某条命令失败,其他命令还执行吗?为什么不回滚?
  • WATCH 实现了什么语义?举个例子。
  • Lua 脚本的原子性是怎么保证的?有什么坑?

8. 应用场景#

8.1 分布式锁#

演进线(面试按这条线讲):

  1. SETNX + EXPIRE 两条命令:中间宕机就死锁 →
  2. SET key value NX EX seconds 一条原子命令解决(不存在才设置 + 过期时间一次完成)。
  3. value 存唯一标识(UUID + 线程 ID):防止 A 的业务超时导致锁过期后,B 拿到锁,A 执行完直接把 B 的锁 DEL 掉(误删)。
  4. 释放锁用 Lua 脚本:GET == 自己的标识 才 DEL——“判断 + 删除”必须原子,否则判断完锁恰好过期被别人抢到还是会误删。
  5. 遗留问题:业务没执行完锁先过期 → Redisson 看门狗自动续期(默认锁 30s,每 1/3 即 10 秒检查一次,业务没完成就续回 30s);主从切换瞬间锁丢失 → Redlock(向多个独立主节点加锁,过半成功才算持有),但 Redlock 有学术争议(Martin Kleppmann 与 antirez 之争,强一致场景建议上 ZooKeeper/etcd),一般业务单实例 + 看门狗足够。

Redisson 额外能力:可重入锁(Hash 结构存线程标识 + 重入次数)、公平锁、读写锁、联锁、RedLock。

8.2 消息队列#

方案持久化ACK消费者组适用
Pub/Sub✗✗✗低可靠广播通知,断线即丢
List(LPUSH+BRPOP)✓✗✗简单阻塞队列,消费即删
Stream(5.0+)✓✓(XACK)✓正经消息队列

Pub/Sub:发布订阅模式,消息不落盘、无确认,消费者断开期间的消息永久丢失,慢消费者还会被缓冲区超时踢掉。

Stream:类似 Kafka 的设计——XADD 写入(ID 为 时间戳-序号,可按 ID 回溯重放)、XREADGROUP 消费者组分摊消费、XACK 确认;未确认消息进入 PEL(Pending Entries List),可用 XPENDING 查看、XCLAIM 转移给别的消费者处理”死消费者”遗留消息。但它是内存队列,容量与可靠性仍不如专业 MQ。

8.3 计数器与排行榜#

  • 计数器:INCR/INCRBY 原子自增,点赞数、阅读量、接口调用配额。
  • 排行榜:ZADD/ZINCRBY 更新分数 → ZREVRANGE 取 Top N → ZREVRANK 查某人排名 → ZRANGEBYSCORE 取分数段。同分排序技巧:score 编码为 分数 * 2^32 - 时间戳(时间早的排前)。
  • 分布式 Session:登录态存 Redis(token → 用户信息),多实例共享,天然支持水平扩容;Spring 有 Spring Session 无缝接入。

8.4 其他#

  • 限流:
    • 固定窗口:INCR + 首次设置 EXPIRE,简单但有临界突刺(两窗口边界可能放过 2 倍流量)。
    • 滑动窗口:Lua + ZSET(member 存请求唯一 ID,score 存时间戳;先 ZREMRANGEBYSCORE 清理窗口外,再 ZCARD 判断是否超限)。
    • 令牌桶:Lua 定时补令牌,允许突发流量。
  • 布隆过滤器:RedisBloom 模块(BF.ADD/BF.EXISTS/BF.RESERVE 可指定误判率),防缓存穿透、推荐去重、爬虫 URL 去重。

📌 第 8 节可能的考法

  • 手写一个不会误删的分布式锁(命令级别)。
  • 锁过期了业务还没执行完怎么办?看门狗的默认参数?
  • 为什么释放锁必须用 Lua?Redlock 了解吗,有什么争议?
  • Pub/Sub、List、Stream 三者做消息队列的取舍?Stream 怎么保证消息不丢?(PEL + XACK)
  • 用 Redis 设计一个排行榜/签到系统/限流器(场景设计题,按上面方案套)

9. 高频面试题自测清单#

检验掌握程度,每题先自己说 30 秒再看要点(覆盖全篇,按出现频率排序):

  1. Redis 为什么快? 内存 + 单线程事件循环 + epoll 多路复用 + 高效数据结构;6.0 多线程仅做网络 IO。
  2. 五种结构的底层编码与应用场景? 背第 2 节总表。
  3. SDS 相比 C 字符串的优势? O(1) 长度、二进制安全、预分配/惰性释放。
  4. 跳表原理?为什么不用红黑树? 多层索引平均 O(logN);实现简单、范围查询友好、免旋转。
  5. ZSet 为什么是 skiplist + dict 双结构? 跳表管范围查,字典管 O(1) 单点查,空间换时间。
  6. RDB/AOF 原理与取舍?混合持久化? 快照恢复快丢数据多、日志丢得少恢复慢;重写时 RDB 头 + AOF 尾。
  7. fork + 写时复制的过程? 子进程写 fork 瞬间快照,父进程改页才复制,极端内存 ×2。
  8. AOF 三种刷盘策略? always / everysec(默认)/ no 的安全-性能折中。
  9. 全量/增量复制流程?backlog 作用? PSYNC + replid/offset,断连窗口小则部分重同步。
  10. 哨兵的工作流程? 主观下线 → quorum 客观下线 → 选 leader → 挑新主(优先级→offset→runid)。
  11. 为什么 16384 个槽? 心跳槽位图 2KB 足够 + 节点规模 ≤1000;CRC16 % 16384 定位,hash tag 同槽。
  12. 穿透/击穿/雪崩? 对比表 + 各自方案;一致性用 Cache Aside + 删缓存 + 延迟双删/binlog。
  13. 过期 key 怎么删? 惰性 + 定期(抽 20 个、25% 阈值)。
  14. 8 种淘汰策略?近似 LRU / LFU? allkeys/volatile × lru/lfu/random + volatile-ttl + noeviction;采样近似省内存。
  15. Redis 事务保证原子性吗? 不保证,运行时错误不回滚;WATCH 是乐观锁 CAS。
  16. Lua 为什么原子? 整个脚本算一条命令,单线程内执行不插队;注意别写长脚本。
  17. 分布式锁完整方案? SET NX EX + 唯一 value + Lua 释放 + Redisson 看门狗/可重入/RedLock。
  18. Stream 相比 Pub/Sub 的优势? 持久化、消费者组、ACK/PEL、按 ID 回溯。

10. 收尾总结#

Redis 的高性能来自”内存 + 单线程 + 好结构”,工程上的考题则集中在四条线:底层编码的取舍逻辑、持久化的安全-性能权衡、高可用三级方案的演进、缓存一致性/三问的方案对比。答题套路都是”先给结论/对比表,再讲原理,最后补一个加分点(listpack、渐进式 rehash、无盘复制、近似 LRU、Redlock 争议)“。理解每个设计背后的权衡,比背结论重要得多。

Redis 八股学习日记
https://emiblog.vercel.app/posts/redis-study-diary/
作者
emicyx
发布于
2026-08-31
许可协议
CC BY-NC-SA 4.0