先来个一个业务场景:在订单服务中,我们需要创建一个定时任务去关闭支付超时的订单并把库存量加回去。这里有三个步骤。
1:定时查询已经超时的订单
2:修改订单的状态,改为已关闭
3:恢复订单中扣减的库存
要是在好几个相同的服务中能共享同一个标记锁,这时候就需要分布式锁。实现分布是锁要满足五点:多进程可见,互斥,可重入,阻塞,高性能,高可用等。
可重入锁,也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。
一般的分布式锁可以通过基于MySql、基于Redis、 基于zookeeper实现的。
基于Redis使用的比较多,这边使用的是开源的Redission框架去实现了各种基于Redis的分布式锁,包括Redlock锁。
二、Redission使用
导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.6</version>
</dependency>
配置config
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
// 配置类
Config config = new Config();
// 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
// 创建客户端
return Redisson.create(config);
}
}
@PostMapping("hhh")
public void hhh() throws InterruptedException {
RLock lock = redissonClient.getLock("lock");
// 尝试加锁
boolean isLock = lock.tryLock();
// 判断是否成功
if(!isLock){
// 获取锁失败,结束任务
return;
}
try {
// 执行任务
clearOrder();
}finally {
// 释放锁
lock.unlock();
}
}
public void clearOrder() throws InterruptedException {
Thread.sleep(500);
}
根据上述代码切到源码可以看到相关的细节。
1:创建一个锁对象
2:看看能不能加锁,当然这是没有别的条件的情况下,只要调用的时候空闲时候就可以调用。在别的含参数的方法中还有的条件,例如在指定时间内调用空闲且当前线程没有被中断的时候才能调用。
这就是最终获取锁的方法。将其中的Lua脚本复制出来。
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "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; 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(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
KEYS[1]; -- 锁的key
ARGV[1]; -- 线程唯一标识
ARGV[2]; -- 锁的自动释放时间
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; --返回了这个key的剩余有效期
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then --要是锁已经存在就判断threadId是否是自己
redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 不存在,获取锁
redis.call('pexpire', KEYS[1],ARGV[1]); -- 设置有效期
return nil; --返回了这个key的剩余有效期
end;
return redis.call('pttl', KEYS[1]);" --代码走到这里,说明获取锁的不是自己,获取锁失败
可见返回的是true
锁如果在执行任务时自动过期,就会引起各种问题, 因此我们需要在锁过期前自动申请续期,这个被称为watch dog。
private void scheduleExpirationRenewal(long threadId) {
RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
this.renewExpiration();
}
}
刷新时间的代码:
private void renewExpiration() {
RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
RedissonLock.this.renewExpiration();
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
刷新过期时间的代码:
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)});
}
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then --判断有没有过期
redis.call('pexpire', KEYS[1], ARGV[1]); --要是过期就重新给他刷新时间
return 1;
end;
return 0;
3:解锁
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[3]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then -- 判断当前锁是否还是被自己持有
return nil;-- 如果已经不是自己,则直接返回
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 是自己的锁,则重入次数-1
if (counter > 0) then -- 判断是否重入次数是否已经为0
-- 大于0说明不能释放锁,重置有效期然后返回
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]); -- 等于0说明可以释放锁,直接删除
redis.call('publish', KEYS[2], ARGV[1]);--发布了一条消息,通知锁已经释放,那些再等待的其它线程,就可以获取锁了
return 1;
end;
return nil;
总结来看,Redis实现分布式锁,具备下列优缺点:
- 优点:实现简单,性能好,并发能力强,如果对并发能力有要求,推荐使用
- 缺点:可靠性有争议,极端情况会出现锁失效问题,如果对安全要求较高,不建议使用