分布式锁及重试机制
Posted 杀马特技术分享
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式锁及重试机制相关的知识,希望对你有一定的参考价值。
分布式锁及重试机制
一、问题来源
点评CRM客户轮转系统的部分数据模型如下:
ShopGroup是点评前台的连锁关系,RotateGroup是后台的连锁关系,从上图可知ShopGroup下的门店会根据一定的条件(如部分门店被销售A持有,部分门店被销售B持有)拆分成不同的轮转组,轮转组(RotateGroup)与门店(Shop),是一对多的关系,但是在现实的业务场景中会发生轮转组合并的情况:
情况一:同一个ShopGroup下不同的两个轮转转组被同一个销售持有:当RotateGroup2持有的销售从Sales-B变成Sales-A后,希望将RotateGroup1和RotateGroup2合并,因为销售持有轮转组的个数是有上限的,这样合并有利于销售尽量多的持有门店
情况二:当前台发生合并的时候,希望后台对应的轮转组也合并
这些场景很频繁的出现在我们的系统中,我们在做客户轮转,包括后期的多BU按需初始化的时候,由于系统是多线程而且是多服务器的,就会导致有时候多个线程或者不同的服务器同时处理同一个ShopGroup下的轮转组的合并,这就会导致很多并发的问题。如何解决这种问题了?
二、分布式锁及重试机制
我们知道,如果是单服务器多线程的情况下,我们可以利用生产者-消费者的模型:
可以让一个生产者线程来按照一定的规则负责分发,或者让消费者线程按照一定的规则来取数据,但是对于多服务器来说,这种可能不适用,除非有个专门的服务器负责各个服务器上面的调度和分发。
这里我们提出一个比较轻量级的解决方案:利用数据库的特性,来做一个简单的分布式锁。
原理
数据库中创建一张表,定义一个唯一键Key字段,当某个消费者线程尝试处理某个资源的时候,会基于该资源生成一个Key,然后往数据库插入一条记录来竞争获取该资源的锁,当插入成功后表示此消费者线程获取了处理该资源的锁,此时其他线程插入同样Key的记录都会失败(数据库的唯一键约束),当该消费者线程处理结束后,清除插入的记录,然后其他的线程又可以自由竞争锁了。
流程图
DistributedLock表
此表中最重要的三个字段是:【唯一键:PRIMARY KEY (`Id`),UNIQUE KEY `UX_KEY_TYPE` (`LockKey`,`Type`)】
LockKey:你自己定义的锁,比如我们上面碰到的问题,就是ShopGroupId,我们期望同一时刻一个ShopGroupId只会被唯一的线程处理。
Type:分布式锁的类型,方便做横向扩展,如果你有其他的业务,但是也是以ShopGroupId为锁,就可以用此类型做区分。
ExpiredTime:过期时间。可能由于某些特殊情况,导致锁没有释放,防止造成死锁,所以需要给锁设置一个过期时间。
获取锁和释放锁的样例代码
获取锁和释放锁:
处理流程:
清除过期的锁
由于一些异常情况(比如:服务器重启)使得锁没有释放,这样会导致死锁,所以我们通过设置过期时间机制,来避免这种异常死锁。通过一个Job,定时的清除已经过期的锁,过期条件:
Math.floor(|Now-CreatedTime|)>ExpiredTime
重试机制
对于现实的应用场景,对于获取分布式锁失败的动作,我们要有机制支持重试。整个重试的原理就有一个重试队列表,然后有个Job定时去扫描重试队列,按照重试次数和一定的优先级来进行重试:
EventRetry表:
样例代码:
重试流程:
三、缺点及其他解决方案
缺点
上面介绍的是一种我们现实系统正在利用的轻量级的解决方案,利用数据库的某些特性,简单快捷的满足了系统的需求。但是上面的方案也有缺点:
由于需要不断的往数据库中插入数据,然后抛出异常,系统开销比较大,对于小并发的场景下满足需求,但是在大并发或者在分布式集群下可能会有问题。
跟数据库的某些特性耦合的太紧,首先是不同的数据库会有不同的特性,其次数据的连接等都是紧俏的资源,没有针对分布式锁这种场景做特殊定制和优化。
业界存在的方案
zookeeper
(1)实现原理:基于zookeeper瞬时有序节点实现的分布式锁,大致思想即为:每个客户端对某个功能加锁时,在 zookeeper上的与该功能对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
(2)优点:锁安全性高,zookeeper可持久化
(3)缺点:性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能
2. memcached分布式锁
(1)实现原理:memcached带有add函数,利用add函数的特性即可实现分布式锁。add和set的区别在于:如果多线程并发set,则每个set都会成功,但最后存储的值以最后的set的线程为准。而add的话则相反,add会添加第一个到达的值,并返回true,后续的添加则都会返回false。利用该点即可很轻松地实现分布式锁。
(2)优点:并发高效
(3)缺点:第一,memcached采用列入LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失。第二,memcached无法持久化,一旦重启,将导致信息丢失。
3. redis分布式锁
redis分布式锁兼具以上两者的优点,既有zookeeper分布式锁高度安全、又有memcached并发场景下效率很好的优点
四、总结
我们的方案比较轻便,实现简单,使用也比较简单,在我们的系统中也经历考验。后面的三种方案是业界普遍采用的方案,但是我们也看到,后面三种方案都比较重,大家可以根据自己的业务场景,选取最合适的方案来满足自己的需求,此文在此只是抛砖引玉。
以上是关于分布式锁及重试机制的主要内容,如果未能解决你的问题,请参考以下文章