分布式锁需要知道的一些基础知识点

前言

分布式锁,是一个知识体系,记住,作为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. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了
  3. 节点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框架

业界标准框架,分析源码的博客一大堆,可以去看一看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值