尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
Redis分布式锁,master 挂了但slave 没有完成复制,锁失效了,怎么办?
Redis 主从切换,数据丢了怎么办?
最近有小伙伴在面试 京东,又遇到了相关的面试题。小伙伴懵了,因为没有遇到过,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
1: Redis主从架构的 分布式锁的 执行流程
在Redis主从架构中,写入的,都是 master Redis实例,master 主实例会向 slave 从实例同步key。
一个业务线程 通过向主Redis实例中写入 key-value 来实现加分布式锁,加锁后开始执行业务代码。
具体如下图所示:
当前,这里也涉及到一个核心问题: 锁过期了,业务还没执行完, 怎么办?
这个也是面试的核心题目,具体请参考下面的答案: Redis锁如何续期?Redis锁超时,任务没完怎么办?
咱们现在聚焦的问题: master Redis实例挂掉了,slave 从Redis 还没有完成复制,导致 Redis分布式锁失效,怎么办?
2:master 挂了但slave 没有完成复制,锁失效了,怎么办?
一般情况下:如果主master Redis实例挂掉了,会选举出一个从Redis实例成为主的。这是redis 集群的故障转移机制。
但是,如果刚刚加锁的key还没有来得及同步到slave Redis中,新选出的主Redis实例中就没有这个key,这个时候业务线程B就能加锁来获取分布式锁,导致锁失效了。
具体如下图:
线程B 加锁成功,也执行业务代码了。
而这个时候A还没有执行结束,所以就会出现并发安全问题,这就是Redis主从架构下的分布式锁失效问题
3:Redis 分布式锁的高可用方案
本质上,Redis 分布式锁的高可用,有两个层面的解决方案:
4: Server端 的高可用方案
Redis 分布式锁的Server端高可用方案, 就是通过配置, 保证Server 尽量可能少的数据丢失。
在redis的配置文件中有两个参数我们可以设置:
min-slaves-to-write 1
min-slaves-max-lag 10
min-slaves-to-write默认情况下是0,min-slaves-max-lag默认情况下是10。
两大相关的配置参数
min-slaves-to-write
:设置主库最少得有 N 个健康的从库存活才能执行写命令。这个配置虽然不能保证 N 个从库都一定能接收到主库的写操作,但是能避免当没有足够健康的从库时,主库无法正常写入,以此来避免数据的丢失。min-slaves-max-lag
:配置从库和主库进行数据复制时的 ACK 消息延迟的最大时间,可以确保从库在指定的时间内,如果 ACK 时间没在规定时间内,则拒绝写入。
以上面配置为例,这两个参数表示至少有1个salve的与master的同步复制延迟不能超过10s,一旦所有的slave复制和同步的延迟达到了10s,那么此时master就不会接受任何请求。
我们可以减小min-slaves-max-lag参数的值,这样就可以避免在发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往master中写入数据。
配置了 Server端 的高可用方案, 那么对于client,我们可以设计好合理的降级措施。
如果 Server端 不可用,需要进行及时预警和合理的降级。 比如,把redis 锁降级为 Zookeeper 分布式锁。
尼恩特别说明:从 CAP定理来说, Redis集群倾向AP(高并发),ZP集群则倾向CP(高可用)。
从写入的流程上来说:
- 在向Redis集群里的主结点写入数据时,写入主节点就立刻告诉客户端写入成功。
- 而在向ZK的主结点写入数据时,并不是立刻告诉客户端写入成功,而是先同步给从结点,至少半数的节点同步成功才能返回“写入成功”给客户端。
这个时候如果ZK的主节点挂了,ZK的ZAB分布式一致性协议能保证一定是数据同步完成的结点被选举为主节点,所以ZK 不会发生分布式锁的失效问题。
但是,ZK是低性能的方案。
5: Client 端 的高可用方案
如果不改用ZK,就是要用Redis Client 端 方案来解决主从架构的分布式锁失效问题。
Client 端 的高可用方案,就是使用 红锁(RedLock)。
什么是RedLock ? 红锁(RedLock) 的设计,就是从 client 客户端 解决了单一 Redis 实例作为分布式锁可能出现的单点故障问题。
红锁(RedLock)是一种分布式锁算法,由 Redis 的作者 Salvatore Sanfilippo(也称为 Antirez)设计,用于在分布式系统中实现可靠的锁机制。
尼恩告诉大家,其实 RedLock底层逻辑和ZK很类似。
5.1 红锁(RedLock)实现原理:
- 多节点加锁: RedLock 不在单个 Redis 实例上加锁,而是在多个独立的 Redis 实例上同时尝试获取锁。通常建议使用奇数个 Redis 实例(如 5 个),以确保系统具有较好的容错性。
- 多数节点同意: 系统只有在获得了大多数 Redis 实例的锁(即 N/2 + 1 个节点,N 为节点总数)之后,才认为成功获取了分布式锁。这样即使部分 Redis 实例发生故障,整体锁服务仍然可用。
- 时间同步: 为防止客户端在持有锁的过程中发生故障而导致锁无法释放,RedLock 会在获取锁时设置一个超时时间。如果客户端在锁超时之前未能完成任务并释放锁,其他客户端可以在锁超时后重新尝试获取。
- 锁释放: 释放锁时,客户端需要向所有 Redis 实例发送释放锁的命令,以确保所有实例上的锁都被清除。
首先要有多个(最好是奇数个)对等的(没有主从关系)Redis结点。
当进行加锁时(比如是用SETNX命令),则这个设置key-value的命令会发给每个Redis结点执行,当且仅当客户端收到超过半数的结点写成功的消息时,才认为加锁成功,才开始执行后面的业务代码。
只有在获得了大多数 Redis 实例的锁(即 N/2 + 1 个节点,N 为节点总数)之后,才认为成功获取了分布式锁。
上图中,Client 1向Redis 1/2/3三个结点去写key-value,假设 在Redis 1和Redis 2写入成功了,Redis 3还没有写入成功的状态,这个时候Client 1就已经认为加锁成功了,实际上已经可以执行业务代码了。
实际的实现过程中,不一定是三个redis 实例,可以是三个 key,三个key 一般会路由到多个 redis 实例上,避免了单点故障问题。
只要 Client 拿到其中的两个key,这个时候Client 就已经认为加锁成功了。
接下来看看 故障发生的场景。
假设有一个Redis结点挂了(如下图所示Redis 1挂了),这个时候假设Client 2也要尝试加锁。
此时Redis 2由于已经被Client 1写过了,没法写入成功,但是Redis 3可以写入成功。
此时Client 2 只有1个结点能写入成功,所以认为加锁不成功,这样Client 2就不会开始错误的执行业务代码,也就不会出现并发安全问题。
尼恩提示:以上内容比较复杂,后面会在《尼恩Java面试宝典 配套视频》中 详细解读。
另外,如果没有 面试机会,可以找尼恩来打造一个绝世好简历,实现 职业逆袭:34岁被裁8月,转架构收一大厂offer, 年薪65W,后逆天改命!
5.2 红锁(RedLock)具体应用:
红锁(RedLock)工作流程,总结如下:
- 客户端尝试顺序地向所有 Redis 实例发送加锁命令。
- 对于每个实例,客户端尝试在指定的超时时间内获取锁。
- 客户端计算已经成功加锁的实例数量,如果达到多数(N/2 + 1),则认为客户端成功获取了分布式锁。
- 如果获取锁失败,客户端需要向所有实例发送释放锁的命令,以避免留下未释放的锁。
在 Java 中的应用:
在 Java 中,可以使用 Redisson 框架来实现 RedLock。RedissonRedLock 实际上是基于 RedissonMultiLock 实现的,从继承关系可以看出这一点。
Redisson 提供了 RedissonMultiLock 类,它可以同时管理多个锁,并保证操作的原子性。
以下是 Redisson 中 RedLock 的简单使用示例:
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(); // 释放锁
}
通过以上机制,RedLock 在分布式环境下提供了一种较为可靠的锁方案,能够应对部分节点故障,并保持锁服务的可用性和安全性。
RedLock 具备以下主要特性:
- 互斥性:在任何时间,只有一个客户端可以获得锁,确保了资源的互斥访问。
- 避免死锁:通过为锁设置一个较短的过期时间,即使客户端在获得锁后由于网络故障等原因未能按时释放锁,锁也会因为过期而自动释放,避免了死锁的发生。
- 容错性:即使一部分 Redis 节点宕机,只要大多数节点(即过半数以上的节点)仍在线,RedLock 算法就能继续提供服务,并确保锁的正确性。
5.3 RedLock 存在问题
RedLock 由于其设计原理和实现上的复杂性,存在一些问题和争议。
RedLock 的性能问题
由于 RedLock 需要在多个节点间进行交互,网络延迟和节点超时确实可能影响加锁的性能。
特别是在节点数量较多或网络状况不佳的情况下,这种影响会更加明显。
RedLock 的并发安全性问题
客户端在持有锁的过程中发生长时间停顿(例如 JVM 的 STW),导致锁实际上已经失效,但客户端由于停顿结束后仍然认为持有锁。
RedLock 被官方废弃
Redisson 官方已经废弃了 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 联锁 是redlock红锁的 基础类。 所以,关键的源码,还是在MultiLock (联锁) 。
实际上,redisson MultiLock (联锁) 使用场景更广。
比如,MultiLock (联锁) 可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加