【二十五】分布式锁(redlock、redis、zookeeper)

一、分布式锁必须保证的特性

     互斥
     无死锁
     容错
     阻塞与非阻塞
     可重入

二、如何使用redis实现分布式锁(根据redlock算法)

redlock算法参考这篇翻译分析:http://zhangtielei.com/posts/blog-redlock-reasoning.html

常用的redis实现的分布式锁的框架是redisson 

1.获取锁        

SET key random_val NX PX 30000     

命令解释:

random_val是由客户端生成的一个随机字符串,它要保证在所有客户端的所有获取锁的请求中都是唯一的。

NX 表示只有当key值不存在时才set

PX 30000 表示这个锁30秒过期 

2.释放锁

当客户端完成了对共享资源的操作之后执行LUA脚本释放锁 

if redis.call("get", KEYS[1]) == ARGV[1] then
   return redis.call("del", KEYS[1])
else
   return 0
end     

其中入参 KEYS[1]就是key,ARGV[1]就是 random_val

命令解释:

通过Key得到分布式锁,判断value是不是同一个人加的锁,如果是才释放锁。

这里一定要用Lua脚本,是因为要用Lua脚本才能保证get和del这两个命令的执行是连续的,是不会被其他命令岔开的。换句话说保证了原子性(redis的原子性跟回滚没有关系,是只要开始执行逻辑,即使报错,后面的逻辑也会执行,并且不会被其他命令岔开)

比如:不会出现这种情况,get了keys[1],还没来得及判断keys[1]的值释放等于argv[1]的值,就被其他线程把keys[1]的值给改了。

3.多数派(奇数个独立的redis节点)

 如果redis部署集群,那么建议有奇数个master节点,比如5个master节点。

获取锁的流程变成:

1.获取当前时间(毫秒数),我们叫它time_begin。

2.按顺序依次向N个Redis节点执行获取锁的操作。

为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败(该Redis节点不可用、该Redis节点上的锁已经被其它客户端持有)

3.计算整个获取锁的过程总共消耗了多长时间.

计算方法是用当前时间减去第1步记录的时间(time_had_lock - time_begin)。

如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。

4.如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。

5.如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作

释放锁的流程变成:

客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。

即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。

设想这样一种情况,客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。

这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。

因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。 

4.崩溃恢复

节点发生崩溃重启可能出现以下不安全的情况:

假设一共有5个Redis节点:A, B, C, D, E。

1.客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。

2.节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。

3.节点C重启后,客户端2锁住了C, D, E,获取锁成功。

这样,客户端1和客户端2同时获得了同一资源锁

解决方法:

1.在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。

为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync。即是把AOF位置为fsync=always

但是也有可能因为跟redis无关的原因丢数据,比如操作系统扯拐了。 

2.延迟重启(推荐)

 一个节点崩溃后,不要马上重启该节点,等待一段时间(该时间大于锁的有效时间)再重启,这时候该节点在重启前锁参与的锁都过期了,重启后就不会对原来的锁资源竞争有影响了

5.安全性        

使用Redlock算法的分布式锁一定要注意时间!!!

第一,每个机器的时间要一样

第二,程序获取到锁资源后一定要在锁自动失效前做完逻辑 

不安全假设:客户端1获得了锁资源,30秒自动释放,然而30秒客户端1都没做完逻辑,客户端2获得了锁跟客户端1修改同样的资源!

 解决方案:

1. 计算“有效时间” 并且在有效时间内完成“业务逻辑”,若不能完成则撤销之前的“操作”
    业务逻辑应该尽可能的精简,不要掺杂不必要的代码

2.实现fencing token机制

fencing token是一个单调递增的数字,当客户端成功获取锁的时候它随同锁一起返回给客户端。而客户端访问共享资源的时候带着这个fencing token,这样提供共享资源的服务就能根据它进行检查,拒绝掉延迟到来的访问请求(避免了冲突)。如下图:

图片流程解释:

客户端1先获取到的锁,fencing token=33

客户端2后获取到的锁,fencing token=34,并且向存储服务中的资源进行了写操作成功。

客户端1从GC pause中恢复过来之后,向存储服务发送写请求,但是带了fencing token = 33。存储服务发现它之前已经处理过34的请求,所以会拒绝掉这次33的请求。

三、如何使用zookeeper实现分布式锁

常用的zk实现的分布式锁的框架是:Curator 

如果想有一个安全性更高的分布式锁,可以使用zookeeper而不是redis

不过zookeeper不是100%的安全,且速度比redis慢一点点。

1.使用zookeeper实现分布式锁的方式

1.客户端尝试创建一个znode节点,比如/lock。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。

2.持有锁的客户端访问共享资源完成后,将znode删掉,这样其它客户端接下来就能来获取锁了。

3.znode应该被创建成ephemeral的。这是znode的一个特性,它保证如果创建znode的那个客户端失去心跳了(Session过期了),那么相应的znode会被自动删除。这保证了锁一定会被释放。

2.安全性

每个客户端都与ZooKeeper的某台服务器维护着一个Session,这个Session依赖定期的心跳(heartbeat)来维持。

如果ZooKeeper长时间收不到客户端的心跳(这个时间称为Session的过期时间),那么它就认为Session过期了,通过这个Session所创建的所有的ephemeral类型的znode节点都会被自动删除。

换句话说如果发生session过期,zk的分布式锁会被删掉,也会出现安全性问题。

比如:

1.客户端1创建了znode节点/lock,获得了锁。

2.客户端1进入了长时间的GC pause。

3.客户端1连接到ZooKeeper的Session过期了。znode节点/lock被自动删除。

4.客户端2创建了znode节点/lock,从而获得了锁。

5.客户端1从GC pause中恢复过来,它仍然认为自己持有锁。

3.ZooKeeper的watch机制

 这个机制可以这样来使用,比如当客户端试图创建/lock的时候,发现它已经存在了,这时候创建失败,但客户端不一定就此对外宣告获取锁失败。

客户端可以进入一种等待状态,等待当/lock节点被删除的时候,ZooKeeper通过watch机制通知它,这样它就可以继续完成创建操作(获取锁)。

这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。

四、对比用redis实现和zookeeper实现

1.在正常情况下,zk实现的分布式锁,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。

2.基于zk的锁支持在获取锁失败之后等待锁重新释放的事件。这让客户端对锁的使用更加灵活。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值