美文网首页
分布锁的设计

分布锁的设计

作者: 土包佬 | 来源:发表于2020-01-19 21:52 被阅读0次

一、什么叫分布锁

在讲分布式锁之前,首先理解什么叫锁,为什么要对对象加锁?

在单虚拟机多线程的环境下,多个线程对同一个变量做操作,就有可能引起数据混乱。

就像是我们在食堂打饭,如果多个人同时在同一窗口打饭,就有可能把打菜的阿姨弄晕,阿姨有可能会不小心把别人要的菜打给另外一个人。

所以一次只能有一个人靠近窗口去打饭,为了达到这目的,就在这个窗口面前设了一个小门。一次只允许一个进里面,当有人进去的时候,就把这个小门锁上,其他人就进不去了。打完了饭出来时,再把这个门的锁打开,下一个人就可以进去。然后重复上一个人的动作,加锁、打饭、解锁。每个人就类似同虚拟机或同进程中的线程,窗口就是共享变量。

那为什么要有分布式锁呢?

分布式环境下,不在同一个虚拟机进程中,要保证每个虚拟机的进程的某些变量同步的,所以要加分布锁。

就类似上面打饭的例子,每个人都是独立虚拟机进程中的线程,窗口就是共享变量,分布式的环境下,这些线程之间是不知道的,就像打饭的都是瞎子,不知道对方的存在的。所以加锁方式会跟上面有些差别,要有协调者处理。

如下面的图,要保证变量A的值是同步的,怎么做,下面会详细讲到。


分布锁的设计

二、分布锁的应用场景

业务场景1:用户重复交易商品库存。

用户已下单,实际已经减库存成功,因某个环节,出现问题,返回给用户不可知状态,用户重新去刷原来的订单,重复减库存。

场景2、mq消息重复。
mq返回给客户端的消息状态为不可知,造成客户端重试,重复产生消息。

场景3、买家在支付时,卖家改商品单价,造成单价和总价没及时同步。

场景4、分布式主键,A虚拟机生成A001,B虚拟机也生了A001,主键值重复。

三、分布锁的实现方式

所以,要解这种分布的问题,整体思路:
1、找到共享资源,例如,用户、消息、订单、商品
2、解决方案,把共享资源串行化,达到互斥
3、问题转化,本地锁解决不了分布式问题,要用分布式锁。

具体实现思路
1、基于数据库实现分布式锁;

创建一个表,这个表中有主键id、方法名、时间戳等字段。然后把方法名设为唯一约束,当a虚拟机方法执行时,插入一条记录到这表中,把这个执行方法名也加到表中,作为方法名的字段的值,如果此时b虚拟机同样的方法想要执行,也会先插入到此表中,这时,因为表中方法名字有同样的方法值,又加了唯一约束,所以会报错,说明已经有方法在执行,如a虚拟机方法执行完毕,再到表中把此记录删除,说明其它的方法是可以执行了。

使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:
1.1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
1.2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
1.3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
1.4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

2、基于redis做分布式锁

利用redis单线程的特征,Redis Setnx命令,是「SET if Not eXists」的缩写,也就是只有不存在的时候会设置有效,当多个线程请求设置时,只有一个是成功的,其它的返回失败。具体实现:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

具体代码请问谷歌,一搜一大把。

分析存在问题:

A、主从模式哨兵机制,主从数据异步,会存在锁失效的问题,主服务器还未同步到从服务器,这时候主挂了,从接管,原来的锁就丢了,如果并发大,业务对数据准确性要求高,例如金额相关,要保障强一致性,建议不采纳

主从模式,实际不能保证数据的强一致,从CAP角度理解是AP模型,强一致性的分式模式,必须是CP模型,所以模型不匹配的。

B、锁时间不可以控制,无法续租期
Redis本身建议:使用RedLock算法来保证,但是问题是需要至少三个Redis主从实例来完成,维护成本很高。这个等同于自己简单实现的一致性协议,细节繁琐,且容易出错。

3、基于zookeeper分布锁

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

具体代码可以看这篇文章:
https://blog.csdn.net/qiangcuo6087/article/details/79067136

4、基于etc的分布式锁

简单KV(key Value),强一致性,高可用,无单点

分布锁的设计

拿锁的时候,选择key,ttl是超时时间,value可以忽略,uuid为该锁的唯一凭证,后面对锁的操作都是对uuid做操作。需要uuid才能做操作。etcd会保证只有一个线程能拿到锁。

详细用法及代码实现

https://www.jianshu.com/p/8a4dc6d900cf

总结

分布锁的设计

由于redis实现无法保证一致性,zookeeper对锁实现使用创建临时节点和watch机制,执行效率,扩展能力、社区活跃度等方面低于etcd,所以我们会选择基于etcd实现。

分布式锁的特殊场景
特殊场景一:
分布式锁只是在同一自然时间的互斥锁,本省不解决幂等性问题。
接入业务需要完善从获得锁到释放锁中间的数据幂等逻辑。

特殊场景二: 锁没有按照日期续约
心跳续约没有成功
马上启动GC,GCs时间太长

特殊场景三: etcd内部协调发生问题
Leader节点挂了,选主中,
raft日志数据同步发生错误或者不一致问题。

部分摘自:

https://blog.csdn.net/xlgen157387/article/details/79036337

https://blog.csdn.net/qiangcuo6087/article/details/79067136

https://www.jianshu.com/p/8a4dc6d900cf

相关文章

网友评论

      本文标题:分布锁的设计

      本文链接:https://www.haomeiwen.com/subject/mdjdzctx.html