当前位置: 首页 > article >正文

【分布式理论10】分布式互斥算法最佳实现:分布式锁的原理与实现

文章目录

    • 一、分布式锁的由来和定义
    • 二、用数据库实现分布式锁(不推荐)
    • 三、 通过 Redis 缓存实现分布式锁
    • 四、 通过 ZooKeeper 实现分布式锁
    • 五、 分布式分段加锁:提高并发能力
    • 六、结论

一、分布式锁的由来和定义

在分布式系统中,多个进程可能会同时访问同一个临界资源,导致数据竞争问题。例如,在秒杀活动中,多个用户同时下单会导致库存扣减的并发冲突。为了解决这一问题,需要引入分布式锁,确保同一时刻只有一个进程能够访问共享资源,从而避免数据不一致问题。

分布式锁的实现方式有多种,包括基于数据库、Redis、ZooKeeper 等方案。其中,数据库实现方式虽然简单,但性能较低,适用于低并发场景。因此,在高并发的应用场景中,通常使用 Redis 和 ZooKeeper 实现分布式锁。

在这里插入图片描述

 

二、用数据库实现分布式锁(不推荐)

用数据库实现分布式锁比较简单,就是创建一张锁表。要锁住临界资源并对其访问时,在锁表中增加一条记录即可;删除某条记录就可释放相应的临界资源。数据库对临界资源做了唯一性约束,如果有访问临界资源的请求同时提交到数据库,数据库会保证只有一个请求能够得到锁,然后只有得到锁的这个请求才可以访问临界资源。

由于此类操作属于数据库 IO 操作,效率不高,而且频繁操作会增大数据库的开销,因此这种方式在高并发、对性能要求较高的场景中使用得并不多。

 

三、 通过 Redis 缓存实现分布式锁

前面提到库存作为临界资源会遭遇高并发的请求访问,为了提高效率,可以将库存信息放到缓存中。以流行的 Redis 为例,用其存放库存信息,当多个进程同时请求访问库存时会出现资源争夺现象,也就是分布式程序争夺唯一资源。为了解决这个问题,需要实现分布式锁。
一个进程持有锁后,就可以访问 Redis 中的库存资源,且在其访问期间其他进程是不能访问的。

如下图:假设有多个扣减服务用于响应用户的下单请求,这些服务接收到请求后会去访问Redis 缓存中存放的库存信息,每接收一次用户请求,就将 Redis 中存放的库存量减去 1。

在这里插入图片描述

 
超时时间的考虑

如果该进程长期没有释放锁,就会造成其他进程饥饿,因此需要考虑锁的过期时间,设置超时时间。

Redis 通过 SETNX(set if not exists)命令和 EXPIRE(设置过期时间)命令可以实现互斥访问。这里的超时时间需要考虑两方面问题。

  • 资源本身的超时时间,一旦资源被使用一段时间后还没有被释放,Redis 就会自动释放该资源,给其他服务使用。
  • 服务访问资源的超时时间,如果一个服务访问资源超过一段时间,那么不管这个服务是否处理完,都要马上释放资源给其他服务使用。

下单服务中的扣减操作属于核心操作,因此会用到第二种方式。如果到达规定时间,下单服务还没有处理完库存资源,就重新申请资源并且延长持有锁的时间

 

代码示例(基于 Redisson 实现 Redis 分布式锁)

这里以 Redisson 为例介绍如何实现分布式锁​。

public void testLock() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://192.168.1.1:6379");
    RedissonClient redisson = Redisson.create(config);
    RLock lock = redisson.getLock("lockName");
    try {
        boolean result = lock.tryLock(5, 10, TimeUnit.SECONDS);
        if (result) {
            // 业务逻辑,如扣减库存
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

[!步骤逻辑]

  1. 进程尝试获取锁:使用 SETNX 命令在 Redis 中存储一个唯一的 Key。
  2. 如果获取成功,则设置 EXPIRE 防止锁未被正常释放。
  3. 如果获取失败,说明其他进程已持有锁,需要等待锁释放后重试。
  4. 持有锁的进程完成任务后,使用 DEL 释放锁。

接着我们说明redisson内部实现加锁流程

  • 进程在申请锁时,会先判断“需要锁定的临界资源的 Key 是否存在?​”​。
  • 如果 Key 不存在,就走右边的流程,获取这个 Key 也就是临界资源,并且保存请求 ID,因为会有很多进程来请求这个 Key,为了区别这些进程需要保存访问进程的请求 ID。同时保存 Key 的过期时间,后面就是执行扣减库存业务了。
  • 如果存在 Key,说明已经有进程获取了这个 Key 的使用权,也就是有进程正在扣减库存了,那么走左边的流程。判断请求 ID 是否存在,即这次的请求 ID 和已经保存的请求 ID 是否一样。
  • 如果一样,说明是同一个进程(客户端)再次请求扣减库存,这种情况会被 Redisson 认作请求重入,也就是同一个请求再次获取同一临界资源,这时 Redisson
    会将其请求重入+1,并且重新设置过期时间,也就是延长这个请求的处理时间。
  • 如果不一样,说明是另外一个进程(客户端)发来的请求,由于之前的进程还没有释放临界资源,因此只返回 Key 的生存时间,告诉这个进程前面的进程还有多久才能释放临界资源,也就是还需要等待多久。

在这里插入图片描述

 

四、 通过 ZooKeeper 实现分布式锁

进程在Zookeeper建立顺序DataNode,即建立进程访问临界资源的顺序,来起到锁的作用。

使用 Redis 缓存实现分布式锁,使同时访问临界资源的进程由并行执行变为串行执行。按照同样的思路,ZooKeeper 中的 DataNode 也可以保证两个进程的访问顺序是串行的,两个库存扣减进程会在 ZooKeeper 上建立顺序的 DataNode,DataNode 的顺序就是进程访问临界资源的顺序,这样避免了多个进程同时访问临界资源,起到了锁的作用。

 

ZooKeeper 实现分布式锁的原理
在 ZooKeeper 中建立一个 Locker 的 DataNode 节点,在此节点下面建立子 DataNode 来保证先后顺序。即便是两个进程同时申请新建节点,也会按照先后顺序建立两个节点。
![[Pasted image 20250211131516.png]]

  1. 库存服务 A 申请锁:在 ZooKeeper 的 Locker 节点下创建 DataNode1,表示可访问库存。
  2. 库存服务 B 申请锁:由于排在 A 后面,因此在 DataNode1 之后创建 DataNode2,按顺序排列。
  3. 库存服务 A 访问库存:A 获取锁后访问库存并完成扣减,期间库存服务 B 需等待。
  4. 库存服务 A 释放锁:A 释放锁并删除 DataNode1。
  5. 库存服务 B 获取锁:DataNode1 删除后,DataNode2 变为序号最前的节点,B 获取锁并进行库存扣减。

总结下来就是;

进程A在 /Locker 下创建顺序子节点,如果序列最小,则获取锁,进程B监听此进程,等待释放。执行完任务后,删除节点,释放锁,进程B获取锁并执行任务。

 

代码示例(基于 Curator 框架)

以Curator 框架作为代码级别的实施基础。
Curator 是 Netflix 公司的一个开源的 ZooKeeper 客户端框架。它对分布式锁做了封装,提供了现成的 API 供我们使用。除了分布式锁之外,它还提供了 leader 选举、分布式队列等常用功能。

String connectString = "192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181";
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 4);
CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retryPolicy);
client.start();
final InterProcessLock lock = new InterProcessMutex(client, "/Locker");
new Thread(() -> {
    try {
        lock.acquire();
        // 业务逻辑
        lock.release();
    } catch (Exception e) {
        e.printStackTrace();
    }
}).start();

 

五、 分布式分段加锁:提高并发能力

通过 Redis 缓存和 ZooKeeper 实现分布式锁依据的都是把并行执行转换成串行执行的思路。为了提高并发能力,可以将临界资源划分成多个分段,每个进程锁定其中一个分段并执行操作。例如,在秒杀系统中,可以将库存拆分为多个小段,每个进程仅锁定一个库存段,避免单一锁造成的性能瓶颈。

原理

  • 将库存数据拆分为多个小段,每段维护独立的库存值。
  • 进程随机或按序访问库存段,若库存段被锁定,则尝试其他库存段。
  • 通过 Redis 或 ZooKeeper 实现分段锁,减少单点竞争,提高并发能力。

示例

假设有 500 个库存,将其划分为 50 段,每段 10 个库存。

  1. 进程 1 获取库存段 1,锁定并扣减库存。
  2. 进程 2 获取库存段 2,锁定并扣减库存。
  3. 进程 3 发现库存段 1 被锁定,则尝试访问库存段 3。

这种方式可以有效提升系统的吞吐量,减少锁竞争,提高系统可用性。

 

六、结论

分布式锁在高并发应用中至关重要,其实现方式多种多样。Redis 适用于短时间持有锁的场景,ZooKeeper 适用于更严格的顺序控制。同时,分段加锁可以进一步优化并发性能,提高系统响应能力。在实际应用中,需要根据业务需求选择合适的分布式锁实现方案。

 


http://www.kler.cn/a/544388.html

相关文章:

  • Flink之Watermark
  • 讲解下SpringBoot中MySql和MongoDB的配合使用
  • windows系统远程桌面连接ubuntu18.04
  • 笔记4——列表list
  • Tomcat添加到Windows系统服务中,服务名称带空格
  • 对比 LVS 负载均衡群集的 NAT 模式和 DR 模式,其各自的优势
  • 【GitHub】装修个人主页
  • Golang常见面试题
  • hadoop之MapReduce:片和块
  • 分发饼干(力扣455)
  • Spring Cloud Gateway:构建高效微服务网关的利器
  • 3.Excel:销售主管大华-前两季度-销售情况❗(16)
  • 排序函数集合:冒泡排序、选择排序、插入排序、快速排序、归并排序、桶排序
  • 如何使用 CSS 隐藏元素
  • 【MySQL例题】我在广州学Mysql 系列——有关数据备份与还原的示例
  • excel 日期转换
  • 比亚迪“璇玑架构”全面接入DeepSeek
  • 《只狼》运行时提示“mfc140u.dll文件缺失”是什么原因?要怎么解决?
  • git客户端版本下载
  • 01docker run
  • 【ROS2综合案例】乌龟跟随
  • This dependency was not found: * @logicflow/core/dist/LogicFlow.css
  • 解决 idea 无法创建java8 模版
  • 详解 JavaScript 中 fetch 方法
  • 【CXX-Qt】0 Rust与Qt集成实践指南(CXX-Qt)
  • 关闭浏览器安全dns解决访问速度慢的问题