一、分布式锁介绍
单机多线程: 在 Java 中,我们通常使用 ReetrantLock 类、synchronized 关键字这类 本地锁 来控制一个 JVM 进程内的多个线程对本地共享资源的访问
分布式系统: 不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁就诞生了。
举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。
一个最基本的分布式锁需要满足:
- 互斥 :任意一个时刻,锁只能被一个线程持有;
- 高可用 :锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。
- 可重入:一个节点获取了锁之后,还可以再次获取锁。
二、基于 Redis 实现分布式锁
1. 如何基于 Redis 实现一个最简易的分布式锁?
不论是本地锁还是分布式锁,核心都在于==“互斥”==。
在 Redis 中, SETNX
命令是可以帮助我们实现互斥。SETNX
即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。
> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0
释放锁的话,直接通过 DEL 命令删除对应的 key 即可
> DEL lockKey
(integer) 1
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
2.为什么要给锁设置一个过期时间?
主要为了避免锁无法被释放
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
- lockKey :加锁的锁名;
- uniqueValue :能够唯一标示锁的随机字符串;
- NX :只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
- EX :过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。
一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。
这种解决办法同样存在漏洞:
- 如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效
- 如果锁的超时时间设置过长,又会影响到性能
3. 如何实现锁的优雅续期?
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel 、Redis Cluster 等多种部署架构。
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
使用方式举例:
// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);
总的来说就是使用Redisson,它带有自动的续期机制
4. 如何实现可重入锁?
所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 synchronized 和 ReentrantLock 都属于可重入锁。
可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。
三、基于 Zookeeper 实现分布式锁
1. Zookeeper概念
Zookeeper是 Apache Hadoop项目下的一个子项目。
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难,所以这时可以使用zookeeper来帮助实现。
- 数据模型
数据模型如下图,zookeeper提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1
-
节点类型
永久节点:不会因为会话结束或者超时而消失;
临时节点:如果会话结束或者超时就会消失;
有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。
-
监听器
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息
2. Zookeeper主要功能
- 配置管理:多个服务器使用同一配置时,对配置修改需要一个一个修改,为了避免这种情况,可以将配置放置在配置中心中,服务器有需要可以进行拉取,zookeeper即可实现配置中心的功能,除此之外常见的配置中心还有阿里的nacos等
- 集群管理:如服务的注册中心等等,管理服务提供方的ip地址端口号url信息,并在服务消费方请求需要时发送给服务消费方,在微服务架构中常见,当然除了zookeeper也可以用nacos
- 分布式锁
3. Zookeeper作为分布式锁的实现
根据上面介绍的zookeeper的特性,可以方便地实现分布式锁。
实现方式:文章来源:https://www.toymoban.com/news/detail-626829.html
- 创建一个锁目录 /lock;
- 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推;
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;
- 否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
- 执行业务代码,完成后,删除对应的子节点。
会话超时的情况:如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了文章来源地址https://www.toymoban.com/news/detail-626829.html
到了这里,关于【分布式】分布式锁的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!