Redis 高级篇 Part 4
😉 本文共21字,阅读时间约8min
分布式锁-redission
功能介绍
基于setnx实现的分布式锁存在下面的问题:
重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。
超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患
主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。
希望的锁:可重入、可重试、自动续期、主从一致
Redisson相当于在Redis的基础上又进行了一层封装。
包含了各种分布式锁的实现,可重入锁,公平锁,联锁,红锁,读写锁,信号量。
可重入锁原理
hash结构,大key为锁名,小key为持有线程名,value为重入次数。
获取锁和释放锁底层都是lua脚本。
-
获取锁:
-
锁不存在,第一次,加锁,设置过期时间
-
锁已存在,为当前线程,重入计数+1,重新设置过期时间
-
锁已存在,非当前线程,返回的是当前锁剩余有效期
// 锁不存在,第一次,加锁,设置过期时间 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; // 锁已存在,为当前线程,重入计数+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]);
-
-
释放锁
-
当前锁已被别人持有,直接返回
-
当前锁还是自己的,重入计数-1。若重入计数为0,直接删除锁。
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;
-
锁重试
- tryAcquire()进行加锁,该加锁逻辑和之前是一样的
- 如果返回锁失效时间,而不是nil,代表加锁失败。
- 订阅持有锁进程释放锁的信号量,阻塞等待。
- 信号量发布,继续执行
- while(true)循环开始
- 尝试获取锁
- 如果失败,继续等释放锁的信号量,阻塞等待
- 信号量发布继续执行循环
期间会不停判断是否超过最大等待时间!
信号量机制使得不会无休止尝试,每一次尝试时都有人释放了锁
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// 未获取到锁,进入重试环节
// 超过wait_time,直接失败
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)) {
// 阻塞时间超过wait_time,直接失败
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
// 超过wait_time,直接失败
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;
}
// 获取锁失败
// 超过wait_time,直接失败
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);
}
// 超过wait_time,直接失败
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
}
自动续期:看门狗机制
- 传入释放时间,无看门狗机制,调用一次lua脚本设置过期时间
- 使用默认释放时间,看门狗逻辑自动续约,调用自动续约函数
- 10s后自动续约
- 续约完成后,递归调用自身(自动续约函数)
- 无限续约
// 被tryLock方法调用
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
// 传入释放时间,无看门狗机制
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 释放时间为默认看门狗时间,30s
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime, commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
// 异步任务完成后,如果获取到锁,自动更新续期
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
private void renewExpiration() {
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// renewExpirationAsync,重置锁有效期
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
// 递归调用,效果是下一个10s执行
if (res) {
// reschedule itself
renewExpiration();
}
});
}
// 10s后执行一次task
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
- 锁释放时,会根据一个map找到该线程持有的看门狗线程,并将其取消掉
假设我们的线程出现了宕机他还会续约吗?当然不会,因为没有人再去调用renewExpiration这个方法,所以等到时间之后自然就释放了。
总结
Redisson分布式锁原理:
•可重入:利用hash结构记录线程id和重入次数
•可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制
•超时续约:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间
主从一致 MutiLock原理
主从同步问题
为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例
此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。
联锁
为了解决这个问题,redission提出来了MutiLock锁,三个主节点都是独立节点,, 这把锁加锁的逻辑需要写入到每一个主节点上,只有所有的服务器都写入成功,此时才是加锁成功。
假设现在某个节点挂了,当前线程还有两个主节点有锁。但其他线程想去获得锁的时候,有一个节点拿不到,就无法加锁成功,就保证了加锁的可靠性。
那么MutiLock 加锁原理是什么呢?笔者画了一幅图来说明
当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.
为什么Redis这么快?
Redis 内部做了非常多的性能优化,比较重要的主要有下面 3 点:
- Redis 基于内存,内存的访问速度是磁盘的上千倍;
- Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
- Redis 内置了多种优化过后的数据结构实现,性能非常高。