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

10 篇文章 0 订阅

前言

本篇主要分享自己遇到以及了解的分布式锁,关于过期时间的坑,提醒自己和大家去正确使用它

一、背景

在微服务项目中,大家多多少少会接触使用到分布式锁,一般采取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();
}

上面这段代码有没有问题?下面我们一起来分析下~

二、分析

像上面的写法,符合我们的常规思维,因为对于缓存,基本都要设置一个过期时间,不能一直留在redis中,也是为了防止程序挂了,没有释放锁,所以都会设置一个过期时间

1、初步分析

好的,提到了过期时间,那么这个时间设置多长合适?

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

这时,你可能会想,我们可以根据业务执行情况,设置个过期时间即可,对于部分执行久的业务,Redisson内部是有个看门狗机制,会帮我们去续期,那么是否就以为我们可以设置很短?频繁续期你觉得合理嘛~

简单来说,看门狗机制就是有个定时器,会去看我们的业务执行完没,没有就帮我们把过期时间延长,看似没有问题吧

2、源码分析

我们来看下源码,无论我们使用哪种方式,最终都会进到这个tryAcquireAsync方法,那就是看门狗机制的核心代码,如下:

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;
    }

从上面可以知道,如果我们指定了过期时间,会直接加锁,没有指定过期的话,默认是30s的时间,但会启动一个监听器,定期去续期,具体方法就是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();
     }
 }

public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
    super(commandExecutor, name);
    this.commandExecutor = commandExecutor;
    this.id = commandExecutor.getConnectionManager().getId();
    this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
    this.entryName = this.id + ":" + name;
}

    public Config() {
        this.transportMode = TransportMode.NIO;
        this.lockWatchdogTimeout = 30000L;
        this.keepPubSubOrder = true;
        this.addressResolverGroupFactory = new DnsAddressResolverGroupFactory();
    }

从什么可以指定,当业务执行了LockWatchdogTimeout(30s)的1/3时间,就会去执行续期,也就是当时间超过10s后就会去续期,续期后的锁的时长重新变成 30 秒,源码如下:

    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

所以,结论是啥?就是以下两种写法都有问题

// 方式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指定
  • 看门狗每过 (过期时间 / 3)就去续期
  • 看门狗机制底层实现,类似Netty的时间轮
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员Forlan

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值