一、前言

之前写的一篇文章《细说分布式锁》介绍了分布式锁的三种实现方式,但是Redis实现分布式锁关于Lua脚本实现、自定义分布式锁注解以及需要注意的问题都没描述。本文就是详细说明如何利用Redis实现重入的分布式锁。

二、方案

死锁问题

当一个客户端获取锁成功之后,假如它崩溃了导致它再也无法和 Redis 节点通信,那么它就会一直持有这个锁,导致其它客户端永远无法获得锁了,因此锁必须要有一个自动释放的时间。

  我们需要保证setnx命令和expire命令以原子的方式执行,否则如果客户端执行setnx获得锁后,这时客户端宕机了,那么这把锁没有设置过期时间,导致其他客户端永远无法获得锁了。

锁被其他线程释放

如果不加任何处理即简单使用 SETNX 实现 Redis 分布式锁,就会遇到一个问题:如果线程 C1 获得锁,但由于业务处理时间过长,锁在线程 C1 还未处理完业务之前已经过期了,这时线程 C2 获得锁,在线程 C2 处理业务期间线程 C1 完成业务执行释放锁操作,但这时线程 C2 仍在处理业务线程 C1 释放了线程 C2 的锁,导致线程 C2 业务处理实际上没有锁提供保护机制;同理线程 C2 可能释放线程 C3 的锁,从而导致严重的问题。

  因此每个线程释放锁的时候只能释放自己的锁,即锁必须要有一个拥有者的标记,并且也需要保证释放锁的原子性操作。

  在释放锁的时候判断拥有者的标记(value是否相同),只有相同时才可以删除,同时利用Lua脚本来达到原子操作,脚本如下:

  1. if redis.call("get", KEYS[1]) == ARGV[1] then 

  2. return redis.call("del", KEYS[1]) 

  3. else 

  4. return 0 

  5. end 

可重入问题

可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,如果没有可重入锁的支持,在第二次尝试获得锁时将会进入死锁状态。

这里有两种解决方案:

  1. 客户端在获得锁后保存value(拥有者标记),然后释放锁的时候将value和key同时传过去。
  2. 利用ThreadLocal实现,获取锁后将Redis中的value保存在ThreadLocal中,同一线程再次尝试获取锁的时候就先将 ThreadLocal 中的 值 与 Redis 的 value 比较,如果相同则表示这把锁所以该线程,即实现可重入锁。

这里的实现的方案是基于单机Redis,之前说的集群问题这里暂不考虑。

三、编码

我们通过自定义分布式锁注解+AOP可以更加方便的使用分布式锁,只需要在加锁的方法上加上注解即可。

Redis分布式锁接口

  1. /**
  2. * Redis分布式锁接口
  3. * Created by 2YSP on 2019/9/20.
  4. */
  5. public interface IRedisDistributedLock {
  6. /**
  7. *
  8. * @param key
  9. * @param requireTimeOut 获取锁超时时间 单位ms
  10. * @param lockTimeOut 锁过期时间,一定要大于业务执行时间 单位ms
  11. * @param retries 尝试获取锁的最大次数
  12. * @return
  13. */
  14. boolean lock(String key, long requireTimeOut, long lockTimeOut, int retries);
  15. /**
  16. * 释放锁
  17. * @param key
  18. * @return
  19. */
  20. boolean release(String key);
  21. }

Redis 分布式锁实现类

  1. /**
  2. * Redis 分布式锁实现类
  3. * Created by 2YSP on 2019/9/20.
  4. */
  5. @Slf4j
  6. @Component
  7. public class RedisDistributedLockImpl implements IRedisDistributedLock {
  8. /**
  9. * key前缀
  10. */
  11. public static final String PREFIX = "Lock:";
  12. /**
  13. * 保存锁的value
  14. */
  15. private ThreadLocal<String> threadLocal = new ThreadLocal<>();
  16. private static final Charset UTF8 = Charset.forName("UTF-8");
  17. /**
  18. * 释放锁脚本
  19. */
  20. private static final String UNLOCK_LUA;
  21. /*
  22. * 释放锁脚本,原子操作
  23. */
  24. static {
  25. StringBuilder sb = new StringBuilder();
  26. sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
  27. sb.append("then ");
  28. sb.append(" return redis.call(\"del\",KEYS[1]) ");
  29. sb.append("else ");
  30. sb.append(" return 0 ");
  31. sb.append("end ");
  32. UNLOCK_LUA = sb.toString();
  33. }
  34. @Autowired
  35. private RedisTemplate redisTemplate;
  36. @Override
  37. public boolean lock(String key, long requireTimeOut, long lockTimeOut, int retries) {
  38. //可重入锁判断
  39. String originValue = threadLocal.get();
  40. if (!StringUtils.isBlank(originValue) && isReentrantLock(key, originValue)) {
  41. return true;
  42. }
  43. String value = UUID.randomUUID().toString();
  44. long end = System.currentTimeMillis() + requireTimeOut;
  45. int retryTimes = 1;
  46. try {
  47. while (System.currentTimeMillis() < end) {
  48. if (retryTimes > retries) {
  49. log.error(" require lock failed,retry times [{}]", retries);
  50. return false;
  51. }
  52. if (setNX(wrapLockKey(key), value, lockTimeOut)) {
  53. threadLocal.set(value);
  54. return true;
  55. }
  56. // 休眠10ms
  57. Thread.sleep(10);
  58. retryTimes++;
  59. }
  60. } catch (Exception e) {
  61. e.printStackTrace();
  62. }
  63. return false;
  64. }
  65. private boolean setNX(String key, String value, long expire) {
  66. /**
  67. * List设置lua的keys
  68. */
  69. List<String> keyList = new ArrayList<>();
  70. keyList.add(key);
  71. return (boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
  72. Boolean result = connection
  73. .set(key.getBytes(UTF8),
  74. value.getBytes(UTF8),
  75. Expiration.milliseconds(expire),
  76. SetOption.SET_IF_ABSENT);
  77. return result;
  78. });
  79. }
  80. /**
  81. * 是否为重入锁
  82. */
  83. private boolean isReentrantLock(String key, String originValue) {
  84. String v = (String) redisTemplate.opsForValue().get(key);
  85. return v != null && originValue.equals(v);
  86. }
  87. @Override
  88. public boolean release(String key) {
  89. String originValue = threadLocal.get();
  90. if (StringUtils.isBlank(originValue)) {
  91. return false;
  92. }
  93. return (boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
  94. return connection
  95. .eval(UNLOCK_LUA.getBytes(UTF8), ReturnType.BOOLEAN, 1, wrapLockKey(key).getBytes(UTF8),
  96. originValue.getBytes(UTF8));
  97. });
  98. }
  99. private String wrapLockKey(String key) {
  100. return PREFIX + key;
  101. }
  102. }

分布式锁注解

  1. @Retention(value = RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface DistributedLock {
  4. /**
  5. * 默认包名加方法名
  6. * @return
  7. */
  8. String key() default "";
  9. /**
  10. * 过期时间 单位:毫秒
  11. * <pre>
  12. * 过期时间一定是要长于业务的执行时间.
  13. * </pre>
  14. */
  15. long expire() default 30000;
  16. /**
  17. * 获取锁超时时间 单位:毫秒
  18. * <pre>
  19. * 结合业务,建议该时间不宜设置过长,特别在并发高的情况下.
  20. * </pre>
  21. */
  22. long timeout() default 3000;
  23. /**
  24. * 默认重试次数
  25. * @return
  26. */
  27. int retryTimes() default Integer.MAX_VALUE;
  28. }

aop切片类

  1. @Component
  2. @Aspect
  3. @Slf4j
  4. public class RedisLockAop {
  5. @Autowired
  6. private IRedisDistributedLock redisDistributedLock;
  7. @Around(value = "@annotation(lock)")
  8. public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, DistributedLock lock) {
  9. // 加锁
  10. String key = getKey(proceedingJoinPoint, lock);
  11. Boolean success = null;
  12. try {
  13. success = redisDistributedLock
  14. .lock(key, lock.timeout(), lock.expire(), lock.retryTimes());
  15. if (success) {
  16. log.info(Thread.currentThread().getName() + " 加锁成功");
  17. return proceedingJoinPoint.proceed();
  18. }
  19. log.info(Thread.currentThread().getName() + " 加锁失败");
  20. return null;
  21. } catch (Throwable throwable) {
  22. throwable.printStackTrace();
  23. return null;
  24. } finally {
  25. if (success){
  26. boolean result = redisDistributedLock.release(key);
  27. log.info(Thread.currentThread().getName() + " 释放锁结果:{}",result);
  28. }
  29. }
  30. }
  31. private String getKey(JoinPoint joinPoint, DistributedLock lock) {
  32. if (!StringUtils.isBlank(lock.key())) {
  33. return lock.key();
  34. }
  35. return joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature()
  36. .getName();
  37. }
  38. }

业务逻辑处理类

  1. @Service
  2. public class TestService {
  3. @DistributedLock(retryTimes = 1000,timeout = 1000)
  4. public String lockTest() {
  5. try {
  6. System.out.println("模拟执行业务逻辑。。。");
  7. Thread.sleep(100);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. return "error";
  11. }
  12. return "ok";
  13. }
  14. }

四、测试

  1. 启动本地redis,启动项目
  2. 打开cmd,利用ab压力测试设置3个线程100个请求。

ab -c 3 -n 100 http://localhost:8000/lock/test

idea控制台输出如下:

enter description here

至此大功告成,代码地址

ps: 遇到一个奇怪的问题,我用 RedisTemplate.execute(RedisScript script, List keys, Object... args) 这个方法,通过加载resource目录下的lua脚本来释放锁的时候一直不成功,参数没任何问题,而且我之前的文章就是用这个方法可以正确的释放锁。

【分布式锁】Redis实现可重入的分布式锁的更多相关文章

  1. java并发编程(一)可重入内置锁

    每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...

  2. 转:【Java并发编程】之一:可重入内置锁

    每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...

  3. 【Java并发编程】之一:可重入内置锁

    每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁.获得内置锁的唯一途径就是进入由这个锁保护的同步代码块 ...

  4. Java锁的深度化--重入锁、读写锁、乐观锁、悲观锁

    Java锁 锁一般来说用作资源控制,限制资源访问,防止在并发环境下造成数据错误 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized(重量级) 和 Reentr ...

  5. Java并发-显式锁篇【可重入锁+读写锁】

    作者:汤圆 个人博客:javalover.cc 前言 在前面并发的开篇,我们介绍过内置锁synchronized: 这节我们再介绍下显式锁Lock 显式锁包括:可重入锁ReentrantLock.读写 ...

  6. Redis分布式锁—Redisson+RLock可重入锁实现篇

    前言 平时的工作中,由于生产环境中的项目是需要部署在多台服务器中的,所以经常会面临解决分布式场景下数据一致性的问题,那么就需要引入分布式锁来解决这一问题. 针对分布式锁的实现,目前比较常用的就如下几种 ...

  7. JUC回顾之-可重入的互斥锁ReentrantLock

    1.什么是可重锁ReentrantLock? 就是支持重新进入的锁,表示该锁能够支持一个线程对资源的重复加锁. 2.ReentrantLock分为公平锁和非公平锁:区别是在于获取锁的机制上是否公平. ...

  8. 浅谈Java中的锁:Synchronized、重入锁、读写锁

    Java开发必须要掌握的知识点就包括如何使用锁在多线程的环境下控制对资源的访问限制 ◆ Synchronized ◆ 首先我们来看一段简单的代码: 12345678910111213141516171 ...

  9. Java内置锁synchronized的可重入性

    学习自 https://blog.csdn.net/aigoogle/article/details/29893667 对我很有帮助 感谢作者

随机推荐

  1. zookeeper基础笔记

    一.安装 1.安装jdk 2.安装Zookeeper 3.单机模式(stand-alone):安装目录/conf   复制 zoo_sample.cfg 并粘贴到当前目录下,命名zoo.cfg. 二. ...

  2. 在执行gem install redis时 : ERROR: Error installing redis: redis requires Ruby version >= 2.2.2

    在执行gem install redis时 提示: gem install redis ERROR: Error installing redis: redis requires Ruby versi ...

  3. CentOS下安装SublimeText

    1.Install the GPG key: sudo rpm -v --import https://download.sublimetext.com/sublimehq-rpm-pub.gpg 2 ...

  4. 使用k8s部署springboot+redis简单应用

    准备 本文将使用k8s部署一个springboot+redis应用,由于是示例,所以功能比较简单,只有设置值和获取值两个api. (1)设置值 (2)获取值 构建Web应用 (1)创建一个spring ...

  5. Scrum 冲刺第六天

    一.每日站立式会议 1.会议内容 1)进行每日工作汇报 张博愉: 昨天已完成的工作:学习如何编写用户手册 今日工作计划:编写测试计划 工作中遇到的困难:文档不知如何动手 张润柏: 昨天已完成的工作:完 ...

  6. 题解 CF1375E Inversion SwapSort

    蒟蒻语 这题是真的奇妙... 想了好久才想明白. 蒟蒻解 考虑冒泡排序是怎样的. 对于相邻的两个数 \(a_i, a_{i+1}\),如果 \(a_i>a_{i+1}\) 那么就交换两个数. 总 ...

  7. 【APIO2019】路灯(ODT & (树套树 | CDQ分治))

    Description 一条 \(n\) 条边,\(n+1\) 个点的链,边有黑有白.若结点 \(a\) 可以到达 \(b\),需要满足 \(a\to b\) 的路径上的边不能有黑的.现给出 \(0\ ...

  8. 题解-CF643G Choosing Ads

    CF643G Choosing Ads \(n\) 和 \(m\) 和 \(p\) 和序列 \(a_i(1\le i\le n)\).\(m\) 种如下操作: 1 l r id 令 \(i\in[l, ...

  9. git clone 速度太慢解决方法

    本来想下载一个翻墙软件,实在是忍受不了每秒十几K的龟速,查阅各种资料,终于找到了失传已久的秘籍 先附图,实测有效,这速度简直要上天了啊啊啊啊啊(只支持HTTPS方式,SSH无效) 方案:使用githu ...

  10. 页面上下载canvas中的内容作为图片

    使用如下代码,获得Canvas图像对应的data URI,也就是平常我们所说的base64地址 var dataUrl = document.getElementById("canvasId ...