举个真实的例子:你的团队刚上线了一个秒杀系统,用Redis锁来防止超卖。测试环境明明跑得好好的,但大促当晚却出现了100件库存卖出了120单的灵异事件。查看日志才发现:就在用户疯狂点击的瞬间,Redis主节点突然挂了,新的主节点还没拿到锁的信息,结果两个用户同时抢到了"同一把锁"。
这就是很多开发者踩过的坑——你以为用了Redis分布式锁就万事大吉,其实这些情况随时可能让锁失效:
- 主节点刚给你加完锁就崩溃了,从节点接班时一脸懵:“什么锁?我没听说过啊”
- 你的程序正处理到一半突然卡住了(比如GC停顿),等回过神来锁早就过期了
- 网络抽风导致锁信息没传到位,多个客户端都觉得自己拿到了锁
为了解决这些头疼问题,Redis作者提出了**红锁(RedLock)**方案。简单来说就是"不要把鸡蛋放在一个篮子里":让多个独立的Redis节点投票决定锁的归属,只有半数以上同意才算真正拿到锁。
但这套方案也引发过激烈争论,有人甚至说它"数学上就不安全"。本文将用最直白的语言:
- 先带你看看传统Redis锁在集群环境为什么容易翻车
- 拆解红锁这个"少数服从多数"的解决方案
- 手把手教你用Java代码实现红锁
- 揭秘Redisson框架如何简化红锁的使用
读完本文你会明白:没有完美的分布式锁,只有适合场景的选择。下次设计系统时,至少能清楚知道手里的锁到底有几成把握。
集群锁的缺陷与挑战
在Redis Cluster环境中,传统的分布式锁存在以下致命缺陷:主从切换导致锁失效。
问题步骤复现:
- 客户端A通过
- SET key random_val NX PX 30000
复制代码 在主节点成功获取锁
- 主节点宕机,Redis Cluster触发故障转移,从节点升级为新主节点
- 由于Redis主从复制是异步的,锁可能未同步到新主节点
- 客户端B向新主节点申请相同资源的锁,成功获取导致数据竞争
- # 主节点写入锁
- SET resource_1 8a3e72 NX PX 10000
- OK
- # 主节点宕机,从节点晋升但未同步锁数据
- # 新主节点处理客户端B的请求
- SET resource_1 5b9fd2 NX PX 10000
- OK # 锁被重复获取!
复制代码 红锁(RedLock)的设计与实现
在N个独立Redis节点(非Cluster模式)中,当客户端在半数以上节点成功获取锁,且总耗时小于锁有效期时,才认为锁获取成功。
实现步骤详解
假设部署5个Redis节点(N=5):
- 获取当前时间:记录开始时间(毫秒精度)
- 依次向所有节点申请锁:
- SET lock_key valueNX PX $ttl
复制代码
- :全局唯一值(如UUID)
- :锁自动释放时间(如10秒)
- 客户端计算获取锁总耗时(T2为最后响应时间)
- 仅当以下两个条件满足时,锁才有效:
成功获取锁的节点数 ≥ 3(N/2 + 1)(确保锁未过期)
- 加锁成功,去操作共享资源
- 释放锁:向所有节点发送Lua脚本删除锁(需验证值)
- if redis.call("get",KEYS[1]) == ARGV[1] then
- return redis.call("del",KEYS[1])
- else
- return 0
- end
复制代码 NPC争议问题
红锁算法自诞生起就伴随着**N(网络延迟)、P(进程暂停)、C(时钟漂移)**三个核心争议,这些现实世界中的不确定因素,动摇了红锁在数学意义上的绝对安全性。
网络延迟(Network Delay)的致命时间差
问题场景:
- 客户端在节点A、B、C成功获取锁,总耗时48ms(小于TTL 50ms)
- 但由于跨机房网络波动,实际锁在节点上的有效时间存在差异:
- 节点A记录的锁过期时间:客户端本地时间+50ms = T+50
- 节点B因网络延迟,实际锁过期时间为T+52
- 节点C因网络拥塞,实际锁过期时间仅T+48
- 在时间窗口到之间,客户端认为锁仍有效,但节点C的锁已提前失效
后果:
其他客户端可能在此期间获取节点C的锁,导致锁状态分裂,多个客户端同时进入临界区。
进程暂停(Process Pause)的「薛定谔锁」
经典案例:- // 伪代码:获取锁后执行业务逻辑
- if (redLock.tryLock()) {
- // 触发Full GC暂停300ms
- System.gc();
-
- // 此时锁已过期,但客户端仍在写数据
- updateInventory();
- }
复制代码 关键时间线:
- T0: 获取锁(TTL=200ms)
- T0+100ms: 进入GC暂停,持续300ms
- T0+400ms: GC结束,继续执行业务逻辑
- 锁实际在T0+200ms已失效,但客户端在T0+400ms仍以为自己持有锁
数据灾难:
其他客户端在T0+200ms到T0+400ms期间可能修改数据,导致最终结果错乱。
时钟漂移(Clock Drift)的时空扭曲
物理机时钟偏移实验数据:
节点时钟误差范围常见诱因节点A±200ms/分钟虚拟机时钟不同步节点B±500ms/天NTP服务异常节点C±10秒/小时宿主机硬件时钟故障连锁反应:
- 客户端计算锁有效期基于本地时钟(假设为T+100ms)
- 但节点B的时钟比实际快30秒,导致其记录的锁过期时间为T-29000ms
- 锁在客户端认为的有效期内提前被节点B自动释放
行业领袖的正面交锋
Martin Kleppmann(《数据密集型应用设计》作者):- “红锁依赖的假设——『客户端能准确感知锁存活时间』,在异步分布式系统中根本无法保证。即使没有节点故障,NPC问题也会导致锁状态的不确定性。”
复制代码 Antirez(Redis作者)的反驳:- "工程实践中可以通过以下手段控制风险:
- 使用带温度补偿的原子钟硬件
- 禁用NTP服务的时钟跳变调整
- 监控进程暂停(如GC日志分析)
- 为锁TTL设置冗余缓冲时间(如额外20%)"
复制代码 红锁的Java实现示例
使用Jedis客户端实现红锁:- package com.morris.redis.demo.redlock;
- import redis.clients.jedis.Jedis;
- import redis.clients.jedis.JedisPool;
- import redis.clients.jedis.params.SetParams;
- import java.util.Collections;
- import java.util.List;
- import java.util.UUID;
- import java.util.concurrent.TimeUnit;
- /**
- * 使用jedis手写RedLock
- */
- public class JedisRedLock {
- public static final int EXPIRE_TIME = 30_000;
- private final List<JedisPool> jedisPoolList;
- private final String lockKey;
- private final String lockValue;
- public JedisRedLock(List<JedisPool> jedisPoolList, String lockKey) {
- this.jedisPoolList = jedisPoolList;
- this.lockKey = lockKey;
- this.lockValue = UUID.randomUUID().toString();
- }
- public void lock() {
- while (!tryLock()) {
- try {
- TimeUnit.MILLISECONDS.sleep(100); // 失败后短暂等待
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
- }
- public boolean tryLock() {
- long startTime = System.currentTimeMillis();
- int successCount = 0;
- try {
- for (JedisPool jedisPool : jedisPoolList) {
- try (Jedis jedis = jedisPool.getResource();) {
- // 原子化加锁:SET lockKey UUID NX PX expireTime
- String result = jedis.set(lockKey, lockValue,
- SetParams.setParams().nx().px(EXPIRE_TIME));
- if ("OK".equals(result)) {
- successCount++;
- }
- }
- }
- // 计算获取锁耗时
- long elapsedTime = System.currentTimeMillis() - startTime;
- // 验证:多数节点成功 且 耗时小于TTL
- return successCount >= (jedisPoolList.size() / 2 + 1) && elapsedTime < EXPIRE_TIME;
- } finally {
- // 若加锁失败,立即释放已获得的锁
- if (successCount < (jedisPoolList.size() / 2 + 1)) {
- unlock();
- }
- }
- }
- public void unlock() {
- String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
- for (JedisPool jedisPool : jedisPoolList) {
- try (Jedis jedis = jedisPool.getResource()) {
- jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
- }
- }
- }
- }
复制代码 手写RedLock的使用:- package com.morris.redis.demo.redlock;
- import redis.clients.jedis.JedisPool;
- import redis.clients.jedis.JedisPoolConfig;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.TimeUnit;
- /**
- * 手写RedLock的使用
- */
- public class JedisRedLockDemo {
- private volatile static int count;
- public static void main(String[] args) throws InterruptedException {
- List<JedisPool> jedisPoolList = new ArrayList<>();
- jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379));
- jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6380));
- jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6381));
- jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6382));
- jedisPoolList.add(new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6383));
- int threadCount = 3;
- CountDownLatch countDownLatch = new CountDownLatch(threadCount);
- ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
- for (int i = 0; i < threadCount; i++) {
- executorService.submit(() -> {
- JedisRedLock jedisRedLock = new JedisRedLock(jedisPoolList, "lock-key");
- jedisRedLock.lock();
- try {
- System.out.println(Thread.currentThread().getName() + "获得锁,开始执行业务逻辑。。。");
- try {
- TimeUnit.SECONDS.sleep(3);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- System.out.println(Thread.currentThread().getName() + "获得锁,结束执行业务逻辑。。。");
- count++;
- } finally {
- jedisRedLock.unlock();
- }
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(count);
- }
- }
复制代码 Redisson中红锁的使用
Redisson已封装红锁实现,自动处理节点通信与锁续期:- package com.morris.redis.demo.redlock;
- import org.redisson.Redisson;
- import org.redisson.RedissonRedLock;
- import org.redisson.api.RLock;
- import org.redisson.api.RedissonClient;
- import org.redisson.config.Config;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.List;
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.TimeUnit;
- /**
- * Redisson中红锁的使用
- */
- public class RedissonRedLockDemo {
- private volatile static int count;
- public static void main(String[] args) throws InterruptedException {
- List<String> serverList = Arrays.asList("redis://127.0.0.1:6379", "redis://127.0.0.1:6380", "redis://127.0.0.1:6381",
- "redis://127.0.0.1:6382", "redis://127.0.0.1:6383");
- List<RedissonClient> redissonClientList = new ArrayList<>(serverList.size());
- for (String server : serverList) {
- Config config = new Config();
- config.useSingleServer()
- .setAddress(server);
- redissonClientList.add(Redisson.create(config));
- }
- List<RLock> lockList = new ArrayList<>(redissonClientList.size());
- for (RedissonClient redissonClient : redissonClientList) {
- lockList.add(redissonClient.getLock("java-lock"));
- }
-
- int threadCount = 3;
- CountDownLatch countDownLatch = new CountDownLatch(threadCount);
- ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
- for (int i = 0; i < threadCount; i++) {
- executorService.submit(() -> {
- RedissonRedLock redissonRedLock = new RedissonRedLock(lockList.toArray(new RLock[0]));
- redissonRedLock.lock();
- try {
- System.out.println(Thread.currentThread().getName() + "获得锁,开始执行业务逻辑。。。");
- try {
- TimeUnit.SECONDS.sleep(3);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- System.out.println(Thread.currentThread().getName() + "获得锁,结束执行业务逻辑。。。");
- count++;
- } finally {
- redissonRedLock.unlock();
- }
- countDownLatch.countDown();
- });
- }
- countDownLatch.await();
- executorService.shutdown();
- System.out.println(count);
- for (RedissonClient redissonClient : redissonClientList) {
- redissonClient.shutdown();
- }
- }
- }
复制代码 Redisson优势:
- 自动续期:通过WatchDog机制延长锁有效期
- 简化API:封装底层细节,支持异步/响应式编程
- 故障容错:自动跳过宕机节点,保证半数以上成功即可
总结
红锁通过多节点投票机制,显著提升了分布式锁的可靠性,但需权衡其实现复杂度与运维成本。建议在以下场景选择红锁:
- 需要跨机房/地域部署
- 业务对数据一致性要求极高
- 已具备独立Redis节点运维能力
对于大多数场景,可优先使用Redisson等成熟框架,避免重复造轮子。若对一致性有极致要求,可考虑ZooKeeper/etcd等基于共识算法的方案。
到此这篇关于redis实现红锁的示例代码的文章就介绍到这了,更多相关redis实现红锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源:https://www.jb51.net/database/339185gm4.htm
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
|