Redis分布式锁这样用,有坑?

背景

在微服务项目中,大家都会去使用到分布式锁,一般也是使用Redis去实现,使用RedisTemplate、Redisson、RedisLockRegistry都行,公司的项目中,使用的是Redisson,一般你会怎么用?看看下面的代码,是不是就是你的写法

String lockKey = "forlan_lock_" + serviceId;
RLock lock = redissonClient.getLock(lockKey);

// 方式1
try {
	lock.lock(5, TimeUnit.SECONDS);
	// 执行业务
	...
} catch (Exception e) {
	e.printStackTrace();
} finally {
	// 释放锁
	lock.unlock();
}

// 方式2
try {
	if (lock.tryLock(5, 5, TimeUnit.SECONDS)) {
		// 获得锁执行业务
		...
	}
} catch (Exception e) {
	e.printStackTrace();
} finally {
	// 释放锁
	lock.unlock();
}

分析

像上面的写法,符合我们的常规思维,一般,为了避免程序挂了的情况,没有释放锁,都会设置一个过期时间
但这个过期时间,一般设置多长?

设置过短,会导致我们的业务还没有执行完,锁就释放了,其它线程拿到锁,重复执行业务
设置过长,如果程序挂了,需要等待比较长的时间,锁才释放,占用资源

这时候,你会说,一般我们可以根据业务执行情况,设置个过期时间即可,对于部分执行久的业务,Redisson内部是有个看门狗机制,会帮我们去续期,简单来说,就是有个定时器,会去看我们的业务执行完没,没有就帮我们进行延时,看似没有问题吧,那我们来简单看下源码,无论我们使用哪种方式,最终都会进到这个方法,就是看门狗机制的核心代码

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1L) {
        // 前面我们指定了过期时间,会进到这里,直接加锁
        return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        // 没有指定过期时间的话,默认采用LockWatchdogTimeout,默认是30s
        RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        // ttlRemainingFuture执行完,添加一个监听器,类似netty的时间轮
        ttlRemainingFuture.addListener(new FutureListener<Long>() {
            public void operationComplete(Future<Long> future) throws Exception {
                if (future.isSuccess()) {
                    Long ttlRemaining = (Long)future.getNow();
                    if (ttlRemaining == null) {
                        RedissonLock.this.scheduleExpirationRenewal(threadId);
                    }
                }
            }
        });
        return ttlRemainingFuture;
    }

scheduleExpirationRenewal方法

private void scheduleExpirationRenewal(final long threadId) {
 if (!expirationRenewalMap.containsKey(this.getEntryName())) {
     Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
         public void run(Timeout timeout) throws Exception {
         	 // renewExpirationAsync就是执行续期的方法
             RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
             // 什么时候触发执行?
             future.addListener(new FutureListener<Boolean>() {
                 public void operationComplete(Future<Boolean> future) throws Exception {
                     RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
                     if (!future.isSuccess()) {
                         RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
                     } else {
                         if ((Boolean)future.getNow()) {
                             RedissonLock.this.scheduleExpirationRenewal(threadId);
                         }

                     }
                 }
             });
         }
     }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 当跑了LockWatchdogTimeout的1/3时间就会去执行续期
     if (expirationRenewalMap.putIfAbsent(this.getEntryName(), new RedissonLock.ExpirationEntry(threadId, task)) != null) {
         task.cancel();
     }
 }

所以,结论是啥?

// 方式1
lock.lock(5, TimeUnit.SECONDS);
// 方式2
lock.tryLock(5, 5, TimeUnit.SECONDS)

我们这两种写法都会导致看门狗机制失效,如果业务执行超过5s,就会出问题

解决

正确的写法应该是,不指定过期时间

// 方式1
lock.lock();
// 方式2
lock.tryLock(5, -1, TimeUnit.SECONDS)

你可以会觉得不妥,不指定的话,就默认按照30s续期时间,然后每10s去看看有没有执行完,没有就续期,
我们也可以指定续期时间,比如指定为15s

config.setLockWatchdogTimeout(15000L);

总结

  • 在使用Redisson实现分布式锁,不应该设置过期时间
  • 看门狗默认续期时间是30s,可以通过setLockWatchdogTimeout指定
  • 看门狗会每internalLockLeaseTime / 3L去续期
  • 看门狗底层实际就是类似Netty的时间轮

原文链接:https://www.cnblogs.com/huozhonghun/p/17323627.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Redis分布式锁这样用,有坑? - Python技术站

(0)
上一篇 2023年4月17日
下一篇 2023年4月17日

相关文章

  • 看动画学算法之Java实现doublyLinkedList

    看动画学算法是一种十分有趣的学习方式,Java实现doublyLinkedList正好可以通过该方法进行学习。下面是实现doublyLinkedList的完整攻略。 准备工作 在进行doublyLinkedList的实现之前,需要进行如下准备工作: 确认Java编译器、JUnit测试框架和Maven构建工具是否已经安装好。 创建一个新的Java项目并在其中添…

    Java 2023年5月19日
    00
  • java实现计算周期性提醒的示例

    下面我将为大家详细讲解如何使用Java实现计算周期性提醒的示例,包括代码实现和演示两个示例。 如何实现周期性提醒 使用计时器:使用Java中自带的计时器类Timer,可以通过该类的schedule(TimerTask task, long delay, long period)方法,设置一个TimerTask任务和一个开始执行的延迟时间、执行周期。在该任务的…

    Java 2023年5月20日
    00
  • springboot自动装配大概原理

    自动装配: pom.xml spring-boot-dependence:核心都依赖在父类工程中! 我们在写入或者引入springboot依赖的时候,不需要指定版,因为有这些仓库的版本 启动器:——spring boot的启动场景 比如spring-boot-starter-web,他就会帮我们导入web环境苏需要的依赖。 springboot会将所…

    Java 2023年4月25日
    00
  • java常见的字符串操作和日期操作汇总

    Java常见的字符串操作 字符串的基本操作 Java String是不可变对象,是对比较字符串最常用最简便的类,常见的字符串操作有: 字符串拼接: 使用+操作符进行字符串拼接,例如 “Hello” + “World”,结果为 “Hello World”。 使用concat()方法进行字符串拼接,例如 “Hello”.concat(” “).concat(“W…

    Java 2023年5月20日
    00
  • 关于java中的多态和对实例化对象的一些理解

    java面向对象三大特征即为:继承封装多态。而多态需要三大必要条件。分别是:继承、方法重写、父类引用指向子类对象。我们先一个一个来理解。 1、首先是继承和重写。这个很简单。因为多态就是建立在不同的重写之上的。也就是说多态就是在使用着一个方法的不同重写。而重写又是依赖着继承关系。 2、这个父类引用指向子类对象。 首先先上代码示例。 public class A…

    Java 2023年5月3日
    00
  • 在IDEA中集成maven详细流程图示例

    下面是“在IDEA中集成Maven”的详细攻略,包含两条流程示例。 在IDEA中集成Maven详细攻略 1. 配置Maven环境 Maven是Java项目的构建工具,需要先安装配置Maven环境。这里给出两种安装方式: 方式一:通过IDEA自带的Maven安装 打开IDEA,选择File-Settings-Build, Execution, Deployme…

    Java 2023年5月20日
    00
  • Jersey实现Restful服务(实例讲解)

    Jersey 是一个开源、功能强大的框架,用于为 Java 应用程序提供面向资源的 RESTful Web Services。它提供了一种简单的必要 API 和工具,用于快速开发并构建可伸缩、互操作的 RESTful Web Services。 以下是 Jersey 实现 RESTful 服务的完整攻略: 一、准备工作 安装 JDK 和 Eclipse 下载…

    Java 2023年5月19日
    00
  • SpringBoot自定义cron表达式注册定时任务

    springBoot自定义cron表达式注册定时任务 一、原理 1、使用Spring自带的TaskScheduler注册任务 2、注册后返回:ScheduledFuture,用于取消定时任务 3、注册任务后不会马上取消任务,所以将任务缓存。在需要取消任务的时候调用取消接口取消 4、cron表达式可以由前端或者后端生成。实现中会校验cron表达式 public…

    Java 2023年4月22日
    00
合作推广
合作推广
分享本页
返回顶部