本文内容主要来自黑马程序员Redis课程相关章节总结
一、基于Redission实现分布式锁
基于setnx实现的分布式锁存在的问题:
- 不可重入,同一线程无法多次获取同一把锁
- 不可重试 ,获取锁只尝试一次就返回false,没有重试机制
- 超时释放,锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
- 主从一致性,如果redis提供了主从集群,主从同步存在延迟,当主宕机时,从机可能没有同步到锁数据
1、导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.3</version>
</dependency>
2、配置Redisson客户端
@Bean
public RedissonClient redissonClient() {
//配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.85.150:6379").setPassword("123456");
//创建redisson客户端
return Redisson.create(config);
}
3、使用Redisson分布式锁
@Resource
private RedissionClient redissonClient;
@Test
void testRedisson() throws Exception{
//获取锁(可重入),指定锁的名称
RLock lock = redissonClient.getLock("anyLock");
//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//判断获取锁成功
if(isLock){
try{
System.out.println("执行业务");
}finally{
//释放锁
lock.unlock();
}
}
}
二、redission可重入锁原理
redission采用hash结构用来存储锁,其中key表示这把锁是否存在,用field标识当前这把锁被哪个线程所持有。当线程初次获取锁时,创建一个hash结构,并将值设为1,以后每次重入,将该值+1,每次释放,则将值-1,直到值为0时删除。
获取锁的Lua脚本
参数说明:
KEYS[1] :锁名称
ARGV[1]: 锁失效时间
ARGV[2]: 锁的小key
-- 判断锁是否存在
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;-- 返回结果nil,表示已获取锁
end;
-- 锁已经存在,判断自己是否持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 已经持有,则重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 重置有效期
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;-- 返回结果nil,表示再次获取锁
end;
-- 未获取锁,返回锁的剩余有效时间
return redis.call('pttl', KEYS[1]);
释放锁的Lua脚本
参数说明:
KEYS[1]: 锁名称
KEYS[2]: 锁的通道名
ARGV[1]: 锁释放的发布消息
ARGV[2]: 锁失效时间
ARGV[3]: 锁的小key
-- 判断当前锁是否还被自己持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil; -- 如果已经不是自己,表示已释放,直接返回
end;
-- 是自己的锁,则重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 判断是否重入次数是否已经为0
if (counter > 0) then
-- 大于0说明不能释放锁,重置有效期然后返回
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else -- 等于0说明可以释放锁,直接删除
redis.call('del', KEYS[1]);
-- 发布锁释放消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
三、redisson锁重试和WatchDog机制
tryLock方法获取锁有以下几种方式:
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
其中,第一种无参数的方式为一次性获取锁,失败则不再重试,而另外两个则需要设置锁等待时间,在等待时间内,如果没有获取锁,会不断进行尝试。
由以下代码可知,方法二调用了方法三,将锁释放时间设置为-1。
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
return tryLock(waitTime, -1, unit);
}
有等待时间的获取锁代码讲解:
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
//将等待时间转化为毫秒
long time = unit.toMillis(waitTime);
//获取系统当前毫秒时间戳
long current = System.currentTimeMillis();
//获取当前线程ID
long threadId = Thread.currentThread().getId();
//调用获取锁lua脚本的返回值
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 返回值为空,表示获取锁,直接返回true
if (ttl == null) {
return true;
}
// 返回值不为空,表示当前线程未获取锁,计算锁等待剩余时间
// 锁等待剩余时间 = 锁等待时间 - 本次获取锁过程的消耗时间
time -= System.currentTimeMillis() - current;
//如果剩余时间已经用完,则当前线程未能获取锁
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
//如果剩余时间未用完,则订阅锁释放消息
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 如果在剩余时间内,没有等到其他线程释放锁,则取消订阅,返回失败
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 如果其他线程释放了锁,则再次不断尝试获取
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
以上代码解释了redisson的锁重试原理。
下面分析当调用方法tryLock(long time, TimeUnit unit),琐释放时间设置为-1时,Redisson是如何处理的。
从上述获取锁的部分"Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId)"开始往下跟踪源码
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
//当锁释放时间不是-1时,执行获取锁lua脚本
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//当锁释放时间为-1时,则设置为30秒释放
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//定时重置锁失效时间
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
internalLockLeaseTime的值为代码设置的看门狗时间,默认为30秒
private long lockWatchdogTimeout = 30 * 1000;
继续跟踪scheduleExpirationRenewal方法,进入到renewExpiration方法;
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
//10秒钟后执行任务
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;
}
//使用lua脚本重置锁失效时间
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;
}
//重置成功,递归调用renewExpiration方法
if (res) {
// reschedule itself
renewExpiration();
} else {
// 取消看门狗
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
因为锁的失效时间是30s,当10s之后,此时这个timeTask 就触发了,他就去进行续约,把当前这把锁续约成30s,如果操作成功,那么此时就会递归调用自己,再重新设置一个timeTask(),于是再过10s后又再设置一个timerTask,完成不停的续约。
为什么使用看门狗不断续约,重置超时时间?
当服务意外宕机,没有执行unlock释放锁时。这时没有人再去调用renewExpiration方法,所以等到时间之后自然就释放了。
四、redission锁的MutiLock原理
解决主从一致问题
为了提高redis的可用性,一般会搭建集群或者主从
当主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中并没有同步到锁信息,则锁信息就丢掉了。
为了解决这个问题,redission提出了MutiLock锁(联锁)。MutiLock加锁的逻辑需要写入到每一个主节点上,只有所有的服务器都写入成功,才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。
使用案例:
@Autowired
private RedissonClient redissonClient1;
private RedissonClient redissonClient2;
private RedissonClient redissonClient3;
private RLock lock;
@BeforeEach
public void setUp() {
RLock lock1 = redissonClient1.getLock("order");
RLock lock2 = redissonClient2.getLock("order");
RLock lock3 = redissonClient3.getLock("order");
//创建联锁
lock = new RedissonMultiLock(lock1, lock2, lock3);
}
@Test
public void testRedissonMultiLock() throws InterruptedException {
//尝试获取锁
boolean isLock = lock.tryLock(10L, TimeUnit.SECONDS);
if(!isLock){
log.error("获取锁失败");
return;
}
try{
log.info("收取锁成功,开始执行业务");
//...
}finally {
log.warn("准备释放锁");
lock.unlock();
}
}
源码分析:
查看RedissonMultiLock的构造方法,在该方法中,redission会将多个RLock锁对象添加到一个locks集合中;
final List<RLock> locks = new ArrayList<>();
/**
* Creates instance with multiple {@link RLock} objects.
* Each RLock object could be created by own Redisson instance.
*
* @param locks - array of locks
*/
public RedissonMultiLock(RLock... locks) {
if (locks.length == 0) {
throw new IllegalArgumentException("Lock objects are not defined");
}
this.locks.addAll(Arrays.asList(locks));
}
再进入tryLock方法,
@Override
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
return tryLock(waitTime, -1, unit);
}
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// try {
// return tryLockAsync(waitTime, leaseTime, unit).get();
// } catch (ExecutionException e) {
// throw new IllegalStateException(e);
// }
long newLeaseTime = -1;
if (leaseTime != -1) {
if (waitTime == -1) {
newLeaseTime = unit.toMillis(leaseTime);
} else {
newLeaseTime = unit.toMillis(waitTime)*2;
}
}
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
long lockWaitTime = calcLockWaitTime(remainTime);
//最多允许获取锁失败的限制个数,默认为0
int failedLocksLimit = failedLocksLimit();
//成功获取锁的集合
List<RLock> acquiredLocks = new ArrayList<>(locks.size());
//遍历locks集合,对每个lock实例都尝试获取锁
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
if (waitTime == -1 && leaseTime == -1) {
lockAcquired = lock.tryLock();
} else {
long awaitTime = Math.min(lockWaitTime, remainTime);
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (RedisResponseTimeoutException e) {
unlockInner(Arrays.asList(lock));
lockAcquired = false;
} catch (Exception e) {
lockAcquired = false;
}
if (lockAcquired) {
acquiredLocks.add(lock);
} else {
//如果成功获取锁的个数满足要求,则跳出循环,已经失败的锁不再重试
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
if (failedLocksLimit == 0) {
//如果有一个锁获取失败,则释放所有已经成功获取的锁,确保锁的一致性
unlockInner(acquiredLocks);
if (waitTime == -1) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// 重置迭代器游标,再次遍历获取锁
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
if (remainTime != -1) {
remainTime -= System.currentTimeMillis() - time;
time = System.currentTimeMillis();
//如果剩余时间小于0,则释放所有锁,返回获取锁失败
if (remainTime <= 0) {
unlockInner(acquiredLocks);
return false;
}
}
}
if (leaseTime != -1) {
acquiredLocks.stream()
.map(l -> (RedissonLock) l)
.map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS))
.forEach(f -> f.syncUninterruptibly());
}
return true;
}
五、redission锁的RedLock原理
RedissonReadLock红锁是基于RedissonMultiLock实现的。它们的区别是,红锁在获取锁的节点超过半数时,即认为成功获取锁。
RedissonRedLock源码,重写了failedLocksLimit方法,不再返回0,而是locks.size()/2 - 1。
public class RedissonRedLock extends RedissonMultiLock {
/**
* Creates instance with multiple {@link RLock} objects.
* Each RLock object could be created by own Redisson instance.
*
* @param locks - array of locks
*/
public RedissonRedLock(RLock... locks) {
super(locks);
}
@Override
protected int failedLocksLimit() {
return locks.size() - minLocksAmount(locks);
}
protected int minLocksAmount(final List<RLock> locks) {
return locks.size()/2 + 1;
}
@Override
protected long calcLockWaitTime(long remainTime) {
return Math.max(remainTime / locks.size(), 1);
}
@Override
public void unlock() {
unlockInner(locks);
}
}