分布式锁
在微服务等多节点部署的环境下,系统会产生分布式事务问题,其解决的方式有很多,如ZooKeeper、Redis、Seata等,本文介绍基于Redis开发的Redisson框架如何实现Redis版本的分布式锁。
使用(基于SpringBoot)
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
</dependencies>
添加配置信息
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://192.168.93.128:6379")
.setDatabase(0)
.setPassword("123456")
.setConnectionPoolSize(50)
.setConnectionMinimumIdleSize(10);
return Redisson.create(config);
}
}
测试接口
@Slf4j
@RestController
public class LockController {
@Autowired
private RedissonClient redissonClient;
@RequestMapping("/test_lock")
public String testLock(@RequestParam("key") String key) {
RLock redissonLock = redissonClient.getLock("lock:" + key);
try {
redissonLock.lock();
// todo 业务代码
return "success";
} catch (Exception e) {
log.error(e + "");
return "error";
} finally {
redissonLock.unlock();
}
}
}
源码分析
上方示例通过redissonLock.lock();和redissonLock.unlock();两个命令即可实现基于Redis的分布式锁,下面会针对该API进行源码分析。
源码调用流程图
加锁源码
lockInterruptibly()源码分析
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
lockInterruptibly(leaseTime, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
// -1和null表示采用默认的锁超时时间
lockInterruptibly(-1, null);
}
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
// 获取当前线程id
long threadId = Thread.currentThread().getId();
// 尝试加锁
Long ttl = tryAcquire(leaseTime, unit, threadId);
// 如果ttl为空表示加锁成功,否则该线程被其他客户端加锁
if (ttl == null) {
// 加锁成功直接返回
return;
}
// 加锁失败对该线程进行订阅等待
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
// 自旋重新获取锁
while (true) {
// 再次尝试获取锁
ttl = tryAcquire(leaseTime, unit, threadId);
// 获取成功返回,否则继续自旋
if (ttl == null) {
break;
}
// 如果锁资源被占用则在等待时间结束后再重试,否则直接尝试加锁
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
// 取消对该线程的订阅
unsubscribe(future, threadId);
}
}
tryAcquire()源码分析
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
// 在lockInterruptibly(-1, null);传入了-1,表示使用默认的leastTime
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 使用异步方式尝试加锁
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 将该锁加入订阅,并进行锁续命
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}
Long ttlRemaining = future.getNow();
// 如果锁再次获取成功,获取新的超时时间,对锁进行续命
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
}
});
// 返回锁的超时时间
return ttlRemainingFuture;
}
tryLockInnerAsync()源码分析
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
// 使用Lua脚本保证原子性,底层进行hash结构的锁赋值操作,并设置超时时间
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 如果该锁不存在,则初始化key fleid value分别为传入的keyName,锁标识,1(表示重入次数),并设置超时时间
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 如果该锁的name和锁标识都相同,则表示是可重入锁,将value+1,并设置超时时间
"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]);",
// getName()传入KEYS[1],表示传入加锁的keyName
// internalLockLeaseTime传入ARGV[1],表示锁的超时时间
// getLockName(threadId)传入ARGV[2],表示锁的唯一标识,由UUID+":"+线程id组成
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
scheduleExpirationRenewal()源码分析
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
// 启用定时任务线程更新锁的超时时间,每internalLockLeaseTime / 3 秒执行一次(10s执行一次)
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果redis中存在该锁,则更新锁的超时时间为30s(默认值)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}
if (future.getNow()) {
// 将自己线程再次加入定时任务队列,实现不断地续命
scheduleExpirationRenewal(threadId);
}
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}
解锁源码
unlock()源码分析
@Override
public void unlock() {
// 调用异步解锁方法
Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
if (opStatus == null) {
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + Thread.currentThread().getId());
}
if (opStatus) {
// 取消续命订阅
cancelExpirationRenewal();
}
}
unlockInnerAsync()源码分析
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果该锁不存在,则发布已经解锁的消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 如果该锁对应的name和锁标识不匹配,说明该客户端没有该锁,无法解锁
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 可重入锁的value-1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 如果重入锁的计数器>0表示该锁仍然有效,更新锁的超时时间
"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;",
// getName()传入KEYS[1],表示传入解锁的keyName
// getChannelName()传入KEYS[2],表示redis内部的消息订阅channel
// LockPubSub.unlockMessage传入ARGV[1],表示向其他redis客户端线程发送解锁消息
// internalLockLeaseTime传入ARGV[2],表示锁的超时时间
// getLockName(threadId)传入ARGV[3],表示锁的唯一标识,由UUID+":"+线程id组成
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
cancelExpirationRenewal() 源码分析
void cancelExpirationRenewal() {
// 将该线程从定时任务中删除
Timeout task = expirationRenewalMap.remove(getEntryName());
if (task != null) {
task.cancel();
}
}