Redis与本地缓存面试考题
04 - Redis与本地缓存 · 高频面试考题背诵指南
目录
- Q1:Redis 五种核心数据类型及底层实现
- Q2:ZSet 的跳表(skiplist)原理
- Q3:Redis 常见业务场景实现
- Q4:RDB 和 AOF 的优缺点对比?生产中如何选择?
- Q5:AOF 的三种写回策略
- Q6:Redis 主从复制原理
- Q7:Redis Sentinel(哨兵)的工作原理
- Q8:Redis Cluster 分片集群原理
- Q9:Redis 过期键删除策略
- Q10:Redis 内存淘汰策略
- Q11:缓存穿透/击穿/雪崩的区别和解决方案
- Q12:缓存与数据库双写一致性(结合招行项目)
- Q13:Redisson 分布式锁 WatchDog 机制
- Q14:Redis Pipeline 和 Lua 脚本的使用场景
- Q15:Caffeine 的 W-TinyLFU 算法原理
- Q16:分布式多节点本地缓存一致性(结合招行项目)
- Q17:Redisson 分布式锁的可重入原理与红锁(Redlock)争议
一、Redis 数据结构与使用场景
Q1:Redis 五种核心数据类型及底层实现
核心考点:每种数据类型的底层数据结构,小数据量时的 ziplist 压缩优化。
背诵话术:
Redis 有 5 种核心数据类型,每种类型底层会根据数据量大小选择不同的编码结构:
数据类型 小数据量时 大数据量时 String int(整数)/ embstr(≤44字节) raw(SDS 简单动态字符串) Hash ziplist(压缩列表) hashtable(哈希表) List ziplist quicklist(双向链表 + ziplist) Set intset(整数集合,纯整数时) hashtable ZSet(有序集合) ziplist skiplist(跳表)+ hashtable SDS(Simple Dynamic String) 相比 C 字符串的优势:记录了字符串长度(O(1) 获取),空间预分配和惰性释放避免频繁内存分配,且是二进制安全的(可存储
\0字符)。ziplist(压缩列表):连续内存的紧凑结构,节省内存,但插入删除需要内存重分配,适合数据量少(默认元素数 < 128,单个值 < 64 字节)的情况。
💡 Redis 7.0 版本变更(ziplist 替换为 listpack):
在 Redis 7.0 中,官方将所有的ziplist编码完全替换为listpack(紧凑列表)。主要原因在于ziplist内部存储了前一个节点占用的字节数(prevlen),当插入或修改元素时,可能会触发“连锁更新”(Cascade Update),严重拖慢性能。而listpack摒弃了prevlen的设计,仅在节点尾部记录当前节点的长度,从而彻底避免了连锁更新问题。
Q2:ZSet 的跳表(skiplist)原理
核心考点:跳表的多层索引结构,查询/插入时间复杂度 O(logN),为什么不用红黑树。
背诵话术:
跳表(Skip List) 是一种以空间换时间的多层链表结构。底层是一条包含所有元素的有序链表,上层是下层的”稀疏索引”——每隔若干个节点提升一个节点到上层,形成多级索引。
查询过程:从最高层的索引链表开始,向右比较大小,如果当前节点值大于目标,则下降到下一层,继续向右比较,直到最底层精确定位。平均时间复杂度 O(logN)。
ZSet 为什么同时持有 skiplist 和 hashtable:skiplist 支持按分数范围查询(如
ZRANGEBYSCORE),hashtable 支持按成员名精确查找分数(如ZSCORE,O(1))。两者互补,构成 ZSet 强大的双向查询能力。ZSet 为什么用跳表而不用红黑树:
- 跳表的范围查询(
ZRANGEBYSCORE)更高效,只需在底层链表上顺序遍历即可;红黑树的范围查询需要中序遍历,实现复杂。- 跳表实现更简单,代码更易维护和调试。
- 跳表的并发修改(无锁化)比平衡树更容易实现。
Q3:Redis 常见业务场景实现
核心考点:各数据类型对应最典型的业务用途。
背诵话术:
排行榜(ZSet):以用户 ID 为 member,积分为 score,存入 ZSet。
ZADD rank 100 user1,ZREVRANGEBYSCORE获取 Top N 排行。医博系统的医生接诊量排行用的就是这个方案。计数器(String + INCR):Redis 的
INCR/INCRBY是原子操作,用于统计接口调用次数、点赞数等,天然防并发。分布式锁(String + SET NX EX):
SET lock_key value NX EX 30,NX 保证只有一个线程能设置成功,EX 设置过期时间防死锁。生产中用 Redisson 封装,自动处理 WatchDog 续期。接口限流(String + INCR / Lua + 滑动窗口):对每个用户 IP 的请求次数计数,超过阈值则拒绝。美团开放平台的 Redis Lua 脚本限流就是这个思路。
购物车(Hash):以用户 ID 为 key,商品 ID 为 field,数量为 value。
HSET cart:user1 item:101 3,HGETALL cart:user1获取整个购物车。消息队列(List):
LPUSH生产,BRPOP阻塞消费,实现简单的消息队列。社交关注(Set):存储用户的关注列表和粉丝列表,利用
SINTER求两个用户共同关注的人。Session 共享(String):分布式系统中将用户 Session 存入 Redis,各服务节点从 Redis 统一读取,解决多节点 Session 不共享问题。
二、Redis 持久化
Q4:RDB 和 AOF 的优缺点对比?生产中如何选择?
核心考点:两种持久化的触发方式、恢复速度、数据安全性。
背诵话术:
RDB(Redis DataBase Backup,快照持久化):
- 原理:在满足配置的触发条件时(如 900s 内有 1 次写操作),Redis 调用
BGSAVE命令,fork()出一个子进程,子进程将当前内存中的全量数据生成一个紧凑的.rdb二进制文件,父进程继续处理请求。- 优点:恢复速度极快(直接加载二进制文件),适合灾备和全量数据迁移,RDB 文件体积小。
- 缺点:两次快照之间的数据可能丢失(最多丢几分钟的数据);
fork()时有短暂的 STW,对实时性要求高的场景有影响。AOF(Append Only File,追加日志持久化):
- 原理:将每一条写命令追加写入 AOF 日志文件,Redis 重启时重放所有命令来恢复数据。
- 优点:数据丢失风险低,默认
everysec模式最多丢 1 秒数据;日志可读,可做数据审计。- 缺点:AOF 文件体积大(记录所有操作,有大量冗余),恢复速度比 RDB 慢;AOF 重写(
BGREWRITEAOF)时也需要 fork 子进程。生产选择:优先开启 RDB + AOF 混合持久化(Redis 4.0+),AOF 重写时先生成 RDB 快照写入 AOF 文件,之后追加写操作。这样既有 RDB 的快速恢复,又有 AOF 的数据完整性,是生产的最佳实践。
Q5:AOF 的三种写回策略
核心考点:性能和数据安全性的取舍,everysec 是生产默认选择。
背诵话术:
AOF 的写回策略控制写命令何时从操作系统的 Page Cache 刷入磁盘(
fsync):
always(同步写):每条写命令执行后立即fsync刷盘。数据丢失风险最低(最多丢失 1 条命令),但 IO 频繁,性能最差,对磁盘压力大。适合对数据安全性要求极高的金融场景。everysec(每秒写,生产默认推荐):每秒执行一次fsync,由后台线程完成。最多丢失 1 秒的数据,在性能和数据安全性之间取得了最佳平衡,是生产环境的首选。no(操作系统控制):Redis 不主动fsync,由操作系统决定何时刷盘(通常 30 秒左右)。性能最好,但丢失数据量最多,不推荐用于重要业务。
三、Redis 高可用
Q6:Redis 主从复制原理
核心考点:全量复制(psync)和增量复制(Replication Buffer),复制积压缓冲区。
背诵话术:
全量复制(首次连接或重连偏移量差异过大):
- 从库向主库发送
psync replicationid offset命令(replicationid 为主库复制 ID,首次为?)。- 主库执行
BGSAVE生成 RDB 快照,将快照发送给从库,从库接收后清空旧数据,加载 RDB 文件。- 主库将生成 RDB 期间新产生的写命令缓存在
Replication Buffer中,RDB 传输完后一并发送给从库执行。增量复制(断线重连,且偏移量在复制积压缓冲区范围内):
- 从库重连后发送自己记录的
replication offset(复制偏移量)。- 主库检查该偏移量是否在 Replication Backlog Buffer(复制积压缓冲区,默认 1MB 的环形缓冲区) 中。
- 如果在,只需将缓冲区中从偏移量开始的命令发给从库(增量复制),大大减少了全量复制的开销。
- 如果不在(主库的 buffer 已被覆盖),则触发全量复制。
Q7:Redis Sentinel(哨兵)的工作原理
核心考点:哨兵的三个职责(监控/通知/自动故障转移),主观下线和客观下线,Leader 选举。
背诵话术:
Sentinel 的三大职责:
- 监控(Monitoring):哨兵节点每秒向主节点、从节点和其他哨兵节点发送 PING 命令,检测它们是否在线。
- 通知(Notification):当发现某个节点出现问题,哨兵会通过发布订阅(
pub/sub)通知客户端和其他哨兵。- 自动故障转移(Automatic Failover):主节点宕机时,哨兵自动选举新的主节点,并通知从节点和客户端更新连接。
故障转移流程:
- 主观下线(SDOWN):某个哨兵判断主节点超时(默认 30s 无响应),标记为”主观下线”。
- 客观下线(ODOWN):该哨兵向其他哨兵询问(
SENTINEL is-master-down-by-addr),当超过半数(quorum配置)的哨兵都认为主节点下线,才标记为”客观下线”,防止单个哨兵误判(如网络分区)。- Leader 选举:哨兵之间通过 Raft 算法选举出一个 Leader 哨兵来主导故障转移。
- 新主节点选择:Leader 哨兵从从节点中选择最优的一个(优先级高、复制进度最靠近主库的)晋升为新主节点,修改其他从节点的复制目标,并通知客户端。
Q8:Redis Cluster 分片集群原理
核心考点:16384 个 Hash Slot、CRC16 哈希分片、MOVED 和 ASK 重定向、Gossip 协议。
背诵话术:
数据分片:Redis Cluster 将所有键的空间划分为 16384 个 Hash Slot(哈希槽),每个 Key 通过
CRC16(key) % 16384计算得到它所属的槽位,每个槽位分配给某个主节点负责。这样数据就被均匀分散到各个节点上,每个节点只负责一部分数据。为什么是 16384 个槽:16384 = 2^14,心跳报文中传输的槽信息用 bitmap 表示,16384 个槽只需要 2KB,既够用又不会让心跳包太大(如果是 65536 个槽则需要 8KB)。
请求重定向:客户端可以连接集群中任意节点,如果访问的 Key 不在当前节点:
MOVED重定向:Key 对应的槽不在当前节点,返回MOVED slotId ip:port,客户端应永久更新本地槽路由表并重定向到正确节点。ASK重定向:在槽迁移过程中,数据可能部分在旧节点、部分已迁移到新节点,返回ASK表示临时重定向,客户端只是此次请求去新节点查询,不更新本地路由表。节点间通信:集群内各节点通过 Gossip 协议 相互传播节点状态信息(上线/下线/槽信息变更),最终达到全集群状态一致,无需中心化的协调节点。
四、Redis 过期与淘汰
Q9:Redis 过期键删除策略
核心考点:定期删除 + 惰性删除的组合策略,为何不用定时删除。
背诵话术:
Redis 采用定期删除 + 惰性删除的组合策略:
惰性删除(Lazy Expiration):Key 过期后不立即删除,只有当客户端访问该 Key 时,Redis 才检查它是否过期,如果已过期则删除并返回 null。优点:对 CPU 影响最小;缺点:大量过期 Key 没有被访问,会一直占用内存。
定期删除(Periodic Expiration):Redis 每隔 100ms(
hz参数控制)随机抽取一批设置了过期时间的 Key 进行检查,删除其中已过期的 Key。这是一种后台主动清理机制,补充了惰性删除的不足。为什么不用定时删除:为每个 Key 都创建一个定时器,过期时立即删除,虽然内存最友好,但大量定时器会严重消耗 CPU 资源,影响 Redis 的服务能力。
淘汰策略是最后一道防线:如果以上两种策略还不够(大量过期 Key 没被及时清理),当内存达到
maxmemory上限时,Redis 的内存淘汰策略(如allkeys-lru)会强制淘汰部分 Key 来腾出内存。
Q10:Redis 内存淘汰策略
核心考点:8 种策略,生产中最常用 allkeys-lru。
背诵话术:
Redis 提供 8 种内存淘汰策略(通过
maxmemory-policy配置):
noeviction(默认):内存满时拒绝所有写请求,直接报错。适合不允许丢失任何数据的场景(如数据库),但会导致业务报错。allkeys-lru(生产最常用):从所有 Key 中淘汰最近最少使用的 Key(LRU 算法)。适合缓存场景,是绝大多数业务的推荐选择。volatile-lru:只对设置了过期时间的 Key 进行 LRU 淘汰。保留了没有过期时间的 Key(可能是重要数据)。allkeys-lfu(Redis 4.0+):从所有 Key 中淘汰访问频率最低的 Key(LFU 算法)。比 LRU 更精准,适合访问模式稳定的场景。volatile-lfu:只对有过期时间的 Key 进行 LFU 淘汰。allkeys-random:随机淘汰所有 Key,适合 Key 访问频率差不多均匀的场景。volatile-random:随机淘汰有过期时间的 Key。volatile-ttl:优先淘汰剩余存活时间(TTL)最短的 Key。招行调度中台的选择:业务规则缓存是读多写少的热数据,使用
allkeys-lru配合合理的maxmemory设置,让 Redis 自动淘汰冷数据,无需手动管理。
五、缓存高频问题
Q11:缓存穿透/击穿/雪崩的区别和解决方案
核心考点:三种问题的本质、场景和对应的解决方案。
背诵话术:
1. 缓存穿透(查询不存在的数据):
- 现象:大量请求查询一个数据库中不存在的 Key(如
id = -1),缓存中也没有,每次都直接穿透到数据库,引起数据库压力激增。恶意攻击者可以利用这一点对数据库发起 DDoS。- 解决方案:
- 布隆过滤器(BloomFilter):将所有存在的数据的 Key 加入布隆过滤器,请求时先判断 Key 是否存在,不存在直接拒绝。布隆过滤器有小概率误判(误判为存在),但不会漏判(存在的一定判存在)。
- 空值缓存:即使数据库查不到数据,也在 Redis 中缓存一个 null 值(设置短过期时间,如 5 分钟),防止反复穿透。
2. 缓存击穿(热点 Key 过期瞬间大量并发):
- 现象:一个热点 Key(如某明星微博)在缓存中过期的瞬间,大量并发请求同时涌向数据库,导致数据库被打崩。
- 解决方案:
- 互斥锁(Mutex):Key 过期后,第一个请求用
SETNX加锁查数据库并重建缓存,其他请求等待。安全但有阻塞。- 逻辑过期(热点 Key 不设 TTL):缓存中永远不让 Key 真正过期,而是在 Value 中存储一个”逻辑过期时间”字段。请求时检查逻辑时间,若已过期则异步启动后台线程重建缓存,当前请求返回旧数据(容忍短暂不一致)。适合允许数据短暂不一致的高并发场景,是我们医院排班缓存采用的方案。
3. 缓存雪崩(大量 Key 同时过期):
- 现象:大量缓存 Key 在同一时刻集体过期(如系统初始化时批量设置了相同的过期时间),导致大量请求同时打到数据库,引发雪崩效应。
- 解决方案:
- 过期时间加随机值:在设置缓存时,在基础过期时间上加上一个随机的偏移量(如
TTL + random(0, 300s)),打散过期时间,防止集中过期。- 多级缓存:Caffeine 本地缓存 + Redis 分布式缓存,即使 Redis 中缓存全部失效,本地缓存还能扛住大部分流量,降低数据库压力。
- Redis 高可用集群:主从 + 哨兵或 Cluster 方案,防止 Redis 服务本身宕机导致的雪崩。
Q12:缓存与数据库双写一致性(结合招行项目)
核心考点:Cache-Aside 的顺序选择,并发下的脏数据问题,延迟双删和 Canal 兜底。
背诵话术:
Cache-Aside 模式(旁路缓存)是最常用的缓存一致性模式:
- 读:先读缓存,命中直接返回;未命中则读数据库,写入缓存,再返回。
- 写:先更新数据库,再删除缓存(而非更新缓存)。
为什么是”先更库再删缓存”而不是”先删缓存再更库”?
如果先删缓存,在删缓存和更库之间的窗口期,其他线程读到缓存 miss,去库里读到旧值(此时库还没更新),并把旧值写入缓存,之后库更新完,缓存里依然是旧值——造成脏数据。而”先更库再删缓存”出现脏数据的概率极低(需要缓存失效 + 极短时间内的并发读写组合)。延迟双删弥补漏洞:
- 更新数据库后,立即删除一次缓存。
- 通过 MQ/异步线程,延迟 500ms~1s 后再删除一次缓存。
第二次延迟删除是为了清理那些在第一次删除和更新库之间、被其他线程”污染”写入的旧值缓存。Canal + Kafka 强一致性兜底(招行项目实战):
对于招行规则缓存这类一致性要求极高的数据,我们引入了 Canal 订阅 MySQL Binlog 的方案。Canal 伪装成 MySQL 从库,实时接收主库的 Binlog(ROW 格式),解析出数据变更事件,推送到 Kafka。消费服务接收到消息后,立即失效 Redis 中对应的缓存条目(同时发送 Redis Pub/Sub 广播,触发各节点 Caffeine 本地缓存失效)。如果失效失败,Kafka 重试机制确保最终一致性。这套方案完全解耦了业务代码与缓存失效逻辑,是一种事件驱动的最终一致性方案。
Q13:Redisson 分布式锁 WatchDog 机制
核心考点:默认 30s 过期,每 10s 续期,业务执行完自动停止,防止死锁。
背诵话术:
使用
setnx + expire自实现分布式锁的两大痛点:
- 过期时间设太短:业务还没执行完,锁就释放了,其他线程可以进来,出现并发安全问题。
- 过期时间设太长:如果客户端宕机,锁会长时间占用(死锁),严重影响系统可用性。
Redisson WatchDog 的解法:Redisson 的
lock()方法(不指定 leaseTime 时)默认将锁的过期时间设为 30 秒,同时在后台启动一个守护线程(WatchDog),以lockWatchdogTimeout / 3(即 10 秒)为间隔,检查当前持有锁的线程是否还在运行:
- 线程仍在运行:执行 Lua 脚本将锁的过期时间重置为 30 秒(续期)。
- 线程已结束或业务主动 unlock():WatchDog 守护线程自动停止,不再续期,锁在 30 秒内自然过期释放。
- 客户端宕机(JVM 挂掉):WatchDog 线程随之消亡,锁最长在 30 秒后自动释放,不会永久死锁。
Redisson 的锁操作底层使用 Lua 脚本来保证”判断 + 操作”的原子性,避免了非原子操作导致的误删他人锁的问题。
Q14:Redis Pipeline 和 Lua 脚本的使用场景
核心考点:Pipeline 减少网络往返,Lua 脚本保证多命令原子性。
背诵话术:
Pipeline(管道):Redis 的 Pipeline 允许客户端一次性发送多条命令(而不是每条命令都等待一次 RTT 响应),服务端批量执行后一次性返回所有结果。这大大减少了网络往返次数(RTT),在批量操作场景(如批量写入缓存、批量查询)下性能提升显著。Pipeline 不保证原子性,命令之间相互独立,任意一条失败不影响其他命令。
Lua 脚本:Redis 会将整个 Lua 脚本作为一个原子操作来执行,执行期间不会被其他命令打断(因为 Redis 是单线程的)。当需要多个命令组合在一起具有原子性时(如:判断 + 扣减,判断 + 设置),就需要 Lua 脚本。
典型案例:
- 分布式限流(Lua):美团 OpenAPI 防刷,用 Lua 脚本实现”计数自增 + 超限判断 + 设置过期”的原子操作。
- 库存扣减(Lua):医博系统号源扣减,用 Lua 实现”判断库存 > 0 → 扣减”的原子操作,防止超卖。
- nonce 防重放(Lua 或 SET NX):美团 OpenAPI 的 nonce 存储和检查,用
SET nonce value NX EX 300保证原子性。- 批量缓存预热(Pipeline):招行调度中台系统启动时,利用 Pipeline 批量将热点规则数据写入 Redis,提升预热速度。
六、本地缓存 Caffeine
Q15:Caffeine 的 W-TinyLFU 算法原理
核心考点:TinyLFU(频率过滤)+ SLRU(大小 LRU 段),为什么比 LRU 命中率更高。
背诵话术:
Caffeine 采用 W-TinyLFU(Window Tiny LFU) 淘汰算法,这也是它被称为”现代最先进的本地缓存”的核心原因。
W-TinyLFU 的两个区域:
- Window Cache(窗口区,占 1%):所有新进入的 Key 先放入窗口区(类似 LRU),给刚进入的 Key 一个”适应期”,防止刚进来的热数据被频率过低而立即淘汰(即解决 LFU 对突发流量不友好的问题)。
- Main Cache(主缓存,占 99%):分为 Protected(保护区,80%)和 Probation(试用区,20%)两段,共同构成 SLRU(Segmented LRU)。高频访问的 Key 在保护区,低频的在试用区。
TinyLFU 准入机制(PK 门控):当窗口区的 Key 需要晋升到主缓存,或新 Key 与主缓存的 Key 竞争时,TinyLFU 会比较两者的历史频率(通过 Count-Min Sketch 近似计数),频率高的才能进入或留在主缓存,频率低的被淘汰。
为什么比 LRU 命中率更高:LRU 只考虑”最近被访问”,容易受到一次性大批量扫描数据(如批量报表查询)的影响,把真正的热数据从缓存中挤出去(”缓存污染”)。W-TinyLFU 通过频率统计,保护了真正的高频热数据不被偶发的批量访问影响,命中率提升可达 10%~30%。
Q16:分布式多节点本地缓存一致性(结合招行项目)
核心考点:本地缓存无法自动同步,需要广播机制;Canal+Kafka 是最优雅的解法。
背诵话术:
Caffeine 是 JVM 进程内的本地缓存,它的数据只在当前服务实例的内存中,在分布式多节点部署下,如果节点 A 更新了数据库并删除了自己的本地缓存,节点 B 和 C 的本地缓存中依然保留着旧数据,出现了缓存不一致问题。
解决方案一:MQ 广播(业务侵入低):业务代码在更新数据库的同时,向 Kafka/RocketMQ 发布一条缓存失效消息(带上 Key 或数据标识)。所有服务节点都订阅这个 Topic,收到消息后立即执行
caffeine.invalidate(key)。优点:解耦彻底,对业务代码侵入小。缺点:如果消息丢失或消费延迟,会有短暂不一致。解决方案二:Redis Pub/Sub 广播:一个节点完成缓存更新后,向 Redis 的特定 Channel 发布通知,所有节点订阅该 Channel,实时接收并失效本地缓存。延迟比 MQ 更低,但 Redis Pub/Sub 不能持久化,如果某个节点消费失败会丢失通知。
招行调度中台的最终方案(Canal + Kafka + Pub/Sub):Canal 监听 MySQL Binlog,数据变更时将事件推入 Kafka,消费服务接收到 Kafka 消息后:① 删除/更新 Redis 中的分布式缓存;② 向 Redis Pub/Sub Channel 广播失效消息,触发所有节点的 Caffeine 本地缓存失效。这种方案完全从数据库变更出发,无需业务代码感知缓存,是最优雅的多级缓存一致性方案。
Q17:Redisson 分布式锁的可重入原理与红锁(Redlock)争议?
核心考点:Hash 结构存储重入次数、Lua 脚本原子加锁与重入、tryLock() 内部等待逻辑、Redlock 争议(时钟漂移/GC STW)。
背诵话术:
可重入原理:
Redisson 实现可重入锁,其底层使用 Redis 的 Hash 结构 来存储锁。Hash 的 key 是锁的名称(如myLock),field 是持有锁的线程标识(UUID + threadId),value 是重入次数(计数器)。
- 首次加锁:若锁不存在,则在 Hash 中创建对应的 field,将 value 设为 1,并设置过期时间。
- 锁重入:若锁已存在,Redisson 会通过 Lua 脚本检查 field 是否与当前请求 of 线程标识一致。若一致,则将 value(重入次数)自增 1,并重新设置锁的过期时间。
- 释放锁:每次释放锁,value 自减 1。只有当 value 减为 0 时,锁才会被彻底删除。这些判断与修改全部由 Lua 脚本 封装以保证原子性。
lock()vstryLock():
lock()是一直阻塞等待,直至获取锁或报错。tryLock(wait, lease, unit)支持超时获取。若在wait时间内未获取到锁,则返回 false。其底层通过订阅 Redis 的锁释放通道(Pub/Sub),在拿不到锁时,线程会订阅锁的释放事件并挂起,直到锁释放被唤醒或等待超时,避免了空转自旋对 Redis 的性能消耗。红锁(Redlock)算法与争议:
- 为什么需要:为了解决哨兵或 Cluster 模式下,主从异步复制导致的“主宕机锁丢失”(客户端 A 在主节点加锁成功,主节点未同步给从节点就挂了,从节点还原为主节点,客户端 B 仍能加锁成功,导致锁失效)。
- 算法流程:客户端向 N 个(N 必须是奇数,如 5)独立的、无主从的 Redis 实例发起加锁请求,只有当在过半数(如 3 个)实例上加锁成功,且总耗时小于锁的有效时间,才认为加锁成功。
- Martin Kleppmann 的争议点:
- 系统时钟漂移:Redlock 强依赖各服务器的本地系统时钟。如果某台服务器发生 NTP 时钟跳变,锁可能会提前过期,导致并发安全失效。
- 垃圾回收(GC)引起的 STW 停顿:客户端在获取锁后发生长时间 GC,在此期间锁在 Redis 上已过期,GC 结束后客户端继续执行临界区代码,导致失效。
- Martin 认为 Redlock 是一个“不上不下”的算法,对性能有影响却又无法保证绝对的数学安全性。如果需要强一致性,应使用 ZooKeeper 或基于 Raft 的强一致性协调系统。







