分布式基础——几种常见的分布式锁
分布式锁
在单机场景下,可以使用语言的内置锁来实现进程同步,如 synchronized
、Lock
等。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
分布式锁:是控制分布式系统不同进程共同访问共享资源的一种锁的实现。
数据库的唯一索引
向表中插入一条唯一索引的记录,此时相当于加锁,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否处于锁定状态。
存在以下几个问题:
- 锁没有失效时间,解锁失败的话其它进程无法再获得该锁;
- 只能是非阻塞锁,插入失败直接就报错了,无法重试;
- 不可重入,已经获得锁的进程也必须重新获取锁。
Redis 的 SETNX 指令
使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。
SETNX 指令和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。
缺点:
- setnx和expire分两步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁。如果执行完
setnx
加锁,正要执行expire设置过期时间时,进程crash掉或者要重启维护了,那这个锁就一直存在了,别的线程永远获取不到锁了。 - 不支持阻塞等待、不可重入
Redis set的扩展命令(set ex px nx)
1 | 一条命令保证原子性执行 |
存在问题:
- 锁过期释放了,业务还没执行完。
- 锁被其他线程误释放。(可以使用lua脚本释放,但第一个问题依旧无法解决)
Redisson
以上的方案可能存在锁过期释放,业务没执行完的问题。其实我们可以给获得锁的线程开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在就自动对锁的过期时间延长,防止锁过期提前释放。
Redisson 实现的原理就是基于看门狗机制:
只要线程一加锁成功,就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此使用Redisson能够解决锁过期释放,业务没执行完问题。并且 Redisson 还是可重入锁,底层依靠 lua 脚本支持。
Redis 的 RedLock 算法
Redis一般都是集群部署的,假设数据在主从同步过程,主节点挂了,Redisson 使用看门狗(守护线程)“续命”的方案可能会出现问题。
- 如果线程一在Redis的master节点上拿到了锁,将键值对写入 redis 的 master 节点
- 但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障
- Redis 触发故障转移,一个slave节点就会升级为master节点,此时新的 master 并不包含线程1写key
- 因此线程2尝试获取同个key的锁也可以成功拿到锁,但线程一也已经拿到锁了,锁的安全性就没了。
上述问题的根本原因主要是由于 redis 异步复制带来的数据不一致问题导致的,因此解决的方向就是保证数据的一致。为了解决这个问题,Redis作者提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的:
使用了多个 Redis master 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
- 尝试从 N 个互相独立 Redis master 实例获取锁;
- 计算获取锁消耗的时间,只有时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功;
- 如果获取锁失败,就到每个实例上释放锁。
也就是说,该方案为了解决数据不一致的问题,直接舍弃了异步复制,只使用 master 节点,同时由于舍弃了 slave,为了保证高可用性,引入了 N 个节点,官方建议是 5。
参考资料