分布式锁
单机环境下,为解决多线程访问共享资源产生的线程安全问题,引入了锁机制。分布式环境下,多个服务实例也存在对共享资源的并发访问,此时需要使用分布式锁,协调不同服务实例对共享资源的访问。
分布式互斥锁实现方案
- 基于关系数据库分布式锁
数据库中存储锁记录,加锁时使用select * from table where lock_name = 'xxx' for update增加排它锁。InnoDB的行级锁是加在索引上的。为避免锁表及减小加锁粒度,最好在lock_name列增加唯一索引。
-
缺点:无法设置超时时间,数据库易成为性能瓶颈。
-
基于Zookeeper的分布式锁
-
多个客户端竞争创建lock临时节点。
-
某个客户端成功创建Lock节点,其他客户端对lock节点设置watcher
-
持有锁的客户端删除lock节点,或该客户端崩溃,由Zook删除lock节点
-
其他客户端获取lock节点被删除的通知,
-
-
基于Redis的分布式锁
使用Redis的动态字符串类型数据结构,存储Key-Value形式的键值对。其中Key为锁名称,Value可以为随机数,uuid等。Key-Value键值对一起,唯一标识一把锁。
基于Redis的分布式锁
注意事项:
-
需为锁设置有效期,避免持有锁的线程异常退出,导致锁一直无法释放。
-
持锁者解锁,加锁和解锁必须是同一个客户端。客户端A加的锁,不允许客户端A以外的客户端解锁。
Redis 分布式锁的工作流程:
image1. 加锁与解锁
1.1 使用setnx命令(set if not exist)加锁
>setnx lock true
>expire lock 5
... do something critical ...
>del lock
存在的问题:
sentnx 与expire 是两条命令,不是原子的。若客户端A加锁成功,尚未设置锁过期时间时异常退出了。则锁一直得不到释放,将导致死锁。
将setnx和expire放到同一个Redis事务中执行的思路是行不通的。expire 依赖于 setnx的执行结果。若setnx未执行成功,expire不应该执行。事务中不存在if-else分支逻辑。
1.2 改进方案
Redis 2.8版本中,作者对set指令进行了扩展,使得setnx 和 expire可以一起执行。只有key设置成功后才会设置key的过期时间。
>set lock true ex 5 nx
... do something critical
> del lock
1.3 超时问题
若临界区代码执行时间较长,以至于超出了锁的过期时间限制。锁过期后,第二个线程可以重新持有这把锁。若此时第一个线程执行完临界区代码,会将第二个线程持有的锁释放掉。其他线程可以在第二个线程临界区代码执行结束前获取到锁,线程不安全。
解决方案:为set指令的value参数设置一个随机数,解锁时先判断线程持有的随机数与value值是否一致。一致时才允许线程解锁。可以使用Lua脚本来保证value匹配与删除key两个指令的原子性。
网友评论