说说:用过分布式锁吗?你们是怎么做选型的?
为什么需要分布式锁?
微服务的架构下,多个应用服务要同时对同一条数据做修改,那么要确保数据的一致性,就只能有一个应用修改成功。
下图中,server1、server2、server3 这三个服务都要修改amount这个数据,每个服务更新的值不同,
为了保证数据的正确性,三个服务都向lock server服务申请分布式排他权限,最终server2拿到了修改权限,即server2将amount更新为2,其他服务由于没有获取到修改权限则返回更新失败。
分布式锁的实现方案
基于数据库的悲观锁或者乐观锁 基于redis实现分布式锁 基于zookeeper实现分布式锁 基于其他的中间件实现分布式锁
基于数据库实现分布式锁
性能比较低,数据库的性能,摆在那儿
和业务相关度高,无论是 悲观锁或者乐观锁, 都是和业务高度耦合的
通用的分布式锁方案,一般是基于 redis、zookeeper等其他的中间件实现。
请参见文末:Redis分布式锁 (图解-秒懂-史上最全)
基于redis实现分布式锁
因为redis是一个单独的非业务服务,不会受到其他业务服务的限制,所有的业务服务都可以向redis发送写入命令,
且只有一个业务服务可以写入命令成功,那么这个写入命令成功的服务即获得了锁,可以进行后续对资源的操作,其他未写入成功的服务,则进行其他处理。
简单加锁:使用set的命令时,同时设置过期时间
早期版本使用 setnx(set if not exists) 指令简单加锁,expire 设置锁过期时间。由于 setnx 和 expire 是两条指令而不是原子指令。如果中间出现问题,有可能造成永远不过期而发生死锁。如果这两条指令可以一起执行就不会出现问题。
新的版本使用set的命令时,同时设置过期时间,示例如下:
127.0.0.1:6379> set unlock "234" EX 100 NX
(nil)
127.0.0.1:6379>
127.0.0.1:6379> set test "111" EX 100 NX
OK
这样就完美的解决了分布式锁的原子性;set 命令的完整格式:
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)
使用set命令实现加锁操作,先展示加锁的简单代码实习,再带大家慢慢解释为什么这样实现。
锁的释放
当客户端1操作完后,释放锁资源,即删除key 。
如果没有删除,则在过期时间之后,锁会自动释放。
之后,其他客户端尝试获取锁时,则会获取锁成功。
锁过期处理
假如服务A加锁成功,锁会在10s后自动释放,但由于业务复杂,执行时间过长,10s内还没执行完,此时锁已经被redis自动释放掉了。
此时服务B就重新获取到了该锁,服务B开始执行他的业务,服务A在执行到第12s的时候执行完了,那么服务A会去释放锁,则此时释放的却是服务B刚获取到的锁。
这会有锁过期和释放其他服务锁这种严重的问题。
那么锁过期这种问题该如何处理的?
虽然可以通过增加删除key时间来处理这个问题,但是并没有从根本上解决。
假设设个100s,绝大多数都是1s后就会释放锁,但是由于服务宕机,则会导致100s内其他服务都无法获取到锁,这也是灾难性的。
我们可以这样做,在锁将要过期的时候,如果服务还没有处理完业务,那么将这个锁再续一段时间。
比如设置key在10s后过期,那么再开启一个守护线程,在第8s的时候检测服务是否处理完,如果没有,则将这个key再续10s后过期。
在Redisson(Redis SDK客户端)中,就已经帮我们实现了这个功能,这个自动续时的我们称其为”看门狗”。
如果进行线程标识,避免错误释放,并且保证释放的原子性?
每个服务在设置value的时候,带上自己服务的唯一标识,如UUID,或者一些业务上的独特标识。
这样在删除key的时候,只删除自己服务之前添加的key就可以了。
如果需要先查看锁是否是自己服务添加的,需要先get取出来判断,然后再进行del。
这样的话就无法保证原子性了。
我们可以通过Lua脚本,将这两个操作合并成一个操作,就可以保证其原子性了。
如果是在单redis实例的情况下,上面的已经完全实现了分布式锁的功能了。
关于 Lua脚本的内容,请阅读 《Java高并发核心编程 卷3》
那么Redis宕机,Key丢失怎么办?
在生产环境上,都会使用redis集群,但是,且主从数据并不是强一致性。
当主节点宕机后,主节点的数据还未来得及同步到从节点,进行主从切换后,新的主节点并没有老的主节点的全部数据,这就会导致刚写入到老的主节点的锁在新的主节点并没有,其他服务来获取锁时还是会加锁成功。
此时则会有2个服务都可以操作公共资源,此时的分布式锁则是不安全的。
redis的作者也想到这个问题,于是他发明了RedLock。
什么是高可用的RedLock?
要实现高可用的RedLock,需要至少5个实例(官方推荐),且每个实例都是master,不需要从库和哨兵。
RedLock算法思想:
不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,n / 2 + 1,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功,避免说仅仅在一个redis实例上加锁而带来的问题。
这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把红锁:
获取当前时间戳,单位是毫秒;
跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;
尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1;
客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
要是锁建立失败了,那么就依次之前建立过的锁删除;
只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
即当客户端在大多数redis实例上申请加锁成功后,且加锁总耗时小于锁过期时间,则认为加锁成功。
释放锁需要向全部节点发送锁释放命令。
第3步为啥要计算申请锁前后的总耗时与锁释放时间进行对比呢?
因为如果申请锁的总耗时已经超过了锁释放时间,那么可能前面申请redis的锁已经被释放掉了,保证不了大于等于3个实例都有锁存在了,锁也就没有意义了
这样的话分布式锁就真的没问题了嘛?
1、得5个redis实例,成本大大增加
2、可以通过上面的流程感受到,这个RedLock锁太重了
3、主从切换这种场景绝大多数的时候不会碰到,偶尔碰到的话,保证最终的兜底操作我觉得也没啥问题。
4、分布式系统中的NPC问题
RedLock是基于redis实现的分布式锁,它能够保证以下特性:
互斥性:在任何时候,只能有一个客户端能够持有锁;避免死锁: 当客户端拿到锁后,即使发生了网络分区或者客户端宕机,也不会发生死锁;(利用key的存活时间) 容错性:只要多数节点的redis实例正常运行,就能够对外提供服务,加锁或者释放锁;
高可用的红锁会导致性能降低
提前说明,使用redis分布式锁,是追求高性能, 在cap理论中,追求的是 ap 而不是cp。
所以,如果追求高可用,建议使用 zookeeper分布式锁。
redis分布式锁可能导致的数据不一致性,建议使用业务补偿的方式去弥补。所以,不太建议使用红锁,但是从学习的层面来说,大家还是一定要掌握的。
聊了这么多的redis实现分布式锁。也简单了解下zookeeper是如何实现分布式锁的吧。
基于zookeeper实现分布式锁
尼恩的这篇博客,非常细致(文末)
Zookeeper 分布式锁 (图解+秒懂+史上最全)
那么实际的工作中,该如何选择分布式锁呢?
AP Redis 分布式锁
CP Redis 红锁(>=5个redis实例)、 或者 Zookeeper分布式锁
参考文献
Redis分布式锁 (图解-秒懂-史上最全)
https://www.cnblogs.com/crazymakercircle/p/14731826.html
Zookeeper 分布式锁 (图解+秒懂+史上最全
https://www.cnblogs.com/crazymakercircle/p/14504520.html
https://www.cnblogs.com/crazymakercircle/p/14731826.html
https://blog.csdn.net/zs18753479279/article/details/115751593
http://blog.csdn.net/jackpk/article/details/30073097
http://www.jb51.net/article/65264.htm
https://juejin.cn/post/6844904116485898248
https://www.jianshu.com/p/4b929f54a8d1
https://blog.csdn.net/weixin_42452888/article/details/127557245
硬核面试题推荐
核心面试题目:什么是索引下推?什么是 MRR 优化?怎么才能更好的为表创建索引? 说说:为什么新生代要两个Survivor区? 一个不行吗?
面试重点难题:Mysql如何实现RR级隔离时,不会幻读? 核心面试题:请说说 HashMap 的时间复杂度是多少? 核心面试题: 强引用、软引用、弱引用、虚引用?重点是 各自的 使用场景? 吊打面试官:Java中String对象的大小? 核心面试难题:Java对象为什么 不一定在堆上分配? 核心面试题:MVCC、间隙锁、Undo Log链、表级锁、行级锁、页级锁、共享锁、排它锁、记录锁等等 核心面试题:为什么新生代要两个Survivor区? 一个不行吗?
硬核文章推荐
一文搞懂:Java高手必备之 Mpsc 无锁队列 (史上最全) 一文搞懂:微服务核心组件 Nacos(史上最全) 一文搞懂:微服务核心组件 sentinel(史上最全) 一文秒懂:多级时间轮,最顶尖的Java调度算法 一文搞懂:缓存之王 Caffeine 架构、源码、原理(5W长文) 高性能组件:环形队列、 条带环形队列 Striped-RingBuffer 架构分析 一文穿透:队列之王 Disruptor 原理、架构、源码 如何优雅的使用 单例模式 ?来看看缓存之王 Caffeine 、链路之王 Skywalking 是如何做的吧! 细思极恐:Java官方JVM 为啥要叫做 HotSpot JVM?背后的水,不知道有多深!!! Java核心实操:内存溢出 实战、内存泄漏实战
硬核电子书
本文收录于:《尼恩Java 面试宝典》V18版
长按二维码,点击“识别图中二维码”即可查看老架构师尼恩个人微信,发暗号 “领电子书” 给尼恩,获取最新PDF。
最新的《尼恩Java面试宝典》
极致经典,不断升级,目前最新为V18
尼恩Java高并发三部曲
《Java高并发核心编程-卷1(加强版)》,不断升级
《Java高并发核心编程-卷2(加强版)》,不断升级
《Java高并发核心编程-卷3(加强版)》,不断升级
尼恩架构笔记100篇+,不断添加