笔记簿
ᴄᴏᴅɪɴɢ ɪs ᴀʀᴛ
首页
关于
搜索
登录
注册
分布式锁思考 - 分布式锁偶现失效场景
#### 场景 大体来说,分布式锁的场景有两种: 1. 为了效率:相当于去重,避免各个系统做重复的事情。比如重复发送一封email 2. 为了正确性:不允许出现任何的失效,不然就可能造成数据不一致 #### 基于单节点的redis实现 为什么强调单节点?因为我们就一个redis主从,没有redis集群。而且redis有官方的分布式锁redlock是基于redis集群的,这个对我们不适用,而且感觉有点过重。 我们一开始的方案是基于setNX(key,value,timeout)。后来发现原来这是jedis的封装,这其实是2个redis命令setnx+expire。也就是说**这不是个原子操作,很可能setnx成功,但是设置过期时间失败导致锁永远无法释放** 翻看redlock的套路才知道,应该这样操作: 1. 获取锁:set key randomValue NX PX 3000 redis的set操作有NX(if not exist)选项和PX(过期时间)选项,可以实现原子操作 2. 释放锁:需要使用LUA脚本实现复合操作的原子性: - get key - 比较random value - random value一致即删除key #### 分析 一些疑问: 1. 必须要设置过期时间吗? 必须要。因为假如获取锁的程序阻塞/崩溃/与redis网络异常等情况,锁将永远不能被释放 2. 必须要设置一个随机值吗? 必须要。考虑以下执行序列: (1) 程序1获取了锁 (2) 程序1假死导致锁超时被释放 (3) 程序2获取了锁 (4) 程序1从假死中恢复,直接释放了锁(没有比较随机值,相当于把程序2的锁给释放了) (5) 程序2的执行将得不到锁的保护 3. 必须使用lua脚本来释放锁吗? 必须要。因为释放锁需要3步(get->比较random value->del),需要保证3个复合操作的原子性。也不能用redis的事务消息,因为redis没有比较和if else这样命令啊…redis要是有类似cas这种操作就好了,compare and delete一步就能搞定 这样处理的redis锁已经是万无一失了吗?其实还存在2个主要问题: 1. 过期时间设置多久合适?如果设置太短,锁就可能在程序完成对共享资源的操作之前失效,从而得不到保护;如果设置的太长,一旦某个获取锁的程序释放锁失败(比如与redis网络异常),那么就可能导致其他系统长时间无法获取锁而无法正常工作 2. 如果程序假死(例如长时间的GC pause)将导致锁过期失效,这时候共享资源其实已经失去了保护(可能这时候有另一个程序获取了锁,而假死的程序恢复过来后同时在操作共享资源)> 另外redlock还存在另一个问题就是强依赖于几个节点之间的系统时钟,一旦发生时钟跳跃,redlock很可能就失效 很多人说可以用更可靠的zookeeper来解决,那基于zookeeper的分布式锁真的万无一失吗?先来看看zookeeper分布式锁的套路 #### zookeeper分布式锁 具体参考官网文档:[传送门](http://zookeeper.apache.org/doc/r3.4.9/recipes.html#sc_recipes_Locks) 1. 获取锁: (1) 客户端调用create()在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推 (2) 客户端调用getChildren()获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得了锁,获取锁流程结束 (3) 否则在前一个节点调用exists()并设置watch监听节点删除消息 (4) 如果exists()返回false,重试第(2)步;否则等待节点删除通知再重试第(2)步直到获取锁 2. 删除锁:删除创建的节点即可 这样设计有以下优点: * 删除节点即释放锁时,只会导致最多一个客户端被唤醒,避免了“惊群效应(herd effect)” * 不存在轮询或者超时 * 临时节点,会在客户端奔溃/假死等情况自动释放锁 * 能够直观的知道竞争锁的数量,甚至能退出锁,debug锁问题等 #### 分析 zookeeper实现的分布式锁相对于redis确实更完善,功能也更丰富。没有redis过期时间的问题,而且能在需要的时候让锁自动释放。然而redis存在的问题中第2点,zookeeper也依然存在。假设有以下执行序列: (1) 客户端1创建了节点/lock/lock-0000000000并且获取到了锁 (2) 客户端1进入长时间的GC pause (3) 客户端1与zookeeper的session过期(心跳检测失败),lock-0000000000被自动删除 (4) 客户端2创建了/lock/lock-0000000001并获得了锁 (5) 客户端1从GC pause中恢复,依然认为自己持有锁 (6) 客户端1和2都认为自己持有锁,就产生了冲突 **类似这种假死造成的锁失效问题,redis和zookeeper目前貌似没有完美的解决方案**