我们公司使用了 6 年的分布式锁,很是牛逼啊

本文深入探讨了基于Redis实现的分布式锁的加锁和释放锁过程,分析了其设计思路与潜在问题,并讨论了续期和高可用性的解决方案。

点击“开发者技术前线”,选择“星标?”

13:21 在看|星标|留言,  真爱

640?wx_fmt=jpeg

转自:踩刀诗人

www.cnblogs.com/chopper-poet/p/10802242.html

前言

提到数据一致性、操作原子性,诸如此类的一些与并发有关的词汇时不知道你第一时间会联想到什么呢?我相信大多数人可能会想到“锁”,为什么是锁呢,这个我不多说,大家心里应该都明白。在单体应用时代,我们使用jvm提供的锁就可以很好的工作,但是到了分布式应用时代,jvm提供的锁就行不通了,那么势必要借助一些跨jvm的临界资源来支持锁的相关语义,比如redis,zookeeper等。

步入正题

我今天就来分享下我司基于redis来实现的分布式锁,2013年投入使用,也算是久经沙场。但是也存在一些设计上的缺陷,这个我后面也会提到,希望大家秉着互相学习的态度文明交流,别一上来就说这不行那不行,还是那句话“适合自己的才是最好的”。

加锁过程分析

640?wx_fmt=jpeg

我第一次读代码的时候,有这么几个疑惑:

Q1:为什么不使用 SET key value [expiration EX seconds|PX milliseconds] [NX|XX]  这个指令来实现key的自动过期呢,反而放到应用代码判断key是否过期?

A1:我们的分布式锁开发的时候SET命令还不支持NX、PX,所以才想出这种办法来实现key过期,NX、PX在2.6.12以后开始支持;

Q2:已经判断了当前key对应的时间戳已经过期了,为什么还要使用getset再获取一次呢,直接使用set指令覆盖不可以吗?

A2:这里其实牵扯到并发的一些事情,如果直接使用set,那有可能多个客户端会同时获取到锁,如果使用getset然后判断旧值是否过期就不会有这个问题,设想一下如下场景:

  1. C1加锁成功,不巧的是,这时C1意外的奔溃了,自然就不会释放锁;

  2. C2,C3尝试加锁,这时key已存在,所以C2,C3去判断key是否已过期,这里假设key已经过期了,所以C2,C3使用set指令去设置值,那两个都会加锁成功,这就闯大祸了;如果使用getset指令,然后判断下返回值是否过期就可以避免这种问题,假如C2跑的快,那C3判断返回的时间戳已经过期,自然就加锁失败;

释放锁过程分析

640?wx_fmt=jpeg

Q1:为什么释放锁时还需要判断key是否过期呢,直接del不是性能更高吗?

A1:考虑这样一种场景:

  1. C1获取锁成功,开始执行自己的操作,不幸的是C1这时被阻塞了;

  2. C2这时来获取锁,由于C1被阻塞了很长时间,所以key对应的value已经过期了,这时C2通过getset加锁成功;

  3. C1尘封了太久终于被再次唤醒,对于释放锁这件事它可是认真的,伴随着一波del操作,悲剧即将发生;

  4. C3来获取锁,好家伙,居然一下就成功了,接着就是一波操作猛如虎,接着就是一堆的客诉过来了;

为什么会这样呢?回想C1被唤醒以后的事情,居然敢直接del,C2活都没干完呢,锁就被C1给释放了,这时C3来直接就加锁成功,所以为了安全起见C3释放锁时得分成两步:1.判断value是否已经过期 2.如果已过期直接忽略,如果没过期就执行del。这样就真的安全了吗?安全了吗?安全了吗?假如第一步和第二步之间相隔了很久是不是也会出现锁被其他人释放的问题呢?是吧?是的!有没有别的解决办法呢?听说借助lua就可以解决这个问题了。

正视自己的缺点

Q1:Redis锁的过期时间小于业务的执行时间该如何续期?

A1:这个暂时没有实现,据说有一个叫Redisson的家伙解决了这个问题,我们也有部分业务在使用,未来有可能会切换到Redisson。

Q2:怎么实现的高可用?

A2:我们采用Failover机制,初始化redis锁的时候会维护一个redis连接池,加锁或者释放锁的时候采用多写的方式来保障一致性,如果某个节点不可用的时候会自动切换到其他节点,但是这种机制可能会导致多个客户端同时获取到锁的情况,考虑这种情况:

  1. C1去redis1加锁,加锁成功后会写到redis2,redis3;

  2. C2也去redis1加锁,但是此时C2到redis1的网络出现问题,这时C2切换到redis2去加锁,由于第一步中的redis多写并不是原子的,所有就有可能导致C2也获取锁成功;

针对这种情况,目前有些业务方是通过数据库唯一索引的方式来规避的,未来会修复这个bug,具体方案目前还没有。

学习者可以进Java高级群,回复:“加群" 我会java我拍谁?

---END---

选择”开发者技术前线 “星标?,内容一触即达。点击原文更多惊喜!

开发者技术前线 汇集技术前线快讯和关注行业趋势,大厂干货,是开发者经历和成长的优秀指南。

历史推荐

Google Python 编程风格指南

2019 秋季最新最全面 JAVA 面试题 附答案

如何用思维导图学 Java 编程?(收藏版)

2019,Vue 开发最佳指南,你都需要学点啥?

640?

点个在看,解锁更多惊喜!

<think>我们正在讨论Redisson如何实现可重入的分布式锁。根据引用[1][2][3][4][5],Redisson使用Redis的hash结构存储锁信息,其中key是锁名称,field是客户端ID+线程ID,value是重入次数。获取锁时,先检查锁是否存在,若不存在则设置锁;若存在且是当前线程持有,则重入次数加1。释放锁时,重入次数减1,直到为0才删除锁。整个过程使用Lua脚本保证原子性。</think>### Redisson 可重入分布式锁实现原理 #### 一、核心数据结构 Redisson 使用 **Redis Hash 结构**存储锁信息: ```redis Key: "lock:business" // 锁名称 Field: "<客户端ID>:<线程ID>" // 持有者标识(如8743c9c0-0795-4907-87fd-6c719a6b4586:1) Value: <重入次数> // 锁的持有次数 ``` 示例数据: ```json { "8743c9c0-0795-4907-87fd-6c719a6b4586:1": 3 } ``` > 该结构表示客户端`8743...`的线程`1`已重入锁3次[^5] #### 二、加锁流程(原子操作) 通过 **Lua脚本** 实现原子操作: ```lua -- 参数说明:KEYS[1]=锁名称, ARGV[1]=超时时间, ARGV[2]=客户端ID:线程ID if (redis.call('exists', KEYS[1]) == 0) then -- 锁不存在:直接获取锁 redis.call('hincrby', KEYS[1], ARGV[2], 1) redis.call('pexpire', KEYS[1], ARGV[1]) return nil -- 返回nil表示成功 end if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 当前线程已持有锁:重入次数+1 redis.call('hincrby', KEYS[1], ARGV[2], 1) redis.call('pexpire', KEYS[1], ARGV[1]) return nil end -- 锁被其他线程持有:返回锁剩余生存时间(毫秒) return redis.call('pttl', KEYS[1]) ``` > 脚本包含三个关键分支:首次获取、重入、锁冲突[^3] #### 三、释放锁流程 ```lua -- 参数说明:KEYS[1]=锁名称, KEYS[2]=解锁消息通道, ARGV[1]=超时时间, ARGV[2]=客户端ID:线程ID local counter = redis.call('hget', KEYS[1], ARGV[2]) if (counter == false) then return nil end if (tonumber(counter) > 1) then -- 重入次数>1:次数减1 redis.call('hincrby', KEYS[1], ARGV[2], -1) redis.call('pexpire', KEYS[1], ARGV[1]) return 0 else -- 最后一次持有:删除锁并发布解锁消息 redis.call('del', KEYS[1]) redis.call('publish', KEYS[2], ARGV[1]) return 1 end ``` #### 四、重入实现关键点 1. **线程标识唯一性** 组合`客户端ID(RedissonClient UUID) + 线程ID`,确保分布式环境下唯一[^3] 2. **重入计数机制** ```java void method1() { lock.lock(); try { method2(); // 嵌套调用 } finally { lock.unlock(); } } void method2() { lock.lock(); // 重入锁:计数+1 try { // ... } finally { lock.unlock(); // 计数-1 } } ``` 3. **锁续期机制** 后台线程(`Watchdog`)定期(默认10秒)检查: - 若线程仍持有锁,重置超时时间为30秒 - 防止业务未完成时锁自动过期[^4] #### 五、与其他实现对比 | 特性 | SETNX+EXPIRE | Redisson | |---------------------|--------------------------|--------------------------| | **可重入性** | ❌ 不支持 | ✔️ 基于Hash结构计数 | | **锁续期** | ❌ 需自行实现 | ✔️ Watchdog自动续期 | | **锁释放原子性** | ❌ DEL非原子 | ✔️ Lua脚本保证原子性 | | **等待队列** | ❌ 轮询消耗资源 | ✔️ Semaphore+Pub/Sub | #### 六、等待获取锁机制 当锁被占用时,通过 **Semaphore + Redis Pub/Sub** 实现高效等待: 1. 订阅锁释放消息通道 2. 使用`Semaphore.tryAcquire(ttl)`限时等待 3. 收到解锁通知后立即重试获取锁[^4] ```java // 伪代码实现 while (true) { Long ttl = tryAcquireLock(); // 尝试获取锁 if (ttl == null) break; // 获取成功 if (ttl > 0) { semaphore.tryAcquire(ttl, MILLISECONDS); // 限时等待 } else { semaphore.acquire(); // 阻塞等待 } } ``` > **设计优势**:Redisson 将复杂的分布式锁逻辑(可重入、锁续期、等待队列)封装为原子操作,开发者只需关注业务代码[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值