当前位置: 首页 > 科技观察

老大问我分布式锁,结果悲剧了...

时间:2023-03-20 16:43:24 科技观察

公司给萌新小园一个光荣而艰巨的项目,需要用到分布式锁,这对小园来说很难。图片来自Pexels。只是听说分布式锁牛逼,别的就不知道了。唉,不懂就问,所以才请教老大。老大:咱们不是天天体验分布式锁吗,给你回忆一下。小元:好的,瓜子凳准备好了。本文结构:为什么要使用分布式锁分布式锁有什么特点?①为什么要用锁来保证共享资源在同一时间只能被一个客户端访问;根据锁的用途,分为以下两种:共享资源只允许一个客户端操作共享资源允许多个客户端操作只允许一个客户端访问:对共享资源的操作不是幂等的。常用于数据的修改和删除操作。上例中:允许多个客户端操作:主要应用场景是对共享资源的操作是幂等的;比如数据查询。既然都是幂等的,为什么还要分布式锁,通常是为了效率或者性能,避免重复操作(尤其是耗资源的操作)。比如我们常见的缓存方案:在上面的例子中:由于这里的资源是幂等的,所以这类资源通常都会被缓存起来,就是常见的锁+缓存的架构。常适用于获取耗费较多资源(时间、内存、CPU等)的幂等资源,如:查询用户信息查询历史订单当然,如果资源只是在一段时间内是幂等的,那么架构应该升级Already:锁+缓存+缓存失效/失效重新获取/缓存定时更新。②为什么需要分布式锁?还是以上面的缓存方案为例,这里稍微改动一下:上面的例子中:分布式锁有什么特点?①互斥在任何时候,只允许一个客户端获得锁。PS:如果多个客户端可以同时获取到锁,那么这个锁就没有意义了,共享资源的安全也得不到保障。老板:我在会议室接待客户A,其他客户要等,你要等我有空再带人来我办公室。小猿猴:明白。接收客户端(非幂等共享资源);等到老板有空(获得锁)。②重入客户端A已经获得了锁,只要锁没有过期,客户端A就可以继续获得锁。锁给我了,我会一直用下去,不准别人抢。这个特性可以很好的支持【锁更新】功能。例如:客户端A获取锁,释放锁时间为10S。快到10S时,客户端A还没有完成任务,需要再申请5S。如果锁不可重入,客户端A将无法续约,锁可能会被其他客户端夺走。小猿猴:行了,老大,3分钟后还有面试。老板:小源,难得你这么好学。我很高兴。让我们的沟通时间推迟10分钟,其他的会议也推迟。③高性能锁的获取效率要足够高;总不能让业务阻塞在获取锁上吧?晓媛:好的,我在钉钉上申请了会议延期10分钟。老板:嗯,我已经接受了会议邀请;小源:老大,你效率真高。④在高可用的分布式微服务环境下,必须保证服务的高可用,否则至少会影响其他业务模块,最严重的会造成服务雪崩。老板:我的手机是24小时开机的。如果开会联系不上我,也可以联系我的秘书。⑤支持阻塞和非阻塞锁。如果获取锁失败,是直接返回failure,还是一直阻塞直到获取成功?不同的业务场景有不同的答案。例如:⑥解锁权限客户端只能释放(解锁)自己加的锁。一种常见的解决方案是在锁中添加一个随机数(或ThreadID)。Boss:Ape,我跟你说了这么多,你明白了吗?笼中鹦鹉:明白,明白。老板:闭嘴,我问的是小猿猴,只有小猿猴才有资格回答。⑦避免死锁。加锁方异常终止,无法主动释放锁;常规做法是在加锁的时候设置一个超时时间。如果不主动释放锁,则利用Redis的自动过期来被动释放锁。秘书插话:老板,你的10分钟会议到了,隔壁的李总等不及了。老板:一不留神就会忘记时间的。我得见李先生。小元:老大,我们还没谈完呢……⑧异常处理常见的异常包括Redis宕机、时钟跳变、网络故障等。小元:无论发生什么,我都会获取锁失败。我应该怎么办?PS:这个比较复杂,需要根据具体业务场景具体分析。对于必须同步处理的业务,必须发出故障告警。对于允许延迟处理的业务,可以考虑记录故障信息,供其他系统处理。分布式锁流行算法SETNX的基本方案是基于Redis的SETNX指令完成锁的获取。①获取锁SETlock:resource_namerandom_valueNXPX30000lock:resource_name:资源名称,被锁定对象的唯一标识。random_value:通常存放锁定方的唯一标签,如“UUID+ThreadID”。NX:仅当Key不存在时才设置,即锁只有在没有被别人锁住的情况下才能上锁。PX:锁定超时。当然,这种加锁方式不支持“锁重入”。②释放锁(LUA脚本)checkValueThenDelete:检测解锁方是否为加锁方,是则允许解锁,否则不允许解锁。伪代码为:publicclassRedisTool{//释放锁成功标记privatestaticfinalLongRELEASE_LOCK_SUCCESS=1L;/***释放分布式锁**@paramjedisRedis客户端*@paramlockKey锁标记*@paramlockValue锁方标记*@return是否释放成功*/publicstaticbooleanreleaseDistributedLock(Jedisjedis,StringlockKey,StringlockValue){Stringscript=""+"ifredis.call('get',KEYS[1])==ARGV[1]然后"+"returnredis.call('del',KEYS[1])"+"else"+"return0"+"end";//Collections.singletonList():用于只有一个元素的场景,减少内存分配Objectresult=jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(lockValue));if(RELEASE_LOCK_SUCCESS.equals(result)){returntrue;}returnfalse;}}Redlock算法该算法由Redis作者antirez提出,作为分布式场景下的锁实现方案。Redlock算法原理:【核心】大部分节点获取锁成功,锁仍然有效。第1步:获取当前时间(以毫秒为单位)。第二步:依次从N个Redis节点获取锁。设置一个随机字符串random_value;设置锁过期时间:注1:获取锁需要设置超时时间(防止某个节点不可用),超时时间要远小于锁有效期(几十毫秒)。注2:一个节点获取锁失败后,立即获取下一个节点的锁(任何类型的失败,包括本节点的锁已经被其他客户端持有)。第三步:计算获取锁的总耗时totalTime。第四步:锁获取成功。成功获取锁:客户端成功获取到大部分节点(>=N/2+1)的锁,totalTime不超过锁的有效时间。重新计算锁的有效时间:锁的初始有效时间减去3.1计算的获取锁所消耗的时间。第五步:获取锁失败。获取失败后,立即向【所有】客户端释放锁(Lua脚本)。第六步:解除锁定。业务完成后立即释放锁(Lua脚本)给【所有】客户端。Redlock算法的优点:高可用,只要大部分节点正常即可。当单个Redis节点的分布式锁故障转移时,锁失效的问题不复存在。Redlock算法存在的问题:Redis节点崩溃会影响锁的安全性:节点崩溃前锁没有持久化,节点重启后锁会丢失;Redis默认的AOF持久化是每秒刷新一次磁盘(fsync),最坏的情况下会丢失1秒的数据。需要避免一直跳:管理员手动修改时钟;使用不跳频调整系统时钟的ntpd(时钟同步)程序,通过多次微调实现时钟修改。客户端阻塞导致锁过期,使共享资源不安全。如果获取锁的时间很长,导致有效时间很短,是否应该立即释放锁?多段算短?MartinKleppmann是使用fencing令牌实现分布式系统的专家,他讨论了RedLock的安全问题。神仙之战:MartinKleppmann认为Redis作者antirez提出的RedLock算法存在安全问题,双方在网上已经讨论和交锋了多轮。Martin指出RedLock算法的核心问题如下:锁过期或网络延迟会导致锁冲突:客户端A进程暂停→锁过期→客户端B持有锁→客户端A恢复并向客户端发起写请求共享资源;网络延迟也会有类似的效果。RedLock安全性对系统时钟有很强的依赖性。fencingtoken算法原理:fencingtoken是一个单调递增的数,当客户端成功获取到锁时,将其与锁一起返回给客户端。客户端访问共享资源时,会带上token。共享资源服务检查令牌并拒绝延迟请求。fencingtoken算法存在的问题:需要对共享资源服务进行改造。如果资源服务也是分布式的,如何保证token在多个资源服务节点上递增。2个fencingtoken到达资源服务的顺序颠倒了,服务检查就会异常。[antirez]既然有fencing机制来维护资源的互斥访问,为什么还需要分布式锁,还需要强安全性?其他分布式锁数据库独占锁:获取锁(selectforupdate,悲观锁)。处理业务逻辑。释放锁(connection.commit())。注意:InnoDB引擎加锁时,在通过索引查找时只会使用行级锁,否则会使用表级锁。所以lock_name必须被索引。ZooKeeper分布式锁:客户端创建一个znode节点,创建成功则获取锁成功。持有锁的客户端完成对共享资源的访问后,znode将被删除。znode被创建为临时的(znode特性),这确保了在创建znode的客户端崩溃后该znode将被自动删除。【问题】Zookeeper基于客户端和一个Zookeeper服务端维护Session,Session依赖定时心跳来维护。如果Zookeeper长时间没有收到客户端心跳,则任务Session到期,该Session创建的所有临时znode节点将被删除。Google的Chubby分布式锁:sequencer机制(类似于fencingtoken)缓解延迟带来的问题。锁持有者可以随时请求定序器。当客户端操作资源时,sequencer被传递给资源服务器。资源服务器检查sequencer的有效性:①调用Chubby的API(CheckSequencer)进行检查。②对比查看客户端和资源服务器当前观察到的sequencer(类似于fencingtoken)。③lock-delay:允许客户端指定一个lock-delay延迟时间来持有锁。当Chubby发现客户端失联后,会在lock-delay时间内组织其他客户端去获取锁;总结一下我们应该使用什么样的分布式锁算法?技术为业,切忌选择“高大上”的炫技;依托业务场景,尽可能选择最简单的方法;最简单的分布式锁引起的偶发异常如何处理?建议增加额外的机制即使是人工干预来保证业务的准确性,通常也比复杂的分布式锁的开发和运维成本要低。分布式锁的另一种玩法,“分而治之”经久不衰:如果共享资源本身可以拆分,那就分开处理。例如,电子商务系统防止超卖。假设秒杀10000个mask,通常的做法是用一把锁来控制所有的资源。另一种玩法是将10000个面具交给20把锁进行控制,整体性能瞬间提升数十倍。PS:这里的超卖只是一个例子。实际场景中的超卖闪购场景较为复杂,请谨慎操作。

猜你喜欢