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。这是最彻底的解决方案。
相关推荐
yangyanping201089 小时前
Go语言学习之对象关系映射GORM
jvm·学习·golang
南汐以墨10 小时前
一个另类的数据库-Redis
数据库·redis·缓存
Cosolar11 小时前
大模型工具调用输出JSON:凭什么能保证不出错?
人工智能·面试·llm
Cosolar11 小时前
Harness:大模型Agent的“操作系统”,2026年AI工程化的核心革命
人工智能·面试·llm
姗姗的鱼尾喵12 小时前
Spring/SpringBoot 面试高频(含IOC/AOP/事务)
java·spring boot·面试
一个有温度的技术博主12 小时前
Redis AOF持久化:用“记账”的方式守护数据安全
redis·分布式·缓存
RATi GORI13 小时前
springBoot连接远程Redis连接失败(已解决)
spring boot·redis·后端
Zzxy13 小时前
Spring Boot 集成 Redisson 实现分布式锁
spring boot·redis
前端Hardy14 小时前
大厂都在偷偷用的 Cursor Rules 封装!告别重复 Prompt,AI 编程效率翻倍
前端·javascript·面试
前端Hardy14 小时前
Cursor Rules 完全指南(2026 最新版)
前端·javascript·面试