前言
分布式锁,是一个知识体系,记住,作为API调用工程师,必须要学习,必须必须必须的。
但是不用你去写,你也写不了,你也写不明白,生产中都是用框架的,也没人敢用你写的。
你要知道的是,哪些做法是错的?带来了什么问题?应该如何解决?
至于能不能写出来,who care? 重要的是分布式锁的思想。
分布式锁分类
1、类cas自旋分布式锁,client只能通过轮询,尝试加锁:mysql / redis
2、server有事件通知机制,client能接收后续锁的变化,无需轮询:zookeeper / etcd
单机redis
单机的redis,由于redis本身单线程,不会存在并发问题。
不考虑单机的高可用问题,出问题的只可能是你写的代码有问题,而核心问题在于:原子性。
关于redis的一个抬杠的问题
一问redis怎么实现分布式锁?很多人都回答:setnx命令。
这个答案严格来说是错的。
setnx只能保证互斥,key过期这个功能提供不了。
显然你用setnx命令实现的分布式锁漏洞百出。
你还不如回答:setnx
是的,我说了setnx,但是我说是setnx命令了么?我说的是redis提供的setnx特性啊?
这个问题如果面试官问我,我会回答:lua脚本 + 定时任务
懂得都懂,不懂的慢慢了解。
加锁
1、命令 SET key uuid NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。
http://doc.redisfans.com/string/set.html
2、把setnx和expire命令写到lua脚本中,交由redis执行。
Redis 保证lua脚本会以原子性(atomic)的方式执行
http://redis.cn/commands/eval.html
直白点讲,redis实现分布锁的上锁这步,还是要借助redis本身提供的命令和机制。
一直强调上锁的原子性,就是为了避免出现死锁(set成功,还没来得及设置过期时间client就宕机,这把锁无解了)。
解锁
1、谁上的锁谁来解:value用uuid或者其他的唯一标识。
2、先get,再判断锁是不是自己上的,最后删除key:这3步必须用lua脚本保证原子性。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
定时任务
有时业务代码执行时间较长,导致锁过期失效了,此时别的client也能获取锁。
可以开个定时任务给锁强行续命。
总结
set nx ex + value唯一标识 + 锁定时续命 + lua脚本解锁
单机redis实现分布式锁,只受制于单机redis自身的问题,挂了就不可用。
另外,重启恢复时,根据业界标准的持久化配置,会丢一秒数据,锁可能就丢失了。
但是,这种情况要出现在:
锁刚上去,redis就宕机,且在锁还未过期时,且在业务代码还在执行中,redis就重启完毕了。
此时,尝试另外尝试获取锁的线程就能拿到锁。所以可以采用延时重启的方案。
redis主从(哨兵)、集群
要明确一点:
主从无非是解决读写分离,故障转移等问题,数据同步是异步的,master和slave里面都是全量数据。
哨兵是在主从基础上的一种高可用运维手段。
集群是数据分片,用化整为零的思想,提升容量和压力的瓶颈。
这些和分布式锁都没关系。
RedLock算法
请注意,redlock应对的是单机redis宕机的问题,他的应用场景是多机redis,而不是主从、cluster,千万别搞混了。
多机的意思就是多个机器,就是多个独立的redis实例,client必须都连上这5个实例。
核心是过半机制:5台机器,我每个机器都去上锁,上锁成功了3台及以上,才算是上锁成功。
你理解为:
单机版的是一把钥匙一把锁,这把锁就一个锁眼,这把钥匙只要能插进这个锁眼,就能锁上。
多机版的是n把钥匙一把锁,这把锁有n个锁眼,必须能插进超过半数的锁眼,这个锁才能锁上。
听起来就怪,就鸡儿的蛋疼。而且你要明白,这个算法是在client实现的。
Redis作者antirez提出了RedLock算法来解决。
至于和分布式系统的专家马丁·克莱普曼博士两人的争论大家自己去看吧
马丁提出了很有建设性、指导性的分析
- 如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高。Redlock则是个过重的实现(heavyweight)。
- 如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于timing)。而且,它没有一个机制能够提供fencing token。那应该使用什么技术呢?Martin认为,应该考虑类似Zookeeper的方案,或者支持事务的数据库。
RedLock算法中,客户端按照下面的步骤来获取锁:
1、获取当前时间的毫秒数T1
2、按顺序依次向N个Redis节点执行获取锁的操作
为了保证在某个在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的一整套操作还需要一个超时时间。
它应该远小于锁的过期时间(几十毫秒量级)。客户端向某个Redis节点获取锁失败后,应立即尝试下一个Redis节点。
这里失败包括Redis节点不可用或者该Redis节点上的锁已经被其他客户端持有。
3、计算整个获取锁过程的总耗时
即当前时间减去第一步记录的时间。公式:T2 = now() - T1
4、判断获取锁是否成功
上锁成功的节点数量过半 && T2 < expireTime
5.1、如果获取锁成功,重新计算锁的过期时间(非常重要)
new_expire_time = expireTime - T2
5.2、如果获取锁失败,客户端立即向所有Redis节点发起释放锁的操作
关于释放锁
antirez在算法描述中特别强调,客户端应该向所有Redis节点发起释放锁的操作。
因为可能客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,
这个节点也成功执行了
SET
操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。
redlock可能会存在哪些问题?
1、节点崩溃,重启丢失锁
5个Redis节点:A, B, C, D, E
- 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)
- 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了
- 节点C重启后,客户端2锁住了C, D, E,获取锁成功
antirez提出了延迟重启(delayed restarts)的概念。
一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。
这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。
2、锁过期
5个Redis节点:A, B, C, D, E
锁过期时间5秒,获取锁时间3秒,留给业务的时间就2秒了,锁很快就过期了。
或者业务执行时间较长,锁依次过期。
这两种情况都会导致锁不安全。
还用定时任务解决么?你确保你的定时任务能每个节点都能延时成功么?失败了怎么办?
那获取锁后计算出的new_expire_time,我判断下,时间太短就丢弃吧,那多短算短呢?
(且站在用户的角度,这个请求没任何业务上的问题,凭什么不能处理?垃圾系统?)
3、redis触发内存置换,非常幸运的把你的锁淘汰了。
(以下是我自己的猜测和思考,笔者并未在其他博客和书中看到有人提这种情况,欢迎指正)
如果有个大SB,把内存淘汰策略设置成了allkeys-random,就很容易出现这种情况。
当然,生产中肯定不能用这种策略啊,但是我们讨论的是针对这个方案存在的漏洞。
你凭什么保证没人会这么设置呢?当然,这个问题单机也存在。
5、clock jump
Martin指出,由于Redlock本质上是建立在一个同步模型之上,对系统的记时假设(timing assumption)有很强的要求。
简单说,某个/某些节点出现时钟向前跳跃导致了提前解锁。
6、假设redis多机没问题,但是client出问题了
client从GC pause中恢复过来以后不知道自己持有的锁已经过期了
虚存造成的缺页故障(page fault)
CPU资源的竞争
网络延迟
不过,这些问题,不只是Redlock,zookeeper实现一样会出问题,redis单机也一样。
他们可以合并归类为:拔网线(简单理解),就是说client和server现在网络不同了,client各种处理和兜底手段都用不上了
导致redis的key过期了,zookeeper中的session过期了,
自然而然就锁不住了,所以这几点不应该算,因为这不是redlock单独面对的问题
综上所述,笔者认为
redlock仅仅解决了单机不能高可用(宕机)的问题,
其他单机会遇到的问题,多机都会遇到,且解决方法更麻烦。
在此基础上又引出了新问题(clock jump)
redlock把单机存在的问题和网络问题都进一步放大了,y1s1,收益很小,代价太大。
http://zhangtielei.com/posts/blog-redlock-reasoning.html
http://zhangtielei.com/posts/blog-redlock-reasoning-part2.html
Redisson框架(Java)
下图是 redisson对于单机版的分布式锁实现流程图。
至于其他的,可自行学习。
Redis总结
不必使用redis多实例作为分布式锁了,成本高的吓人,即便使用,那也要使用redisson
推荐就用单机,剥离业务,这台redis实例就作为分布式锁来用,其他的什么都不干。
忽略停电、断网的问题,因为这些问题谁来都一样,不必纠结。
redis性能(qps)(马士兵在课上讲的,笔者没实验过)
业务和redis在同一台机器上 10W(这个不现实)
同一物理局域网 4W(这个可以有,就把业务和redis这么部署)
NAT,云主机(阿里云的业务访问华为云的redis) 千 / 百
zookeeper实现
watch机制 + 心跳机制 + 临时顺序节点
redis和zk二者对比
1、setnx的排他性
利用 临时顺序节点中的顺序特性:
zk-server保证在同一父节点下创建的顺序节点,哪怕高并发情况下,也会设置好递增编号,保证不重复。
至于谁抢到锁了?谁编号最小谁就抢到锁了。
2、设置超时时间
利用 临时顺序节点中的临时特性:
client的session失效后,client创建的临时节点自动删除,所以不会由于client断开而死锁
3、定时续命
心跳机制:
哪怕client没有业务请求要发送,依然会定时发送保活请求给zk-server,维持心跳(更新session的timeout)
4、value = uuid
redis中的锁,是key,所有client都有可能误删
zk中的锁,是父节点下的多个子节点中的其中一个,且都是每个client自己负责创建出来的。
5、解锁原子性
因为保证了一定是自己上的锁,所以直接就一个delete命令,也无所谓原子不原子了。
redis单节点唯一的问题就是怕挂掉,其他无论从代码实现、资源消耗、效率来说,都很nice。
那zk能解决这个高可用的问题么? 肯定能,这里不多介绍。
举几个例子
redis主从同步,是异步的。
zk的数据同步是两阶段提交 + 过半机制。
redis持久化按照业界标准,宕机重启丢一秒数据。
zk的持久化是快照+事务日志(zk的事务日志类似redis的aof开了always)。
redis为什么快啊?因为,为了性能redis舍弃了太多。
所以高可用这点,zk强于redis
redis多机实现,其实是无主模型,获取锁太费劲
zk是主从模型,增删改请求都是leader去处理,抢锁leader帮你处理。
Curator框架
业界标准框架,分析源码的博客一大堆,可以去看一看。