RedLock:Redis 分布式锁的高可用方案

RedLock:Redis 分布式锁的高可用方案

一、RedLock 要解决什么问题

单 Redis 节点锁的缺陷

复制代码
时刻T1: 客户端A → 在 Master 加锁成功
时刻T2: Master 宕机(锁数据还没同步到 Slave)
时刻T3: Slave 提升为新 Master(没有锁信息)
时刻T4: 客户端B → 在新 Master 加锁成功
时刻T5: 客户端A 和 B 同时持有锁 → 互斥失效!

即使使用 Redis Sentinel 或 Cluster,也存在这个问题------Redis 主从复制是异步的,切换瞬间可能丢数据。

RedLock 的思路

不依赖单个 Redis 节点,而是同时在多个独立 Redis 节点 上加锁,多数成功才算加锁成功:

复制代码
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│ Redis-1 │  │ Redis-2 │  │ Redis-3 │  │ Redis-4 │  │ Redis-5 │
└────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘
     │            │            │            │            │
     ✅           ✅           ❌           ✅           ❌
     
成功3个 ≥ 多数(3/5) → 加锁成功

即使 1-2 个节点宕机或网络不通,锁依然有效。

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、RedLock 算法流程

加锁步骤

复制代码
1. 记录开始时间 T1

2. 依次在 N 个独立 Redis 节点上执行加锁:
   SET lock_key <random_value> NX PX <ttl>
   - 每个节点设置一个较短的超时(如 5-50ms)
   - 某个节点超时或失败则立即跳过,继续下一个

3. 记录结束时间 T2

4. 计算加锁耗时:elapsed = T2 - T1

5. 判断是否加锁成功:
   - 成功节点数 ≥ N/2 + 1(多数)
   - 且 elapsed < ttl(锁还没过期)
   
6. 如果成功:
   - 锁的实际有效时间 = ttl - elapsed
   
7. 如果失败:
   - 向所有节点发送 DEL 释放锁(包括加锁失败的节点)

解锁步骤

复制代码
向所有 N 个节点发送释放命令(Lua 脚本验证 value):
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
end
return 0

时间线示意

复制代码
          ├─── ttl = 30秒 ───────────────────────────┤
          │                                            │
T1────────T2──────────────────────────────────────────→ 锁过期
│  加锁耗时  │        锁实际可用时间                     │
│  elapsed   │     = ttl - elapsed                     │
│  = 200ms   │     = 29.8秒                            │

三、为什么需要多数成功

多数派(Quorum)原理

这是分布式系统的经典思想,同样用于 Raft、Paxos、ZAB 等共识算法:

复制代码
5个节点中任意 3 个构成多数派
  
客户端A 加锁成功:节点 1, 2, 3
客户端B 加锁尝试:
  - 节点 1:已有锁 → 失败
  - 节点 2:已有锁 → 失败
  - 节点 3:已有锁 → 失败  
  - 节点 4:加锁成功
  - 节点 5:加锁成功
  成功 2 个 < 多数(3) → 加锁失败 ✅ 互斥保证

任何两个多数派至少有一个交集节点------这保证了两个客户端不可能同时获得多数派。

节点宕机的容错

复制代码
5 个节点,允许 2 个同时宕机(N=5, 多数=3, 容错=2)
3 个节点,允许 1 个同时宕机(N=3, 多数=2, 容错=1)

公式:容错数 = N - (N/2 + 1) = (N-1)/2

节点数 N 多数 容错
3 2 1
5 3 2
7 4 3

四、关键约束条件

1. 节点必须独立

复制代码
✅ 正确:5 个独立的 Redis 实例(单机模式)
❌ 错误:1 个 Redis Cluster 的 5 个主节点
❌ 错误:5 个 Redis Sentinel 的 5 个主节点

RedLock 要求每个节点是完全独立的,没有主从复制关系。如果用 Cluster/Sentinel,它们内部的主从同步问题依然存在。

2. 时钟假设

RedLock 依赖一个假设:各节点的时钟漂移在可接受范围内

如果某个节点时钟突然跳变(NTP 校时跳跃),可能导致锁提前过期:

复制代码
节点3 时钟突然快了 20 秒
→ 锁在节点3 上提前 20 秒过期
→ 如果其他节点也到期了,锁可能失效

3. 加锁时间必须远小于 TTL

复制代码
ttl = 30 秒
加锁耗时 elapsed = 200ms
锁有效时间 = 30 - 0.2 = 29.8秒 ✅ 充裕

如果网络延迟导致 elapsed = 28秒
锁有效时间 = 30 - 28 = 2秒 ❌ 几乎没用
→ 应该视为加锁失败

五、代码示例

Redisson 实现

java 复制代码
@Configuration
public class RedLockConfig {

    @Bean
    public RedissonClient redisson1() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://redis-node1:6379");
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redisson2() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://redis-node2:6379");
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redisson3() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://redis-node3:6379");
        return Redisson.create(config);
    }
}
@Service
public class PaymentService {

    @Resource private RedissonClient redisson1;
    @Resource private RedissonClient redisson2;
    @Resource private RedissonClient redisson3;

    public void deductBalance(String accountId, BigDecimal amount) {
        // 从 3 个独立 Redis 节点各获取一把锁
        RLock lock1 = redisson1.getLock("lock:account:" + accountId);
        RLock lock2 = redisson2.getLock("lock:account:" + accountId);
        RLock lock3 = redisson3.getLock("lock:account:" + accountId);

        // 组合为 RedLock
        RLock redLock = redisson1.getRedLock(lock1, lock2, lock3);

        try {
            // 尝试加锁:等待10秒,持有30秒
            boolean acquired = redLock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!acquired) {
                throw new BusinessException("获取锁超时");
            }
            // 临界区
            accountRepository.deduct(accountId, amount);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            redLock.unlock();
        }
    }
}

手动实现(理解原理)

java 复制代码
public class SimpleRedLock {
    
    private final List<RedisTemplate<String, String>> redisNodes;
    private final int quorum; // 多数 = N/2 + 1

    public SimpleRedLock(List<RedisTemplate<String, String>> nodes) {
        this.redisNodes = nodes;
        this.quorum = nodes.size() / 2 + 1;
    }

    public boolean tryLock(String lockKey, String requestId, long ttlMs) {
        long startTime = System.currentTimeMillis();
        int successCount = 0;

        // 依次在每个节点加锁
        for (RedisTemplate<String, String> node : redisNodes) {
            try {
                Boolean result = node.opsForValue()
                    .setIfAbsent(lockKey, requestId, ttlMs, TimeUnit.MILLISECONDS);
                if (Boolean.TRUE.equals(result)) {
                    successCount++;
                }
            } catch (Exception e) {
                // 节点不可用,跳过
            }
        }

        long elapsed = System.currentTimeMillis() - startTime;

        // 判断:多数成功 且 耗时未超过 TTL
        if (successCount >= quorum && elapsed < ttlMs) {
            return true; // 加锁成功
        } else {
            // 失败:释放所有节点的锁
            unlock(lockKey, requestId);
            return false;
        }
    }

    public void unlock(String lockKey, String requestId) {
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then return redis.call('del',KEYS[1]) else return 0 end";
        for (RedisTemplate<String, String> node : redisNodes) {
            try {
                node.execute(new DefaultRedisScript<>(script, Long.class),
                    Collections.singletonList(lockKey), requestId);
            } catch (Exception e) {
                // 节点不可用,忽略
            }
        }
    }
}

六、RedLock 争议

Martin Kleppmann 的批评

分布式系统专家 Martin Kleppmann 发表文章 "How to do distributed locking" 指出 RedLock 的问题:

问题1:GC 停顿导致锁失效
复制代码
时刻T1: 客户端A 获得 RedLock,TTL=30秒
时刻T2: 客户端A 发生 Full GC,暂停 35 秒
时刻T3: 锁已过期(30秒到了)
时刻T4: 客户端B 获得同一把 RedLock
时刻T5: 客户端A GC 结束,以为自己还持有锁 → 两个客户端同时操作
问题2:时钟跳变
复制代码
时刻T1: 客户端A 在节点 1,2,3 加锁成功(TTL=30秒)
时刻T2: 节点1 时钟跳变,NTP 向前调了 30 秒
时刻T3: 节点1 上的锁"过期"了(实际才过了几秒)
时刻T4: 客户端B 在节点 1,4,5 加锁成功 → 互斥失效
他的建议

如果锁仅用于效率优化(避免重复工作):单 Redis 节点就够了,偶尔失效可接受。

如果锁用于正确性保证 (数据一致性):应该用 fencing token(递增令牌):

复制代码
客户端获取锁时同时获得一个递增 token
写入存储时携带 token
存储端拒绝 token 小于当前值的写入

客户端A: token=33,GC暂停...
客户端B: token=34,写入成功
客户端A: GC恢复,token=33 < 34 → 写入被拒绝 ✅

Antirez(Redis 作者)的回应

  • GC 停顿问题:所有分布式锁都有这个问题,不是 RedLock 特有的
  • 时钟跳变:合理的运维可以避免大幅跳变(渐进式 NTP 校时)
  • RedLock 在合理假设下是正确的

争议总结

观点 Martin Kleppmann Antirez
RedLock 安全吗 不够安全,依赖时钟假设 在合理假设下安全
推荐方案 需要正确性用 ZooKeeper + fencing token RedLock 足够大多数场景
适用性 RedLock 不比单节点强多少 RedLock 明显强于单节点

七、什么时候该用/不该用 RedLock

适合用 RedLock

  • 金融级场景:支付扣款、余额操作
  • 库存操作:防止超卖
  • 对互斥性要求高,但能接受极端情况下的短暂失效
  • 已有多个 Redis 实例的基础设施

不需要 RedLock

  • 普通业务防重:单 Redis 节点 + 幂等兜底即可
  • 效率型锁:避免重复计算,偶尔失效无业务影响
  • 缓存击穿保护:单节点锁足够

应该用 ZooKeeper 而非 RedLock

  • 强一致性要求:不允许任何双写的场景
  • Leader 选举
  • 分布式协调(配置分发、服务发现)

八、替代方案对比

方案 一致性 性能 复杂度 容错
单 Redis 锁 弱(主从切换可能丢) 最高 最低
RedLock 中(依赖时钟假设) N/2 节点宕机
ZooKeeper 锁 强(ZAB 协议共识) N/2 节点宕机
etcd 锁 强(Raft 协议共识) 中高 N/2 节点宕机
数据库锁 强(事务保证) 依赖数据库高可用

九、实际项目中的选择建议

复制代码
                    ┌─ 效率型(防重复工作)→ 单 Redis 节点
                    │
需要分布式锁 ───────┼─ 一般业务(订单、库存)→ 单 Redis + 幂等兜底
                    │                        或 Redisson 普通锁
                    │
                    └─ 强一致(资金、对账)→ ZooKeeper / etcd
                                            或 RedLock + fencing token

大多数业务场景:Redisson 普通锁 + 业务层幂等 就足够了。RedLock 适合在已有多 Redis 节点基础设施、且对互斥性有更高要求时使用。

十、总结

概念 一句话
RedLock 在 N 个独立 Redis 节点上加锁,多数成功才算成功
多数派 N/2+1 个节点同意 = 共识达成
为什么需要 单节点主从切换可能丢锁
核心假设 节点间时钟漂移在可接受范围内
争议点 GC停顿和时钟跳变可能导致失效
实际建议 大多数场景单节点锁+幂等即可,极端场景考虑 ZooKeeper
相关推荐
Amir_zy2 小时前
Redis测试方法全攻略:冒烟、压测与稳定性验证
redis·压力测试
IvorySQL2 小时前
PostgreSQL 日报| RI 快速路径并发读取缺陷(8 月 13 日)
数据库·postgresql
highreport3 小时前
HighReport报表工具定时调度和邮件定时发送
运维·服务器·数据库
01_ice3 小时前
数据库基础
数据库
naierfengdian3 小时前
分布式风电助力乡村能源转型的技术路径
分布式·能源
deepdata_cn3 小时前
向量数据库是非结构化数据的AI检索底座
数据库·人工智能
梁辰兴3 小时前
软件工程:数据库设计
数据库·软件工程·数据库设计·逻辑设计·概念设计·梁辰兴·物理设计
10mAh3 小时前
【Git】误删分支、reset --hard 后提交丢了怎么办?——reflog、fsck 与安全恢复实战
数据库·git·安全·回归
J_bean3 小时前
剖析 MySQL InnoDB 共享行锁 (S) & 排他行锁 (X)
数据库·mysql·共享行锁·s lock·排他行锁·x lock