分布式锁中-基于 Redis 的实现如何防重入
theme: smartblue
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第 9 天,点击查看活动详情
篇幅太长看着也累,每天进步一点点
欢迎关注微信公众号「架构染色」交流和学习
前情回顾
分布式锁系列内容规划如下,本篇是第 5 篇:
- 《分布式锁上-初探》
- 《分布式锁中-基于 Zookeeper 的实现是怎样》
- 《分布式锁中-基于 etcd 的实现很优雅》
- 《分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇》
- 《分布式锁中-基于 Redis 的实现如何防重入》(本篇)
- 《分布式锁中-基于 Redis 的实现很多样 - Redission 篇》(写作中)
- 《分布式锁中-多维度的对比各种分布式锁实现》(写作中)
- 《分布式锁下-分布式锁客户端的抽象、适配与加固》(写作中)
一、背景
昨晚同事小窗咨询说,当前基于 Jedis 实现的分布式锁,在他们的一个业务场景中不合适,沟通之后了解到他们是想要一个防重入的锁,那同事所描述的重入是什么意思呢,看下图:
上图是举例描述一个重入的场景,有一个请求被重复提交给 Service-A 了(可能是用户重复点击提交请求,也可能是 RPC 的重试所触发,也可能是其他的情况),对于 Service-A 来说因为没有幂等机制导致 DB 中插入了多条记录,虽然这种情况很少见,但对 Service-A 说一旦遇到,产生了垃圾数据就会比较麻烦;因此同事并不希望相同的请求因为一些偶发异常,而导致自己产生重复处理、记录。
二、需求
同事是希望对现有的分布式锁增加防重入的能力,以便达到在某个时间窗口内,重复多余的请求只会被处理一次,如下图:
三、分析
如何将这个能力融入现有的分布式锁组件中呢?梳理之后,给锁增加类型属性,传统的分布式锁若归类为重用锁(A 持锁后,B 来抢锁只要没超时就一直重试抢锁,抢到就用),而这种防重入场景下的锁可理解为”一次性“锁(A 持锁后,B 来抢锁只要锁已存在,则立即放弃),这个归类命名并不权威,只是这么区分方便理解。这么区分之后,将新需求整理如下:
- 使用者在构建锁的时候,可指定锁类型为”一次性“锁,并设定过期时间,不续租,不主动释放锁,锁过期后会被自动删除;
- 使用者在构建锁的时候,可指定锁类型为”一次性“锁,并设定过期时间,持锁后会自动续租,若持锁客户端是存活状态则会在完成一个请求的处理后主动释放锁。若持锁的客户端挂掉了,等锁过期后会自动被删除。但只要锁被释放后,就可以继续抢锁。
四、设计
调整加锁流程的逻辑,当发现锁已经存在后,增加一段逻辑,判断是否是”一次性“锁(下图褐色部分,应该是褐色吧),如果是则立即返回。如此即实现了在某个时间窗口内,锁是”一次性“的效果。流程图如下:
五、待确认事项
5.1 好像哪里有点不放心
我们使用的是 SET 指令来实现加锁的逻辑,指令形式如下:
SET键值[NX | XX] [GET] [EX 秒 | PX 毫秒 |
EXAT unix 时间秒 | PXAT unix 时间毫秒 | 保持]
1)加锁成功的逻辑是这样:
- 判断 key 是否存在
- 若 key 不存在,就设置 key
- 给 key 指定过期时间
2)加锁不成功的逻辑是这样:
- 判断 key 是否存在
- 若 key 已存在,则返回
SetParams params = SetParams.setParams().nx().ex(lockState.getLeaseTTL());
String result = client.set(lockState.getLockKey(), lockState.getLockValue(), params);
上边代码是之前《分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇》中写的加锁逻辑,其中只根据正常加锁的返回值来判断是否加锁成功,即 result 是不是 "OK",但 key 已存在导致加锁不成功的返回值到底是什么,应该如何判断呢?
5.1 SET 的返回值都有什么
在官网中,查看 SET 返回值的描述,为方便大家,这里直接贴出结果,应该很多同学都没看过这段描述吧。
简单字符串回复:
OK
如果SET
正确执行。空回复:
(nil)
如果SET
由于用户指定了NX
或XX
选项但不满足条件而未执行操作。如果命令与
GET
选项一起发出,则上述内容不适用。它会改为如下回复,无论是否SET
实际执行:批量字符串回复:存储在键中的旧字符串值。
空回复:
(nil)
如果密钥不存在。
通过官网给出的描述可以得知,当前 SET 指令的使用方式,只要返回的不是“OK",就是锁已存在了,所以将 《分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇》示例中tryLock
的逻辑中,加入一个判断锁类型的逻辑即可,即如果锁 key 已存在,并且锁是”一次性“锁,则不循环等待而是立即返回。
至于这个”一次性“锁的时间窗口应该是多少则由使用方自行决定。
``` public boolean tryLock(long waitTime, TimeUnit waitUnit) throws DtLockException { long totalMillisSeconds = waitUnit.toMillis(waitTime); long start = System.currentTimeMillis(); //重试,直到成功或超过指定时间 while (true) { // 抢锁 try { SetParams params = SetParams.setParams().nx().ex(lockState.getLeaseTTL()); String result = client.set(lockState.getLockKey(), lockState.getLockValue(), params); if (RESULT_OK.equals(result)) { manualKeepAlive(); log.info("[jedis-lock] lock success 线程:{} 加锁成功,key:{} , value:{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue()); lockState.setLockSuccess(true); return true; } else { // 增加判断,如果锁的类型是lockOnce,则立即返回。 //----伪代码 begin ----- if(lockType == lockOnce){ return false; } //----伪代码 end ----- if (System.currentTimeMillis() - start >= totalMillisSeconds) { return false; } Thread.sleep(sleepMillisecond); } } catch (Exception e) { Throwable cause = e.getCause(); if (cause instanceof SocketTimeoutException) {//忽略网络抖动等异常 } log.error("[jedis-lock] lock failed:" + e); throw new DtLockException("[jedis-lock] lock failed:" + e.getMessage(), e); }
} }
```
六、总结
本篇介绍了如何基于 Redis 的特性来实现一个”一次性的“分布式锁,如果前面几篇都已看过的话,这里很容易理解。好,本篇就此结束了,感谢您的花费宝贵的时间来读这篇文章,希望能对您有所帮助。
另外,请您留意,分布式锁系列内容规划如下,本篇是第 5 篇:
- 《分布式锁上-初探》
- 《分布式锁中-基于 Zookeeper 的实现是怎样》
- 《分布式锁中-基于 etcd 的实现很优雅》
- 《分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇》
- 《分布式锁中-基于 Redis 实现的锁可以提供防重入能力嘛》(本篇)
- 《分布式锁中-基于 Redis 的实现很多样 - Redission 篇》(写作中)
- 《分布式锁中-多维度的对比各种分布式锁实现》(写作中)
- 《分布式锁下-分布式锁客户端的抽象、适配与加固》(写作中)
七、最后说一句(请关注,莫错过)
如果这篇文章对您有帮助,或者有所启发的话,欢迎关注微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。