zookeeper 分布式锁,java 开发技术教程
在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,就需要用到分布式锁。
分布式一致性问题
假设某商城有一个商品库存剩 10 个,用户 A 想要买 6 个,用户 B 想要买 5 个,在理想状态下,用户 A 先买走了 6 了,库存减少 6 个还剩 4 个,此时用户 B 应该无法购买 5 个,给出数量不足的提示;而在真实情况下,用户 A 和 B 同时获取到商品剩 10 个,A 买走 6 个,在 A 更新库存之前,B 又买走了 5 个,此时 B 更新库存,商品还剩 5 个,这就是典型的电商“秒杀”活动。
不做处理将会出现各种不可预知的后果。那么在这种高并发多线程的情况下,解决问题最有效最普遍的方法就是给共享资源或对共享资源的操作加一把锁,来保证对资源的访问互斥。
在 Java JDK 已经为我们提供了这样的锁,利用ReentrantLcok
或者synchronized
,即可达到资源互斥访问的目的。
但是在分布式系统中,由于分布式系统的分布性,即多线程和多进程并且分布在不同机器中,这两种锁将失去原有锁的效果,需要我们自己实现分布式锁——分布式锁。
分布式锁需要具备哪些条件
获取/释放锁的性能要好
判断是否获得锁必须是原子性的操作,否则可能导致多个请求都获取到锁
网络中断或宕机无法释放锁时,锁必须被清除,不然会发生死锁
可重入一个线程中多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;
阻塞锁和非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。
分布式锁实现方式
一、数据库锁
基于 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 的值
第二种采用 setnx(),get(),getset()
(1) 线程 Asetnx,值为超时的时间戳(t1),如果返回 true,获得锁。
(2) 线程 B 用 get 命令获取 t1,与当前时间戳比较,判断是否超时,没超时 false,如果已超时执行步骤 3
(3) 计算新的超时时间 t2,使用 getset 命令返回 t3(这个值可能其他线程已经修改过),如果 t1==t3,获得锁,如果 t1!=t3 说明锁被其他线程获取了
(4) 获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)
RedLock 算法
redlock 算法是 redis 作者推荐的一种分布式锁实现方式
(1) 获取当前时间;
(2) 尝试从 5 个相互独立 redis 客户端获取锁
(3) 计算获取所有锁消耗的时间,当且仅当客户端从多数节点获取锁,并且获取锁的时间小于锁的有效时间,认为获得锁
(4) 重新计算有效期时间,原有效时间减去获取锁消耗的时间
(5) 删除所有实例的锁
redlock 算法相对于单节点 redis 锁可靠性要更高,但是实现起来条件也较为苛刻
(1) 必须部署 5 个节点才能让 Redlock 的可靠性更强
(2) 需要请求 5 个节点才能获取到锁,通过 Future 的方式,先并发向 5 个节点请求,再一起获得响应结果,能缩短响应时间,不过还是比单节点 redis 锁要耗费更多时间
然后由于必须获取到 5 个节点中的 3 个以上,所以可能出现获取锁冲突,即大家都获得了 1-2 把锁,结果谁也不能获取到锁,这个问题,redis 作者借鉴了 raft 算法的精髓,通过冲突后在随机时间开始,可以大大降低冲突时间,但是这问题并不能很好的避免,特别是在第一次获取锁的时候,所以获取锁的时间成本增加了
如果 5 个节点有 2 个宕机,此时锁的可用性会极大降低,首先必须等待这两个宕机节点的结果超时才能返回,另外只有 3 个节点,客户端必须获取到这全部 3 个节点的锁才能拥有锁,难度也加大了
如果出现网络分区,那么可能出现客户端永远也无法获取锁的情况
介于这种情况,下面我们来看一种更可靠的分布式锁 zookeeper 锁
zookeeper 分布式锁
=============
![](https://upload-images.jianshu.io/upload_images/4685968-e9377631da7741
51.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/2886/format/webp)
zookeeper 是一个为分布式应用提供一致性服务的软件,它内部是一个分层的文件系统目录树结构,规定统一个目录下只能有一个唯一文件名
数据模型
永久节点
节点创建后,不会因为会话失效而消失
临时节点
与永久节点相反,如果客户端连接失效,则立即删除节点
顺序节点
与上述两个节点特性类似,如果指定创建这类节点时,zk 会自动在节点名后加一个数字后缀,并且是有序的
监视器(watcher):
当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch 被触发时,ZooKeeper 将会向客户端发送且仅发送一条通知,因为 watch 只能被触发一次
根据 zookeeper 的这些特性来实现分布式锁
创建一个锁目录 lock
希望获得锁的线程 A 就在 lock 目录下,创建临时顺序节点
获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁
线程 B 获取所有节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)
线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是最小的节点,获得锁。
评论