简单的记录下分布式锁的东西,尤其是用redis实现的分布式锁

基本概念

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。与互斥锁和读写锁之类的很类似,只不过它可以控制多个系统间的同步访问。

分布式锁需要满足的条件

一个简单的分布式锁需要满足的条件有:

  1. 需要是互斥的
  2. 需要可重入
  3. 可以是阻塞或者非阻塞
  4. 高可用
  5. 加锁和释放锁性能
  6. 无死锁

其实上面基本上最重要最基本的就是需要互斥然后无死锁

分布式锁的实现方式

一个分布式锁,有很多的实现方式,如数据库,redisZooKeeper

数据库

基于数据库表

这种最简单的,为表中一个字段加上唯一索引,加锁就是直接插入一条记录,插入成功表示成功获取到锁,唯一索引重复插入记录会引起错误,那就是加锁失败了。释放锁删除记录即可。

这个方法问题挺多,如下:

  1. 单点问题
  2. 没有失效时间,解锁失败就导致死锁
  3. 这是一个非阻塞的锁
  4. 非重入锁

基于数据库表中的排他锁

主要是使用数据库中自带的排他锁来实现,例如在mysqlInnoDB引擎中可以使用select for update,开启事务后会自动锁住该行记录(注意查询语句条件中需要有唯一索引,否则会锁表)。之后提交事务自动解锁。

这个锁也有问题:

  1. 单点
  2. 非重入锁
  3. 最重要的是这个锁是阻塞的,同时也导致了如果事务长时间不提交,会大量占用数据库连接,大量的连接可能会撑爆数据库连接池
  4. 最后如果表中数据太少,数据库对SQL优化,最后生成的执行计划可能不会用到索引,如果全表扫描就会锁表

综上所述,用数据实现分布式锁感觉不是很靠谱

基于缓存

缓存性能要比数据库好,而且集群部署也比较简单,解决锁性能和单点的问题。基于缓存主要就以redis为例。

单节点

在单节点的redis中实现一个分布式锁很简单,加锁操作如下:

1
> SETNX key value

这个方法就是在key不存在的时候设置value返回1,如果key存在直接返回0

这个方法没有过期时间,如果需要原子操作就直接用SET操作

1
> SET key value EX seconds NX

这样的一个原子操作就可以在key不存在时候设置value并且设置过期时间

解锁操作需要先取出value判断是否为自己持有的锁,再进行删除,防止删掉别人的锁,没办法用一个命令操作完成,只能借助lua脚本的力量了

1
2
3
4
5
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end

在用Rediseval命令执行下即可

1
> eval lua_script

这个锁的问题:

  1. 单节点容易故障
  2. 无法重入
  3. 超时时间无法准确控制,太短业务未执行完锁就被释放了,太长了如果自己释放失败会导致其他的人白白多等一会

分布式

为了解决单点单点问题,redis作者提出了redlock算法,具体可参考这里。简单描述下这个算法:

  1. 有 N 个Redis实例
  2. 每次加锁前获取当前时间戳,设置锁的有效时间T
  3. 依次向N个实例发送加锁请求,设置连接超时时间等条件
  4. 计算出加锁耗费时间T1和成功加锁节点N1
  5. 只有当T>T1 && N1 > N/2时候加锁成功,即加锁耗费时间小于锁的过期时间并且加锁成功的节点大于一半才是加锁成功
  6. 如果加锁失败,再一次给所有实例解锁,不管是否加锁成功的节点

关于这个算法的问题,可以看看antirezMartin的争论,最关键的问题是基于自动过期机制,原因是只要由于程序自身的原因导致代码卡住,例如 gc 之类的东西,等到锁过期还不知道,那么就会导致同时多个客户端持有锁

总的来说,从理论上来分析,Redlock算法是有问题的,因为它基于对时钟正确性的假设,没办法在程序pause的时候知道锁是否过期了。简单的解决办法就是使用fencing token,一个可以单调递增的东西,但是redis只能在单节点上生成这样的东西,这样又缺乏高可靠性,最终可能要引入一个一致性协议来完成,这不就是做了etcdZooKeeper这样的活了吗

基于 ZooKeeper

其实我没用过Zookeeper,所以直接抄一段伪代码好了,理论上来说这样的实现要比redis的实现是安全的

获取锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void lock(){
path = 在父节点下创建临时顺序节点
while(true){
children = 获取父节点的所有节点
if(path是children中的最小的){
代表获取了节点
return;
}else{
添加监控前一个节点是否存在的watcher
wait();
}
}
}

watcher中的内容{
notifyAll();
}

释放锁

1
2
3
public void release(){
删除上述创建的节点
}

这个实现方法更加安全,用在关键性的应用更加让人放心,因为:

  1. 自动释放锁
  2. 可阻塞,可非阻塞
  3. 可重入
  4. 无单点问题

缺点可能就是性能没那么好吧…

总结

基本上主要说了下基于redis实现的分布式锁的单点问题和redlock算法存在的问题

Martin分析说,你需要考虑自己用分布式锁的目的,是为了高性能还是正确性。如果是为了高性能并且正确性没多高的场景下,直接使用redis单节点的分布式锁,因为就是多个客户端拿到锁也没什么多大的问题,最多重复执行几次。但是在需要正确性的场景下,redlock算法的问题也不能保证正确性,所以还是建议使用一个合适的一致性协调系统,例如Zookeeperetcd等,且保证存在fencing token