Redisson分布式锁看门狗
看门狗的精妙之处:存在一种业务情况,那就是加锁的时候设置了时间,但是实际业务代码执行时长大于设置时间,就会导致业务数据不一致。被加锁的资源被其他线程访问到。看门狗有个自动续时的功能,在分布式锁快要过期。
引入依赖
<!--jedis客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>
<!--jedis客户端-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.0</version>
</dependency>
代码块
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁
*
* @author dlf
* @date 2023/6/5 17:20
*/
@Component
public class RedisLockDemo {
private final RedissonClient redissonClient;
public RedisLockDemo(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public static void main(String[] args) throws InterruptedException {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + "Ip" + ":" + "port").setDatabase(0).setPassword("xxx");
RedisLockDemo redisLockDemo = new RedisLockDemo(Redisson.create(config));
redisLockDemo.reentrantLock();
new Thread(redisLockDemo::reentrantLock_expire, "线程一").start();
TimeUnit.SECONDS.sleep(30);
new Thread(redisLockDemo::reentrantLock_expire, "线程二").start();
TimeUnit.SECONDS.sleep(30);
}
/**
* 加锁
*/
public void reentrantLock() {
RLock lock = redissonClient.getLock("reentrant-lock-no-expire");
//没有获取到锁,返回
//默认的看门狗模式
lock.lock();
long startTime = System.currentTimeMillis();
try {
//模拟业务超时
System.err.println("模拟业务开始!!!");
TimeUnit.SECONDS.sleep(60);
System.err.println("模拟业务结束,耗时:" + (System.currentTimeMillis() - startTime) / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.err.println("手动释放锁,共耗时=" + (System.currentTimeMillis() - startTime) / 1000);
lock.unlock();
}
}
/**
* 加锁,指定锁的时长是25s
*/
public void reentrantLock_expire() {
RLock lock = redissonClient.getLock("reentrant-lock-no-expire");
long startTime = System.currentTimeMillis();
lock.lock(25, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName() + "获取到锁");
try {
// 模拟业务操作耗时
System.out.println(Thread.currentThread().getName() + "模拟业务开始");
TimeUnit.SECONDS.sleep(60);
System.out.println(Thread.currentThread().getName() + "模拟业务结束,共耗时=" + (System.currentTimeMillis() - startTime) / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "手动释放锁,共耗时=" + (System.currentTimeMillis() - startTime) / 1000);
lock.unlock();
}
}
public void release() {
this.redissonClient.shutdown();
}
}
运行结果
总结:
Redisson的lock()方法不指定过期时间的话,默认的过期时间是30秒,当过期时间超过1/3时,看门狗会自动续期(比如过期时间是30秒,则在10s的时候,看门狗就会自动续期),续期后的锁的时长重新变成30s
Redisson的lock(long leaseTime, TimeUnit unit)方法指定过期时间时,当到达过期时间时锁会自动释放,也就是说在这种情况下,看门狗失效。尝试去释放锁的的时候发现已经不在了。
Redisson的锁与线程相关,每个线程只能释放自己的锁,不能释放别的线程的锁。
分析:
无论是有参的还是午餐的lock()方法,其实调用的都是
tryAcquire
//不指定过期时间
@Override
public void lock() {
try {
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
//指定过期时间
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
lock(leaseTime, unit, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
这两个访问最终调用的都是lock(long leaseTime, TimeUnit unit, boolean interruptibly) 方法,唯一的区别是lock()方法传入的是参数leaseTime值是-1,而lock(long leaseTime, TimeUnit unit)传入的参数值是leaseTime。
让我们接着看看lock(long leaseTime, TimeUnit unit, boolean interruptibly)方法。
它的主要逻辑是:
若锁不存在,则设置锁,并设置过期时间,然后返回nil。
若锁存在且由本线程持有,则锁计数加一,并重设过期时间,然后返回nil;
否则返回锁的过期时间;
然后,看下看门狗是如何给锁续期的呢?直接查看scheduleExpirationRenewal方法。
protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
//重入加锁
oldEntry.addThreadId(threadId);
} else {
//第一次加锁,触发定时任务。
entry.addThreadId(threadId);
renewExpiration();
}
}
接着看看renewExpiration这个方法,
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
// 借助Netty的Timeout实现自动续期
// 超时时间为1/3过期时间,确保在过期前能够重设过期时间
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// reschedule itself
renewExpiration();
}
});
}
//过期时间超过1/3的话则会需求
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
续期的方法是renewExpirationAsync方法。这个方法也是一个LUA脚本,这个脚本的主要逻辑是,如果锁存在的话,则将过期时间重新设置为30s。
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), 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(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}