1、redis分布式锁

redis 最普通的分布式锁

  • 加锁: 第一个最普通的实现方式,就是在 redis 里使用 setnx 命令创建一个 key,这样就算加锁。
SET resource_name my_random_value NX PX 30000
  • 解锁: 用下面的lua脚本保证原子性
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else 
    return 0
end

问题:

1、依赖过期时间
事物提交前,必须先判断锁是否过期,然而判断过期和提交事物是两个操作,如果判断没有过期,在提交是之前发生GC,GC后锁过期,再执行提交操作会出问题,如图:

解决: 1、数据库乐观锁,在每条记录后面增加一个version字段

  2、Martin的fencing token方案:每个获取锁生成一个token, 写入数据是存储服务判断token是不是当前最大的,不是则写入失败,如图:

RedLock 算法

这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把锁:
获取当前时间戳,单位是毫秒;
跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;
尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1;
客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
要是锁建立失败了,那么就依次之前建立过的锁删除;
只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。

Redis 官方给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:https://redis.io/topics/distlock 。

问题:

1、两个客户端同时获得锁的问题(时间跳跃,节点崩溃,客户端GC延迟都可能导致这个问题):

  • Client 1 acquires lock on nodes A, B, C. Due to a network issue, D and E cannot be reached.
  • The clock on node C jumps forward, causing the lock to expire.
  • Client 2 acquires lock on nodes C, D, E. Due to a network issue, A and B cannot be reached.
  • Clients 1 and 2 now both believe they hold the lock.

解决:

1、Disqus提出的 delay restart, 宕机节点在所有的锁超时后再启动

2、延迟启动仍然是依赖时间的,所以,第二个方案是设置fsync=always,每次写入同步到磁盘才回复客户端。

  • fsync=allways 会极大消弱Redis的性能,因为这种模式下每次write后都会调用fsync(Linux为调用fdatasync)。
  • none 如果设置为no,则write后不会有fsync调用,由操作系统自动调度刷磁盘,性能是最好的。
  • everysec 为最多每秒调用一次fsync,这种模式性能并不是很糟糕,一般也不会产生毛刺,这归功于Redis引入了BIO线程,所有fsync操作都异步交给了BIO线程。

2、zk 分布式锁

方法1(非公平锁):

zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

方法2(公平锁):

创建一个锁目录 /lock;
当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点;
客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
执行业务代码,完成后,删除对应的子节点。

羊群效应:

一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应,一只羊动起来,其它羊也会一哄而上),而我们只希望它的后一个子节点收到通知。

读写锁:

这个时候我规定所有创建节点必须有序,当你是读请求(要获取共享锁)的话,如果 没有比自己更小的节点,或比自己小的节点都是读请求 ,则可以获取到读锁,然后就可以开始读了。若比自己小的节点中有写请求 ,则当前客户端无法获取到读锁,只能等待前面的写请求完成。
如果你是写请求(获取独占锁),若 没有比自己更小的节点 ,则表示当前客户端可以直接获取到写锁,对数据进行修改。若发现 有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁 ,等待所有前面的操作完成。

redis 分布式锁和 zk 分布式锁的对比

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
  • 如果是 redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

3、Mysql分布式锁

在Mysql新建一张表,设置一个unique key,这个key就是要锁的key(商品ID),同一个key在数据库只能插入一条,可以新增一个expire_time设置锁的过期时间。

CREATE TABLE distributed_lock (
    key varchar(64) primary key "锁唯一key",
    value varchar(64) not null  "加锁终端",  
    expire_time datetime not null "过期时间"
);
  • 加锁的时候插入一条记录,key即是锁,value是加锁线程设置的唯一标识,expire是锁的过期时间
  • 如果加锁失败,查询数据库已存在的锁,判断是否过期,过期就删除锁,并重新加锁
  • 解锁,将key和value作为条件,delete对应的记录即可

总结

redis分布式锁是用的对最多的,有现成的开源库Redission可以用,能满足大部分需求。一般情况下使用redis锁就可以。

zookeeper分布式锁可以实现读写锁,公平锁和非公平锁,能实现的锁种类更多,也是一种很好用的锁,但是需要自己实现。

MySQL实现起来稍显麻烦,也需要自己实现。

三种锁均不是绝对安全的锁,都可能存在失效的情况。
唯一安全的锁是数据库乐观锁,请看https://juejin.cn/post/7390689983789023247

参考

https://juejin.cn/post/7390689983789023247


杜若
70 声望3 粉丝