分布式锁介绍
在并发场景中,为了保证临界资源的数据一致性,我们会经常使用到“锁”这个工具对临界资源进行保护,让混乱的并发访问行为退化为秩序的串行访问行为.
在本地环境中,由于多线程之间能够共享进程的数据,因此可以比较简单地实现进程内的互斥锁;然而在分布式场景中,有时我们需要跨域多个物理节点执行加锁操作,因此我们就需要依赖到类似于 redis、mysql 这样的状态存储组件,在此基础之上实现所谓的“分布式锁”技术.
核心性质
分布式锁应当具备如下几项核心性质:
独占性:对于同一把锁,在同一时刻只能被一个取锁方占有,这是作为“锁”工具最基础的一项性质
健壮性:即不能产生死锁(dead lock). 假如某个占有锁的使用方因为宕机而无法主动执行解锁动作,锁也应该能够被正常传承下去,被其他使用方所延续使用
对称性:加锁和解锁的使用方必须为同一身份. 不允许非法释放他人持有的分布式锁
高可用:当提供分布式锁服务的基础组件中存在少量节点发生故障时,不应该影响到分布式锁服务的稳定性
分布式锁分类
主动轮询型:该模型类似于单机锁中的主动轮询 + cas 乐观锁模型,取锁方会持续对分布式锁发出尝试获取动作,如果锁已被占用则会不断发起重试,直到取锁成功为止(类似自旋锁)
watch 回调型:在取锁方发现锁已被他人占用时,会创建 watcher 监视器订阅锁的释放事件,随后不再发起主动取锁的尝试;当锁被释放后,取锁方能通过之前创建的 watcher 感知到这一变化,然后再重新发起取锁的尝试动作
redis属于主动轮询型
主动轮询型 与 watch回调型 优劣:
1.在并发激烈程度较高时倾向于 watch 回调型分布式锁;
(分布式场景中的轮询,可能是多次无意义的网络IO,因此 watch 回调型 能避免大量的网络IO)
2.在并发激烈程度较低时倾向于 redis 轮询型分布式锁;
(自旋锁在高并发场景下,可能一直抢不到锁,因此自旋时间长,浪费CPU资源)
设计思路
原子性操作
redis 基于内存实现数据的存储,因此足够高轻便高效. 此外,redis 基于单线程模型完成数据处理工作,支持 SETNX 原子指令(set only if not exist),能够很方便地支持分布式锁的加锁操作.
SETNX key value
同时 使用EVAL
命令在Redis上执行Lua脚本时,Redis会将整个Lua脚本作为一个命令进行执行。
Redis 在 执行命令时,是单线程执行的,因此不存在并发问题
过期时间
在 分布式场景下,如果一个持有锁的物理节点,突然因为故障而导致服务运行不了,同时锁也无法释放就导致其他物理节点无法获取该锁,业务无法进行
分布式锁对应的 kv 数据时设置一个过期时间 expire time,这样即便使用方因为异常原因导致无法正常解锁,锁对应的数据项也会在达到过期时间阈值后被自动删除,实现释放分布式锁的效果.
还会有什么问题?
因为锁的持有者并不能精确预判到自己持锁后处理业务逻辑的实际耗时,因此此处设置的过期时间只能是一个偏向于保守的经验值,假如因为一些异常情况导致占有锁的使用方在业务处理流程中的耗时超过了设置的过期时间阈值,就会导致锁被提前释放,其他取锁方可能取锁成功,最终引起数据不一致的并发问题.(违背了锁的独占性)
解决方案
看门狗策略
执行流程:
• 在执行 redis 分布式锁的上锁操作时,通过 setNEX 指令完成锁数据的设置,携带了一个默认的锁数据过期时间
• 确认上锁成功后,异步启动一个 watchDog 守护协程,按照锁默认过期时间 1/4 ~ 1/3 的节奏(可自由设置),持续地对锁数据进行 expire 续期操作
• 在解锁成功后,会负责关闭 watchDog,回收协程资源. (由于看门狗续期操作会先检查锁的所有权再延期数据,因此实际上使用方只要删除了锁数据,续期操作就不会生效了. 回收看门狗协程是为了规避协程泄漏问题)
弱一致性问题
redis 走的是 AP 路线,为了保证服务的可用性和吞吐量,redis 在进行数据的主从同步时,采用的是异步执行机制.
场景:
• moment1:使用方 A 在 redis master 节点加锁成功,完成了锁数据的写入操作
• moment2:redis master 宕机了,锁数据还没来得及同步到 slave 节点
• moment3:未同步到锁数据的 slave 节点被哨兵升级为新的 master
• moment4:使用方 B 前来取锁,由于新 master 中确实锁数据,所以使用方 B 加锁成功
解决方案
多数派原则
所谓多数派原则,就是做出一项决议之前,让所有的参与者进行投票表决,
只有投赞同票的人数达到参与者总人数的一半以上成为多数派时,这项决议才被通过.