深浅色
Redis(背诵版)
1. 数据结构与底层编码
- string:int / embstr(小于等于 44 字节) / raw(sds)。
- list:quicklist = 双向链表 + 每节点 listpack。
- hash:listpack(小)/ hashtable(大)。
- set:intset(全整数且小)/ listpack / hashtable。
- zset:listpack(小)/ skiplist + dict(大)。
- 追问:zset 为什么同时用跳表和字典?-> 字典 O(1) 查分值,跳表 O(logN) 做范围查询,各取所长。
2. 持久化
- RDB:定时 fork 子进程生成快照,体积小、恢复快;缺点是可能丢数据,fork 时因 COW 内存可能翻倍。
- AOF:追加写命令,appendfsync 三档(always / everysec / no),默认 everysec 最多丢 1 秒;文件大、恢复慢。
- AOF 重写:fork 子进程按当前数据生成最小命令集,期间新命令写入重写缓冲再合并。
- 混合持久化(4.0+ 默认):AOF 前半是 RDB 格式、后半是增量命令,兼顾恢复速度和丢数据少。
- 追问:fork 会阻塞吗?-> 会短暂阻塞(拷贝页表),内存越大阻塞越久。
3. 过期删除与内存淘汰
- 过期删除 = 惰性删除(访问时检查)+ 定期删除(每 100ms 随机抽查 20 个,过期比例大于 25% 就继续)。
- 淘汰策略 8 种:noeviction、volatile 系与 allkeys 系的 lru / lfu / random、volatile-ttl。
- 4.0 引入 LFU,更适合热点数据;LRU 是近似算法(随机采样若干 key 淘汰)。
- 实践:纯缓存用 allkeys-lru 或 allkeys-lfu;别用 noeviction。
- 追问:为什么不用精确 LRU?-> 维护完整链表内存开销大,近似够用。
4. 缓存穿透 / 击穿 / 雪崩
- 穿透(查不存在的数据):缓存空值带短 TTL、布隆过滤器、参数校验。
- 击穿(热点 key 过期瞬间):互斥锁重建(SETNX)、逻辑过期(不设 TTL 异步刷新)、singleflight。
- 雪崩(大量 key 同时过期或 Redis 挂了):TTL 加随机抖动、多级缓存、限流降级、集群高可用。
- 追问:布隆过滤器能删元素吗?-> 标准布隆不能,要用计数布隆。
5. 分布式锁
- 加锁:SET key value NX PX 30000,value 用唯一标识(UUID)。
- 解锁:必须用 Lua 保证「判断是自己的锁 + 删除」原子。
- 续期:看门狗定时续期,避免业务没跑完锁先过期。
- Redlock 争议:需多数节点加锁成功,但依赖时钟假设,Kleppmann 认为不安全;通常单机 + 哨兵/集群就够。
- 追问:为什么不能直接 DEL?-> 可能删掉别人刚加的锁。
6. 主从 / 哨兵 / Cluster
- 主从:一主多从,异步复制,主挂了要手动切。
- 哨兵:监控 + 自动故障转移,至少 3 个且为奇数;切换期间短暂不可用。
- Cluster:16384 个槽,按 CRC16(key) 取模 16384 定位;多 key 操作要求同槽,用 hash tag
{user}:1。 - 追问:Cluster 下跨槽 mget 会怎样?-> 报 CROSSSLOT 错误。
7. 大 key / 热 key
- 发现:redis-cli --bigkeys、--hotkeys(需 LFU)、memory usage key、slowlog。
- 大 key 危害:删除/过期时阻塞主线程、网络传输慢、集群倾斜。
- 治理:拆分分桶、用 unlink 异步删、hscan 分批读、value 控制在 10KB 内。
- 热 key 治理:本地缓存、读写分离、key 打散加随机后缀、限流。
- 追问:为什么 DEL 大 key 会阻塞?-> 单线程同步释放内存,unlink 交给后台线程。
8. 管道 / Lua / 事务
- Pipeline:批量发命令减少 RTT,但不保证原子,中间可能插入别的命令。
- Lua:服务端原子执行整段脚本,适合复杂逻辑和分布式锁;脚本别太长(阻塞主线程)。
- 事务:MULTI / EXEC,不支持回滚(运行时错误继续执行),和 MySQL 事务不是一个概念。
- 结论:要原子用 Lua,要省 RTT 用 Pipeline,事务用得少。
- 追问:WATCH 的作用?-> 乐观锁,EXEC 前 key 被改过就放弃执行。
9. 缓存与数据库一致性
- 推荐 Cache Aside:先更新 DB,再删除缓存,配合延迟双删或 binlog 订阅(Canal)异步删。
- 为什么删而不是更新?-> 更新有并发写覆盖问题,删除更简单(懒加载)。
- 为什么先 DB 后缓存?-> 先删缓存再更新 DB,在「删完还没更新」的窗口被读回旧值写进缓存,会长期脏。
- 追问:能强一致吗?-> 不能,只能最终一致;要强一致就别用缓存。
10. 单线程为什么快 / 6.0 多线程
- 原因:纯内存操作、避免锁和上下文切换、I/O 多路复用(epoll)、高效数据结构。
- 真正的瓶颈是网络 I/O 和协议解析,不是 CPU 计算。
- 6.0 多线程:只把网络 I/O 读写和协议解析放到 IO 线程,命令执行仍是单线程,保证原子性。
- 追问:单线程下慢命令的后果?-> 阻塞所有请求,所以禁用 keys 全量扫描、大 key 操作、长 Lua。