分布式锁中-基于 Redis 的实现如何防重入

语言: CN / TW / HK

theme: smartblue

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第 9 天,点击查看活动详情

篇幅太长看着也累,每天进步一点点

欢迎关注微信公众号「架构染色」交流和学习

前情回顾

分布式锁系列内容规划如下,本篇是第 5 篇:

  1. 《分布式锁上-初探
  2. 《分布式锁中-基于 Zookeeper 的实现是怎样》
  3. 《分布式锁中-基于 etcd 的实现很优雅》
  4. 《分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇》
  5. 《分布式锁中-基于 Redis 的实现如何防重入》(本篇)
  6. 《分布式锁中-基于 Redis 的实现很多样  - Redission 篇》(写作中)
  7. 《分布式锁中-多维度的对比各种分布式锁实现》(写作中)
  8. 《分布式锁下-分布式锁客户端的抽象、适配与加固》(写作中)

一、背景

昨晚同事小窗咨询说,当前基于 Jedis 实现的分布式锁,在他们的一个业务场景中不合适,沟通之后了解到他们是想要一个防重入的锁,那同事所描述的重入是什么意思呢,看下图:

image.png

上图是举例描述一个重入的场景,有一个请求被重复提交给 Service-A 了(可能是用户重复点击提交请求,也可能是 RPC 的重试所触发,也可能是其他的情况),对于 Service-A 来说因为没有幂等机制导致 DB 中插入了多条记录,虽然这种情况很少见,但对 Service-A 说一旦遇到,产生了垃圾数据就会比较麻烦;因此同事并不希望相同的请求因为一些偶发异常,而导致自己产生重复处理、记录。

二、需求

同事是希望对现有的分布式锁增加防重入的能力,以便达到在某个时间窗口内,重复多余的请求只会被处理一次,如下图:

image.png

三、分析

如何将这个能力融入现有的分布式锁组件中呢?梳理之后,给锁增加类型属性,传统的分布式锁若归类为重用锁(A 持锁后,B 来抢锁只要没超时就一直重试抢锁,抢到就用),而这种防重入场景下的锁可理解为”一次性“锁(A 持锁后,B 来抢锁只要锁已存在,则立即放弃),这个归类命名并不权威,只是这么区分方便理解。这么区分之后,将新需求整理如下:

  1. 使用者在构建锁的时候,可指定锁类型为”一次性“锁,并设定过期时间,不续租,不主动释放锁,锁过期后会被自动删除;
  2. 使用者在构建锁的时候,可指定锁类型为”一次性“锁,并设定过期时间,持锁后会自动续租,若持锁客户端是存活状态则会在完成一个请求的处理后主动释放锁。若持锁的客户端挂掉了,等锁过期后会自动被删除。但只要锁被释放后,就可以继续抢锁。

四、设计

调整加锁流程的逻辑,当发现锁已经存在后,增加一段逻辑,判断是否是”一次性“锁(下图褐色部分,应该是褐色吧),如果是则立即返回。如此即实现了在某个时间窗口内,锁是”一次性“的效果。流程图如下:

image.png

五、待确认事项

5.1 好像哪里有点不放心

我们使用的是 SET 指令来实现加锁的逻辑,指令形式如下:

SET键值[NX | XX] [GET] [EX 秒 | PX 毫秒 | EXAT unix 时间秒 | PXAT unix 时间毫秒 | 保持]

1)加锁成功的逻辑是这样:

  1. 判断 key 是否存在
  2. 若 key 不存在,就设置 key
  3. 给 key 指定过期时间

2)加锁不成功的逻辑是这样:

  1. 判断 key 是否存在
  2. 若 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由于用户指定了NXXX选项但不满足条件而未执行操作。

如果命令与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 篇:

  1. 《分布式锁上-初探
  2. 《分布式锁中-基于 Zookeeper 的实现是怎样》
  3. 《分布式锁中-基于 etcd 的实现很优雅》
  4. 《分布式锁中-基于 Redis 的实现需避坑 - Jedis 篇》
  5. 《分布式锁中-基于 Redis 实现的锁可以提供防重入能力嘛》(本篇)
  6. 《分布式锁中-基于 Redis 的实现很多样  - Redission 篇》(写作中)
  7. 《分布式锁中-多维度的对比各种分布式锁实现》(写作中)
  8. 《分布式锁下-分布式锁客户端的抽象、适配与加固》(写作中)

七、最后说一句(请关注,莫错过)

如果这篇文章对您有帮助,或者有所启发的话,欢迎关注微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

点击链接即可一键三连:关注、点赞、转发。