为什么要分布式锁
单体应用中,可以通过synchronized等相关锁实现线程间共享数据的独占,但是在分布式环境下,线程锁是不能跨应用的,所以需要通过一个分布式存储组件来实现分布式锁。常用的分布式锁实现组件有Redis和ZooKeeper,由于Redis是AP(可用性)架构的,ZooKeeper是CP(一致性)架构的,根据实际的应用场景,两种实现方案都可以。
大多数场景下,用Redis实现分布式锁就可以了,主要是Redis性能比ZooKeeper更好,实现的分布式锁效率更高。如果需要在强一致性条件下实现分布式锁,那么可以考虑使用ZooKeeper。
此处介绍的是Redis实现分布式锁,一些Redis客户端组件已经为我们封装实现了Redis分布式锁了,比如Redisson。生产环境下,建议使用这些组件实现的分布式锁,以防止自己实现时考虑不周导致线上问题。这些三方组件是通过大量用户验证了的,安全性和性能更高一些。
Redisson实现分布式锁
原理图:
例如:
源码分析:
getLock()-获取锁对象
获取锁对象,其实就是获取一个RedissonLock对象(锁的大部分逻辑都是在该对象中实现的),主要是一些初始化工作。
RLock lock = redisson.getLock("lock");
@Override
public RLock getLock(String name) {
return new RedissonLock(connectionManager.getCommandExecutor(), name);
}
RedissonLock继承了RedissonExpirable抽象类,实现了RLock接口,如下图:
RedissonLock构造方法:
// name为锁名称
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
// 对应的RedissonExpirable构造方法
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
// id为通过UUID.randomUUID()生成的uuid
this.id = commandExecutor.getConnectionManager().getId();
// internalLockLeaseTime为redis连接配置文件配置的看门狗超时时间,默认30秒
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
}
lock()-加锁:
lock.lock( 1,TimeUnit.SECONDS);
----------------------------------------
public void lock() {
try {
// 获取锁
lockInterruptibly();
} catch (InterruptedException e) {
// 处理中断异常
Thread.currentThread().interrupt();
}
}
--------------------------------
public void lockInterruptibly() throws InterruptedException {
lockInterruptibly(-1, null);
}
获取锁:
获取锁成功返回ttl剩余过期时间为null,不再继续执行。获取锁失败返回ttl剩余过期时间不为null,while循环再次获取锁,这里用到了Semaphore实现阻塞ttl时间后轮序和唤醒通知机制。
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 通过lua脚本原子地设置锁名称到redis,并设置过期时间(默认30秒)
// 是一个hash结构,key为锁名称,field为uuid+当前线程id
// ttl为当前锁剩余过期时间毫秒数
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
// 请求获取锁成功,ttl返回null,不再执行后面逻辑
if (ttl != null) {
// ttl不为空,表示当前锁已被占用,没有获取到锁
// redis的发布订阅,没有抢到锁的请求订阅一个channel,哪里发布的这个channel呢?主请求解锁时发布的
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
while(true) {
// 再次尝试获取锁,刷新ttl
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
// 获取到了锁
return;
}
if (ttl >= 0L) {
// 仍然没有获取到锁,自旋获取锁,非公平
// 信号量,阻塞ttl时间后再while,让出cpu,因为信号量的凭证数量为0
// 如果获取到锁的请求在ttl之内就自行完毕,难道这里还要等ttl个时间?有唤醒功能
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
this.getEntry(threadId).getLatch().acquire();
}
}
} finally {
// 取消订阅
this.unsubscribe(future, threadId);
}
}
}
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
// 获取异步结果
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
尝试加锁:
加锁成功走续命逻辑,加锁失败会还行上级方法中的while循环获取锁逻辑。
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
// lock()不会进来
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// leaseTime获取的是redis配置中的时间,默认30秒
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;
}
// tryLockInnerAsync()方法异步执行成功后
// 得到当前锁过期剩余时间
Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
// 加锁成功,走这里
// 超时刷新,锁续命逻辑,延时任务xxx/3,主线程锁存在,重置为30秒
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
续命逻辑:
后台异步任务。加锁成功后,每隔30/3=10秒重置当前锁的过期时间。
private void scheduleExpirationRenewal(final long threadId) {
// tryLock的实现,暂不分析
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
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,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// 当前锁存在,将锁过期时间重置为30秒
"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()) {
// reschedule itself
// 递归实现续命逻辑
scheduleExpirationRenewal(threadId);
}
}
});
}
// 默认每30/3=10秒间歇性执行
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}
unlock()-解锁
主动解锁。
lock.unlock();
public void unlock() {
// 解锁
Boolean opStatus = (Boolean)this.get(this.unlockInnerAsync(Thread.currentThread().getId()));
if (opStatus == null) {
// 释放当前锁失败
throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + Thread.currentThread().getId());
} else {
if (opStatus) {
// 成功解锁,取消锁续命定时任务
this.cancelExpirationRenewal();
}
}
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 锁不存在,发布消息,等待的请求订阅了该channel
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
// 锁已被释放
"return nil;" +
"end; " +
// 之前这里设置的是1
"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.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}