Redlock 算法:是分布式锁的“圣杯”还是“鸡肋”

老规矩,来一个比喻,大家看看恰当不,欢迎来到分布式锁领域的**"角斗场"------**

在这里,两位大人物曾进行过一场艰巨的论战:

  • 反方:Martin Kleppmann
  • 正方:Salvatore Sanfilippo

争论的焦点只有一个:**Redlock 算法到底能不能在极端故障下保证锁的安全性?**今天,我们不站队,只谈底层、谈原理、谈工程取舍。努力把 Redlock 扒开了揉碎了,看看它到底是解决主从切换痛点的"圣杯",还是过度设计的"鸡肋"。

核心痛点:单点 Redis 的"阿喀琉斯之踵" 啊哈

咱们知道在生产环境中,Redis 通常是 主从集群 (Master-Slave) 部署的。

主从切换导致的锁丢失

  1. 加锁 :客户端 A 向 Master 节点成功写入锁 lock:order:1

  2. 异步复制 :Master 准备将数据同步给 Slave。注意:Redis 的复制是异步的!

  3. 故障发生:就在数据还没同步到 Slave 的瞬间,Master 宕机了。

  4. 选举上位 :Sentinel 或 Cluster 机制触发,Slave 被提升为新的 Master

  5. 悲剧 :新 Master 的数据里没有 lock:order:1

  6. 并发冲突 :客户端 B 向新 Master 申请锁,成功拿到锁

  7. 结果A 和 B 同时持有锁,互斥性彻底失效。超卖、数据错乱随之而来

    单点Redis:只有一个保安队长。他把钥匙给了你,然后突然晕倒了。新来的队长(Slave)完全不知道刚才发生过什么,又把钥匙给了另一个人。
    后果:两个人都拿着钥匙进了同一个房间。

AP 模型 (Availability + Partition tolerance) 的代价:为了高可用,牺牲了强一致性

Redlock 算法:用"投票"换取"一致"

对此, antirez 提出了 Redlock 算法;不要依赖单个节点,要依赖"多数派"

算法五步走 (The 5 Steps)

部署了 N=5 个独立的 Redis 实例(注意:是独立的,不是主从关系,每个都是 Master)

  1. 获取当前时间 :客户端记录开始时间 T_start

  2. 依次尝试加锁 :客户端按顺序(或并行)向 5 个实例发起加锁请求。

    • 命令:SET resource_name my_random_value NX PX 30000
    • 关键点:必须设置一个较短的超时时间(如自动释放时间),防止某个节点挂死导致客户端无限等待。
  3. 统计成功数 :计算有多少个节点成功返回了加锁结果。

    • 成功条件 :成功节点数 >= N/2 + 1 (即 5 个中至少 3 个成功)。
  4. 验证有效性

    • 计算耗时 T_elapsed = T_now - T_start
    • 锁有效条件T_elapsed < 锁的有效期 (TTL)
    • 如果耗时太长,说明网络太慢或节点太卡,即使凑够了票数,锁也可能已经过期了,视为失败。
  5. 失败清理

    • 如果加锁失败(票数不够 或 超时),客户端必须向所有 5 个节点发送 释放锁 指令(即使有些节点根本没加上),以清理残留状态。

    Redlock = 公司有 5 个保安队长,各自独立保管一把钥匙的副本。
    你想进门,必须至少获得 3 个队长 的同意(给你盖章)。
    哪怕其中 2 个队长晕倒了(宕机),或者其中 2 个队长糊涂了(数据不一致),只要剩下的 3 个清醒的队长同意了,你的进门许可就是有效的。
    安全性:因为总共只有 5 个人,不可能同时有两拨人各自凑齐 3 票(3+3=6 > 5)。这就是数学上的互斥保证。

世纪论战:Martin vs. Antirez

Martin Kleppmann 的质疑 (The Critic)

Martin 发表了著名文章《How to do distributed locking》,指出了 Redlock 的致命弱点:

  1. 时钟漂移 (Clock Drift)
    • Redlock 依赖客户端本地时间计算锁的有效期 (TTL - elapsed)。
    • 如果客户端机器时间跳变(如 NTP 同步导致时间回退),或者不同节点时间不同步,可能导致锁在客户端认为"有效"时,在服务端其实已经"过期"了。
  2. GC 停顿 (Stop-The-World)
    • 如果客户端在拿到锁后,发生了长时间 GC 停顿。
    • 停顿期间,锁在 Redis 端已经过期释放了。
    • 其他客户端拿到了锁。
    • 客户端 GC 结束醒来,以为自己还持有锁 ,继续操作临界区。-> 互斥性失效
    • 注:这个问题 Redisson 的看门狗也解决不了,因为看门狗线程也会被 GC 停住。
  3. 异步刷盘 (Async fsync)
    • 如果 Redis 节点配置了异步刷盘,节点宕机重启后,内存中的锁数据可能丢失。这会导致"多数派"中实际有效的节点数不足。

Antirez 的反击 (The Defender)

  1. 概率极低:他认为时钟漂移和 GC 停顿同时发生的概率极低,对于大多数互联网业务是可以接受的。
  2. fencing token (围栏令牌) :他承认单纯靠锁不够,建议在业务层引入 Fencing Token (每次加锁返回一个递增的版本号),在操作资源(如写数据库)时带上这个版本号,资源方拒绝处理旧版本号的请求。
    • 这其实是把一致性压力转移到了业务资源层(如数据库)

业界真相:为什么大厂慎用 Redlock

阿里、美团、字节等大厂的核心生产环境 中,极少直接使用原生的 Redlock 算法

  1. 性能损耗巨大
    • 单次加锁需要网络交互 N 次 (至少 5 次)。
    • 延迟 = Max(Network_Latency) * N
    • 在高并发秒杀场景下,这会成为巨大的瓶颈,QPS 直接掉一个数量级。
  2. 运维复杂度极高
    • 需要维护 5 个完全独立的 Redis 实例(不能是主从,必须是物理隔离的 Master)。
    • 这增加了基础设施成本和管理难度。
  3. 依然无法解决 GC 问题
    • 正如 Martin 所说,JVM 的 STW 是语言层面的问题,Redlock 救不了。
  4. "杀鸡用牛刀"
    • 对于 99% 的业务,"单点 Redis + 数据库乐观锁兜底" 已经足够安全且高效。为了那 0.01% 的极端情况,引入 Redlock 的复杂性和性能代价,ROI (投入产出比) 太低。

架构师洞察

"在大厂,可用性 (Availability) 往往优于 强一致性 (Consistency)。如果 Redis 集群挂了,宁愿暂时降级(返回'系统繁忙'),也不愿意为了强一致而让整个系统变得缓慢且复杂。"

替代方案:如果不信 Redlock,我们信什么

如果业务真的对一致性要求极高(如金融转账),或者担心 Redis 主从切换丢锁,我们有更成熟的组合拳。

方案 A:Redis (高性能) + 数据库乐观锁 (强兜底)

  • 第一道防线:Redis 分布式锁(单机或哨兵模式)。抗住 99.9% 的并发流量,快速失败。
  • 第二道防线 :数据库 UPDATE ... WHERE version = old_version
    • 即使 Redis 锁丢了,两个线程同时进入 DB 层。
    • 线程 A:UPDATE stock SET num=num-1, version=v+1 WHERE id=1 AND version=v (影响行数 1,成功)。
    • 线程 B:UPDATE ... WHERE version=v (影响行数 0,失败,抛出异常或重试)。
  • 优点:既有了 Redis 的高性能,又有了 DB 的强一致性(ACID)。
  • 缺点:DB 有写压力,但通常扣减库存的 QPS 远小于读 QPS,可接受。

方案 B:ZooKeeper / Etcd (CP 模型)

  • 适用:并发量不大,但对一致性要求极高的场景(如金融核心、元数据管理)。
  • 原理:利用 ZK 的 CP 特性,天然解决主从切换丢数据问题。
  • 缺点:性能不如 Redis,运维成本高。

方案 C:数据库唯一索引 (Unique Index)

  • 适用:防重提交、幂等性控制。
  • 原理 :插入一张 lock_table(biz_id) 设为唯一索引。
  • 优点:绝对可靠,依托 DB 事务。
  • 缺点:性能最差,不适合高频短锁

我们系统业务代码------简版

那肯定不能把真的代码放出来,让大家"鞭尸"

复制代码
@Autowired
private RedissonClient redissonClient;
@Autowired
private ProductMapper productMapper;

public void buyProductSafe(String productId, int userId) {
    RLock lock = redissonClient.getLock("lock:product:" + productId);
    boolean isLocked = false;
    
    try {
        // 1. 尝试获取 Redis 锁 (启用看门狗)
        // 这里的目的是"限流"和"减少 DB 冲突",而不是绝对的互斥
        isLocked = lock.tryLock(5, -1, TimeUnit.SECONDS);
        
        if (!isLocked) {
            throw new BusinessException("排队人数过多,请稍后重试");
        }

        // 2. 执行数据库乐观锁更新
        // 即使 Redis 锁失效,这里也能保证数据一致性
        int affectedRows = productMapper.deductStockWithVersion(productId, 1);
        
        if (affectedRows == 0) {
            // 更新失败,说明库存不足或版本号不匹配(并发冲突)
            throw new BusinessException("库存不足或并发冲突,请重试");
        }
        
        // 3. 创建订单...
        
    } catch (Exception e) {
        // 异常处理
        throw e;
    } finally {
        if (isLocked && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

// Mapper XML 示例
// UPDATE product_stock 
// SET stock = stock - 1, version = version + 1 
// WHERE id = #{id} AND stock >= 1 AND version = #{oldVersion}
// 注意:实际场景中通常不需要查旧 version,直接用 stock >= 1 判断即可,
// 因为 stock 本身就是原子递减的。
// 更简单的写法:
// UPDATE product_stock SET stock = stock - 1 WHERE id = #{id} AND stock > 0
  • 在这个方案中,Redis 锁的作用是**"漏斗"**,过滤掉大部分并发请求,减轻数据库压力。
  • 数据库的 WHERE stock > 0 (或 version 校验) 是**"铁闸"**,保证最终数据的绝对正确。
  • 即使 Redis 主从切换导致锁丢失,最多也就是多几个请求打到数据库,由数据库的原子性来决出胜负,绝不会超卖

彩蛋:

Q1: 请简述 Redlock 算法的原理,以及它解决了什么问题

  1. Redlock 是为了解决 Redis 主从架构下,Master 宕机导致锁数据未同步到 Slave,进而引发锁丢失的问题。
  2. 它通过部署 N 个独立的 Redis 实例,要求客户端必须获得 N/2 + 1 个实例的加锁成功响应,并验证总耗时小于锁有效期,才认为加锁成功。
  3. 利用"多数派"原则保证任意时刻只有一个客户端能凑齐票数,从而实现互斥。

Q2: Martin Kleppmann 对 Redlock 的主要批评是什么

  1. 时钟漂移:依赖客户端本地时间计算有效期,若时间不同步或跳变,可能导致锁安全性失效。
  2. GC 停顿:客户端长时间 GC 停顿会导致锁在服务端已过期,但客户端醒来后仍认为自己持有锁。
  3. 异步刷盘 :节点宕机重启后,若数据未持久化,会导致"多数派"计数虚高。
    他建议引入 Fencing Token 机制,在资源层(如 DB)做最终校验

Q3:在生产环境中,你会选择 Redlock 吗?为什么?

回答通常不会首选 Redlock

原因:

  1. 性能差:网络交互次数多,延迟高,不适合高并发。
  2. 复杂度高:需维护多个独立实例。
  3. 性价比低 :对于绝大多数业务,"Redis 锁 + 数据库乐观锁兜底" 方案既能满足高性能,又能通过 DB 的 ACID 特性保证最终一致性,且实现简单、容错性强。
    只有在极特殊场景(如无法使用 DB 兜底的纯内存操作,且对一致性要求极高)下,才会考虑 Redlock 或直接转向 ZooKeeper/Etcd。

Q4: 如果必须用 Redis 实现强一致性锁,除了 Redlock 还有什么办法?

  1. Redis + 数据库乐观锁(最推荐)。
  2. Redisson 的 MultiLock (注意:这只是逻辑组合,底层若仍是主从,依然有丢锁风险,除非底层是多主)。
  3. 迁移到 CP 模型中间件:如 ZooKeeper 或 Etcd。这是最彻底的解决方案。
相关推荐
程序员阿峰2 小时前
【JavaScript面试题-this 绑定】请说明 `this` 在不同场景下的指向(默认、隐式、显式、new、箭头函数)。
前端·javascript·面试
_饭团2 小时前
指针核心知识:5篇系统梳理2
c语言·笔记·学习·leetcode·面试·改行学it
m0_716667072 小时前
趣味项目与综合实战
jvm·数据库·python
m0_662577972 小时前
Python虚拟环境(venv)完全指南:隔离项目依赖
jvm·数据库·python
霖霖总总2 小时前
[Redis小技巧16]Redis 安全加固与加密传输指南:从基础到高级策略
数据库·redis
四谎真好看2 小时前
Redis学习笔记(实战篇2)
redis·笔记·学习·学习笔记
阿贵---2 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
2401_873204653 小时前
使用Scrapy框架构建分布式爬虫
jvm·数据库·python
m0_716667073 小时前
工具、测试与部署
jvm·数据库·python