04 - Redis与本地缓存 · 高频面试考题背诵指南


目录


一、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 为什么用跳表而不用红黑树

  1. 跳表的范围查询ZRANGEBYSCORE)更高效,只需在底层链表上顺序遍历即可;红黑树的范围查询需要中序遍历,实现复杂。
  2. 跳表实现更简单,代码更易维护和调试。
  3. 跳表的并发修改(无锁化)比平衡树更容易实现。

Q3:Redis 常见业务场景实现

核心考点:各数据类型对应最典型的业务用途。

背诵话术

  • 排行榜(ZSet):以用户 ID 为 member,积分为 score,存入 ZSet。ZADD rank 100 user1ZREVRANGEBYSCORE 获取 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 3HGETALL 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),复制积压缓冲区。

背诵话术

全量复制(首次连接或重连偏移量差异过大)

  1. 从库向主库发送 psync replicationid offset 命令(replicationid 为主库复制 ID,首次为 ?)。
  2. 主库执行 BGSAVE 生成 RDB 快照,将快照发送给从库,从库接收后清空旧数据,加载 RDB 文件。
  3. 主库将生成 RDB 期间新产生的写命令缓存在 Replication Buffer 中,RDB 传输完后一并发送给从库执行。

增量复制(断线重连,且偏移量在复制积压缓冲区范围内)

  1. 从库重连后发送自己记录的 replication offset(复制偏移量)。
  2. 主库检查该偏移量是否在 Replication Backlog Buffer(复制积压缓冲区,默认 1MB 的环形缓冲区) 中。
  3. 如果在,只需将缓冲区中从偏移量开始的命令发给从库(增量复制),大大减少了全量复制的开销。
  4. 如果不在(主库的 buffer 已被覆盖),则触发全量复制。

Q7:Redis Sentinel(哨兵)的工作原理

核心考点:哨兵的三个职责(监控/通知/自动故障转移),主观下线和客观下线,Leader 选举。

背诵话术

Sentinel 的三大职责

  1. 监控(Monitoring):哨兵节点每秒向主节点、从节点和其他哨兵节点发送 PING 命令,检测它们是否在线。
  2. 通知(Notification):当发现某个节点出现问题,哨兵会通过发布订阅(pub/sub)通知客户端和其他哨兵。
  3. 自动故障转移(Automatic Failover):主节点宕机时,哨兵自动选举新的主节点,并通知从节点和客户端更新连接。

故障转移流程

  1. 主观下线(SDOWN):某个哨兵判断主节点超时(默认 30s 无响应),标记为”主观下线”。
  2. 客观下线(ODOWN):该哨兵向其他哨兵询问(SENTINEL is-master-down-by-addr),当超过半数(quorum 配置)的哨兵都认为主节点下线,才标记为”客观下线”,防止单个哨兵误判(如网络分区)。
  3. Leader 选举:哨兵之间通过 Raft 算法选举出一个 Leader 哨兵来主导故障转移。
  4. 新主节点选择: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。
  • 解决方案
    1. 布隆过滤器(BloomFilter):将所有存在的数据的 Key 加入布隆过滤器,请求时先判断 Key 是否存在,不存在直接拒绝。布隆过滤器有小概率误判(误判为存在),但不会漏判(存在的一定判存在)。
    2. 空值缓存:即使数据库查不到数据,也在 Redis 中缓存一个 null 值(设置短过期时间,如 5 分钟),防止反复穿透。

2. 缓存击穿(热点 Key 过期瞬间大量并发)

  • 现象:一个热点 Key(如某明星微博)在缓存中过期的瞬间,大量并发请求同时涌向数据库,导致数据库被打崩。
  • 解决方案
    1. 互斥锁(Mutex):Key 过期后,第一个请求用 SETNX 加锁查数据库并重建缓存,其他请求等待。安全但有阻塞。
    2. 逻辑过期(热点 Key 不设 TTL):缓存中永远不让 Key 真正过期,而是在 Value 中存储一个”逻辑过期时间”字段。请求时检查逻辑时间,若已过期则异步启动后台线程重建缓存,当前请求返回旧数据(容忍短暂不一致)。适合允许数据短暂不一致的高并发场景,是我们医院排班缓存采用的方案。

3. 缓存雪崩(大量 Key 同时过期)

  • 现象:大量缓存 Key 在同一时刻集体过期(如系统初始化时批量设置了相同的过期时间),导致大量请求同时打到数据库,引发雪崩效应。
  • 解决方案
    1. 过期时间加随机值:在设置缓存时,在基础过期时间上加上一个随机的偏移量(如 TTL + random(0, 300s)),打散过期时间,防止集中过期。
    2. 多级缓存:Caffeine 本地缓存 + Redis 分布式缓存,即使 Redis 中缓存全部失效,本地缓存还能扛住大部分流量,降低数据库压力。
    3. Redis 高可用集群:主从 + 哨兵或 Cluster 方案,防止 Redis 服务本身宕机导致的雪崩。

Q12:缓存与数据库双写一致性(结合招行项目)

核心考点:Cache-Aside 的顺序选择,并发下的脏数据问题,延迟双删和 Canal 兜底。

背诵话术

Cache-Aside 模式(旁路缓存)是最常用的缓存一致性模式:

  • :先读缓存,命中直接返回;未命中则读数据库,写入缓存,再返回。
  • :先更新数据库,再删除缓存(而非更新缓存)。

为什么是”先更库再删缓存”而不是”先删缓存再更库”?
如果先删缓存,在删缓存和更库之间的窗口期,其他线程读到缓存 miss,去库里读到旧值(此时库还没更新),并把旧值写入缓存,之后库更新完,缓存里依然是旧值——造成脏数据。而”先更库再删缓存”出现脏数据的概率极低(需要缓存失效 + 极短时间内的并发读写组合)。

延迟双删弥补漏洞

  1. 更新数据库后,立即删除一次缓存。
  2. 通过 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 自实现分布式锁的两大痛点

  1. 过期时间设太短:业务还没执行完,锁就释放了,其他线程可以进来,出现并发安全问题。
  2. 过期时间设太长:如果客户端宕机,锁会长时间占用(死锁),严重影响系统可用性。

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 的两个区域

  1. Window Cache(窗口区,占 1%):所有新进入的 Key 先放入窗口区(类似 LRU),给刚进入的 Key 一个”适应期”,防止刚进来的热数据被频率过低而立即淘汰(即解决 LFU 对突发流量不友好的问题)。
  2. 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() vs tryLock()

  • lock() 是一直阻塞等待,直至获取锁或报错。
  • tryLock(wait, lease, unit) 支持超时获取。若在 wait 时间内未获取到锁,则返回 false。其底层通过订阅 Redis 的锁释放通道(Pub/Sub),在拿不到锁时,线程会订阅锁的释放事件并挂起,直到锁释放被唤醒或等待超时,避免了空转自旋对 Redis 的性能消耗。

红锁(Redlock)算法与争议

  • 为什么需要:为了解决哨兵或 Cluster 模式下,主从异步复制导致的“主宕机锁丢失”(客户端 A 在主节点加锁成功,主节点未同步给从节点就挂了,从节点还原为主节点,客户端 B 仍能加锁成功,导致锁失效)。
  • 算法流程:客户端向 N 个(N 必须是奇数,如 5)独立的、无主从的 Redis 实例发起加锁请求,只有当在过半数(如 3 个)实例上加锁成功,且总耗时小于锁的有效时间,才认为加锁成功。
  • Martin Kleppmann 的争议点
    1. 系统时钟漂移:Redlock 强依赖各服务器的本地系统时钟。如果某台服务器发生 NTP 时钟跳变,锁可能会提前过期,导致并发安全失效。
    2. 垃圾回收(GC)引起的 STW 停顿:客户端在获取锁后发生长时间 GC,在此期间锁在 Redis 上已过期,GC 结束后客户端继续执行临界区代码,导致失效。
    3. Martin 认为 Redlock 是一个“不上不下”的算法,对性能有影响却又无法保证绝对的数学安全性。如果需要强一致性,应使用 ZooKeeper 或基于 Raft 的强一致性协调系统。