2、分布式锁实现原理与最佳实践(二)

常见分布式锁的原理

4.1 Redisson

Redis 2.6之后才可以执行lua脚本,比起管道而言,这是原子性的,模拟一个商品减库存的原子操作:

//lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10
jedis.set("product_stock_10016", "15");  
//初始化商品10016的库存
String script = " local count = redis.call('get', KEYS[1]) " +
        " local a = tonumber(count) " +
        " local b = tonumber(ARGV[1]) " +
        " if a >= b then " +
        "   redis.call('set', KEYS[1], a-b) " +
        "   return 1 " +
        " end " +
        " return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), 
                        Arrays.asList("10"));
System.out.println(obj);

在这里插入图片描述
4.1.1 尝试加锁的逻辑
在这里插入图片描述
上面的org.redisson.RedissonLock#lock()通过调用自己方法内部的lock方法的org.redisson.RedissonLock#tryAcquire方法。之后调用 org.redisson.RedissonLock#tryAcquireAsync:
在这里插入图片描述
首先调用内部的org.redisson.RedissonLock#tryLockInnerAsync:设置对应的分布式锁
在这里插入图片描述
到这里获取锁的逻辑就结束了,如果这里没有获取到,在Future的回调里面就会直接return,会在外层有一个while true的循环,订阅释放锁的消息准备被唤醒。如果说加锁成功,就开始执行锁续命逻辑。
在这里插入图片描述
4.1.2 锁续命逻辑
lua脚本最后是以毫秒为单位返回key的剩余过期时间。成功加锁之后org.redisson.RedissonLock#scheduleExpirationRenewal中将会调用org.redisson.RedissonLock#renewExpiration,这个方法内部就有锁续命的逻辑,是一个定时任务,等10s执行。
执行的时候尝试执行的续命逻辑使用的是Lua脚本,当前的锁有值,就续命,没有就直接返回0:
在这里插入图片描述
返回0之后外层会判断,延时成功就会再次调用自己,否则延时调用结束,不再为当前的锁续命。所以这里的续命不是一个真正的定时,而是循环调用自己的延时任务。
在这里插入图片描述
4.1.3 循环间隔抢锁机制
如果一开始就加锁成功就直接返回。
如果一开始加锁失败,没抢到锁的线程就会在while循环中尝试加锁,加锁成功就结束循环,否则等待当前锁的超时时间之后再次尝试加锁。所以实现逻辑默认是非公平锁:
在这里插入图片描述
里面有一个subscribe的逻辑,会监听对应加锁的key,当锁释放之后publish对应的消息,此时如果没有到达对应的锁的超时时间,也会尝试获取锁,避免时间浪费。
4.1.4 释放锁和唤醒其他线程的逻辑
前面没有抢到锁的线程会监听对应的queue,后面抢到锁的线程释放锁的时候会发送一个消息。
在这里插入图片描述
订阅的时候指定收到消息时候的逻辑:会唤醒阻塞之后执行while循环

4.1.5 重入锁的逻辑
存在对应的锁,就对对应的hash结构的value直接+1,和Java重入锁的逻辑是一致的。
在这里插入图片描述

4.2 RedLock解决非单体项目的Redis主从架构的锁失效

https://redis.io/docs/manual/patterns/distributed-locks/
查看Redis官方文档,对于单节点的Redis ,使用setnx和lua del删除分布式锁是足够的,但是主从架构的场景下:锁先加在一个master节点上,默认是异步同步到从节点,此时master挂了会选择slave为master,此时又可以加锁,就会导致超卖。但是如果使用zookeeper来实现的话,由于zk是CP的,所以CP不存在这样的问题。
Redis文档中给出了RedLock的解决办法,使用redLock真的可以解决吗?
4.2.1 RedLock 原理
基于客户端的实现,是基于多个独立的Redis Master节点的一种实现(一般为5)。client依次向各个节点申请锁,若能从多数个节点中申请锁成功并满足一些条件限制,那么client就能获取锁成功。它通过独立的N个Master节点,避免了使用主备异步复制协议的缺陷,只要多数Redis节点正常就能正常工作,显著提升了分布式锁的安全性、可用性。
在这里插入图片描述
注意图中所有的节点都是master节点。加锁超过半数成功,就认为是成功。具体流程:
获取锁

获取当前时间T1,作为后续的计时依据;

按顺序地,依次向5个独立的节点来尝试获取锁 SET resource_name my_random_value NX PX 30000;

计算获取锁总共花了多少时间,判断获取锁成功与否;

时间:T2-T1;

多数节点的锁(N/2+1);

当获取锁成功后的有效时间,要从初始的时间减去第三步算出来的消耗时间;

如果没能获取锁成功,尽快释放掉锁。

释放锁

向所有节点发起释放锁的操作,不管这些节点有没有成功设置过。

public String redlock() {
    String lockKey = "product_001";
    //这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
    RLock lock1 = redisson.getLock(lockKey);
    RLock lock2 = redisson.getLock(lockKey);
    RLock lock3 = redisson.getLock(lockKey);

    /**
     * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
     */
    RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
    try {
        /**
         * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
         * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
         */
        boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
        if (res) {
            //成功获得锁,在这里处理业务
        }
    } catch (Exception e) {
        throw new RuntimeException("lock fail");
    } finally {
        //无论如何, 最后都要解锁
        redLock.unlock();
    }

    return "end";
}

但是,它的实现建立在一个不安全的系统模型上的,它依赖系统时间,当时钟发生跳跃时,也可能会出现安全性问题。分布式存储专家Martin对RedLock的分析文章,Redis作者的也专门写了一篇文章进行了反驳。

Martin Kleppmann:How to do distributed locking

https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
Antirez:Is Redlock safe?

http://antirez.com/news/101
4.2.2 RedLock 问题一:持久化机制导致重复加锁
如果是上面的架构图,一般生产都不会配置AOF的每一条命令都落磁盘,一般会设置一些间隔时间,比如1s,如果ABC节点加锁成功,有一个节点C恰好是在1s内加锁,还没有落盘,此时挂了,就会导致其他客户端通过CDE又会加锁成功。
4.2.3 RedLock 问题二:主从下重复加锁
在这里插入图片描述
除非多部署一些节点,但是这样会导致加锁时间变长,这样比较下来效果就不如zk了。
4.2.4 RedLock 问题三:时钟跳跃导致重复加锁
C节点发生了时钟跳跃,导致加上的锁没有到达实际的超时时间,就被误以为超时而释放,此时其他客户端就可以重复加锁了。
4.3 Curator

InterProcessMutex 可重入锁的分析
在这里插入图片描述
五、业务中使用分布式锁的注意点

获取的锁要设置有效期,假设我们未设置key自动过期时间,在Set key value NX 后,如果程序crash或者发生网络分区后无法与Redis节点通信,毫无疑问其他 client 将永远无法获得锁,这将导致死锁,服务出现中断。
SETNX和EXPIRE命令去设置key和过期时间,这也是不正确的,因为你无法保证SETNX和EXPIRE命令的原子性。
自己使用 setnx 实现Redis锁的时候,注意并发情况下不要释放掉别人的锁(业务逻辑执行时间超过锁的过期时间),导致恶性循环。一般:
1)加锁的时候需要指定value的内容是当前进程中的当前线程的唯一标记,不要使用线程ID作为当前线程的锁的标记,因为不同实例上的线程ID可能是一样的。
2)释放锁的逻辑会写在finally ,释放锁时候要判断锁对应的value,而且要使用lua脚本实现原子 del 操作。因为if逻辑判断完之后也可能失效导致删除别人的锁。
3)针对扣减库存这个逻辑,lua脚本里面实现Redis比较库存、扣减库存操作的原子性。通过判断Redis Decr命令的返回值即可。此命令会返回扣减后的最新库存,若小于0则表示超卖。

5.1 自己实现分布式锁的坑

setnx不关心锁的顺序导致删除别人的锁
锁失效之后,别人加锁成功,自己把别人的锁删了。
我们无法预估程序执行需要的锁的时间。

public String deductStock() {
    String lockKey = "lock:product_101";
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "deltaqin");
    stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

    try {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    } finally {
        stringRedisTemplate.delete(lockKey);
    }

    return "end";
}

setnx关心锁的顺序还是删除了别人的锁
并发会卡在各种地方,卡住的时候过期了,就会删掉别人加的锁:
错误的原因还是因为解锁的逻辑不是原子性的,这里可以参考Redisson的解锁逻辑使用lua脚本实现。

public String deductStock() {
    String lockKey = "lock:product_101";
    String clientId = UUID.randomUUID().toString();
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
    if (!result) {
        return "error_code";
    }
    try {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }
    } finally {
        if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
            // 卡在这里,锁过期了,其他线程又可以加锁,此时又把其他线程新加的锁删掉了
            stringRedisTemplate.delete(lockKey);
        }
    }
    return "end";
}

解决办法
这种问题解决的办法就是使用锁续命,比如使用一个定时任务间隔小于锁的超时时间,每隔一段时间就给锁续命,除非线程自己主动删除。这也是Redisson的实现思路。
5.2 锁优化:分段加锁逻辑

针对一个商品,要开启秒杀的时候,会将商品的库存预先加载到Redis缓存中,比如有100个库存,此时可以分为5个key,每一个key有20个库存。可以把分布式锁的性能提升5倍。
例如:

product_10111_stock = 100

product_10111_stock1 = 20

product_10111_stock2 = 20

product_10111_stock3 = 20

product_10111_stock4 = 20

product_10111_stock5 = 20

请求来了可以随机可以轮询,扣减完之后就标记不要下次再分配到这个库存。

六、分布式锁的真相与选择

6.1 分布式锁的真相

需要满足的几个特性
互斥:不同线程、进程互斥。

超时机制:临界区代码耗时导致,网络原因导致。可以使用额外的线程续命保证。

完备的锁接口:阻塞的和非阻塞的接口都要有,lock和tryLock。

可重入性:当前请求的节点+ 线程唯一标识。

公平性:锁唤醒时候,按照顺序唤醒。

正确性:进程内的锁不会因为报错死锁,因为崩溃的时候整个进程都会结束。但是多实例部署时死锁就很容易发生,如果粗暴使用超时机制解决死锁问题,就默认了下面这个假设:

锁的超时时间 >> 获取锁的时延 + 执行临界区代码的时间 + 各种进程的暂停(比如 GC)

但上述假设其实无法保证的。
将分布式锁定位为,可以容忍非常小概率互斥语义失效场景下的锁服务。一般来说,一个分布式锁服务,它的正确性要求越高,性能可能就会越低。
6.2 分布式锁的选择

数据库:db操作性能较差,并且有锁表的风险,一般不考虑。

优点:实现简单、易于理解

缺点:对数据库压力大

Redis:适用于并发量很大、性能要求很高而可靠性问题可以通过其他方案去弥补的场景。

优点:易于理解

缺点:自己实现、不支持阻塞

Redisson:相对于Jedis其实更多用在分布式的场景。

优点:提供锁的方法,可阻塞

Zookeeper:适用于高可靠(高可用),而并发量不是太高的场景。

优点:支持阻塞

缺点:需理解Zookeeper、程序复杂

Curator

优点:提供锁的方法

缺点:Zookeeper,强一致,慢

Etcd:安全和可靠性上有保证,但是比较重。

不推荐自己编写的分布式锁,推荐使用Redisson和Curator实现的分布式锁。

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

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

相关文章

【Redis】事务

文章目录 事务的概念事务操作MULTIEXECDISCARDWATCHUNWATCH 事务的概念 Redis 的事务和 MySQL 的事务概念上是类似的. 都是把⼀系列操作绑定成⼀组. 让这⼀组能够批量执 ⾏ Redis 的事务和 MySQL 事务的区别 弱化的原⼦性: redis 没有 “回滚机制”. 只能做到这些操作 “批量执…

基于 STM32F7 和神经网络的实时人脸特征提取与匹配算法实现

本文讨论了如何使用 STM32F7 和神经网络模型来实现实时人脸特征提取与匹配算法。首先介绍了 STM32F7 的硬件和软件特点,然后讨论了人脸特征提取和匹配算法的基本原理。接下来,我们将重点讨论如何在 STM32F7 上实现基于神经网络的人脸特征提取与匹配算法&…

2023年亚太杯数学建模A题解题思路(*基于OpenCV的复杂背景下苹果目标的识别定位方法研究)

摘要 由于要求较高的时效性和劳力投入,果实采摘环节成为苹果生产作业中十分重要的一部分。而对于自然环境下生长的苹果,光照影响、枝叶遮挡和果实重叠等情况普遍存在,这严重影响了果实的准确识别以及采摘点的精确定位。针对在复杂背景下苹果的…

Spring - Mybatis-设计模式总结

Mybatis-设计模式总结 1、Builder模式 2、工厂模式 3、单例模式 4、代理模式 5、组合模式 6、模板方法模式 7、适配器模式 8、装饰者模式 9、迭代器模式 虽然我们都知道有26个设计模式,但是大多停留在概念层面,真实开发中很少遇到,…

【迅搜03】全文检索、文档、倒排索引与分词

全文检索、文档、倒排索引与分词 今天还是概念性的内容,但是这些概念却是整个搜索引擎中最重要的概念。可以说,所有的搜索引擎就是实现了类似的概念才能称之为搜索引擎。而且今天的内容其实都是相关联的,所以不要以为标题上有四个名词就感觉好…

基于JavaWeb+SSM+Vue微信阅读小程序的设计和实现

基于JavaWebSSMVue微信阅读小程序的设计和实现 源码获取入口Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏[Java 源码获取 源码获取入口 Lun文目录 第1章 绪论 1 1.1 课题背景 1 1.2 课题意义 1 1.3 研究内容 1 第2章 开发环境与技术 3 2.1 MYSQL数据库 3 2.2 JSP技…

微信小程序富文本拓展rich-text

微信小程序富文本插件 功能介绍 支持解析<style>标签中的全局样式支持自定义默认的标签样式支持自动设置标题 若html中存在title标签,将自动把title标签的内容设置到页面的标题上,并在回调bindparse中返回,可以用于转发支持添加加载提示 可以在Parser标签内添加加载提…

电机应用-直流有刷电机多环控制实现

目录 直流有刷电机多环控制实现 硬件设计 直流电机三环&#xff08;速度环、电流环、位置环&#xff09;串级PID控制-位置式PID 编程要点 配置ADC可读取电流值 配置基本定时器6产生定时中断读取当前电路中驱动电机的电流值并执行PID运算 配置定时器1输出PWM控制电机 配…

jenkins + gitlab 自动部署(webhook)

Jenkins是一个流行的开源CI/CD工具&#xff0c;可以与Git等版本控制系统集成&#xff0c;实现自动构建、测试和部署。Webhook是一种机制&#xff0c;可以在Git仓库中设置&#xff0c;在代码提交或合并请求时触发Jenkins构建任务&#xff0c;以完成自动化部署。 实操 设备信息 …

DELL MD3600F存储重置管理软件密码

注意&#xff1a;密码清除可能会导致业务秒断&#xff0c;建议非业务时间操作 针对一台控制器操作即可&#xff0c;另一控制器会同步操作 重置后密码为空&#xff01; 需求&#xff1a;重置存储管理软件密码 管理软件中分配物理磁盘时提示输入密码(类似是否了解风险确认操作的提…

前端(HTML + CSS + JS)

文章目录 一、HTML1. 概念&#xff08;1&#xff09;HTML 文件基本结构&#xff08;2&#xff09;HTML代码框架 2. 、HTML常见标签 二、CSS1. CSS基本语法规范2. 用法&#xff08;1&#xff09; 引用方式&#xff08;2&#xff09;选择器&#xff08;3&#xff09;常用元素属性…

面向对象三大特性,类与接口,java重写与重载,对象相等的判断, hashCode 与 equals

文章目录 2.1 面向对象三大特性2.1.1 封装 继承 多态2.1.2 其中Java 面向对象编程三大特性&#xff1a;封装 继承 多态2.1.3 关于继承如下 3 点请记住&#xff1a;2.1.4 什么是多态机制&#xff1f;Java语言是如何实现多态的&#xff1f;2.1.5 Java实现多态有三个必要条件&…

H5ke12--2--学生选课表格的编辑

方法1不可以修改的用label,如何按了哪一行 就会在下面有个文本显示可编辑的一行 方法2每一行后面都有一个编辑, 3对每一个修改,每一个td失去焦点都会有,直接到达我们服务器 注意 如果用span的每一个html元素都可以自己定义属性 Data-属性名,data-Address links也要给为span 1…

Qt学习(2)

1.QObject 只有继承了QObject类的类&#xff0c;才具有信号槽的能力。所以&#xff0c;为了使用信号槽&#xff0c;必须继承QObject。凡是QObject类&#xff08;不管是直接子类还是间接子类&#xff09;&#xff0c;都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽&…

【LeetCode】挑战100天 Day14(热题+面试经典150题)

【LeetCode】挑战100天 Day14&#xff08;热题面试经典150题&#xff09; 一、LeetCode介绍二、LeetCode 热题 HOT 100-162.1 题目2.2 题解 三、面试经典 150 题-163.1 题目3.2 题解 一、LeetCode介绍 LeetCode是一个在线编程网站&#xff0c;提供各种算法和数据结构的题目&…

nohup 实现远程运行不关机操作

nohup 实现远程运行不宕机操作 python nohup 实现远程运行不宕机操作 - python教程网 远程运行最怕断电&#xff0c;训练了几个小时的数据说没就没&#xff0c;或者停止运行。 用nohup 记录代码的输出&#xff0c;还可以不受断电的影响。 方法 1. 用nohup 运行一个python文…

HTML网站稳定性状态监控平台源码

这是一款网站稳定性状态监控平台源码&#xff0c;它基于UptimeRobot接口进行开发。当您的网站遇到故障时&#xff0c;该平台能够通过邮件或短信通知您。下面是对安装过程的详细说明&#xff1a; 安装步骤 将源码上传至您的主机或服务器&#xff0c;并进行解压操作。 在Uptim…

Redis高并发缓存架构

前言&#xff1a; 针对缓存我们并不陌生&#xff0c;而今天所讲的是使用redis作为缓存工具进行缓存数据。redis缓存是将数据保存在内存中的&#xff0c;而内存的珍贵性是不可否认的。所以在缓存之前&#xff0c;我们需要明确缓存的对象&#xff0c;是否有必要缓存&#xff0c;怎…

C++之模版初阶(简单使用模版)

前言 在学习C的模版之前&#xff0c;咱们先来说一说模版的概念&#xff0c;模版在我们的日常生活中非常常见&#xff0c;比如我们要做一个ppt&#xff0c;我们会去在WPS找个ppt的模版&#xff0c;我们只需要写入内容即可&#xff1b;比如我们的数学公式&#xff0c;给公式套值&…

DBS note5:Relational Algebra(关系代数)

目录 一、关系代数简介 二、Projection () 三、Selection () 四、Union () 五、Set Difference (-) 六、Intersection () 七、Cross Product () 八、Joins () 九、Rename () 十、Group By / Aggregation () 一、关系代数简介 关系代数中的所有运算符都接受一个关系并…