本文记录Redisson实现分布式锁原理
分布式锁特性
不管使用什么中间件,下述几点是实现分布式锁必须要考虑到的.
- 互斥
- 避免死锁
- 性能保证:
高并发分布式系统中,线程互斥等待会成为性能瓶颈,需要好的中间件和实现来保证性能. - 锁特性:
分布式锁最好实现如Java Lock的一些功能如:锁判断,超时设置,可重入性等
Redis实现之Redisson原理
本次演示涉及的Redisson版本为
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.1</version>
</dependency>
- 互斥:
通过Redis数据结构来保证分布式锁的唯一性 - 避免死锁:
通过Redis设置有效期保证锁的释放 - 性能保证:
通过Redis部署方式(主从或集群)实现锁分段,且Redis本身作为缓存中间件满足大部分场景性能需求 - 锁特性:
针对分布式锁的使用,提供超时设置,锁中断,可重入性,锁续期(看门狗),锁判断(订阅模式)等特性
原理简图
下述为整理的原理简图,大致描述了加锁解锁的功能流程.
源码分析
针对上述流程简图,对关键代码进行分析
- 加锁
/**
* leaseTime:租赁时间,当前锁最久可使用时间,默认30S,可通过看门狗进行自动续期
* unit:时间单位
* interruptibly:是否可被中断,默认不可中断
*/
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
// 当前线程Id,用于组成锁的唯一键
long threadId = Thread.currentThread().getId();
/*
* 加锁核心逻辑.
* 当其他线程使用同一加锁关键字加锁时,返回的ttl为关键字锁的剩余租赁时间
*/
Long ttl = tryAcquire(leaseTime, unit, threadId);
// ttl为null时,证明当前线程加锁成功
if (ttl == null) {
return;
}
/*
* 当锁被占用的情况下,线程通过Redis订阅解锁事件,及时感知解锁操作,并再次竞争锁.
* 同时通过分布式信号量,完成竞争限流
*/
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
// 循环竞争锁
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
// 锁获取成功
if (ttl == null) {
break;
}
// 等待解锁消息发布
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 获取成功或失败后,取消订阅
unsubscribe(future, threadId);
}
}
/**
* 异步的加锁方法,由tryAcquire调用,实现加锁的核心逻辑
*/
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
/*
* leaseTime=-1为内置默认的锁租赁时间标志,默认为看门狗的超时时间30S
*/
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 异步执行Lua脚本
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
// 针对异常进行处理
if (e != null) {
return;
}
// 加锁成功,开启看门狗,进行锁续期操作
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
/**
* 定义异步类,执行加锁Lua脚本,保证执行的原子性
*/
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
/*
* Lua脚本解释:
* 如果当前锁关键字不存在,新增Redisson锁,并设置过期时间,返回null,结束;
* 如果Redisson锁存在,且加锁线程为当前线程,Redisson锁重入次数加1,重置过期时间,返回null,结束;
* 否则,当前锁关键字存在,返回其剩余租赁时间
*/
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
注:Redisson锁数据结构:
Hash:{"myLock":{"key":"761e9b81-5e36-494c-a94d-24ae725b3747:28","value":1}}
myLock:加锁的关键字,按照业务自定义
key:锁的唯一键,由Redisson连接池中连接Id及当前线程Id组合而成,使用:分割
value:当前锁重入的次数
- 解锁
/**
* 通过线程Id进行解锁操作
*/
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
// 解锁核心操作,异步执行Lua脚本
RFuture<Boolean> future = unlockInnerAsync(threadId);
future.onComplete((opStatus, e) -> {
// 取消当前线程注册的看门狗
cancelExpirationRenewal(threadId);
// 异常处理
if (e != null) {
result.tryFailure(e);
return;
}
// 当前线程未持有锁,进行解锁时的异常处理
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
// 解锁成功
result.trySuccess(null);
});
return result;
}
/**
* 定义异步类,执行解锁Lua脚本,保证执行的原子性
*/
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
/*
* 如果锁关键字对应的Redisson锁下线程不为当前线程,返回null
* 如果锁关键字对应的Redisson锁下线程为当前线程,重入次数减1:当重入次数大于0,重设锁失效时间,返回0;当重入次数小于等于0,删除Redisson锁,并发布解锁事件,返回1
* 均不符合,返回null
*/
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
测试代码
package com.pig4cloud.pig.admin.lock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.junit.Test;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Slf4j
@SpringBootTest
public class TestRedission {
CountDownLatch countDownLatch;
/**
* 测试多线程竞争
*/
@Test
public void expirationRenewalTest() throws InterruptedException {
countDownLatch = new CountDownLatch(3);
Thread t1 = new Thread(new RedissionLockRunnable());
t1.start();
Thread t2 = new Thread(new RedissionLockRunnable());
t2.start();
countDownLatch.await();
}
/**
* 测试锁重入
*/
@Test
public void tryLockInnerAsyncTest() throws InterruptedException {
countDownLatch = new CountDownLatch(1);
Thread t1 = new Thread(new RedissionLockReentrantRunnable());
t1.start();
countDownLatch.await();
}
RedissonClient redissonClient = createRedissonClient();
private class RedissionLockRunnable implements Runnable{
@Override
public void run() {
try {
System.out.println("Thread:" + Thread.currentThread().getName() + ",time: " + System.currentTimeMillis());
RLock lock = redissonClient.getLock("myLock");
if (lock.tryLock(100000, TimeUnit.MILLISECONDS)) {
try {
Thread.sleep(10000);
} finally {
countDownLatch.countDown();
lock.unlock();
}
} else {
countDownLatch.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private class RedissionLockReentrantRunnable implements Runnable{
@Override
public void run() {
try {
System.out.println("Thread:" + Thread.currentThread().getName() + ",time: " + System.currentTimeMillis());
RLock lock = redissonClient.getLock("myLock");
if (lock.tryLock(10000, TimeUnit.MILLISECONDS)) {
try {
Thread.sleep(1000);
lock.tryLock();
Thread.sleep(1000);
} finally {
countDownLatch.countDown();
lock.unlock();
}
} else {
countDownLatch.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private RedissonClient createRedissonClient() {
Config config = new Config();
String node = "redis://127.0.0.1:6379";
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(50000)
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5);
if (StringUtils.isNotBlank("")) {
serverConfig.setPassword("");
}
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
查看Redisson锁的Redis指令:
-- 查看当前订阅者
pubsub channels;
-- 查看当前锁结构
HSCAN myLock;
-- 查看锁剩余有效期
ttl myLock;