Apache ZooKeeper - 使用ZK实现分布式锁(非公平锁/公平锁/共享锁 )

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
MSE Nacos/ZooKeeper 企业版试用,1600元额度,限量50份
简介: Apache ZooKeeper - 使用ZK实现分布式锁(非公平锁/公平锁/共享锁 )


什么是分布式锁

什么是分布式锁,以及分布式锁在日常工作的使用场景。明确了这些,我们才能设计出一个安全稳定的分布式锁。

在日常开发中,我们最熟悉也常用的分布式锁场景是在开发多线程的时候。为了协调本地应用上多个线程对某一资源的访问,就要对该资源或数值变量进行加锁,以保证在多线程环境下系统能够正确地运行。在一台服务器上的程序内部,线程可以通过系统进行线程之间的通信,实现加锁等操作。而在分布式环境下,执行事务的线程存在于不同的网络服务器中,要想实现在分布式网络下的线程协同操作,就要用到分布式锁。


分布式死锁

在单机环境下,多线程之间会产生死锁问题。同样,在分布式系统环境下,也会产生分布式死锁的问题。

当死锁发生时,系统资源会一直被某一个线程占用,从而导致其他线程无法访问到该资源,最终使整个系统的业务处理或运行性能受到影响,严重的甚至可能导致服务器无法对外提供服务。

所以当我们在设计开发分布式系统的时候,要准备一些方案来面对可能会出现的死锁问题,当问题发生时,系统会根据我们预先设计的方案,避免死锁对整个系统的影响。常用的解决死锁问题的方法有超时方法和死锁检测。

超时方法

在解决死锁问题时,超时方法可能是最简单的处理方式了。超时方式是在创建分布式线程的时候,对每个线程都设置一个超时时间。当该线程的超时时间到期后,无论该线程是否执行完毕,都要关闭该线程并释放该线程所占用的系统资源。之后其他线程就可以访问该线程释放的资源,这样就不会造成分布式死锁问题。但是这种设置超时时间的方法也有很多缺点,最主要的就是很难设置一个合适的超时时间。如果时间设置过短,可能造成线程未执行完相关的处理逻辑,就因为超时时间到期就被迫关闭,最终导致程序执行出错。

死锁检测

死锁检测是处理死锁问题的另一种方法,它解决了超时方法的缺陷。与超时方法相比,死锁检测方法主动检测发现线程死锁,在控制死锁问题上更加灵活准确。你可以把死锁检测理解为一个运行在各个服务器系统上的线程或方法,该方法专门用来探索发现应用服务上的线程是否发生了死锁。如果发生死锁,就会触发相应的预设处理方案。


分类

在介绍完分布式锁的基本性质和潜在问题后,接下来我们就通过 ZooKeeper 来实现两种比较常用的分布式锁。

排他锁

排他锁也叫作独占锁,从名字上就可以看出它的实现原理。当我们给某一个数据对象设置了排他锁后,只有具有该锁的事务线程可以访问该条数据对象,直到该条事务主动释放锁。否则,在这期间其他事务不能对该数据对象进行任何操作。


共享锁

另一种分布式锁的类型是共享锁。它在性能上要优于排他锁,这是因为在共享锁的实现中,只对数据对象的写操作加锁,而不为对象的读操作进行加锁。这样既保证了数据对象的完整性,也兼顾了多事务情况下的读取操作。可以说,共享锁是写入排他,而读取操作则没有限制。


实现

接下来就通过 ZooKeeper 来实现一个排他锁。

创建锁

首先,我们通过在 ZooKeeper 服务器上创建数据节点的方式来创建一个共享锁。其实无论是共享锁还是排他锁,在锁的实现方式上都是一样的。唯一的区别在于,共享锁为一个数据事务创建两个数据节点,来区分是写入操作还是读取操作。

在 ZooKeeper 数据模型上的 Locks_shared 节点下创建临时顺序节点,临时顺序节点的名称中带有请求的操作类型分别是 R 读取操作、W 写入操作。

获取锁

当某一个事务在访问共享数据时,首先需要获取锁。ZooKeeper 中的所有客户端会在 Locks_shared 节点下创建一个临时顺序节点。根据对数据对象的操作类型创建不同的数据节点,如果是读操作,就创建名称中带有 R 标志的顺序节点,如果是写入操作就创建带有 W 标志的顺序节点。


释放锁

事务逻辑执行完毕后,需要对事物线程占有的共享锁进行释放。我们可以利用 ZooKeeper 中数据节点的性质来实现主动释放锁和被动释放锁两种方式。

主动释放锁是当客户端的逻辑执行完毕,主动调用 delete 函数删除ZooKeeper 服务上的数据节点。

而被动释放锁则利用临时节点的性质,在客户端因异常而退出时,ZooKeeper 服务端会直接删除该临时节点,即释放该共享锁。

这种实现方式正好和上面介绍的死锁的两种处理方式相对应。到目前为止,我们就利用 ZooKeeper 实现了一个比较完整的共享锁。

如下图所示,在这个实现逻辑中,首先通过创建数据临时数据节点的方式实现获取锁的操作。创建数据节点分为两种,分别是读操作的数据节点和写操作的数据节点。当锁节点删除时,注册了该 Watch 监控的其他客户端也会收到通知,重新发起创建临时节点尝试获取锁。当事务逻辑执行完成,客户端会主动删除该临时节点释放锁。


Demo

NG 负载, 后端两个节点

NG配置如下


Jmeter配置


方案零 缺陷版本

不加锁的情况下,我们来压一下

这是老板打来电话了。。。。。。


方案一 非公平锁方案



缺陷 (羊群效应)

上实现方式在并发问题比较严重的情况下,性能较低,主要原因是,所有的连接都在对同一个节点进行监听,当服务器检测到删除事件时,要通知所有的连接,所有的连接同时收到事件,再次并发竞争,这就是羊群效应。


如何避免呢,我们看下面这种方式


方案二 公平锁方案

上述方案是一个公平锁的实现,通过zk提供的临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力,避免羊群效应。

代码实现,Curator框架提供了InterProcessMutex

https://curator.apache.org/getting-started.html

@PostMapping("/stock/deductWithLock")
    public Object deductWithLock(Integer id) throws Exception {
        InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);
        try {
            interProcessMutex.acquire();
            orderService.reduceStock(id);
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw e;
            }
        }finally {
            interProcessMutex.release();
        }
        return "visit:" + port;
    }

方案三 共享锁方案

实现:InterProcessReadWriteLock

https://curator.apache.org/curator-recipes/shared-reentrant-read-write-lock.html

用法都很简单,主要是搞清楚实现原理

行了,到这儿吧先


相关文章
|
7月前
|
NoSQL 调度 Redis
分布式锁—3.Redisson的公平锁
Redisson公平锁(RedissonFairLock)是一种基于Redis实现的分布式锁,确保多个线程按申请顺序获取锁,从而实现公平性。其核心机制是通过队列和有序集合管理线程的排队顺序。加锁时,线程会进入队列并等待,锁释放后,队列中的第一个线程优先获取锁。RedissonFairLock支持可重入加锁,即同一线程多次加锁不会阻塞。新旧版本在排队机制上有所不同,新版本在5分钟后才会重排队列,而旧版本在5秒后就会重排。释放锁时,Redisson会移除队列中等待超时的线程,并通知下一个排队的线程获取锁。通过这种机制,RedissonFairLock确保了锁的公平性和顺序性。
|
3月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
226 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
4月前
|
NoSQL Redis
分布式锁设计吗,你是如何实现锁类型切换、锁策略切换基于限流的?
本方案基于自定义注解与AOP实现分布式锁,支持锁类型(如可重入锁、公平锁等)与加锁策略(如重试、抛异常等)的灵活切换,并结合Redisson实现可重入、自动续期等功能,通过LUA脚本保障原子性,兼顾扩展性与实用性。
96 0
|
5月前
|
缓存 NoSQL Java
【📕分布式锁通关指南 11】源码剖析redisson之读写锁的实现
Redisson 的 `RedissonReadWriteLock` 提供了高效的分布式读写锁实现,适用于读多写少的场景。通过 Redis 与 Lua 脚本结合,确保读锁并行、写锁互斥,以及读写之间的互斥,保障了分布式环境下的数据一致性。它支持可重入、自动过期和锁释放机制,提升了系统并发性能与资源控制能力。
125 0
|
7月前
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
382 17
|
9月前
|
安全
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
545 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
7月前
|
监控 NoSQL Java
分布式锁—2.Redisson的可重入锁
本文主要介绍了Redisson可重入锁RedissonLock概述、可重入锁源码之创建RedissonClient实例、可重入锁源码之lua脚本加锁逻辑、可重入锁源码之WatchDog维持加锁逻辑、可重入锁源码之可重入加锁逻辑、可重入锁源码之锁的互斥阻塞逻辑、可重入锁源码之释放锁逻辑、可重入锁源码之获取锁超时与锁超时自动释放逻辑、可重入锁源码总结。
|
9月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
319 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
8月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
250 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
|
9月前
|
NoSQL Java Redis
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
本文详细解析了Redisson可重入锁的加锁流程。首先从`RLock.lock()`方法入手,通过获取当前线程ID并调用`tryAcquire`尝试加锁。若加锁失败,则订阅锁释放通知并循环重试。核心逻辑由Lua脚本实现:检查锁是否存在,若不存在则创建并设置重入次数为1;若存在且为当前线程持有,则重入次数+1。否则返回锁的剩余过期时间。此过程展示了Redisson高效、可靠的分布式锁机制。
317 0
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁

热门文章

最新文章

推荐镜像

更多