Redisson 分布式锁实战与 watch dog 机制解读

背景

Redisson官网的介绍,Redisson是一个Java Redis客户端,与Spring 提供给我们的 RedisTemplate 工具没有本质的区别,可以把它看做是一个功能更强大的客户端(虽然官网上声称Redisson不只是一个Java Redis客户端)

我想我们用到 Redisson 最多的场景一定是分布式锁,一个基础的分布式锁具有三个特性:

  1. 互斥:在分布式高并发的条件下,需要保证,同一时刻只能有一个线程获得锁,这是最最基本的一点。
  2. 防止死锁:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
  3. 可重入:我们知道ReentrantLock是可重入锁,那它的特点就是同一个线程可以重复拿到同一个资源的锁。

实现的方案有很多,这里,就以我们平时在网上常看到的redis分布式锁方案为例,来对比看看 Redisson 提供的分布式锁有什么高级的地方。

普通的 Redis 分布式锁的缺陷

我们在网上看到的redis分布式锁的工具方法,大都满足互斥、防止死锁的特性,有些工具方法会满足可重入特性。

如果只满足上述3种特性会有哪些隐患呢?redis分布式锁无法自动续期,比如,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题,我已经在秒杀系统故障排查文章中,看到好多因为这个缺陷导致的超卖了。

Redisson 提供的分布式锁

Redisson 锁的加锁机制如上图所示,线程去获取锁,获取成功则执行lua脚本,保存数据到redis数据库。

如果获取失败: 一直通过while循环尝试获取锁(可自定义等待时间,超时后返回失败),获取成功后,执行lua脚本,保存数据到redis数据库。

Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。

同时 redisson 还有公平锁、读写锁的实现。

使用样例如下,附有方法的详细机制释义

  1. private void redissonDoc() throws InterruptedException {
  2. //1. 普通的可重入锁
  3. RLock lock = redissonClient.getLock("generalLock");
  4. // 拿锁失败时会不停的重试
  5. // 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
  6. lock.lock();
  7. // 尝试拿锁10s后停止重试,返回false
  8. // 具有Watch Dog 自动延期机制 默认续30s
  9. boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);
  10. // 拿锁失败时会不停的重试
  11. // 没有Watch Dog ,10s后自动释放
  12. lock.lock(10, TimeUnit.SECONDS);
  13. // 尝试拿锁100s后停止重试,返回false
  14. // 没有Watch Dog ,10s后自动释放
  15. boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);
  16. //2. 公平锁 保证 Redisson 客户端线程将以其请求的顺序获得锁
  17. RLock fairLock = redissonClient.getFairLock("fairLock");
  18. //3. 读写锁 没错与JDK中ReentrantLock的读写锁效果一样
  19. RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
  20. readWriteLock.readLock().lock();
  21. readWriteLock.writeLock().lock();
  22. }

watch dog 的自动延期机制

如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。

Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。

默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。

watch dog 核心源码解读

  1. // 直接使用lock无参数方法
  2. public void lock() {
  3. try {
  4. lock(-1, null, false);
  5. } catch (InterruptedException e) {
  6. throw new IllegalStateException();
  7. }
  8. }
  9. // 进入该方法 其中leaseTime = -1
  10. private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
  11. long threadId = Thread.currentThread().getId();
  12. Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
  13. // lock acquired
  14. if (ttl == null) {
  15. return;
  16. }
  17. //...
  18. }
  19. // 进入 tryAcquire(-1, leaseTime, unit, threadId)
  20. private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
  21. return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
  22. }
  23. // 进入 tryAcquireAsync
  24. private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
  25. if (leaseTime != -1) {
  26. return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
  27. }
  28. //当leaseTime = -1 时 启动 watch dog机制
  29. RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
  30. commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
  31. TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
  32. //执行完lua脚本后的回调
  33. ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
  34. if (e != null) {
  35. return;
  36. }
  37. if (ttlRemaining == null) {
  38. // watch dog
  39. scheduleExpirationRenewal(threadId);
  40. }
  41. });
  42. return ttlRemainingFuture;
  43. }

scheduleExpirationRenewal 方法开启监控:

  1. private void scheduleExpirationRenewal(long threadId) {
  2. ExpirationEntry entry = new ExpirationEntry();
  3. //将线程放入缓存中
  4. ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
  5. //第二次获得锁后 不会进行延期操作
  6. if (oldEntry != null) {
  7. oldEntry.addThreadId(threadId);
  8. } else {
  9. entry.addThreadId(threadId);
  10. // 第一次获得锁 延期操作
  11. renewExpiration();
  12. }
  13. }
  14. // 进入 renewExpiration()
  15. private void renewExpiration() {
  16. ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  17. //如果缓存不存在,那不再锁续期
  18. if (ee == null) {
  19. return;
  20. }
  21. Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
  22. @Override
  23. public void run(Timeout timeout) throws Exception {
  24. ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  25. if (ent == null) {
  26. return;
  27. }
  28. Long threadId = ent.getFirstThreadId();
  29. if (threadId == null) {
  30. return;
  31. }
  32. //执行lua 进行续期
  33. RFuture<Boolean> future = renewExpirationAsync(threadId);
  34. future.onComplete((res, e) -> {
  35. if (e != null) {
  36. log.error("Can't update lock " + getName() + " expiration", e);
  37. return;
  38. }
  39. if (res) {
  40. //延期成功,继续循环操作
  41. renewExpiration();
  42. }
  43. });
  44. }
  45. //每隔internalLockLeaseTime/3=10秒检查一次
  46. }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
  47. ee.setTimeout(task);
  48. }
  49. //lua脚本 执行包装好的lua脚本进行key续期
  50. protected RFuture<Boolean> renewExpirationAsync(long threadId) {
  51. return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
  52. "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
  53. "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  54. "return 1; " +
  55. "end; " +
  56. "return 0;",
  57. Collections.singletonList(getName()),
  58. internalLockLeaseTime, getLockName(threadId));
  59. }

关键结论

上述源码读过来我们可以记住几个关键情报:

  1. watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
  2. watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
  3. 从可2得出,如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;

看到3的时候,可能会有人有疑问,如果释放锁操作本身异常了,watch dog 还会不停的续期吗?下面看一下释放锁的源码,找找答案。

  1. // 锁释放
  2. public void unlock() {
  3. try {
  4. get(unlockAsync(Thread.currentThread().getId()));
  5. } catch (RedisException e) {
  6. if (e.getCause() instanceof IllegalMonitorStateException) {
  7. throw (IllegalMonitorStateException) e.getCause();
  8. } else {
  9. throw e;
  10. }
  11. }
  12. }
  13. // 进入 unlockAsync(Thread.currentThread().getId()) 方法 入参是当前线程的id
  14. public RFuture<Void> unlockAsync(long threadId) {
  15. RPromise<Void> result = new RedissonPromise<Void>();
  16. //执行lua脚本 删除key
  17. RFuture<Boolean> future = unlockInnerAsync(threadId);
  18. future.onComplete((opStatus, e) -> {
  19. // 无论执行lua脚本是否成功 执行cancelExpirationRenewal(threadId) 方法来
  20. cancelExpirationRenewal(threadId);
  21. if (e != null) {
  22. result.tryFailure(e);
  23. return;
  24. }
  25. if (opStatus == null) {
  26. IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
  27. + id + " thread-id: " + threadId);
  28. result.tryFailure(cause);
  29. return;
  30. }
  31. result.trySuccess(null);
  32. });
  33. return result;
  34. }
  35. // 此方法会停止 watch dog 机制
  36. void cancelExpirationRenewal(Long threadId) {
  37. ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  38. if (task == null) {
  39. return;
  40. }
  41. if (threadId != null) {
  42. task.removeThreadId(threadId);
  43. }
  44. if (threadId == null || task.hasNoThreads()) {
  45. Timeout timeout = task.getTimeout();
  46. if (timeout != null) {
  47. timeout.cancel();
  48. }
  49. EXPIRATION_RENEWAL_MAP.remove(getEntryName());
  50. }
  51. }

释放锁的操作中 有一步操作是从 EXPIRATION_RENEWAL_MAP 中获取 ExpirationEntry 对象,然后将其remove,结合watch dog中的续期前的判断:

  1. EXPIRATION_RENEWAL_MAP.get(getEntryName());
  2. if (ent == null) {
  3. return;
  4. }

可以得出结论:

如果释放锁操作本身异常了,watch dog 还会不停的续期吗?不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。

参考

Redisson实现分布式锁(1)---原理

Redisson 官方文档

谈谈基于Redis分布式锁(下)- Redisson源码解析

Redisson 分布式锁实战与 watch dog 机制解读的更多相关文章

  1. Redis分布式锁实战

    什么是分布式锁 在单机部署的情况下,要想保证特定业务在顺序执行,通过JDK提供的synchronized关键字.Semaphore.ReentrantLock,或者我们也可以基于AQS定制化锁.单机部 ...

  2. Redisson 分布式锁实现之前置篇 → Redis 的发布/订阅 与 Lua

    开心一刻 我找了个女朋友,挺丑的那一种,她也知道自己丑,平常都不好意思和我一块出门 昨晚,我带她逛超市,听到有两个人在我们背后小声嘀咕:"看咱前面,想不到这么丑都有人要." 女朋友 ...

  3. Redisson 分布式锁源码 02:看门狗

    前言 说起 Redisson,比较耳熟能详的就是这个看门狗(Watchdog)机制. 本文就一起看看加锁成功之后的看门狗(Watchdog)是如何实现的? 加锁成功 在前一篇文章中介绍了可重入锁加锁的 ...

  4. Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端

    开心一刻 一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解 劝解员:兄弟,别跳 跳楼人:我不想活了 劝解员:你想想你媳妇 跳楼人:媳妇跟人跑了 劝解员:你还有兄弟 跳楼人:就是跟我兄弟跑的 劝解 ...

  5. Redisson 分布式锁源码 09:RedLock 红锁的故事

    前言 RedLock 红锁,是分布式锁中必须要了解的一个概念. 所以本文会先介绍什么是 RedLock,当大家对 RedLock 有一个基本的了解.然后再看 Redisson 中是如何实现 RedLo ...

  6. Redisson分布式锁的简单使用

    一:前言 我在实际环境中遇到了这样一种问题,分布式生成id的问题!因为业务逻辑的问题,我有个生成id的方法,是根据业务标识+id当做唯一的值! 而uuid是递增生成的,从1开始一直递增,那么在同一台机 ...

  7. Redisson分布式锁实现

    转: Redisson分布式锁实现 2018年09月07日 15:30:32 校长我错了 阅读数:3303   转:分布式锁和Redisson实现 概述 分布式系统有一个著名的理论CAP,指在一个分布 ...

  8. [转帖]SpringBoot集成redisson分布式锁

    SpringBoot集成redisson分布式锁 https://www.cnblogs.com/yangzhilong/p/7605807.html 前几天同事刚让增加上这一块东西. 百度查一下 啥 ...

  9. 又长又细,万字长文带你解读Redisson分布式锁的源码

    前言 上一篇文章写了Redis分布式锁的原理和缺陷,觉得有些不过瘾,只是简单的介绍了下Redisson这个框架,具体的原理什么的还没说过呢.趁年前项目忙的差不多了,反正闲着也是闲着,不如把Rediss ...

随机推荐

  1. 钩子与API截获

    http://www.pudn.com/Download/type/id/19.html

  2. 基于SpringBoot+Mybatis+MySQL5.7的轻语音乐网

    一个基于SpringBoot+Mybatis+MySQL5.7的轻语音乐网站项目 1.主要用到的技术: 使用maven进行项目构建 使用Springboot+Mybatis搭建整个系统 使用ajax连 ...

  3. C# 编译机器码过程原理之再谈反射

    一.引言 我们知道在Java中有虚拟机,代码运行时虚拟机把Java语言编译成与机器无关的字节码,然后再把字节码编译成机器指令执行,那么在.NET中程序是如何运行的呢? 其实运行原理是一样的,.NET中 ...

  4. Vue2+Koa2+Typescript前后端框架教程--04班级管理示例(路由调用控制器)

    上篇文章分享了简单的三层模式和基础文件夹的创建,本篇将以示例的形式详细具体的展示Router.Controller.Service.Model之间业务处理和数据传输. 1. 班级管理数据模型创建.数据 ...

  5. intellij idea svn不能更新和提交

    进入设置–version control – subversion如下图,将前边的选项的勾全部去掉,点击ok

  6. [leetcode]82. Remove Duplicates from Sorted List

    第一题:遍历链表,遇到重复节点就连接到下一个. public ListNode deleteDuplicates(ListNode head) { if (head==null||head.next= ...

  7. Java对象赋值与引用

    当需要创建多个相同类型的对象且有某些字段的值是相同的,如果直接 get,set 的话,属性多的时候代码会很长,于是乎,以下代码产生了( java 基础差没搞清楚赋值与引用) 复制代码 1 User u ...

  8. 变量提升(hoisting)

    JavaScript的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行.这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting). con ...

  9. OpenGL投影矩阵(Projection Matrix)构造方法

    (翻译,图片也来自原文) 一.概述 绝大部分计算机的显示器是二维的(a 2D surface).在OpenGL中一个3D场景需要被投影到屏幕上成为一个2D图像(image).这称为投影变换(参见这或这 ...

  10. ACID隔离性

    数据库ACID 一致性 原子性  隔离性  持久性 隔离性: 1.读未提交 2.读已提交 3.可重复读 4.串行 读未提交:容易引起脏读 读已提交:容易引起幻读(前后读到的行数不一致) 场景: A事务 ...