分布式锁(Redis)

一、序言

本文和大家聊聊分布式锁以及常见的解决方案。

二、什么是分布式锁

未命名文件 (4).png
假设一个场景:一个库存服务部署在上面三台机器上,数据库里有 100 件库存,现有 300 个客户同时下单。并且这 300 个客户均摊到上面的三台机器上(即三台机器上分别有 100 个客户)。如果库存服务采取的是传统的进程锁或线程锁,我们会发现三台机器上在检测库存时都能满足(因为每台机器有 100 个客户,刚好满足 100 件库存)。此时会出现只有 100 件库存,却卖出了 300 件的现象(即超卖现象)。
未命名文件 (6).png
为了解决上述在分布式环境中存在的问题,我们需要使用分布式锁。分布式锁是一种在分布式系统中实现线程或进程同步访问共享资源的机制。它的主要目标是在分布式环境下,确保在同一时间只有一个线程或进程可以访问特定的资源

三、分布式锁方案

分布式锁的实现方式主要有三种:

  1. 基于数据库的分布式锁:这种方案主要是在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引。想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。这种方案的缺点包括数据库单点问题、没有锁超时机制、不可重入、非公平锁、非阻塞锁等。
  2. 基于 Redis 的分布式锁:Redis 的分布式锁主要是通过 SETNX 和 EXPIRE 命令来实现的。SETNX 可以用来尝试获取锁。EXPIRE 命令用来设置锁的超时时间,防止死锁。此外,还有基于 Redlock 算法的 Redisson 分布式锁。
  3. 基于 Zookeeper 的分布式锁:Zookeeper 是一个开源的分布式协调服务,它提供了一种高效且可靠的分布式锁实现机制。Zookeeper 的分布式锁主要是通过临时顺序节点和使用 watch 机制来实现。

四、Redis 分布式锁

4.1 Redis 分布式锁实现方式

Redis 分布式锁的实现通常基于 Redis 的原子性操作(比如 SETNX、EXPIRE、DEL 等),主要思想是通过在Redis 中设置一个特定的键值对来表示锁的状态,当某个节点需要获取锁时,会尝试在 Redis 中设置这个键值对,如果设置成功,则获取到锁,可以执行相应的操作;如果设置失败,则表示锁已经被其他节点持有,当前节点需要等待或重试。

public class RedisLock {

    private Jedis jedis;
    private String lockKey;

    // 构造器
    public RedisLock(Jedis jedis) {
        this.jedis = jedis;
        this.lockKey = "lock";
    }

    // 获取锁
    public boolean tryLock() {
        // 使用 set key value NX 命令尝试获取锁
        String result = jedis.set(lockKey, "1", SetParams.setParams().nx());
        return "OK".equals(result);
    }

    // 释放锁
    public void unLock() {
        // 释放锁,即删除对应的键
        jedis.del(lockKey);
    }
}

4.2 Redis 分布式锁过期

在之前,我们利用 set key value nx 这个互斥命令实现了最基本的分布式锁。但是,现在有一个问题:如果有一个业务在获取锁之后,由于未知原因发生了业务阻塞或者在业务完成之后忘记了释放锁,这将会导致当前业务会永久性的持有该锁。为了解决 Redis 分布式锁无法释放的问题,我们采用给锁设置超时时间:

public class RedisLock {

    private Jedis jedis;
    private String lockKey;

    // 构造器
    public RedisLock(Jedis jedis) {
        this.jedis = jedis;
        this.lockKey = "lock";
    }

    // 获取锁
    public boolean tryLock() {
        // 使用 set key value NX EX seconds 命令尝试获取锁, 并设置过期时间
        String result = jedis.set(lockKey, "1", SetParams.setParams().nx().ex(5));
        return "OK".equals(result);
    }

    // 释放锁
    public void unLock() {
        // 释放锁,即删除对应的键
        jedis.del(lockKey);
    }
}

在上面的代码中,我们利用 set key value nx ex seconds 命令给锁设定了超时时间解决了 Redis 分布式锁被占用而无法释放的问题(设定了超时时间,就算发生了业务阻塞,锁最终也会被释放)。

4.3 Redis 分布式锁误解锁

上面的 Redis 分布式锁引入了超时机制后会带来一个问题。我们先假设一个场景:

  1. 业务 A 获取到锁之后发生了业务阻塞,锁被超时释放了。
  2. 业务 B 正常获取到锁执行业务。此时,业务 A 恢复执行,并在执行完成后释放掉了锁(此时锁是属于业务 B 的)
  3. 业务 C 争抢到锁,但是业务 B 与业务 C 是互斥的此时就会导致并发问题(业务 B 与业务 C 是互斥的,但是同时在执行)。
public class RedisLock {

    private Jedis jedis;
    private String lockKey;

    // 构造器
    public RedisLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        // lockKey 不再为固定值
        this.lockKey = lockKey;
    }

    // 获取锁
    public boolean tryLock() {
        // 使用 set key value NX EX seconds 命令尝试获取锁, 并设置过期时间
        String result = jedis.set(lockKey, "1", SetParams.setParams().nx().ex(5));
        return "OK".equals(result);
    }

    // 释放锁
    public void unLock() {
        // 释放锁,即删除对应的键
        jedis.del(lockKey);
    }
}

为了解决分布式锁误解锁的问题,Redis 分布式锁的 key 不再为一个固定值。业务 A 有自己的 lockKey,业务 B 与业务 C 有相同的 lockKey。此时,业务 A 只能释放自己的锁,业务 B 与业务 C 拥有相同的 lockKey,当业务 B 没有释放锁时,业务 C 是无法获取到锁的,从而保证了业务 B 与业务 C 的互斥。

4.4 Redis 分布式锁续约

在 Redis 分布式锁误解锁的例子中,我们似乎使用不同的 lockKey 解决了误解锁的问题。但是当我们再深入思考一下会发现还有一个问题。我们现假设:

  1. 首先,业务 B 获取到锁之后发生了业务阻塞,锁被超时释放了。
  2. 然后,业务 C 争抢到锁,由于业务 B 与业务 C 是互斥的此时就会导致并发问题。

每一个业务的执行时间大抵是不尽相同的。在之前的例子中我们使用 jedis.set(lockKey, "1", SetParams.setParams().nx().ex(5))将锁的的释放时间设置成了 5 秒,如果业务能够在 5 秒内执行完成倒是没什么问题,若发生了业务阻塞或业务执行时间大于我们设定的过期时间呢?
针对以上的问题,我们通常采用一种称为 Watch Dog(看门狗)的机制去解决。

public class RedisLock {

    private final Jedis jedis;
    private final String lockKey;
    private final ScheduledExecutorService executorService;

    // 构造器
    public RedisLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.executorService = Executors.newSingleThreadScheduledExecutor();
    }

    // 尝试获取锁
    @SneakyThrows
    public boolean tryLock(long leaseTime, TimeUnit timeUnit)  {
        String result = jedis.set(lockKey, "1", SetParams.setParams().nx().px(timeUnit.toMillis(leaseTime)));
        if ("OK".equals(result)) {
            // 获取锁成功时自动启动 watch dog
            startWatchdog(leaseTime, timeUnit);
            return true;
        }
        return false;
    }

    // 释放锁
    public void unlock() {
        jedis.del(lockKey);
        stopWatchdog(); // 释放锁时停止 watch dog
    }

    // 启动 Watchdog
    public void startWatchdog(long leaseTime, TimeUnit timeUnit) {
        long leaseTimeMillis = timeUnit.toMillis(leaseTime);
        // 续期检测时间间隔为租约时间的 1/3
        long checkIntervalMillis = leaseTimeMillis / 3;
        executorService.scheduleAtFixedRate(() -> {
            long ttl = jedis.pttl(lockKey);
            if (ttl > 0) {
                // 续约锁
                jedis.pexpire(lockKey, leaseTimeMillis);
            } else {
                // 锁过期后停止 watch dog
                stopWatchdog();
            }
            // 周期性执行任务
        }, checkIntervalMillis, checkIntervalMillis, TimeUnit.MILLISECONDS);
    }

    // 停止 Watchdog
    public void stopWatchdog() {
        executorService.shutdown();
    }
}

在上面的代码中采用了 Watch Dog 机制周期性的去给锁续期,在业务完成之后,调用 unlock() 方法便可释放锁,并且停止 Watch Dog。

4.5 Redis 分布式锁重试

现在的 Redis 分布式锁已经解决了一部分问题,但是我们假设一个场景:

  1. 有三个业务(业务 A,业务 B,业务 C)同时争抢锁,业务 A 首先抢到了锁
  2. 业务 A 的执行时间很短,业务 B 与业务 C 此时应该如何处理

现有两种处理方式:

  1. 业务 B 与业务 C 直接返回失败信息
  2. 业务 B 与业务 C 自动重试争抢锁

在高并发的场景下,第一种方式业务 B 与业务 C 获取到锁的成功率会很小(因为第一次没抢到就返回失败信息了),第二种方式显然会更高(业务 B 与业务 C 会重试获取锁,如果在重试时锁空闲了便能获取到)。为了系统的稳定性和可靠性我们通常会采用第二种方式。

public class RedisLock {

    private final Jedis jedis;
    private final String lockKey;
    private final ScheduledExecutorService executorService;

    // 构造器
    public RedisLock(Jedis jedis, String lockKey) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.executorService = Executors.newSingleThreadScheduledExecutor();
    }

    // 获取锁
    @SneakyThrows
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit timeUnit)  {
        long start = System.currentTimeMillis();
        long end = start + timeUnit.toMillis(waitTime);
        // while 循环进行锁重试
        while (System.currentTimeMillis() < end) {
            String result = jedis.set(lockKey, "1", SetParams.setParams().nx().px(timeUnit.toMillis(leaseTime)));
            if ("OK".equals(result)) {
                // 获取锁成功时自动启动 watch dog
                startWatchdog(leaseTime, timeUnit);
                return true;
            }
            // 尝试等待一段时间再重试
            TimeUnit.MILLISECONDS.sleep(100);
        }
        return false;
    }

    // 释放锁
    public void unlock() {
        jedis.del(lockKey);
        stopWatchdog(); // 释放锁时停止 watch dog
    }

    // 启动 Watchdog
    public void startWatchdog(long leaseTime, TimeUnit timeUnit) {
        long leaseTimeMillis = timeUnit.toMillis(leaseTime);
        // 续期检测时间间隔为租约时间的 1/3
        long checkIntervalMillis = leaseTimeMillis / 3;
        executorService.scheduleAtFixedRate(() -> {
            long ttl = jedis.pttl(lockKey);
            if (ttl > 0) {
                // 续约锁
                jedis.pexpire(lockKey, leaseTimeMillis);
            } else {
                // 锁过期后停止 watch dog
                stopWatchdog();
            }
        }, checkIntervalMillis, checkIntervalMillis, TimeUnit.MILLISECONDS);
    }

    // 停止 Watchdog
    public void stopWatchdog() {
        executorService.shutdown();
    }
}

五、Redis 分布式锁的问题

5.1 如何实现可重入分布式锁

之前我们基于 set key value 实现的分布式锁,但是这样的锁是不可重入的。如果我们想实现可重入的分布式锁可以基于 Hash 类型,采用 hset key field value 这样的命令实现(重入一次 value 自增 +1)。

5.2 锁过期与锁续约的冲突

锁过期是为了防止锁一直被占用无法释放,锁续约是为了防止锁被提前释放。如果锁无限续约那么锁设置过期时间就无意义了,所以锁在续约时需要一些兜底方案(例如:有一个最大的续约时间)。除此之外,应该在设置锁过期时间和锁续约时间时充分考虑业务的执行时间,从而尽可能提前避免一些问题。

5.3 锁重试为什么需要等待

在我们设计锁重试时有这么一行代码 TimeUnit.MILLISECONDS.sleep(100)(即休眠 100 ms)。为什么需要休眠呢?现假设一个场景:

  1. 业务 A 与业务 B 同时争抢锁
  2. 业务 A 先抢到了锁,执行业务需要 1s

业务 A 既然执行时间需要 1s,如果业务 B 在重试的时候不休眠就会白白浪费系统资源。如果休眠 100 ms,最多会重试 10 次,这样很大程度上节省了系统资源。

往期推荐

  1. JDK 动态代理
  2. ThreadLocal
  3. HashMap 源码分析(三)
  4. Spring 三级缓存
  5. RBAC 权限设计(二)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/559056.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI绘画 究竟在哪些方面降低了门槛

AI绘画的产物是图像。图像对人类的认知、情感和文化发展起着重要的作用&#xff0c;包括信息传递、创造性表达、历史记录、审美享受和交流。 从原来的纸笔调色板到数字时代的数字板、绘图软件&#xff0c;再到AI绘画时代&#xff0c;任何人都可以用几行简单的文字创作出高质量…

在PostgreSQL中如何创建和使用自定义函数,包括内置语言(如PL/pgSQL)和外部语言(如Python、C等)?

文章目录 一、使用内置语言 PL/pgSQL 创建自定义函数示例代码使用方法 二、使用外部语言 Python 创建自定义函数安装 PL/Python 扩展示例代码使用方法 三、使用外部语言 C 创建自定义函数编写 C 代码编译为共享库在 PostgreSQL 中注册函数注意事项 总结 PostgreSQL 是一个强大的…

【Java开发指南 | 第十七篇】Java 方法

读者可订阅专栏&#xff1a;Java开发指南 |【CSDN秋说】 文章目录 简介语法实例构造方法 简介 Java中的方法是用来执行特定任务的一组语句&#xff0c;可以重复使用。它们包含在类或对象中&#xff0c;并通过调用来执行。 举个例子&#xff0c;println() 是一个方法&#xff…

论文阅读:BEVBert: Multimodal Map Pre-training for Language-guided Navigation

BEVBert&#xff1a;语言引导导航的多模态地图预训练 摘要 现存的问题&#xff1a;目前大多数现有的预训练方法都采用离散的全景图来学习视觉-文本关联。这要求模型隐式关联全景图中不完整、重复的观察结果&#xff0c;这可能会损害智能体的空间理解。 本文解决方案&#xf…

Android开发:应用百度智能云中的身份证识别OCR实现获取个人信息的功能

百度智能云&#xff1a; 百度智能云是百度提供的公有云平台&#xff0c;于2015年正式开放运营。该平台秉承“用科技力量推动社会创新”的愿景&#xff0c;致力于将百度在云计算、大数据、人工智能的技术能力向社会输出。 百度智能云为金融、城市、医疗、客服与营销、能源、制造…

【漏洞复现】泛微e-cology ProcessOverRequestByXml接口存在任意文件读取漏洞

漏洞描述 泛微e-cology依托全新的设计理念,全新的管理思想。 为中大型组织创建全新的高效协同办公环境。 智能语音办公,简化软件操作界面。 身份认证、电子签名、电子签章、数据存证让合同全程数字化。泛微e-cology ProcessOverRequestByXml接口存在任意文件读取漏洞 免责声…

怎么看自己是不是公网IP?

当我们需要进行网络连接或者网络配置的时候&#xff0c;经常会遇到需要知道自己是否拥有公网IP的情况。公网IP是全球唯一的IP地址&#xff0c;在互联网上可直接访问和被访问&#xff0c;而私有IP则是在本地网络中使用&#xff0c;无法从互联网上直接访问。我们将介绍如何查看自…

Java - 阿里巴巴命名规范

文章目录 前言一、编程规约(一) 命名风格(二) 常量定义(三) 代码格式(四) OOP 规约(五) 日期时间(六) 集合处理(七) 并发处理(八) 控制语句(九) 注释规约(十) 前后端规约(十一) 其他 二、异常日志(一) 错误码(二) 异常处理(三) 日志规约 三、单元测试四、安全规约五、MySQL 数据…

C# winform s7.net expected 22 bytes.”

S7.Net.PlcException:“Received 12 bytes: 32-02-00-00-00-00-00-00-00-00-81-04, expected 22 bytes.” 原因是博图的连接机制未勾选

【Java框架】Spring框架(二)——Spring基本核心(AOP)

目录 面向切面编程AOPAOP的目标&#xff1a;让我们可以“专心做事”专心做事专心做事解决方案1.0专心做事解决方案2.0蓝图 AOP应用场景AOP原理AOP相关术语术语理解 AOP案例实现前置/后置/异常/最终增强的配置实现1.依赖2.业务类3.日志类4.配置切入点表达式匹配规则举例 环绕增强…

Spring AOP (二)

本篇将介绍Spring AOP的相关原理。 一、代理模式 Spring 在实现AOP时使用了代理模式这种设计模式&#xff0c;什么是代理模式呢我们来了解一下。 代理模式可以理解为当我们需要调用某个类的方法时&#xff0c;在我们与这个目标类之间增加一个代理类&#xff0c;我们要使用目标…

vue2响应式 VS vue3响应式

Vue2响应式 存在问题&#xff1a; 新增属性&#xff0c;删除属性&#xff0c;界面不会更新。 直接通过下标修改数组界面不会自动更新。 Vue2使用object.defineProperty来劫持数据是否发生改变&#xff0c;如下&#xff1a; 能监测到获取和修改属性&#xff1a; 新增的属性…

【C++打怪之路】-- C++开篇

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;C打怪之路&#xff0c;python从入门到精通&#xff0c;魔法指针&#xff0c;进阶C&#xff0c;C语言&#xff0c;C语言题集&#xff0c;C语言实现游戏&#x1f448; 希望得到您的订阅和支持~ &…

【C语言】深入解析选择排序算法

一、算法原理二、算法性能分析三、C语言实现示例四、总结 一、算法原理 选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法。它的工作原理是不断地选择剩余元素中的最小&#xff08;或最大&#xff09;元素&#xff0c;放到已排序的序列的末尾&#xff…

securecrt 批量登录服务器介绍

一、前言 在有一些IT环境中&#xff0c;可能存在各种情况的服务器&#xff0c;因为各种原因不能统一部署类似ansible、saltstack等批量操控软件&#xff0c;当遇到需要对这些服务器进行某项信息的排查或调整配置时&#xff0c;你是否还是通过securecrt一台一台登录后进行操作&a…

endnote21从安装到使用!文献引用!Mac版

视频学习和资源获取 新建库 选择上方导航栏处的File下的New 软件 软件界面可以分成四个部分 2是个人图书馆 3是对某一分类中文献的展示 最右侧是对具体一篇文献的摘要、编辑以及PDF 有回形针标志意味着这篇有全文&#xff0c;也就是有pdf 如果没有回形针代表它只有引文信…

社交媒体数据恢复:BF Messager

BF Messenger 数据恢复方法 一、前言 BF Messenger&#xff08;BF加密聊天软件&#xff09;是一款基于布尔式循环移位加密算法的聊天应用程序。它使用对称密钥加密技术&#xff0c;用户可以在安全的环境下进行私密聊天。除此之外&#xff0c;该应用还具有防截屏、应用锁屏、密…

LeetCode in Python 55. Jump Game (跳跃游戏)

跳跃游戏的游戏规则比较简单&#xff0c;若单纯枚举所有的跳法以判断是否能到达最后一个下标需要的时间复杂度为O()&#xff0c;为此&#xff0c;本文采用贪心策略&#xff0c;从最后一个下标开始逆着向前走&#xff0c;若能跳到第一个元素则表明可以完成跳跃游戏&#xff0c;反…

本地主机搭建服务器后如何让外网访问?快解析内网端口映射

本地主机搭建应用、部署服务器后&#xff0c;在局域网内是可以直接通过计算机内网IP网络地址进行连接访问的&#xff0c;但在外网电脑和设备如何访问呢&#xff1f;由于内网环境下&#xff0c;无法提供公网IP使用&#xff0c;外网访问内网就需要一个内外网转换的介质。这里介绍…

stm32开发之netxduo组件之mqtt客户端的使用记录

前言 1使用mqtt协议的简单示例记录 代码 MQTT服务端(C# 编写,使用MQTTnet提供的示例代码) 主程序 namespace ConsoleApp1;public class Program {public static async Task Main(string[] args){await Run_Server_With_Logging();}}public static async Task Run_Server_Wi…
最新文章