谈谈几种分布式锁实现

2023-05-16

大家好,我是易安!今天我们呢谈一谈常见的分布式锁的几种实现方式。

什么是分布式锁

在JVM中,在多线程并发的情况下,我们可以使用同步锁或Lock锁,保证在同一时间内,只能有一个线程修改共享变量或执行代码块。但现在我们的服务基本都是基于分布式集群来实现部署的,对于一些共享资源,在分布式环境下使用Java锁的方式就失去作用了。

这时,我们就需要实现分布式锁来保证共享资源的原子性。除此之外,分布式锁也经常用来避免分布式中的不同节点执行重复性的工作,例如一个定时发短信的任务,在分布式集群中,我们只需要保证一个服务节点发送短信即可,一定要避免多个节点重复发送短信给同一个用户。

因为数据库实现一个分布式锁比较简单易懂,直接基于数据库实现就行了,不需要再引入第三方中间件,所以这是很多分布式业务实现分布式锁的首选。但是数据库实现的分布式锁在一定程度上,存在性能瓶颈。

接下来我们一起了解下数据库实现的分布式锁,其性能瓶颈到底在哪,以及其它实现方式来优化分布式锁。我们以电商系统的订单库来讲解今天的内容。

数据库实现分布式锁

我们创建了一个锁表,通过创建和查询数据来保证一个数据的原子性:

CREATE TABLE `order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` int(11) DEFAULT NULL,
  `pay_money` decimal(10, 2) DEFAULT NULL,
  `status` int(4) DEFAULT NULL,
  `create_date` datetime(0) DEFAULT NULL,
  `delete_flag` int(4) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_status`(`status`) USING BTREE,
  INDEX `idx_order`(`order_no`) USING BTREE
) ENGINE = InnoDB

如果是校验订单的幂等性,就要先查询该记录是否存在数据库中,查询的时候要防止幻读,如果不存在,就插入到数据库,否则,放弃操作。

select id from `order` where `order_no`= 'xxxx' for update

最后注意下,除了查询时防止幻读,我们还需要保证查询和插入是在同一个事务中,因此我们需要申明事务,具体的实现代码如下:

 @Transactional
 public int addOrderRecord(Order order) {
  if(orderDao.selectOrderRecord(order)==null){
               int result = orderDao.addOrderRecord(order);
              if(result>0){
                      return 1;
              }
         }
  return 0;
 }

到这,我们订单幂等性校验的分布式锁就实现了。我想你应该能发现为什么这种方式会存在性能瓶颈了。在RR事务级别,select的for update操作是基于间隙锁gap lock实现的,这是一种悲观锁的实现方式,所以存在阻塞问题。

因此在高并发情况下,当有大量的请求进来时,大部分的请求都会进行排队等待。为了保证数据库的稳定性,事务的超时时间往往又设置得很小,所以就会出现大量事务被中断的情况。

除了阻塞等待之外,因为订单没有删除操作,所以这张锁表的数据将会逐渐累积,我们需要设置另外一个线程,隔一段时间就去删除该表中的过期订单,这就增加了业务的复杂度。

除了这种幂等性校验的分布式锁,有一些单纯基于数据库实现的分布式锁代码块或对象,是需要在锁释放时,删除或修改数据的。如果在获取锁之后,锁一直没有获得释放,即数据没有被删除或修改,这将会引发死锁问题。

Zookeeper实现分布式锁

除了数据库实现分布式锁的方式以外,我们还可以基于Zookeeper实现。Zookeeper是一种提供“分布式服务协调“的中心化服务,正是Zookeeper的以下两个特性,分布式应用程序才可以基于它实现分布式锁功能。

顺序临时节点: Zookeeper提供一个多层级的节点命名空间(节点称为Znode),每个节点都用一个以斜杠(/)分隔的路径来表示,而且每个节点都有父节点(根节点除外),非常类似于文件系统。

节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),每个节点还能被标记为有序性(SEQUENTIAL),一旦节点被标记为有序性,那么整个节点就具有顺序自增的特点。一般我们可以组合这几类节点来创建我们所需要的节点,例如,创建一个持久节点作为父节点,在父节点下面创建临时节点,并标记该临时节点为有序性。

Watch机制: Zookeeper还提供了另外一个重要的特性,Watcher(事件监听器)。ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知给用户。

我们熟悉了Zookeeper的这两个特性之后,就可以看看Zookeeper是如何实现分布式锁的了。

首先,我们需要建立一个父节点,节点类型为持久节点(PERSISTENT) ,每当需要访问共享资源时,就会在父节点下建立相应的顺序子节点,节点类型为临时节点(EPHEMERAL),且标记为有序性(SEQUENTIAL),并且以临时节点名称+父节点名称+顺序号组成特定的名字。

在建立子节点后,对父节点下面的所有以临时节点名称name开头的子节点进行排序,判断刚刚建立的子节点顺序号是否是最小的节点,如果是最小节点,则获得锁。

如果不是最小节点,则阻塞等待锁,并且获得该节点的上一顺序节点,为其注册监听事件,等待节点对应的操作获得锁。

当调用完共享资源后,删除该节点,关闭zk,进而可以触发监听事件,释放该锁。

alt

以上实现的分布式锁是严格按照顺序访问的并发锁。一般我们还可以直接引用Curator框架来实现Zookeeper分布式锁,代码如下:

InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) )
{
    try
    {
        // do some work inside of the critical section here
    }
    finally
    {
        lock.release();
    }
}

Zookeeper实现的分布式锁,例如相对数据库实现,有很多优点。Zookeeper是集群实现,可以避免单点问题,且能保证每次操作都可以有效地释放锁,这是因为一旦应用服务挂掉了,临时节点会因为session连接断开而自动删除掉。

由于频繁地创建和删除结点,加上大量的Watch事件,对Zookeeper集群来说,压力非常大。且从性能上来说,与Redis实现的分布式锁相比,还是存在一定的差距。

Redis实现分布式锁

相对于前两种实现方式,基于Redis实现的分布式锁是最为复杂的,但性能是最佳的。

大部分开发人员利用Redis实现分布式锁的方式,都是使用SETNX+EXPIRE组合来实现,在Redis 2.6.12版本之前,具体实现代码如下:

public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);//设置锁
    if (result == 1) {//获取锁成功
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);//通过过期时间删除锁
        return true;
    }
    return false;
}

这种方式实现的分布式锁,是通过setnx()方法设置锁,如果lockKey存在,则返回失败,否则返回成功。设置成功之后,为了能在完成同步代码之后成功释放锁,方法中还需要使用expire()方法给lockKey值设置一个过期时间,确认key值删除,避免出现锁无法释放,导致下一个线程无法获取到锁,即死锁问题。

如果程序在设置过期时间之前、设置锁之后出现崩溃,此时如果lockKey没有设置过期时间,将会出现死锁问题。

在 Redis 2.6.12版本后SETNX增加了过期时间参数:

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

我们也可以通过Lua脚本来实现锁的设置和过期时间的原子性,再通过jedis.eval()方法运行该脚本:

    // 加锁脚本
    private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
    // 解锁脚本
    private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

虽然SETNX方法保证了设置锁和过期时间的原子性,但如果我们设置的过期时间比较短,而执行业务时间比较长,就会存在锁代码块失效的问题。我们需要将过期时间设置得足够长,来保证以上问题不会出现。

这个方案是目前最优的分布式锁方案,但如果是在Redis集群环境下,依然存在问题。由于Redis集群数据同步到各个节点时是异步的,如果在Master节点获取到锁后,在没有同步到其它节点时,Master节点崩溃了,此时新的Master节点依然可以获取锁,所以多个应用服务可以同时获取到锁。

Redlock算法

Redisson由Redis官方推出,它是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson是基于netty通信框架实现的,所以支持非阻塞通信,性能相对于我们熟悉的Jedis会好一些。

Redisson中实现了Redis分布式锁,且支持单点模式和集群模式。在集群模式下,Redisson使用了Redlock算法,避免在Master节点崩溃切换到另外一个Master时,多个应用同时获得锁。我们可以通过一个应用服务获取分布式锁的流程,了解下Redlock算法的实现:

在不同的节点上使用单个实例获取锁的方式去获得锁,且每次获取锁都有超时时间,如果请求超时,则认为该节点不可用。当应用服务成功获取锁的Redis节点超过半数(N/2+1,N为节点数)时,并且获取锁消耗的实际时间不超过锁的过期时间,则获取锁成功。

一旦获取锁成功,就会重新计算释放锁的时间,该时间是由原来释放锁的时间减去获取锁所消耗的时间;而如果获取锁失败,客户端依然会释放获取锁成功的节点。

具体的代码实现如下:

1.首先引入jar包:

<dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.8.2</version>
</dependency>

2.实现Redisson的配置文件:

@Bean
public RedissonClient redissonClient() {
    Config config = new Config();
    config.useClusterServers()
            .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
            .addNodeAddress("redis://127.0.0.1:7000).setPassword("1")
            .addNodeAddress("
redis://127.0.0.1:7001").setPassword("1")
            .addNodeAddress("
redis://127.0.0.1:7002")
            .setPassword("
1");
    return Redisson.create(config);
}

3.获取锁操作:

long waitTimeout = 10;
long leaseTime = 1;
RLock lock1 = redissonClient1.getLock("lock1");
RLock lock2 = redissonClient2.getLock("lock2");
RLock lock3 = redissonClient3.getLock("lock3");

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功,且设置总超时时间以及单个节点超时时间
redLock.trylock(waitTimeout,leaseTime,TimeUnit.SECONDS);
...
redLock.unlock();

总结

实现分布式锁的方式有很多,有最简单的数据库实现,还有Zookeeper多节点实现和redis缓存实现。我们可以分别对这三种实现方式进行性能压测,可以发现在同样的服务器配置下,Redis的性能是最好的,Zookeeper次之,数据库最差。

从实现方式和可靠性来说,Zookeeper的实现方式简单,且基于分布式集群,可以避免单点问题,具有比较高的可靠性。因此,在对业务性能要求不是特别高的场景中,我建议使用Zookeeper实现的分布式锁。

本文由 mdnice 多平台发布

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

谈谈几种分布式锁实现 的相关文章

  • go 的时间操作

    未完 前言 本篇所有输入 xff0c 均用 p 代替 span class token comment 用 p 简写 span p span class token operator 61 span fmt span class token
  • Vm挂载虚拟硬盘(傻瓜式教程)

    Vm挂载虚拟硬盘 xff08 傻瓜式教程 xff09 第一步 xff1a 添加虚拟磁盘 打开vm xff0c 单机选择红帽的系统 编辑虚拟机设置 点击下面的添加 选择硬盘然后下一步 如果没有特殊的磁盘格式要求就默认推荐就好了 使用物理硬盘
  • 【学习笔记】在 Github Page 上托管基于 Vue 的项目

    环境 vscode 前言 本篇博文仅仅提供如何将 vue 项目部署在 github page 的基本操作 xff0c 至于项目的内容由读者自己决定 同时这是最基本的操作 xff0c 所以在复杂的项目部署中 xff0c 请根据具体情况 xff
  • 【学习笔记】查看你正在使用的 C++ 标准

    目录 查看 C 43 43 标准查看你的 gcc xff0c g 43 43 版本C 43 43 标准与 gcc 编译器的版本的对应关系C 43 43 标准与 Visual studio 的对应关系Visual studio 版本与 C 4
  • 【学习笔记】C++ 中的 virtual 关键字与虚函数

    目录 先决条件前言演示示例virtual 关键字的作用虚函数的规则参考与拓展深入拓展 先决条件 了解 C 43 43 中的多态这个概念 前言 virtual 关键字是面对对象中 xff0c 用于修饰类中的成员函数的关键字 被 virtual
  • 【经典回顾】HTTP 的请求与响应报文

    文章目录 前言请求报文请求行方法URL版本 首部行实体主体 响应报文状态行首部行实体主体 参考与拓展 前言 基于 HTTP1 1 xff0c 也就是目前最常用的 HTTP 协议版本 xff0c 涉及部分 HTTP 2 请求报文 让我们先来看
  • 【学习笔记】go 生成随机数

    目录 核心代码示例参考与拓展 核心 go 的标准库 xff08 math rand xff09 中已经为我们提供了产生伪随机数字的核心方法 xff0c 它们分别是用于产生种子的 rand Send value 和用于产生伪随机数的 rand
  • 【教程】油猴脚本开发入门教程

    目录 先决条件配置本地开发环境 可选 快速插入复杂的 HTML设置 CSS 样式发布与更新你的脚本常见标签简析 96 64 connect 96 96 64 grant 96 96 64 include 96 96 64 require 9
  • 【学习笔记】memcpy_s 函数与坑

    目录 函数原型函数描述参数描述返回值坑参考拓展 函数原型 errno t span class token function memcpy s span span class token punctuation span span clas
  • 【学习笔记】指向常量的指针和常量指针

    目录 指向常量的指针常量指针对比指向常量的指针与常量指针拓展参考 指向常量的指针 指向常量的指针 xff0c 即 pointer to const xff0c 即指针指向的是一个常量 xff0c 你应该把这个词 xff08 指向常量的指针
  • 【学习笔记】内存的连续分配管理方式

    目录 先决条件单一连续分配固定分区分配动态分区分配补充内部碎片和外部碎片基于顺序搜索的动态分区策略 xff08 算法 xff09 参考与扩展 先决条件 这里介绍的这些内存分配方式都是非常古老的内存分配方式 xff0c 基本已经不在现代操作系
  • 【教程】visual studio debug 技巧总结

    更新中 基础的调试技巧基本更新完毕 xff0c visual studio 提供了强大的调试功能 xff0c 许多东西需要大家动手体验 目录 环境调试器的基本使用更改执行流断点基本的断点操作跟踪点条件断点条件表达式命中次数过滤器 函数断点缩
  • Vm虚拟机创建raid5盘+热备盘

    Vm虚拟机创建raid5盘 43 热备盘 打开vm xff0c 然后创建四个新的虚拟硬盘 xff08 组建raid5盘最少需要3个硬盘 xff0c 我们留一个做热备盘 xff09 创建硬盘的步骤我在前面的博客有写 xff0c 这里就不掩饰了
  • 用VS2012导入工程时出现error MSB8020错误

    导入别人工程后进行编译出现如下错误 xff1a 解决方法 xff1a 在工程名后右击 属性 xff0c 将平台工具集改为自己安装版本的平台工具集如下图所示 xff1a 然后再次进行编辑就可以啦
  • 【学习笔记】windows 下的 shared memory(共享内存)原理与实践

    目录 先决条件共享内存介绍在 Win 下实现共享内存开发环境P1CreateFileMappingMapViewOfFileUnmapViewOfFileCloseHandle P2OpenFileMapping 示例补充File Mapp
  • 【学习笔记】同一个 solution 的不同 project 使用相同的头文件

    目录 环境前言在项目中引入文件添加额外的包含的目录 环境 OS xff1a win10IDE xff1a visual studio 2017 前言 有时候在开发中 xff0c 同一个 solution 下的不同 project 需要共享一
  • 【学习笔记】在 windows 下创建多线程 C++

    目录 先决条件传统的创建方式使用 CreateThread 函数实例 更安全的方式 beginthreadex实例 终止线程补充WaitForMultipleObjects 函数实例 参考 先决条件 最好了解以下内容 了解内核对象了解进程
  • 【教程】在 visual studio 共享和重用项目属性

    目录 环境前言同一项目中 xff0c 不同开发模式和平台的共享不同项目共享和重用项目属性进阶 参考 环境 os xff1a windows 10IDE xff1a visual studio 2015 前言 在 visual studio
  • 【学习笔记】C 语言中未开辟地址的指针作为函数参数传递的问题

    目录 问题描述有问题的做法正确的做法总结 问题描述 有时候我们希望传递一个空指针给一个函数 xff0c 然后该函数在堆上开辟动态内存 xff0c 然后该函数执行完后 xff0c 返回这个动态内存的地址 有问题的做法 先来看下面的一段程序 x

随机推荐

  • 【学习笔记】顺序容器的表格方式总结 C++

    目录 顺序容器及其特点顺序容器操作向顺序容器添加元素insertemplace 参考 更新中 顺序容器及其特点 名字访问元素插入 xff0c 删除元素vector xff08 可变大小数组 xff09 支持快速随机访问在尾部之外的位置插入或
  • 【学习笔记】C++ 下字符串与数字的拼接

    目录 环境sprintfto string 与 to wstring itoa 环境 OS xff1a win 10 IDE xff1a Visual Studio 2017 sprintf 描述 xff1a sprintf 是一种 C 风
  • 【教程】Windows 下 C++ 项目内存泄漏检查

    更新中 目录 环境Visual Stuido Profiling Tools打开方式使用查看原始类型报告查看 Managed 类型报告 参考与拓展 环境 windows 10IDE xff1a Visual Studio 2015 Visu
  • 【学习笔记】读取文件中的字符串与 fgets 的坑

    目录 前言环境问题模拟与复现正确的手法回顾 前言 今天写一个读取文件中字符串的函数 xff0c 理论上应该是很简单的 xff0c 但是写的时候发现输出的结果总是比文件中的内容少一个字符 xff0c 并且通过排查 xff0c 问题就是出在 f
  • [Atcoder Yahoo Contest 2019]D.Ears(动态规划)

    Score 600 600 6 0 0 points 题面 传送门 翻译有时间再补 题解 体验感极差 xff0c 考试的时候手残把1打成了2Debug了半个小时 害的F题都没做 先将题目转换一下 给你一条链 顺次连接着 n 43
  • 内网渗透-基础环境

    解决依赖 xff0c scope安装 打开要给cmd powershell 打开远程 Set ExecutionPolicy RemoteSigned scope CurrentUser 我试了好多装这东西还是得科学上网 xff0c 不然不
  • ubuntu(Linux)配置允许远程登陆

    安装ubuntu后默认不可以以root方式登录系统 xff0c 需要做以下配置 1 使用sudo i 命令可以让用户切换到root用户 xff0c guo用户是安装ubuntu时配置的用户 xff0c 因人而异 xff1b 2 配置root
  • Y9000P Ubuntu/Windows 双系统安装

    一 xff1a 配置介绍 Y9000P默认系统Win11 xff0c 系统盘500G xff0c 从盘2T xff0c 内存32G xff0c 显卡3060 二 xff1a Windows系统分盘 系统盘 xff08 磁盘1 xff09 建
  • axios的使用

    axios是基于Promise的HTTP库 xff0c 适用于各种前端框架 不同于普通http请求后的回调 xff0c Promise有更好的操作性 axios可以自动转换JSON数据 客户端支持防御XSRF攻击 axios的简单使用 安装
  • 怎么通过SQL取出数据库中JSON字段中的值

    我们的数据库中经常会遇到很多JSON的字段 xff0c 自己写的也好 xff0c 别人写的也好 一般我们取这个值的话 xff0c 会创建一个typeHandler来取值 那么如果我们想直接取到JSON里的值该怎么办呢 xff1f 其实很简单
  • GCC使用说明

    超详细的参考官方手册下载地址 https download csdn net download qq 34991787 16188604 GCC代表 GNU编译器合集 可编译C C 43 43 Objective C Objective C
  • 用顺序表实现的简易通讯录(第一版)

    实现一个通讯录 xff1b 通讯录可以用来存储1000个人的信息 xff0c 每个人的信息包括 xff1a 姓名 性别 年龄 电话 住址 提供方法 xff1a 1 添加联系人信息 2 删除指定联系人信息 3 查找指定联系人信息 4 修改指定
  • ubuntu没有rc.local文件

    当我们设置开机自启时候 xff0c 一般都在rc local文件里设置 xff0c 但是有的Ubuntu版本没有这个文件 了 xff0c 此时我们可以自己创建一个 1 创建一个rc local service文件 sudo vim etc
  • 阿里云快速网站搭建详解

    一 网站建站流程 主要步骤 要有一个域名 购买主机 要有数据库 一般购买主机赠送 解析域名 下载网站程序 演示用的WordPress 上传程序 安装程序 配置数据库 网站基本信息 管理员信息等 二 DNS服务器快速入门 DNS服务概述 DN
  • OpenStack ussuri 私有云平台搭建

    一 OpenStack简介 openstack是一个云操作系统 这个操作系统控制着数据中心中的计算 存储和网络资源 所有这些资源的管理都是通过API来来实现的 并且管理资源都有相应的认证机制 在openstack中有一个叫做dashboar
  • 重磅!阿里版本【ChatGPT】开放测评!

    前两天突然爆出惊人消息 xff1a 阿里版ChatGPT开放测评了 xff01 在本月初 xff0c 已经有诸多关于阿里巴巴即将推出类似ChatGPT产品的传闻 数日前 xff0c 首批曝光的天猫精灵 鸟鸟分鸟 脱口秀版GPT基于大型模型的
  • debian-dhcp实验(傻瓜教程)

    安装apt get install y isc dhcp server 我这里已经安装过了 我们尝试启动服务端 xff0c 发现失败了 xff0c 这里因为我们没有绑定网卡 看一下网卡 我这是ens33 为了防止配置dhcp影响我的外网 x
  • 如何免费使用ChatGPT 4?

    自从ChatGPT发布以来 xff0c 它就取得了巨大的成功 无论是常春藤法学考试还是商学院作业 xff0c ChatGPT都被用于各种试验 统计数据显示 xff0c ChatGPT每月吸引约9600万用户 随着ChatGPT的巨大成功 x
  • 利用ChatGPT,一分钟制作思维导图

    大家好 xff0c 我是易安 xff01 今天我来教你如何使用ChatGPT xff0c 一分钟制作出一份思维导图 大纲选题 想到一个课题 xff0c 然后人工梳理出内容大纲 xff0c 是个挺费精力的事情 但利用ChatGPT来做这件事
  • 谈谈几种分布式锁实现

    大家好 xff0c 我是易安 xff01 今天我们呢谈一谈常见的分布式锁的几种实现方式 什么是分布式锁 在JVM中 xff0c 在多线程并发的情况下 xff0c 我们可以使用同步锁或Lock锁 xff0c 保证在同一时间内 xff0c 只能