数据结构 · 缓存三大问题 · 持久化 · 集群 · 分布式锁
| 类型 | 底层编码 | 典型场景 | 停车场类比 |
|---|---|---|---|
| String | int / embstr / raw | 缓存、计数器、分布式锁、Session | 停车场的临时寄存柜 |
| List | quicklist (ziplist + linkedlist) | 消息队列、最新列表、栈/队列 | 排队取车的顺序列表 |
| Hash | ziplist / hashtable | 对象存储(用户信息、商品详情) | 停车场不同类型的车位(VIP/普通/临时) |
| Set | intset / hashtable | 去重、标签、共同好友、抽奖 | 今日会员ID集合(自动去重) |
| ZSet | ziplist / skiplist + hashtable | 排行榜、延迟队列、带权重的集合 | 停车场车位使用率排行榜 |
| HyperLogLog | 稀疏/密集编码 | UV 统计(基数估算,0.81%误差) | 今天大约来了多少不同车主 |
| Bitmap | String 的位操作 | 签到、在线状态、布隆过滤器 | 365天的签到记录(1位=1天) |
| Geo | ZSet + GeoHash | 附近的人、门店定位 | 停车场周边地图坐标 |
• 范围查询更优:跳表范围查询 O(logN + M),红黑树需要中序遍历
• 实现简单:跳表代码量远小于红黑树,便于维护
• 内存灵活:跳表可通过调整概率控制空间/时间平衡
• 并发友好:跳表更容易实现并发(虽然 Redis 单线程不需要)
面试说出"跳表范围查询更优 + 实现简单"就能拿到基本分。主动提一句"Redis 作者 antirez 亲自解释过"更加分。
是什么:查询一个数据库中也不存在的数据,缓存永远 miss,请求全部打到 DB
停车场:有人反复问"有没有特殊车位(如充电桩车位)"——车位表上没有登记,管理员每次都要去现场查看
解决方案:
1. 布隆过滤器:在缓存前加一层,不存在的 key 直接拦截
2. 缓存空值:查不到也缓存 null,设短 TTL(如 60s)
3. 参数校验:非法请求直接拦截
是什么:一个热点 key 过期的瞬间,大量并发请求同时打到 DB
停车场:停车场入口的车牌识别系统突然故障,10辆车同时涌向入口
解决方案:
1. 互斥锁:只允许一个线程查 DB 并回填缓存,其他等
2. 逻辑过期:不设 TTL,在 value 中存逻辑过期时间,过期后异步更新
3. 热点 key 永不过期(后台定时刷新)
是什么:大量 key 同时过期,或 Redis 宕机,请求全打到 DB
停车场:入口系统全面宕机,所有车都涌向同一个入口
解决方案:
1. TTL 加随机值:避免大量 key 同时过期
2. Redis 集群:高可用,避免单点宕机
3. 多级缓存:本地缓存 (Caffeine) + Redis
4. 限流降级:DB 扛不住就限流
穿透:数据本来就不存在 → 布隆过滤器 / 缓存空值
击穿:一个热点 key 过期 → 互斥锁 / 逻辑过期
雪崩:大量 key 同时过期 → 随机 TTL + 集群
记忆口诀:穿=没有的数据,击=一个热点挂了,雪=大面积崩了
| 维度 | RDB (快照) | AOF (追加日志) |
|---|---|---|
| 原理 | 定时把内存数据全量快照写入磁盘 | 每条写命令追加到日志文件 |
| 文件 | dump.rdb(二进制,紧凑) | appendonly.aof(文本,可读) |
| 恢复速度 | 快(直接加载二进制) | 慢(重放所有写命令) |
| 数据安全 | 可能丢失最后一次快照后的数据 | 最多丢 1 秒(everysec 策略) |
| 文件大小 | 小(二进制压缩) | 大(但可 BGREWRITEAOF 压缩) |
| 对性能影响 | fork 子进程时可能有瞬间卡顿 | 每秒 fsync,开销稍大 |
| 触发方式 | save / bgsave / 配置自动触发 | always / everysec / no |
RDB = 每天打烊时给整个停车场拍一张全景照片(恢复快,但打烊前的订单可能丢了);AOF = 每辆车进出就记一笔流水账(更安全,但翻账本恢复慢)
| 策略 | 含义 | 数据安全 | 性能 |
|---|---|---|---|
| always | 每条命令都 fsync 到磁盘 | 最高(不丢数据) | 最差 |
| everysec | 每秒 fsync 一次 | 最多丢 1 秒 | 推荐 ✅ |
| no | 交给 OS 决定何时刷盘 | 可能丢较多 | 最好 |
生产环境推荐 RDB + AOF 混合持久化(Redis 4.0+):AOF 重写时把当前数据以 RDB 格式写入 AOF 文件头部,后续命令继续用 AOF 追加。兼顾恢复速度和数据安全。面试说出这个是加分项。
一主多从,主写从读,数据单向复制
停车场:总停车场有完整车位数据,分停车场同步只读副本
❌ 主挂了需手动切换
在主从基础上加哨兵进程,自动监控 + 故障转移
停车场:哨兵监控主节点,主节点宕机自动选举从节点升主
✅ 自动故障转移
16384 个哈希槽分散到多个节点,每个节点负责一部分
停车场:停车场连锁按区域分流(1-4000号车位A区管,4001-8000号B区管)
✅ 自动分片 + 高可用
• 节点间心跳包要携带槽信息,槽太多心跳包大
• 16384 个槽用 2KB 就能表示(每个bit=1个槽),压缩好传
• 集群规模一般不超过 1000 节点,16384 够用且不浪费
• Redis 作者原话:选择 16384 是心跳大小和集群规模的平衡
Redis 分布式锁的经典陷阱:锁过期了但业务还没执行完 → 别人拿到锁 → 并发安全问题。解决:Redisson 的看门狗机制(Watchdog),自动续期。
只要客户端还活着且没释放锁,看门狗就会一直续期。客户端宕机后看门狗也停了,锁自然过期释放。
单节点 Redis 锁在主从切换时可能丢失锁信息。RedLock 的方案:
• 向 N 个独立 Redis 实例(通常 5 个)同时加锁
• 超过半数(N/2 + 1)加锁成功 → 获取锁成功
• 总耗时不能超过锁的过期时间
• 任何一步失败 → 释放所有已加的锁
争议:Martin Kleppmann 认为 RedLock 不安全,antirez 回怼。面试说出"了解争议"即可。
Redis 采用 惰性删除 + 定期删除 双管齐下:
• 惰性删除:不主动删,访问 key 时才检查是否过期 → 省CPU,但可能占内存
• 定期删除:每秒 10 次随机抽取一批 key 检查,过期则删除 → 折中方案
惰性 = 车主来取车时发现过期才删除;定期 = 管理员每隔一段时间检查一批过期车位。两种结合才能既不太累也不浪费
| 策略 | 含义 | 场景 |
|---|---|---|
| noeviction | 不淘汰,内存满了直接报错 | 数据不能丢 |
| allkeys-lru | 所有key中淘汰最久未使用的 | 最常用 ✅ |
| allkeys-lfu | 所有key中淘汰使用频率最低的 | 热点数据明显 |
| allkeys-random | 所有key中随机淘汰 | 无访问热点 |
| volatile-lru | 设了TTL的key中淘汰最久未用的 | 只淘汰可过期数据 |
| volatile-lfu | 设了TTL的key中淘汰频率最低的 | 同上+频率优先 |
| volatile-random | 设了TTL的key中随机淘汰 | 同上+无偏好 |
| volatile-ttl | 设了TTL的key中淘汰TTL最小的 | 优先淘汰快过期的 |
面试最常问:"Redis 内存满了怎么办?"→ allkeys-lru。能说出 LRU 和 LFU 的区别(最近最少使用 vs 最不经常使用)就稳了。
① 基于内存:纳秒级读写,不涉及磁盘IO
② 单线程模型:避免上下文切换和锁竞争开销
③ IO 多路复用(epoll):一个线程处理大量并发连接
④ 高效数据结构:跳表、ziplist、quicklist 等专为性能优化
单线程原因:Redis 性能瓶颈是内存和网络而非 CPU,多线程加锁开销反而降低性能。
Redis 6.0 引入 多线程 IO:命令执行仍是单线程,但网络读写用多线程。默认关闭,需配置开启。
穿透(数据不存在)→ 布隆过滤器 / 缓存空值
击穿(热点 key 过期)→ 互斥锁 / 逻辑过期
雪崩(大量 key 同时过期)→ TTL 加随机值 / 集群 / 多级缓存
口诀:穿=没有的数据,击=一个热点,雪=大面积崩
生产推荐 RDB + AOF 混合持久化(Redis 4.0+):AOF 重写时以 RDB 格式写入头部,兼顾恢复速度和数据安全。配置:aof-use-rdb-preamble yes
加锁:SET key value NX EX seconds(原子操作)
释放锁:Lua 脚本先判断是不是自己的锁再删除
坑:① 锁过期业务没完 → 看门狗续期 ② 主从切换丢锁 → RedLock ③ 不可重入 → Redisson
惰性删除 + 定期删除
惰性:访问时才检查过期(省CPU但占内存)
定期:每秒10次随机抽样检查删除(折中方案)
不是定时删除:每个key设定时器太耗内存
8种策略,生产推荐 allkeys-lru(最久未使用淘汰)
如果热点数据明显用 allkeys-lfu(最不经常使用淘汰)
如果数据必须保留用 noeviction
槽数量需要在心跳包大小和集群规模间平衡。16384 个槽只需 2KB 位图表示,心跳包小传输快。集群节点一般不超过 1000,16384 够用。
常用方案:Cache Aside Pattern
• 读:先读缓存,miss 则读 DB 并回填
• 写:先更新 DB,再删除缓存(不是更新缓存!)
为什么删不更:写多读少时频繁更新缓存浪费;并发写可能导致缓存和DB不一致
强一致性:用 Canal 监听 binlog 异步更新缓存
① List + LPUSH/BRPOP:简单,但不支持消费组、消息确认
② Pub/Sub:发布订阅,消息不持久化,离线丢失
③ Stream(5.0+):支持消费组、消息确认、持久化,最接近专业MQ
BigKey = 一个 key 对应的 value 过大(如 100KB+ 的 String,万元素的集合)
危害:阻塞其他命令、网络拥塞、内存不均
解决:① 拆分(大 Hash 拆小 Hash)② 压缩(JSON→MsgPack)③ 本地缓存(不走 Redis)④ 删除用 UNLINK(异步删除)
HotKey = 某个 key 被超高频率访问
发现:redis-cli --hotkeys / monitor 分析 / 业务预判
解决:① 本地缓存(Caffeine)② 读写分离(从节点分担读)③ key 分片(hot_key_1/2/3 随机读)
① Redis 支持更丰富的数据结构(Memcached 只有 String)
② Redis 支持持久化(Memcached 纯内存)
③ Redis 单线程(Memcached 多线程)
④ Redis 支持发布订阅/Lua/事务/集群
⑤ Memcached 更适合纯 KV 缓存场景
结论:新项目基本都选 Redis
MULTI → 命令入队 → EXEC 一次性执行。不支持回滚(与 MySQL 不同)。
原因:Redis 作者认为出错应该是编程错误,不需要回滚来恢复,保持简单。
替代:Lua 脚本(原子执行多条命令)比事务更实用。
将多条命令打包一次发送,减少网络 RTT。
注意:Pipeline 不是原子操作(中间可能插别的命令),只是网络优化。
适合批量操作(如批量 set/get),不适合依赖前一条结果的操作。
缓存三问题:
穿=没有→布隆/空值 | 击=热点→锁 | 雪=大面积→随机TTL
持久化:
RDB=快照(快/可能丢) | AOF=日志(慢/更安全) | 推荐:混合
集群:
主从→哨兵(自动切换)→Cluster(16384槽分片)
分布式锁:
SET NX EX + Lua释放 | 看门狗续期 | RedLock多节点
淘汰策略:
生产=allkeys-lru | 内存满→淘汰最久未用
双写一致性:
更新DB→删缓存 | 不更新缓存 | 强一致用Canal
Redis三原则:① 合理设置TTL避免内存暴涨 → ② 热点Key做永不过期+异步刷新 → ③ 分布式锁要有看门狗续期