需求

  1. 互斥性:只有一个客户端能持有锁
  2. 不死锁:保证后续客户端加锁
  3. 容错性:大部分redis节点正常运行,客户端就可以加锁解锁
  4. 阻塞性:其他客户端尝试请求锁的时间内阻塞状态
  5. 可重入性:在锁里面加锁
  6. 不能自己失效:在业务执行过程中,锁不能自己失效(异步续命)
  7. 解铃还须系铃人:持有锁的线程才能解自己上的锁

Redisson框架很好的实现

Java手写实现

基于spring整合的redisTemplate对redis进行操作
使用异步任务线程池ScheduledThreadPoolExecutor进行异步续命

  1. @Slf4j
  2. public class RedisLock {
  3. private RedisTemplate<String, String> redisTemplate;
  4. /**
  5. * 避免自己的锁被别的线程释放了
  6. */
  7. private final ThreadLocal<String> local = new ThreadLocal<>();
  8. /**
  9. * 用于实现可重入锁
  10. */
  11. private final ThreadLocal<Integer> reentrant = new ThreadLocal<>();
  12. /**
  13. * 分布式锁key前缀
  14. */
  15. private static final String PREFIX = "LOCK_";
  16. private String lockKey;
  17. /**
  18. * 用于异步续命的对象
  19. */
  20. private RenewLockTimes renewLockTimes;
  21. /**
  22. * 创建锁对象
  23. *
  24. * @param key 要锁的key
  25. * @param redisTemplate
  26. * @author YangYudi
  27. * @date 2021/10/29 9:49
  28. */
  29. public RedisLock(String key, RedisTemplate<String, String> redisTemplate) {
  30. this.lockKey = PREFIX + key;
  31. this.redisTemplate = redisTemplate;
  32. }
  33. /**
  34. * 加锁
  35. *
  36. * @param tryTime 尝试时间 单位秒
  37. * @param timeout 锁时间 单位秒
  38. * @return java.lang.Boolean
  39. * @author YangYudi
  40. * @date 2021/10/29 9:49
  41. */
  42. public Boolean tryLock(long tryTime, long timeout) {
  43. long startTime = System.currentTimeMillis();
  44. //确保本线程的值唯一
  45. String uuid = UUID.randomUUID().toString();
  46. Boolean lock = false;
  47. if (StringUtils.isEmpty(local.get())) {
  48. //每个线程的value都不一样
  49. //使用threadLocal保证线程安全
  50. local.set(uuid);
  51. lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, timeout, TimeUnit.SECONDS);
  52. //没拿到锁就阻塞 自旋锁 直到拿到锁就不阻塞
  53. while (lock != null && !lock) {
  54. lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, timeout, TimeUnit.SECONDS);
  55. //超时就直接返回false
  56. if (System.currentTimeMillis() - startTime >= (tryTime * 1000)) {
  57. log.error("{} 放弃拿到锁", lockKey);
  58. break;
  59. }
  60. }
  61. if (lock != null && lock) {
  62. //得到锁了 开启异步续命
  63. renewLockTimes = new RenewLockTimes(lockKey, timeout, redisTemplate);
  64. renewLockTimes.schedule();
  65. }
  66. } else {
  67. //可重入性
  68. lock = true;
  69. }
  70. //可重入锁
  71. //加锁成功后计数器加一
  72. if (lock != null && lock) {
  73. Integer count = reentrant.get() == null ? 0 : reentrant.get();
  74. reentrant.set(++count);
  75. log.info("加锁成功,{}", lockKey);
  76. }
  77. return lock;
  78. }
  79. /**
  80. * 加锁
  81. *
  82. * @param tryTime
  83. * @return java.lang.Boolean
  84. * @author YangYudi
  85. * @date 2021/10/29 9:49
  86. */
  87. public Boolean tryLock(long tryTime) {
  88. return tryLock(tryTime, 10);
  89. }
  90. /**
  91. * 加锁
  92. *
  93. * @return java.lang.Boolean
  94. * @author YangYudi
  95. * @date 2021/10/29 9:49
  96. */
  97. public Boolean tryLock() {
  98. return tryLock(5, 10);
  99. }
  100. /**
  101. * 释放锁
  102. *
  103. * @return java.lang.Boolean
  104. * @author YangYudi
  105. * @date 2021/10/29 9:49
  106. */
  107. public void releaseLock() {
  108. //get和delete是两个方法 没有原子性 如果不用lua脚本 就需要用到监听和事务来完成
  109. //保证本线程的锁只能被本线程释放
  110. //先判断锁是不是本线程的 如果是就删除key返回1 否则返回 0
  111. String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
  112. "then\n" +
  113. " return redis.call(\"del\",KEYS[1])\n" +
  114. "else\n" +
  115. " return 0\n" +
  116. "end";
  117. Integer count = reentrant.get();
  118. if (count == null || count - 1 <= 0) {
  119. RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
  120. //执行lua脚本 只有当前线程才能释放锁
  121. Long execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), local.get());
  122. if (!ObjectUtils.isEmpty(execute) && execute == 1) {
  123. //锁释放
  124. log.info("{} 锁释放", lockKey);
  125. //结束续命
  126. renewLockTimes.shutdown();
  127. local.remove();
  128. reentrant.remove();
  129. }
  130. } else {
  131. //可重入锁 加几次就要解几次锁
  132. reentrant.set(--count);
  133. }
  134. }
  135. private static class RenewLockTimes {
  136. private String key;
  137. private long timeout;
  138. /**
  139. * 续命的时间
  140. */
  141. private long plusTime;
  142. private RedisTemplate<String, String> redisTemplate;
  143. private ScheduledExecutorService service;
  144. public RenewLockTimes(String key, long timeout, RedisTemplate<String, String> redisTemplate) {
  145. this.key = key;
  146. this.timeout = timeout;
  147. this.redisTemplate = redisTemplate;
  148. plusTime = timeout / 3;
  149. service = new ScheduledThreadPoolExecutor(1);
  150. }
  151. /**
  152. * 定时续命
  153. *
  154. * @author YangYudi
  155. * @date 2020/12/24 16:29
  156. */
  157. public void schedule() {
  158. service.scheduleAtFixedRate(() -> {
  159. //先获取key的时间 再判断时间是否快结束了 快结束了就续命
  160. //这段代码用lua脚本做好一点
  161. Long expire = redisTemplate.getExpire(key);
  162. if (!ObjectUtils.isEmpty(expire) && expire > 0 && expire <= plusTime) {
  163. redisTemplate.expire(key, expire + plusTime, TimeUnit.SECONDS);
  164. log.info("{} 续命成功", key);
  165. }
  166. },
  167. 1, timeout / 3, TimeUnit.SECONDS);
  168. }
  169. /**
  170. * 停止续命
  171. *
  172. * @author YangYudi
  173. * @date 2020/12/24 16:29
  174. */
  175. public void shutdown() {
  176. service.shutdown();
  177. }
  178. }
  179. }

调用

测试是否支持异步续命,可以在释放锁之前让线程睡眠

  1. public String buy() {
  2. int buy = 0;
  3. //传入要锁住的key和redisTemplate
  4. RedisLock redisLock = new RedisLock("1", redisTemplate);
  5. try {
  6. //加锁
  7. redisLock.tryLock();
  8. buy = orderService.buy();
  9. try {
  10. Thread.sleep(30000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. } finally {
  15. //解锁
  16. redisLock.releaseLock();
  17. }
  18. return String.valueOf(buy);
  19. }