别再纠结Redis/zookeeper,告诉你分布式锁的正确解决方案

网友投稿 749 2022-05-29

1 秒杀场景下的数据一致性问题

某商品库存10,A想买6,B想买5。

1.1 做梦

A先买走6,库存剩4,此时B应该无法购买5,给出数量不足提示

1.2 现实

AB获取到商品都剩10,A买走6,在A更新库存前,B又买走5,此时B更新库存,商品还剩5。

1.3 想当然地解决方案

给共享资源或对共享资源的操作加锁,来保证对资源的访问互斥。利用ReentrantLcok或者synchronized即可。

但是在分布式系统中,由于分布式系统的分布性,这两种锁将失去原有锁的效果。

必须使用分布式锁。

2 分布式锁的要求

获取/释放锁的性能好

获得锁必须是原子性的

网络抖动或者宕机等原因导致无法释放锁时,锁必须被清除,不然会发生死锁

可重入

阻塞锁和非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。

3 分布式锁实现方式

一、数据库锁

基于MySQL锁表

完全依靠数据库唯一索引来实现,当想要获得锁时,即向数据库中插入一条记录,释放锁时就删除这条记录

这种方式存在以下问题:

锁没有失效时间,解锁失败会导致死锁,其他线程无法再获取到锁,因为唯一索引insert都会返回失败

只能是非阻塞锁,insert失败直接就报错了,无法进入队列进行重试

不可重入,同一线程在没有释放锁之前无法再获取到锁

采用乐观锁

增加版本号,根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败

二、缓存锁

这里主要是几种基于redis的

基于setnx、expire

基于setnx(set if not exist)的特点,当缓存里key不存在时,才会去set,否则直接返回false

如果返回true则获取到锁,否则获取锁失败,为了防止死锁,我们再用expire命令对这个key设置一个超时时间来避免。

但是这里看似完美,实则有缺陷,当我们setnx成功后,线程发生异常中断,expire还没来的及设置,那么就会产生死锁。

解决上述问题有两种方案

采用redis2.6.12版本以后的set,它提供了一系列选项

EX seconds – 设置键key的过期时间,单位时秒

PX milliseconds – 设置键key的过期时间,单位时毫秒

NX – 只有键key不存在的时候才会设置key的值

XX – 只有键key存在的时候才会设置key的值

别再纠结Redis/zookeeper,告诉你分布式锁的正确解决方案

第二种采用setnx(),get(),getset()

(1) 线程Asetnx,值为超时的时间戳(t1),如果返回true,获得锁。

(2) 线程B用get 命令获取t1,与当前时间戳比较,判断是否超时,没超时false,如果已超时执行步骤3

(3) 计算新的超时时间t2,使用getset命令返回t3(这个值可能其他线程已经修改过),如果t1==t3,获得锁,如果t1!=t3说明锁被其他线程获取了

(4) 获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)

RedLock算法

zookeeper分布式锁

zookeeper是一个为分布式应用提供一致性服务的软件,它内部是一个分层的文件系统目录树结构,规定统一个目录下只能有一个唯一文件名

数据模型

永久节点

节点创建后,不会因为会话失效而消失

临时节点

与永久节点相反,如果客户端连接失效,则立即删除节点

顺序节点

与上述两个节点特性类似,如果指定创建这类节点时,zk会自动在节点名后加一个数字后缀,并且是有序的

##监视器(watcher):

当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次

根据zookeeper的这些特性来实现分布式锁

创建一个锁目录lock

希望获得锁的线程A就在lock目录下,创建临时顺序节点

获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁

线程B获取所有节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)

线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁。

小结

在分布式系统中,共享资源互斥访问问题非常普遍,而针对访问共享资源的互斥问题,常用的解决方案就是使用分布式锁,这里只介绍了几种常用的分布式锁,分布式锁的实现方式还有有很多种,根据业务选择合适的分布式锁

下面对上述几种锁进行一下比较:

数据库锁

优点:直接使用数据库,使用简单。

缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。

缓存锁

优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。

缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失效了锁的作用。

zookeeper锁

优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。

缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。

Redis ZooKeeper 分布式

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java并发编程(五)---线程通信
下一篇:华为云创新技术再加码,为政企智能升级全面加速
相关文章