Redis分布式锁
CAP
分布式系统有三个指标:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
分布式锁
分布式锁的刚需
独占性
任何时刻只能有且只有一个线程持有
高可用
- 集群环境下,不能因为一个节点宕机而出现获取锁和释放锁失败的情况
- 高并发情况下,性能依旧良好
防死锁
必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
不乱抢
不能私自释放其他线程获取的锁
重入性
同一个节点的同一个线程如果获取锁之后,它也可以再次获取这个锁
简单实现
1 |
|
问题:客户端宕机会导致锁一直未被释放,造成死锁。
优化:
1 |
|
问题:由于SETNX
和EXPIRE
并非原子操作,可能存在刚SETNX
完就宕机的情况,一样会导致死锁。
优化:
1 |
|
防止误删
上方锁的简易实现中,还存在一定问题:
一般情况下,我们设置的超时时间需要大于业务代码的执行时间,但如果业务代码中出现了异常情况,导致执行完成时间大于锁的超时时间,这就会导致锁过期后被删除,其他线程抢占到锁,如果此时代码执行完了,要去释放锁,就会把其他线程占用的锁给释放了。
修改:
- 在获取锁时存入线程标识(可以用UUID)
- 在释放锁时先获取锁种的线程标识,判断是否与当前线程一致:
- 如果一致则释放
- 如果不一致则不释放
1 |
|
上方为示例解释,线程标识判断需在业务代码中完成
原子性问题
上方防止误删的优化代码仍有一些问题:
由于 判断线程标识一致性 和 释放锁 的操作不是原子性的,如果在判断线程标识一致性之后,发生了阻塞,此时锁过期被删除,另一个线程抢占到锁,当前线程阻塞结束再去释放锁,又会出现误删除问题。
修改:
使用Lua脚本解决多条命令原子性问题
1
2
3
4
5if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
前置知识:
Lua中调用Redis的方式
1 |
|
Redis中调用脚本的方式
1 |
|
后面的0是脚本需要的key类型的参数个数。
作用是区分key数组和arg数组,例如
a b c d
,假设numkeys是2,说明前2个参数a和b是key,后2个参数c和d是arg。
Redis调用脚本传参:
1 |
|
分布式锁的简单实现(Python)
1 |
|
其他问题
基于SETNX实现的分布式锁还存在其他一些问题:
不可重入
同一个线程无法多次获取同一把锁
超时释放
锁超时释放虽然可以避免死锁,但如果业务执行耗时较长,也会导致锁释放,存在安全隐患
主从一致性
如果Redis提供了主从集群,主从同步存在延迟,当主机宕机时,如果从机来不及复制,新上位的主机没有锁数据,就有可能有多个线程获取到锁
现有的成熟工具集合:Redisson
python可以使用python-redis-lock包,其内部也实现了锁的自动续约