分布式锁Redission对于(不可重入、不可重试、超时释放、主从一致性)四个问题的应对

news2024/11/19 9:28:32

文章目录

    • 1 Redission介绍
    • 2 Redission快速入门
    • 3 Redission可重入锁原理
    • 4 Redission锁重试和WatchDog机制
    • 5 Redission锁的MutiLock原理

基于setnx实现的分布式锁存在下面的问题:

1653546070602

重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。

**超时释放:**我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患。

主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。

1 Redission介绍

那么什么是Redission呢

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redission提供了分布式锁的多种多样的功能

1653546736063

针对上述四个问题,Redission提供了自己的解决方案

首先Redission支持可重入锁

当第一次获取锁失败时,如果已经设置了等待时间的话,则Redisson会等待一段时间RetryInterval重新尝试获取锁。如果在重试次数RetryAttempts内获取锁成功,则返回获取锁成功的结果;如果重试次数内仍然无法获取锁,则返回获取锁失败的结果。

在Redisson中,提供了看门狗机制(Watchdog),用于监控锁的状态,防止节点宕机或网络故障等原因导致的锁未正常释放而引起的死锁问题。看门狗机制的原理是在获取锁成功后,启动一个线程,周期性的去检查锁的状态,如果锁已经过期(超过设定时间未被正常释放),则自动释放该锁(在Redisson实现中为异步操作,会立刻向Redis节点发送释放锁的请求),避免其它线程因为该锁无法获取而导致程序假死等情况。

Redission的MultiLock机制可以在分布式环境下保证数据的原子性操作,从而解决主从集群中因主机宕机导致的死锁问题。当向集群写数据时,Redission会创建一个MultiLock对象,该对象会对参与写入操作的所有锁进行加锁,并在写入操作执行完成后,对所有锁进行解锁操作。在这个过程中,如果主机宕机导致锁无法被解除,则会触发Redisson的WatchDog机制,WatchDog会自动检测并解锁所有死锁的锁。

2 Redission快速入门

引入依赖:

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

配置Redisson客户端:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.150.101:6379")
            .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

如何使用Redission的分布式锁

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        }
        
    }  
}

在 VoucherOrderServiceImpl

注入RedissonClient

@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 这个代码不用了,因为我们现在要使用分布式锁
        //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }

3 Redission可重入锁原理

在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记录重入的状态的,比如当前没有人持有这把锁,那么state=0,假如有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,如果是对于synchronized而言,他在c语言代码中会有一个count,原理和state类似,也是重入一次就加一,释放一次就-1 ,直到减少成0 时,表示当前这把锁没有被人持有。

在Redission中也支持支持可重入锁

在分布式锁中,他采用hash结构用来存储锁,其中大key表示表示这把锁是否存在,用小key表示当前这把锁被哪个线程持有,所以接下来我们一起分析一下当前的这个lua表达式

这个地方一共有3个参数

KEYS[1] : 锁名称

ARGV[1]: 锁失效时间

ARGV[2]: id + “:” + threadId; 锁的小key

exists: 判断数据是否存在 name:是lock是否存在,如果==0,就表示当前这把锁不存在

redis.call(‘hset’, KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ,写成一个hash结构

Lock{

​ id + “:” + threadId : 1

}

如果当前这把锁存在,则第一个条件不满足,再判断

redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1

此时需要通过大key+小key判断当前这把锁是否是属于自己的,如果是自己的,则进行

redis.call(‘hincrby’, KEYS[1], ARGV[2], 1)

将当前这个锁的value进行+1 ,redis.call(‘pexpire’, KEYS[1], ARGV[1]); 然后再对其设置过期时间,如果以上两个条件都不满足,则表示当前这把锁抢锁失败,最后返回pttl,即为当前这把锁的失效时间

如果小伙帮们看了前边的源码, 你会发现他会去判断当前这个方法的返回值是否为null,如果是null,则对应则前两个if对应的条件,退出抢锁逻辑,如果返回的不是null,即走了第三个分支,在源码处会进行while(true)的自旋抢锁。

"if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call('pttl', KEYS[1]);"

1653548087334

4 Redission锁重试和WatchDog机制

说明:由于课程中已经说明了有关tryLock的源码解析以及其看门狗原理,所以笔者在这里给大家分析lock()方法的源码解析,希望大家在学习过程中,能够掌握更多的知识

抢锁过程中,获得当前线程,通过tryAcquire进行抢锁,该抢锁逻辑和之前逻辑相同

1、先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null

2、判断当前这把锁是否是属于当前线程,如果是,则返回null

所以如果返回是null,则代表着当前这哥们已经抢锁完毕,或者可重入完毕,但是如果以上两个条件都不满足,则进入到第三个条件,返回的是锁的失效时间,同学们可以自行往下翻一点点,你能发现有个while( true) 再次进行tryAcquire进行抢锁

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
    return;
}

接下来会有一个条件分支,因为lock方法有重载方法,一个是带参数,一个是不带参数,如果带参数传入的值是-1,如果传入参数,则leaseTime是他本身,所以如果传入了参数,此时leaseTime != -1 则会进去抢锁,抢锁的逻辑就是之前说的那三个逻辑

if (leaseTime != -1) {
    return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}

如果是没有传入时间,则此时也会进行抢锁, 而且抢锁时间是默认看门狗时间 commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()

ttlRemainingFuture.onComplete((ttlRemaining, e) 这句话相当于对以上抢锁进行了监听,也就是说当上边抢锁完毕后,此方法会被调用,具体调用的逻辑就是去后台开启一个线程,进行续约逻辑,也就是看门狗线程

RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                        commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                        TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
    if (e != null) {
        return;
    }

    // lock acquired
    if (ttlRemaining == null) {
        scheduleExpirationRenewal(threadId);
    }
});
return ttlRemainingFuture;

此逻辑就是续约逻辑,注意看commandExecutor.getConnectionManager().newTimeout() 此方法

Method( new TimerTask() {},参数2 ,参数3 )

指的是:通过参数2,参数3 去描述什么时候去做参数1的事情,现在的情况是:10s之后去做参数一的事情

因为锁的失效时间是30s,当10s之后,此时这个timeTask 就触发了,他就去进行续约,把当前这把锁续约成30s,如果操作成功,那么此时就会递归调用自己,再重新设置一个timeTask(),于是再过10s后又再设置一个timerTask,完成不停的续约

那么大家可以想一想,假设我们的线程出现了宕机他还会续约吗?当然不会,因为没有人再去调用renewExpiration这个方法,所以等到时间之后自然就释放了。

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

5 Redission锁的MutiLock原理

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

1653553998403

为了解决这个问题,redission提出来了MutiLock锁,使用这把锁咱们就不使用主从了,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

1653554055048

那么MutiLock 加锁原理是什么呢?笔者画了一幅图来说明

当我们去设置了多个锁时,redission会将多个锁添加到一个集合中,然后用while循环去不停去尝试拿锁,但是会有一个总共的加锁时间,这个时间是用需要加锁的个数 * 1500ms ,假设有3个锁,那么时间就是4500ms,假设在这4500ms内,所有的锁都加锁成功, 那么此时才算是加锁成功,如果在4500ms有线程加锁失败,则会再次去进行重试.

1653553093967

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaScript 特性 this与“bind“和“call“,“apply“的理解

本文的目的是帮助JavaScript初学者更好地理解"bind"方法&#xff0c;并帮助那些对"this"的理解不太清楚的人更好地理解"bind"方法和"this"之间的关系。特别是对于那些对"this"的理解不太清楚的人是有所帮助的。在深入学习&q…

通信端点(应用端点)与套接字的关系

为了实现以太网通信的分层与解耦,应用程序通常并不直接驱动TCP/IP协议栈发送以太网数据包。它们通过所在系统提供的一个叫做套接字的东西,调用套接字的接口函数创建套接字处理程序。用套接字处理程序发送自己的应用数据。 返回的套接字处理程序,就代表了这个应用程序。这就…

一篇文章搞定《RecyclerView缓存复用机制》

------《RecyclerView缓存复用机制》 前言零、为什么要缓存一、RecyclerView如何构建我们的列表视图二、缓存过程三、缓存结构1、mChangedScrap/mAttachedScrap2、mCachedViews3、mViewCacheExtension4、mRecyclerPool 四、总结 前言 本篇文章&#xff0c;暂时不加入预加载进行…

沃尔玛入驻教程:中国卖家如何免费、快速入驻沃尔玛walmart.com?

作为一家全球知名的零售巨头&#xff0c;沃尔玛&#xff08;Walmart&#xff09;的在线商城walmart.com拥有庞大的消费者基础和巨大的商机。对于中国的卖家来说&#xff0c;入驻沃尔玛的平台是一个很好的机会&#xff0c;但是有没有什么方法可以免费、快速入驻呢&#xff1f;有…

CTF-PHP反序列化漏洞4-实例理解POP链(经典赛题)

作者&#xff1a;Eason_LYC 悲观者预言失败&#xff0c;十言九中。 乐观者创造奇迹&#xff0c;一次即可。 一个人的价值&#xff0c;在于他所拥有的。可以不学无术&#xff0c;但不能一无所有&#xff01; 技术领域&#xff1a;WEB安全、网络攻防 关注WEB安全、网络攻防。我的…

软件测试 - 缺陷管理

1. 缺陷的定义 产品不满足用户的需求或者测试执行时实际结果和预期结果不一致都属于缺陷。 2. 缺陷的判定标准及产生原因 软件不满足下述任何一种都算作是软件的缺陷&#xff0c;缺陷的概念是包括bug概念的。 未达到需求说明书指明的功能出现了需求说明书指明不应该出现的错…

cordova-10 打apk

Android升级到api 31后&#xff0c;ionic打出来的release包默认是一个aab文件&#xff0c;要想走以前的手动签名和zipalign的流程我需要打一个没有签名的apk的包&#xff0c;参考以下文章&#xff1a; Android Platform Guide - Apache Cordova Ionic Cordova Build for Andro…

Ubuntu: 搭建 NFS 服务器

文章目录 1. 前言2. 测试环境3. NFS 服务器搭建3.1 安装 NFS 服务3.2 导出 NFS 共享目录3.3 重启 NFS 服务 4. 客户端连接 NFS 服务端5. VMware Ubuntu6. Linux 内核 NFS 支持 1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&…

Seurat -- 数据集的整合

文章目录 briefPerforming integration on datasets normalized with LogNormalizePerforming integration on datasets normalized with SCTransform brief 这里主要根据seurat的教程走的&#xff0c;描述了多个单细胞数据集的整合&#xff0c;其中数据集的integration并不是…

java的对象模型

背过面试题的朋友都知道&#xff0c;Java对象是保存在堆内存中&#xff0c;在内存中&#xff0c;一个Java对象包含三部分&#xff1a;对象头&#xff0c;实例数据&#xff0c;对其填充。其中对象头是一个很关键的部分&#xff0c;因为对象头中包含锁状态标志&#xff0c;线程持…

使用MindSDK的at-server组件开发从机模组

使用MindSDK的at-server组件开发从机模组 文章目录 使用MindSDK的at-server组件开发从机模组引言AT命令应用场景AT命令技术简介MindSDK中的at-server组件及样例工程at_port.cat_cmd_led.cmain.c 基于AT命令的人机交互应用使用串口调试助手软件发送AT命令使用Python脚本发送AT命…

Java高阶数据结构 并查集 最小生成树

并查集与最小生成树 文章目录 Java高阶数据结构 & 并查集 & 最小生成树1. 并查集1.1 并查集的原理1.1.1 例子&#xff1a;1.1.2 这样存储有什么好处呢&#xff1f; 1.2 并查集的代码实现1.2.1 类的定义与属性1.2.2 构造方法1.2.3 获取下标的方法1.2.4 获得根节点1.2.5 …

1.SpringCloud技术实用02

SpringCloud技术实用02 0.学习目标 1.Nacos配置管理 Nacos除了可以做注册中心&#xff0c;同样可以做配置管理来使用。 1.1.统一配置管理 当微服务部署的实例越来越多&#xff0c;达到数十、数百时&#xff0c;逐个修改微服务配置就会让人抓狂&#xff0c;而且很容易出错。…

ES+Redis+MySQL 高可用架构设计

一、背景 二、ES高可用方案 三、会员Redis缓存方案 四、高可用会员主库方案 五、异常会员关系治理 六、展望&#xff1a;更精细化的流控和降级策略 一、背景 会员系统是一种基础系统&#xff0c;跟公司所有业务线的下单主流程密切相关。如果会员系统出故障&#xff0c;会…

网络安全信息收集初探之域名信息收集

网络安全信息收集初探之域名信息收集 域名信息收集工具oneforall收集子域名扫描单个域名批量扫描域名oneforall 额外参数 google hacking 证书收集子域名证书子域名在线收集网站子域名收集的各种细节 域名信息收集工具 oneforall收集子域名 扫描单个域名 python oneforall.p…

进阶自动化测试,这3点你一定要知道的...

自动化测试指软件测试的自动化&#xff0c;在预设状态下运行应用程序或系统&#xff0c;预设条件包括正常和异常&#xff0c;最后评估运行结果。将人为驱动的测试行为转化为机器执行的过程。 自动化测试框架一般可以分为两个层次&#xff0c;上层是管理整个自动化测试的开发&a…

云渲染农场具有什么特点?

众所周知&#xff0c;渲染农场的出现是为了解决长时间的图像渲染问题。渲染农场的底层搭建原理是利用很多计算机、网络和操作系统来构建一个庞大的计算群组&#xff0c;把一个渲染任务从一台机器分发到这个计算群组&#xff0c;从而达到短时间内能够快速得到渲染结果。 到了20…

分布式搜索引擎es 面试突击

es elastocsearch 倒排索引是在数据查询之前建立&#xff0c;在查询的时候可以直接通过关键词定位到文档内容。用空间换时间 分布式架构原理说一下&#xff1f; es底层是基于lucene来的 大概就是一个用于全文检索的jar包 用es来做分布式的搜索引擎 可以承载一秒钟几千的…

【Vue工程】005-Vue Router

【Vue工程】005-Vue Router 文章目录 【Vue工程】005-Vue Router一、概述1、Slogan2、官网3、参考文章 二、安装三、基本使用1、定义路由2、创建路由实例3、在 main.ts 注册路由4、在 App.vue 定义路由出口 四、嵌套路由1、修改路由2、定义嵌套路由出口 五、配置404页面六、声明…

联合索引该如何选择合适的列?

前面一篇文章&#xff0c;松哥和大家聊了 MySQL 中的索引合并&#xff0c;虽然 MySQL 提供了索引合并机制来提升 SQL 执行的效率&#xff0c;然而在具体实践中&#xff0c;如果能避免发生索引合并是最好的&#xff0c;毕竟这是没办法的办法&#xff0c;是一个下下策。发生索引合…