尼恩说在前面
Redis分布式锁,master 挂了但slave 没有完成复制,锁失效了,怎么办? Redis 主从切换,数据丢了怎么办?
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
本文目录
- 尼恩说在前面
- 1:Redis主从架构的 分布式锁的 执行流程
- 2:master 挂了但slave 没有完成复制,锁失效了,怎么办?
- 3:Redis 分布式锁的高可用方案
- 4:Server端 的高可用方案
- 两大相关的配置参数
- 5:Client 端 的高可用方案
- 5.1 红锁(RedLock)实现原理:
- 5.2 红锁(RedLock)具体应用:
- 5.3 RedLock 存在问题
-RedLock 的性能问题
-RedLock 的并发安全性问题
-RedLock 被官方废弃
- 6:RedissonMultiLock 联锁 源码
- RedissonMultiLock 联锁 的使用参考代码:
- 源码:使用lock方法 获取联锁
- 源码:使用lockInterruptibly 方法 获取联锁
- 源码:使用lockInterruptibly 方法 获取联锁
- unlock释放 联锁
- 子锁的释放:通过lua脚本释放内部的子锁
- 说在最后:有问题找老架构取经
1:Redis主从架构的 分布式锁的 执行流程
2:master 挂了但slave 没有完成复制,锁失效了,怎么办?
3:Redis 分布式锁的高可用方案
4:Server端 的高可用方案
min-slaves-to-write 1
min-slaves-max-lag 10
两大相关的配置参数
min-slaves-to-write
:设置主库最少得有 N 个健康的从库存活才能执行写命令。这个配置虽然不能保证 N 个从库都一定能接收到主库的写操作,但是能避免当没有足够健康的从库时,主库无法正常写入,以此来避免数据的丢失。min-slaves-max-lag
:配置从库和主库进行数据复制时的 ACK 消息延迟的最大时间,可以确保从库在指定的时间内,如果 ACK 时间没在规定时间内,则拒绝写入。
在向Redis集群里的主结点写入数据时,写入主节点就立刻告诉客户端写入成功。 而在向ZK的主结点写入数据时,并不是立刻告诉客户端写入成功,而是先同步给从结点,至少半数的节点同步成功才能返回“写入成功”给客户端。
5:Client 端 的高可用方案
5.1 红锁(RedLock)实现原理:
多节点加锁: RedLock 不在单个 Redis 实例上加锁,而是在多个独立的 Redis 实例上同时尝试获取锁。通常建议使用奇数个 Redis 实例(如 5 个),以确保系统具有较好的容错性。 多数节点同意: 系统只有在获得了大多数 Redis 实例的锁(即 N/2 + 1 个节点,N 为节点总数)之后,才认为成功获取了分布式锁。这样即使部分 Redis 实例发生故障,整体锁服务仍然可用。 时间同步: 为防止客户端在持有锁的过程中发生故障而导致锁无法释放,RedLock 会在获取锁时设置一个超时时间。如果客户端在锁超时之前未能完成任务并释放锁,其他客户端可以在锁超时后重新尝试获取。 锁释放: 释放锁时,客户端需要向所有 Redis 实例发送释放锁的命令,以确保所有实例上的锁都被清除。
5.2 红锁(RedLock)具体应用:
客户端尝试顺序地向所有 Redis 实例发送加锁命令。 对于每个实例,客户端尝试在指定的超时时间内获取锁。 客户端计算已经成功加锁的实例数量,如果达到多数(N/2 + 1),则认为客户端成功获取了分布式锁。 如果获取锁失败,客户端需要向所有实例发送释放锁的命令,以避免留下未释放的锁。
RedissonClient redisson = // 初始化 Redisson 客户端
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
try {
if (multiLock.tryLock()) {
// 成功获取锁,执行业务逻辑
} else {
// 获取锁失败
}
} finally {
multiLock.unlock(); // 释放锁
}
互斥性:在任何时间,只有一个客户端可以获得锁,确保了资源的互斥访问。 避免死锁:通过为锁设置一个较短的过期时间,即使客户端在获得锁后由于网络故障等原因未能按时释放锁,锁也会因为过期而自动释放,避免了死锁的发生。 容错性:即使一部分 Redis 节点宕机,只要大多数节点(即过半数以上的节点)仍在线,RedLock 算法就能继续提供服务,并确保锁的正确性。
5.3 RedLock 存在问题
RedLock 的性能问题
RedLock 的并发安全性问题
RedLock 被官方废弃
6:RedissonMultiLock 联锁 源码
public class RedissonRedLock extends RedissonMultiLock {
public RedissonRedLock(RLock... locks) {
super(locks);
}
protected int failedLocksLimit() {
return this.locks.size() - this.minLocksAmount(this.locks);
}
protected int minLocksAmount(List<RLock> locks) {
return locks.size() / 2 + 1;
}
protected long calcLockWaitTime(long remainTime) {
return Math.max(remainTime / (long)this.locks.size(), 1L);
}
public void unlock() {
this.unlockInner(this.locks);
}
}
RedissonMultiLock 联锁 的使用参考代码:
public static void main(String[] args) throws Exception {
Config config = new Config();
// 1. 这里的Redis集群是我本地搭建的一套集群,因为是研究源码,所以配置直接硬编码到代码里
config.useClusterServers()
.addNodeAddress("redis://192.168.0.107:7001")
.addNodeAddress("redis://192.168.0.107:7002")
.addNodeAddress("redis://192.168.0.110:7003")
.addNodeAddress("redis://192.168.0.110:7004")
.addNodeAddress("redis://192.168.0.111:7005")
.addNodeAddress("redis://192.168.0.111:7006");
RedissonClient redisson = Redisson.create(config);
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1,lock2,lock3);
// 代码片段二、
lock.lock();
// 代码片段六、
lock.unlock();
}
源码:使用lock方法 获取联锁
RedissonMultiLock类中
public void lock(long leaseTime, TimeUnit unit) {
try {
// 1. 代码片段三、
lockInterruptibly(leaseTime, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
源码:使用lockInterruptibly 方法 获取联锁
@Override
public void lockInterruptibly() throws InterruptedException {
// 这里的-1后面会用到,具体-1代表是什么意思,后面的代码分析,参考代码片段四、
lockInterruptibly(-1, null);
}
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
// 1. 通过代码片段三可以知道,leaseTime为-1 unit=null
// baseWaitTime = 锁的个数(3个) * 1500 = 4500毫秒
long baseWaitTime = locks.size() * 1500;
long waitTime = -1;
// leaseTime肯定是-1,所以这里成立,不走else逻辑了,这里的代码写的就感觉很有意思,上面等于-1,下面等于-1还if判断
if (leaseTime == -1) {
// waitTime= 4500毫秒
waitTime = baseWaitTime;
unit = TimeUnit.MILLISECONDS;
} else {
waitTime = unit.toMillis(leaseTime);
if (waitTime <= 2000) {
waitTime = 2000;
} else if (waitTime <= baseWaitTime) {
waitTime = ThreadLocalRandom.current().nextLong(waitTime/2, waitTime);
} else {
waitTime = ThreadLocalRandom.current().nextLong(baseWaitTime, waitTime);
}
waitTime = unit.convert(waitTime, TimeUnit.MILLISECONDS);
}
// 这里有个死循环逻辑,其实就是不停的去获取锁
while (true) {
// 1. 代码片段五、waitTime = 4500毫秒,leaseTime = -1
if (tryLock(waitTime, leaseTime, unit)) {
return;
}
}
}
源码:使用lockInterruptibly 方法 获取联锁
// waitTime = 4500毫秒,leaseTime = -1
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);
// }
// 1. newLeaseTime = -1,其实这里的参数值,都会影响对程序的逻辑以及加锁释放锁
// 1.现在是真的想不通这个逻辑,先等于-1,然后在if判断,和下面的remainTime一样
long newLeaseTime = -1;
if (leaseTime != -1) {
newLeaseTime = unit.toMillis(waitTime)*2;
}
// 当前时间
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
// 这里其实就是返回remainTime,calcLockWaitTime是给他什么参数,返回什么参数,也挺有意思的。
long lockWaitTime = calcLockWaitTime(remainTime);
// 这里会返回一个固定的值0
int failedLocksLimit = failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
// 1. 拿到锁的迭代器
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
// waitTime = 4500毫秒,leaseTime = -1 参数传递进来的,所以会走else逻辑
if (waitTime == -1 && leaseTime == -1) {
lockAcquired = lock.tryLock();
} else {
// 这里去lockWaitTime和remainTime中的最小值(lockWaitTime = 0,就是上面那个固定值,remainTime=-1),
// 所以awaitTime=-1,这个-1其实很关键,在tryLock中,-1代表了如果获取锁成功了,就会启动一个lock watchDog,不停的刷新锁的生存时间
long awaitTime = Math.min(lockWaitTime, remainTime);
// 这里就是获取锁,等待awaitTime=4500毫秒,获取锁成功,启动一个watchDog
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} 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 && leaseTime == -1) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// reset iterator
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
if (remainTime != -1) {
// 如果获取锁成功,当前时间减去获取锁耗费的时间time
remainTime -= (System.currentTimeMillis() - time);
time = System.currentTimeMillis();
if (remainTime <= 0) {
// 如果remainTime <0 说明获取锁超时,那么就释放掉这个锁
unlockInner(acquiredLocks);
// 返回false,说明加锁失败
return false;
}
}
}
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
return true;
}
unlock释放 联锁
// 释放锁的话,就是依次调用所有的锁的释放的逻辑,lua脚本,同步等待所有的锁释放完毕,才会返回
@Override
public void unlock() {
List<RFuture<Void>> futures = new ArrayList<RFuture<Void>>(locks.size());
for (RLock lock : locks) {
// 代码片段七、
futures.add(lock.unlockAsync());
}
for (RFuture<Void> future : futures) {
future.syncUninterruptibly();
}
}
子锁的释放:通过lua脚本释放内部的子锁
这里的释放锁的底层lua脚本,和加锁很类似,就不做具体的分析了,一眼看上去,其实还是很简单的
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;" +
"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;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
说在最后:有问题找老架构取经
空窗1年/空窗2年, 如何 起死回生 ?
上岸奇迹:中厂大龄34岁,被裁8月收一大厂offer, 年薪65W,转架构后逆天改命!
案例2:42岁被裁2年,天快塌了,急救1个月,拿到开发经理offer,起死回生
案例3:35岁被裁6个月, 职业绝望,转架构急救上岸,DDD和3高项目太重要了
案例4:失业15个月,学习40天拿offer, 绝境翻盘,如何实现?
被裁之后,100W 年薪 到手, 如何 人生逆袭?
100W案例,100W年薪的底层逻辑是什么? 如何实现年薪百万? 如何远离 中年危机?
如何 逆天改命,包含AI、大数据、golang、Java 等
实现职业转型,极速上岸
关注职业救助站公众号,获取每天职业干货
助您实现职业转型、职业升级、极速上岸
---------------------------------
实现架构转型,再无中年危机
关注技术自由圈公众号,获取每天技术千货
一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经
请加尼恩个人微信 免费拿走
暗号,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看"和"赞",谢谢