Redis 深入
Java 高级 · 存储与搜索
Redis 是面试与实战的常青树,但"会用 get/set"只是入门。真正的高手要回答:RDB 和 AOF 怎么配合做到"快且丢得少"?主从复制全量/增量怎么发生?分布式锁为什么手搓会翻车、Redisson 看门狗怎么续租?大 key/热 key 如何拖垮单线程? 本文把持久化、高可用、分布式锁与缓存治理讲透。
一、线程模型:为什么 Redis 这么快
- 内存存储:所有数据在内存,读写纳秒级。
- 单线程命令执行:Redis 6 前命令执行是单线程的(避免锁竞争与上下文切换);6+ 引入 IO 多线程(网络读写多线程),但命令执行仍单线程——所以"单线程"指执行阶段。
- 高效数据结构:SDS、跳表、ziplist/listpack、dict 等精心设计的底层结构,操作复杂度低。
- IO 多路复用:基于 epoll,单线程也能扛高并发连接。
TIP
正因为命令单线程串行执行,Redis 才能实现"原子性"而不需锁——单条命令(如 INCR、LPUSH)天然原子。但 Lua 脚本和 MULTI 事务也是整体串行,长脚本会阻塞整个实例。这也是"大 key 操作慢 = 全实例卡顿"的根因。
二、持久化:RDB + AOF 混合
2.1 RDB(快照)
定时 fork 子进程把内存全量dump成 .rdb(二进制压缩)。优点:文件紧凑、恢复快、对主线程影响小(fork 后子进程写、COW 机制);缺点:可能丢最后一次快照后的数据(最多丢一个周期)。
2.2 AOF(追加日志)
记录每条写命令(append-only),可配置 appendfsync:
always:每次写都 fsync,最安全最慢。everysec:每秒 fsync(默认,折中,宕机最多丢 1s)。no:交给 OS,最快最不安全。
AOF 过大时 bgrewriteaof 用子进程重写成最小命令集(如把 100 次 INCR 合成 SET k 100),aof-use-rdb-preamble yes 让重写后的 AOF 以 RDB 格式开头 + AOF 增量结尾——即混合持久化(Redis 4+ 默认),兼顾恢复速度与最小丢失。
2.3 过期删除
键过期不是到点就删,而是两策略配合:activeExpireCycle(定期抽样删,10% CPU 预算)+ lazyfree(访问时发现过期惰性删)。大量 key 同时过期会触发"过期风暴"集中删除,可加随机 TTL 抖动避免。
三、高可用:主从 → 哨兵 → Cluster
3.1 主从复制
从节点 replicaof 主节点。复制分两阶段:
- 全量复制:初次或复制缓冲(master_repl_offset)已丢弃时,主
bgsave发 RDB,慢且重。 - 增量复制:断线重连且复制缓冲还在,只需补增量命令(靠
replication offset对齐)。 - 主从异步复制:主写完即返回,从可能落后,强一致场景需注意。
3.2 哨兵(Sentinel)
哨兵集群监控主从、自动故障转移:主宕机 → 哨兵间投票确认 → 选新主 → 通知客户端新地址。哨兵本身也要≥3节点防"哨兵自己脑裂"。
3.3 Cluster(数据分片)
数据按 16384 个 slot 分片,每个主节点负责一部分 slot。CRC16(key) % 16384 路由。特点:
- 每个主带从,主挂从顶上(类似哨兵但内置)。
- 槽迁移(resharding):
redis-cli --cluster reshard把 slot 从旧节点迁到新节点(迁移中 slot 处于MIGRATING/IMPORTING状态,先搬数据再上线,平滑)。 MOVED/ASK重定向:客户端访问错误节点时返回重定向(智能客户端会缓存 slot 映射)。
四、分布式锁:为什么手搓会翻车 + Redisson 看门狗
4.1 手搓锁的三个坑
// 错误示范 1: SETNX + 固定过期, 业务超时锁提前释放
SETNX lock:order 1
EXPIRE lock:order 30 // 若这之间崩溃, 锁无过期 → 死锁
// 错误示范 2: 锁到期了自己早执行完, 但误删了别人的锁
DEL lock:order // 没校验 value 是不是自己翻车点:① SETNX 与 EXPIRE 非原子,崩溃会死锁;② 固定过期时间,业务执行超过过期就锁失效、且可能删别人的锁;③ 释放需"校验 value == 自己"用 Lua 原子完成。
4.2 Redisson 看门狗(Watch Dog)
正确的分布式锁:SET key uuid NX PX 30000(原子加锁 + 唯一 value + 过期),释放用 Lua 校验 value 再删。Redisson 更进一步——看门狗自动续租:
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient client = Redisson.create(config);
RLock lock = client.getLock("order:lock:" + orderId);
try {
// tryLock 抢到后, 看门狗每 1/3 过期时间(默认 10s)自动续期, 直到 unlock
if (lock.tryLock(3, TimeUnit.SECONDS)) {
deductStock(orderId); // 临界区, 即使执行很久锁也不会提前释放
}
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock(); // 释放并取消续租
}DANGER
SETNX+ 固定过期做锁:业务超时就翻车(锁提前释放 + 可能误删他人锁)。务必唯一 value + Lua 释放,或用 Redisson 看门狗。- Big Key / Hot Key:大 value(如 50MB 的 zset)操作慢,阻塞单线程;热 key(如爆款商品库存)打爆单节点。需拆分、本地缓存兜底、Cluster 打散。
- 缓存与数据库双写不一致:先删缓存再更库、或先更库再删缓存都各有窗口。核心场景用"延迟双删"或 Canal 订阅 binlog 同步、或加分布式锁串行化。
keys *在生产执行:阻塞整实例(O(N) 遍历),应改用SCAN游标。
五、缓存治理三连:穿透 / 击穿 / 雪崩
- 穿透(查不存在的 key,打爆 DB):空值缓存 + 布隆过滤器(BitMap 预判 key 是否存在)。
- 击穿(热点 key 失效瞬间,大量请求穿到 DB):互斥重建(只放一个线程回源,
SETNX占位)/ 逻辑过期(不真过期,异步刷新)。 - 雪崩(大量 key 同时失效 + 宕机):TTL 加随机抖动、Redis 高可用(Cluster/哨兵)、本地缓存兜底、限流。
六、生产落地
- 缓存设计:TTL + 随机抖动;空值/熔断防穿透;互斥重建/逻辑过期防击穿;多副本/本地缓存防雪崩。
- 锁优先 Redisson(自动续期、可重入、公平锁),禁止手搓易错锁。
- 大促前
redis-cli --bigkeys扫描 + 热点分析;Cluster 避免 hash tag 不当致数据倾斜。 - 持久化:开混合 AOF;主从 + 哨兵/Cluster 保证高可用。
📚 官方文档与参考资料
- Redis 官方文档:https://redis.io/docs/latest/
- Redis 命令参考:https://redis.io/commands/
- Redis 复制(Replication):https://redis.io/docs/latest/operate/oss_and_stack/reference/replication/
✏️ 练习与自测
- 对比 RDB 与 AOF 的优缺点,说明"混合持久化"如何扬长避短(RDB 开头 + AOF 增量)。
- 手写"先判断库存再扣减"的 Lua 脚本(原子),并解释为何要原子化、释放锁为何也要 Lua 校验 value。
- 缓存与数据库双写,列举两种一致性方案(延迟双删 / binlog 同步)及其残留不一致窗口。
- 大 key 为何会拖垮单线程?给出检测(
--bigkeys)与治理(拆分/异步删除UNLINK)方案。
本节小结
Redis 的"快"来自内存 + 单线程命令执行(无锁、天然原子)+ 高效数据结构 + IO 多路复用;"稳"来自 RDB/AOF 混合持久化 + 主从(全量/增量) + 哨兵/Cluster 高可用。高并发三件事:锁用 Redisson 看门狗防误删与提前释放、Big/Hot Key 要治理、缓存一致性靠延迟双删或 binlog 同步。理解单线程模型,你就懂了"为什么大 key 是 Redis 的头号天敌"。