周一至五早 8 点半! 精品技术文章准时送上!
目录
一, 写在前面
二, Redisson 实现 Redis 分布式锁的底层原理
(1)加锁机制
(2)锁互斥机制
(3)watch dog 自动延期机制
(4)可重入加锁机制
(5)锁释放机制
(6)此种方案 Redis 分布式锁的缺陷
三, 未完待续
一, 写在前面
现在面试, 一般都会聊聊分布式系统这块的东西. 通常面试官都会从服务框架 (Spring Cloud,Dubbo) 聊起, 一路聊到分布式事务, 分布式锁, ZooKeeper 等知识. 所以咱们这篇文章就来聊聊分布式锁这块知识, 具体的来看看 Redis 分布式锁的实现原理.
说实话, 如果在公司里落地生产环境用分布式锁的时候, 一定是会用开源类库的, 比如 Redis 分布式锁, 一般就是用 Redisson 框架就好了, 非常的简便易用.
大家如果有兴趣, 可以去看看 Redisson 的官网, 看看如何在项目中引入 Redisson 的依赖, 然后基于 Redis 实现分布式锁的加锁与释放锁. 下面给大家看一段简单的使用代码片段, 先直观的感受一下:
怎么样, 上面那段代码, 是不是感觉简单的不行! 此外, 人家还支持 Redis 单实例, Redis 哨兵, Redis cluster,Redis master-slave 等各种部署架构, 都可以给你完美实现.
二, Redisson 实现 Redis 分布式锁的底层原理
好的, 接下来就通过一张手绘图, 给大家说说 Redisson 这个开源框架对 Redis 分布式锁的实现原理.
(1)加锁机制
咱们来看上面那张图, 现在某个客户端要加锁. 如果该客户端面对的是一个 Redis cluster 集群, 他首先会根据 hash 节点选择一台机器. 这里注意, 仅仅只是选择一台机器! 这点很关键! 紧接着, 就会发送一段 lua 脚本到 Redis 上, 那段 lua 脚本如下所示:
为啥要用 lua 脚本呢? 因为一大坨复杂的业务逻辑, 可以通过封装在 lua 脚本中发送给 Redis, 保证这段复杂业务逻辑执行的原子性.
那么, 这段 lua 脚本是什么意思呢? 这里 KEYS[1]代表的是你加锁的那个 key, 比如说: RLock lock = redisson.getLock("myLock"); 这里你自己设置了加锁的那个锁 key 就是 "myLock".
ARGV[1]代表的就是锁 key 的默认生存时间, 默认 30 秒. ARGV[2]代表的是加锁的客户端的 ID, 类似于下面这样: 8743c9c0-0795-4907-87fd-6c719a6b4586:1
给大家解释一下, 第一段 if 判断语句, 就是用 "exists myLock" 命令判断一下, 如果你要加锁的那个锁 key 不存在的话, 你就进行加锁. 如何加锁呢? 很简单, 用下面的命令: hset myLock
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1, 通过这个命令设置一个 hash 数据结构, 这行命令执行后, 会出现一个类似下面的数据结构:
上述就代表 "8743c9c0-0795-4907-87fd-6c719a6b4586:1" 这个客户端对 "myLock" 这个锁 key 完成了加锁. 接着会执行 "pexpire myLock 30000" 命令, 设置 myLock 这个锁 key 的生存时间是 30 秒. 好了, 到此为止, ok, 加锁完成了.
(2)锁互斥机制
那么在这个时候, 如果客户端 2 来尝试加锁, 执行了同样的一段 lua 脚本, 会咋样呢? 很简单, 第一个 if 判断会执行 "exists myLock", 发现 myLock 这个锁 key 已经存在了. 接着第二个 if 判断, 判断一下, myLock 锁 key 的 hash 数据结构中, 是否包含客户端 2 的 ID, 但是明显不是的, 因为那里包含的是客户端 1 的 ID.
所以, 客户端 2 会获取到 pttl myLock 返回的一个数字, 这个数字代表了 myLock 这个锁 key 的剩余生存时间. 比如还剩 15000 毫秒的生存时间. 此时客户端 2 会进入一个 while 循环, 不停的尝试加锁.
(3)watch dog 自动延期机制
客户端 1 加锁的锁 key 默认生存时间才 30 秒, 如果超过了 30 秒, 客户端 1 还想一直持有这把锁, 怎么办呢?
简单! 只要客户端 1 一旦加锁成功, 就会启动一个 watch dog 看门狗, 他是一个后台线程, 会每隔 10 秒检查一下, 如果客户端 1 还持有锁 key, 那么就会不断的延长锁 key 的生存时间.
(4)可重入加锁机制
那如果客户端 1 都已经持有了这把锁了, 结果可重入的加锁会怎么样呢? 比如下面这种代码:
这时我们来分析一下上面那段 lua 脚本. 第一个 if 判断肯定不成立,"exists myLock" 会显示锁 key 已经存在了. 第二个 if 判断会成立, 因为 myLock 的 hash 数据结构中包含的那个 ID, 就是客户端 1 的那个 ID, 也就是 "8743c9c0-0795-4907-87fd-6c719a6b4586:1"
此时就会执行可重入加锁的逻辑, 他会用:
incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1 , 通过这个命令, 对客户端 1 的加锁次数, 累加 1. 此时 myLock 数据结构变为下面这样:
大家看到了吧, 那个 myLock 的 hash 数据结构中的那个客户端 ID, 就对应着加锁的次数
(5)释放锁机制
如果执行 lock.unlock(), 就可以释放分布式锁, 此时的业务逻辑也是非常简单的. 其实说白了, 就是每次都对 myLock 数据结构中的那个加锁次数减 1. 如果发现加锁次数是 0 了, 说明这个客户端已经不再持有锁了, 此时就会用:"del myLock" 命令, 从 Redis 里删除这个 key. 然后呢, 另外的客户端 2 就可以尝试完成加锁了. 这就是所谓的分布式锁的开源 Redisson 框架的实现机制.
一般我们在生产系统中, 可以用 Redisson 框架提供的这个类库来基于 Redis 进行分布式锁的加锁与释放锁.
(6)上述 Redis 分布式锁的缺点
其实上面那种方案最大的问题, 就是如果你对某个 Redis master 实例, 写入了 myLock 这种锁 key 的 value, 此时会异步复制给对应的 master slave 实例. 但是这个过程中一旦发生 Redis master 宕机, 主备切换, Redis slave 变为了 Redis master.
接着就会导致, 客户端 2 来尝试加锁的时候, 在新的 Redis master 上完成了加锁, 而客户端 1 也以为自己成功加了锁. 此时就会导致多个客户端对一个分布式锁完成了加锁. 这时系统在业务语义上一定会出现问题, 导致各种脏数据的产生.
所以这个就是 Redis cluster, 或者是 Redis master-slave 架构的主从异步复制导致的 Redis 分布式锁的最大缺陷: 在 Redis master 实例宕机的时候, 可能导致多个客户端同时完成加锁.
三, 未完待续
下一篇文章, 给大家分享一下电商系统中, 大促销的活动场景下, 每秒上千订单的时候如何对 Redis 分布式锁进行高并发的优化.
敬请关注:
《每秒上千订单的高并发场景下如何完成分布式锁的性能优化?》
来源: https://juejin.im/post/5bf3f15851882526a643e207