分布式锁
主动轮询型
Redis:SETNX(非阻塞,成功失败都直接返回)
解锁操作:1. 检查value 2. 删除记录
要使这个过程原子化,不可拆分,可以使用lua脚本自定义组装同一个redis节点下的多笔操作形成一个具备原子性的事务。
MySQL
- 建立一张表存储分布式锁记录
- 基于唯一键的特性
- 可以新增一个字段标识使用方身份(先检查释放锁动作执行者的身份,身份合法时才进行解锁)
主动轮询型存在的问题
- 死锁(redisson中通过watchdog解决)
- CAP中保证AP,可用性和分区容错性,数据弱一致性问题:redisson中使用redlock解决
看门狗(watch dog)(和etcd租约续约机制相似)
redlock:一把红锁,物理意义上有多个锁在不同的节点;多数原则:半数以上
当一个节点(或进程)尝试获取锁时,它会先尝试在多个不同的节点上获取锁。如果大多数节点都成功获取了锁,那么这个锁就可以视为获取成功,进而执行相应的操作。如果获取锁的节点数量不足,则视为获取失败,需要进行相应的处理。
具体的实现方式可能会有所不同,但一般包括以下步骤:
- 客户端在多个节点上尝试获取锁。
- 每个节点独立判断是否可以获取锁,如果可以则返回成功,否则返回失败。
- 客户端根据返回结果来判断是否成功获取锁。
监听回调型
etcd:
插入是否成功,插入不成功,在该锁添加监听。(删除的时候只有插入锁的人才可以解锁)
租约续约机制解决死锁(后台守护协程负责续约,续约前会校验这个锁是否还属于该用户)
版本revision用于秩序取锁(锁前缀+递增的版本号,每一个只需监听最近的一个比自己小的版本号,排成队列解锁),避免惊群效应
zk:zab
zookeeper
基于 ZooKeeper 的锁与基于 Redis 的锁的不同之处在于 Lock 成功之前会一直阻塞,这与我们单机场景中的 mutex.Lock
很相似。
其原理也是基于临时 Sequence 节点和 watch API,例如我们这里使用的是 /lock
节点。Lock 会在该节点下的节点列表中插入自己的值,只要节点下的子节点发生变化,就会通知所有 watch 该节点的程序。这时候程序会检查当前节点下最小的子节点的 id 是否与自己的一致。如果一致,说明加锁成功了。
这种分布式的阻塞锁比较适合分布式任务调度场景,但不适合高频次持锁时间短的抢锁场景。按照 Google 的 Chubby 论文里的阐述,基于强一致协议的锁适用于 粗粒度
的加锁操作。这里的粗粒度指锁占用时间较长。我们在使用时也应思考在自己的业务场景中使用是否合适。
etcd
etcd 中没有像 Zookeeper 那样的 Sequence 节点。所以其锁实现和基于 Zookeeper 实现的有所不同。在上述示例代码中使用的 etcdsync 的 Lock 流程是:
- 先检查
/lock
路径下是否有值,如果有值,说明锁已经被别人抢了 - 如果没有值,那么写入自己的值。写入成功返回,说明加锁成功。写入时如果节点被其它节点写入过了,那么会导致加锁失败,这时候到 3
- watch
/lock
下的事件,此时陷入阻塞 - 当
/lock
路径下发生事件时,当前进程被唤醒。检查发生的事件是否是删除事件(说明锁被持有者主动 unlock),或者过期事件(说明锁过期失效)。如果是的话,那么回到 1,走抢锁流程。
惊群效应:指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个事件的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。