Redis单点方式:
首先,Redis客户端为了获取锁,向Redis节点发送如下命令:
SET resource_name my_random_value NX PX 30000
上面的命令如果执行成功,则客户端成功获取到了锁,接下来就可以访问共享资源了;而如果上面的命令执行失败,则说明获取锁失败。
注意,在上面的SET
命令中:
my_random_value
是由客户端生成的一个随机字符串,它要保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。NX
表示只有当resource_name
对应的key值不存在的时候才能SET
成功。这保证了只有第一个请求的客户端才能获得锁,而其它客户端在锁被释放之前都无法获得锁。PX 30000
表示这个锁有一个30秒的自动过期时间。当然,这里30秒只是一个例子,客户端可以选择合适的过期时间。
最后,当客户端完成了对共享资源的操作之后,执行下面的Redis Lua脚本来释放锁:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这段Lua脚本在执行的时候要把前面的my_random_value
作为ARGV[1]
的值传进去,把resource_name
作为KEYS[1]
的值传进去。
总结:
1.必须设置过期时间,过期时间需要谨慎考虑,太短锁没等主动释放就过期,太长一旦拥有锁的对象出现问题,别人会很长时间无法使用。
2.setnx 和 过期时间必须是原子的
3.value必须是随机的,保证解铃还须系铃人,别人解不了
4.释放必须用Lua原子方式进行。因为Get和Del是分布进行的,没发指定删除随机value的key,所以必须原子性,防止删除的时候不是自己的锁,而是过期后别人加上去的锁。
另外:单机的问题无法达到高可用,当redis宕了后,锁失效,通过slave方式处理也可能会有1s内的数据同步问题,如果正好赶上master上加上锁,还没同步到slave上,master宕,slave变成master时锁不对。
多redis集群方式:(客户端需要做的事情太多,不太好用还是用zk吧)
- 获取当前时间。
- 完成获取锁的整个过程(与N个Redis节点交互)。
- 再次获取当前时间。
- 把两个时间相减,计算获取锁的过程是否消耗了太长时间,导致锁已经过期了。如果没过期,
- 客户端持有锁去访问共享资源。
考虑:如果Client1 获取lock1后,执行了Full GC,到了过期时间,lock1过期。Client2获取lock2,执行数据操作。Client1恢复运行,执行数据操作!这样两个Client同时拿到了锁。
可以让Lock Server在分配锁的时候返回一个递增数字,在执行数据操作的时候,需要提交这个数字,记录最后一次执行的数字,并且只接受比这个数字大的请求。保证请求数据操作的Client是按拿到锁的顺序执行的。但redis暂时不能返回这个数字。
使用分布式锁的方式有两种情况:
- 为了效率(efficiency),协调各个客户端避免做重复的工作。即使锁偶尔失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。比如重复发送了一封同样的email。
- 为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生,因为一旦发生,就可能意味着数据不一致(inconsistency),数据丢失,文件损坏,或者其它严重的问题。
结论:
- 如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高。Redlock则是个过重的实现(heavyweight)。
- 如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于timing)。而且,它没有一个机制能够提供fencing token。那应该使用什么技术呢?Martin认为,应该考虑类似Zookeeper的方案,或者支持事务的数据库。
http://zhangtielei.com/posts/blog-redlock-reasoning.html
一个基于ZooKeeper构建分布式锁的描述(当然这不是唯一的方式):
- 客户端尝试创建一个znode节点,比如
/lock
。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。 - 持有锁的客户端访问共享资源完成后,将znode删掉,这样其它客户端接下来就能来获取锁了。
- znode应该被创建成ephemeral的。这是znode的一个特性,它保证如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这保证了锁一定会被释放。
看起来这个锁相当完美,没有Redlock过期时间的问题,而且能在需要的时候让锁自动释放。但仔细考察的话,并不尽然。
ZooKeeper是怎么检测出某个客户端已经崩溃了呢?实际上,每个客户端都与ZooKeeper的某台服务器维护着一个Session,这个Session依赖定期的心跳(heartbeat)来维持。如果ZooKeeper长时间收不到客户端的心跳(这个时间称为Sesion的过期时间),那么它就认为Session过期了,通过这个Session所创建的所有的ephemeral类型的znode节点都会被自动删除。
设想如下的执行序列:
- 客户端1创建了znode节点
/lock
,获得了锁。 - 客户端1进入了长时间的GC pause。
- 客户端1连接到ZooKeeper的Session过期了。znode节点
/lock
被自动删除。 - 客户端2创建了znode节点
/lock
,从而获得了锁。 - 客户端1从GC pause中恢复过来,它仍然认为自己持有锁。
最后,客户端1和客户端2都认为自己持有了锁,冲突了。这与之前Martin在文章中描述的由于GC pause导致的分布式锁失效的情况类似。
ZooKeeper的watch机制。这个机制可以这样来使用,比如当客户端试图创建/lock
的时候,发现它已经存在了,这时候创建失败,但客户端不一定就此对外宣告获取锁失败。客户端可以进入一种等待状态,等待当/lock
节点被删除的时候,ZooKeeper通过watch机制通知它,这样它就可以继续完成创建操作(获取锁)。这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。这样的特性Redlock就无法实现。
小结一下,基于ZooKeeper的锁和基于Redis的锁相比在实现特性上有两个不同:
- 在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。
- 基于ZooKeeper的锁支持在获取锁失败之后等待锁重新释放的事件。这让客户端对锁的使用更加灵活。
http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html