拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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

RedLockRedis 分布式锁的高可用方案一、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 个同时宕机N5, 多数3, 容错2 3 个节点允许 1 个同时宕机N3, 多数2, 容错1公式容错数 N - (N/2 1) (N-1)/2节点数 N多数容错321532743四、关键约束条件1. 节点必须独立✅ 正确5 个独立的 Redis 实例单机模式 ❌ 错误1 个 Redis Cluster 的 5 个主节点 ❌ 错误5 个 Redis Sentinel 的 5 个主节点RedLock 要求每个节点是完全独立的没有主从复制关系。如果用 Cluster/Sentinel它们内部的主从同步问题依然存在。2. 时钟假设RedLock 依赖一个假设各节点的时钟漂移在可接受范围内。如果某个节点时钟突然跳变NTP 校时跳跃可能导致锁提前过期节点3 时钟突然快了 20 秒 → 锁在节点3 上提前 20 秒过期 → 如果其他节点也到期了锁可能失效3. 加锁时间必须远小于 TTLttl 30 秒 加锁耗时 elapsed 200ms 锁有效时间 30 - 0.2 29.8秒 ✅ 充裕 如果网络延迟导致 elapsed 28秒 锁有效时间 30 - 28 2秒 ❌ 几乎没用 → 应该视为加锁失败五、代码示例Redisson 实现ConfigurationpublicclassRedLockConfig{BeanpublicRedissonClientredisson1(){ConfigconfignewConfig();config.useSingleServer().setAddress(redis://redis-node1:6379);returnRedisson.create(config);}BeanpublicRedissonClientredisson2(){ConfigconfignewConfig();config.useSingleServer().setAddress(redis://redis-node2:6379);returnRedisson.create(config);}BeanpublicRedissonClientredisson3(){ConfigconfignewConfig();config.useSingleServer().setAddress(redis://redis-node3:6379);returnRedisson.create(config);}}ServicepublicclassPaymentService{ResourceprivateRedissonClientredisson1;ResourceprivateRedissonClientredisson2;ResourceprivateRedissonClientredisson3;publicvoiddeductBalance(StringaccountId,BigDecimalamount){// 从 3 个独立 Redis 节点各获取一把锁RLocklock1redisson1.getLock(lock:account:accountId);RLocklock2redisson2.getLock(lock:account:accountId);RLocklock3redisson3.getLock(lock:account:accountId);// 组合为 RedLockRLockredLockredisson1.getRedLock(lock1,lock2,lock3);try{// 尝试加锁等待10秒持有30秒booleanacquiredredLock.tryLock(10,30,TimeUnit.SECONDS);if(!acquired){thrownewBusinessException(获取锁超时);}// 临界区accountRepository.deduct(accountId,amount);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}finally{redLock.unlock();}}}手动实现理解原理publicclassSimpleRedLock{privatefinalListRedisTemplateString,StringredisNodes;privatefinalintquorum;// 多数 N/2 1publicSimpleRedLock(ListRedisTemplateString,Stringnodes){this.redisNodesnodes;this.quorumnodes.size()/21;}publicbooleantryLock(StringlockKey,StringrequestId,longttlMs){longstartTimeSystem.currentTimeMillis();intsuccessCount0;// 依次在每个节点加锁for(RedisTemplateString,Stringnode:redisNodes){try{Booleanresultnode.opsForValue().setIfAbsent(lockKey,requestId,ttlMs,TimeUnit.MILLISECONDS);if(Boolean.TRUE.equals(result)){successCount;}}catch(Exceptione){// 节点不可用跳过}}longelapsedSystem.currentTimeMillis()-startTime;// 判断多数成功 且 耗时未超过 TTLif(successCountquorumelapsedttlMs){returntrue;// 加锁成功}else{// 失败释放所有节点的锁unlock(lockKey,requestId);returnfalse;}}publicvoidunlock(StringlockKey,StringrequestId){Stringscriptif redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end;for(RedisTemplateString,Stringnode:redisNodes){try{node.execute(newDefaultRedisScript(script,Long.class),Collections.singletonList(lockKey),requestId);}catch(Exceptione){// 节点不可用忽略}}}}六、RedLock 争议Martin Kleppmann 的批评分布式系统专家 Martin Kleppmann 发表文章“How to do distributed locking”指出 RedLock 的问题问题1GC 停顿导致锁失效时刻T1: 客户端A 获得 RedLockTTL30秒 时刻T2: 客户端A 发生 Full GC暂停 35 秒 时刻T3: 锁已过期30秒到了 时刻T4: 客户端B 获得同一把 RedLock 时刻T5: 客户端A GC 结束以为自己还持有锁 → 两个客户端同时操作问题2时钟跳变时刻T1: 客户端A 在节点 1,2,3 加锁成功TTL30秒 时刻T2: 节点1 时钟跳变NTP 向前调了 30 秒 时刻T3: 节点1 上的锁过期了实际才过了几秒 时刻T4: 客户端B 在节点 1,4,5 加锁成功 → 互斥失效他的建议如果锁仅用于效率优化避免重复工作单 Redis 节点就够了偶尔失效可接受。如果锁用于正确性保证数据一致性应该用fencing token递增令牌客户端获取锁时同时获得一个递增 token 写入存储时携带 token 存储端拒绝 token 小于当前值的写入 客户端A: token33GC暂停... 客户端B: token34写入成功 客户端A: GC恢复token33 34 → 写入被拒绝 ✅AntirezRedis 作者的回应GC 停顿问题所有分布式锁都有这个问题不是 RedLock 特有的时钟跳变合理的运维可以避免大幅跳变渐进式 NTP 校时RedLock 在合理假设下是正确的争议总结观点Martin KleppmannAntirezRedLock 安全吗不够安全依赖时钟假设在合理假设下安全推荐方案需要正确性用 ZooKeeper fencing tokenRedLock 足够大多数场景适用性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/21 个节点同意 共识达成为什么需要单节点主从切换可能丢锁核心假设节点间时钟漂移在可接受范围内争议点GC停顿和时钟跳变可能导致失效实际建议大多数场景单节点锁幂等即可极端场景考虑 ZooKeeper
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门