【Redis】分布式锁之 Redission

news2024/12/23 23:14:44

一、基于setnx实现的分布式锁问题


重入问题:获得锁的线程应能再次进入相同锁的代码块,可重入锁能防止死锁。例如在HashTable中,方法用synchronized修饰,若在一个方法内调用另一个方法,不可重入会导致死锁。而synchronized和Lock锁都是可重入的。
不可重试:目前的分布式锁只能尝试一次,合理的情况是线程在获得锁失败后应能再次尝试。
超时释放:加锁时增加过期时间可防止死锁,但如果卡顿时间超长,虽采用了 lua 表达式防止删锁时误删别人的锁,但毕竟没有锁住,存在安全隐患。
主从一致性:若 Redis 提供主从集群,向集群写数据时,主机异步同步数据给从机,若同步前主机宕机,会出现死锁问题。


二、Redission 快速入门


引入依赖:根据项目需求引入 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);
    }
}


配置 Redisson 客户端:进行 Redisson 客户端的配置。

@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();
        }
        
    }
}

使用 Redission 的分布式锁:在VoucherOrderServiceImpl中注入RedissonClient,以使用 Redisson 的分布式锁功能。

@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();
        }
 }


三、Redission 可重入锁原理


在分布式锁中,Redission 采用 hash 结构存储锁。大 key 表示锁是否存在,小 key 表示当前锁被哪个线程持有。下面分析 lua 表达式的三个参数:

KEYS[1]:锁名称。
ARGV[1]:锁失效时间。
ARGV[2]:id + ":" + threadId,即锁的小 key。


执行过程如下:

redis.call('hset', KEYS[1], ARGV[2], 1),往 Redis 中写入数据,形成 hash 结构,如Lock{id + ":" + threadId : 1}。

"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]);"


若当前锁存在,第一个条件不满足,接着判断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])设置过期时间。若以上两个条件都不满足,则抢锁失败,返回锁的失效时间。


查看源码会发现,会判断当前方法的返回值是否为null。若为null,对应前两个条件,退出抢锁逻辑;若返回值不是null,即走第三个分支,在源码处会进行while(true)的自旋抢锁。

四、Redission 锁重试和 WatchDog 机制


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

先判断当前锁是否存在,若不存在,插入一把锁,返回null。
判断当前锁是否属于当前线程,若是,则返回null。


若返回值为null,代表当前线程已抢锁完毕或可重入完毕;若以上两个条件都不满足,则进入第三个条件,返回锁的失效时间。

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

接下来根据lock方法的重载情况进行处理。若传入参数,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()方法实现的,该方法表示在一定时间后执行特定任务。以锁失效时间为 30s,10s 后触发任务进行续约,将锁续约成 30s,若操作成功,会递归调用自己,重新设置任务,实现不停续约。若线程出现宕机,则不会续约,等到时间后自然释放锁。

五、Redission 锁的 MutiLock 原理


为提高 Redis 的可用性,通常会搭建集群或主从。以主从为例,写命令在主机上,主机会将数据同步给从机,但在主机还未将数据写入从机时宕机,哨兵会选举一个 slave 变成 master,此时新的 master 中没有锁信息,锁就丢失了。

Redission 提出 MutiLock 锁来解决这个问题。使用 MutiLock 锁不使用主从,每个节点地位相同,加锁逻辑需写入到每个节点上,只有所有服务器都写入成功才是加锁成功。若某个节点挂了,只要有一个节点拿不到锁,都不算加锁成功,保证了加锁的可靠性。

当设置多个锁时,Redission 会将多个锁添加到一个集合中,用while循环不停尝试拿锁,但有一个总共的加锁时间,为需要加锁的个数乘以 1500ms。例如有 3 个锁,时间就是 4500ms,在这时间内所有锁加锁成功才算加锁成功,若有线程加锁失败,则会再次重试。

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

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

相关文章

mysql练习题使用的表

dept(部门表):部门编号&#xff0c;部门名字&#xff0c;部门地点 salgrode工资等级表&#xff1a;等级&#xff0c;最高工资&#xff0c;最低工资 emp表&#xff1a;员工编号&#xff0c;员工名字&#xff0c;工作&#xff0c;领导编号MGR&#xff0c;入职时间&#xff0c;工…

Spring Boot 整合MyBatis-Plus 实现多层次树结构的异步加载功能

文章目录 1&#xff0c;前言2&#xff0c;什么是多层次树结构&#xff1f;3&#xff0c;异步加载的意义4&#xff0c;技术选型与实现思路5&#xff0c;具体案例5.1&#xff0c;项目结构5.2&#xff0c;项目配置&#xff08;pom.xml&#xff09;5.3&#xff0c;配置文件&#xf…

c++难点核心笔记(二)

系列文章目录 c难点&核心笔记(一) 继续接着上一章记录的重点内容包括函数&#xff0c;类和对象&#xff0c;指针和引用&#xff0c;C对象模型和this指针等内容&#xff0c;继续给大家分享&#xff01;&#xff01; 文章目录 系列文章目录友元全局函数做友元类做友元成员函…

国庆节怎么利用PHP发送文字短信

国庆节作为中国重要的法定节假日之一&#xff0c;不仅是全民欢庆的时刻&#xff0c;也是商家们进行促销活动的黄金时期。发送营销短信成为许多商家吸引顾客、提高销量的重要手段。 支持免费对接试用乐讯通PaaS平台 找好用的短信平台,选择乐讯通,短信群发|短信平台|群发短信软件…

分布式事务(1)

1.分布式事务 首先我们看看项目中的下单业务整体流程&#xff1a; 由于订单、购物车、商品分别在三个不同的微服务&#xff0c;而每个微服务都有自己独立的数据库&#xff0c;因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务&#xff1a; 交易服…

Python办公自动化教程(002):PDF的拆分与合并

1、PyPDF2 介绍 介绍&#xff1a; PyPDF2是一个用于处理PDF文件的Python库&#xff0c;它提供了丰富的功能来读取、编辑、合并、拆分PDF文档&#xff0c;以及提取文本、图像和其他内容。 功能&#xff1a; 读取PDF&#xff1a;PyPDF2可以轻松地打开和读取PDF文件&#xff0c;获…

pytorch 神经网络模型 2D+3D 可视化,这个工具库够猛!

生信碱移 torch模块可视化 小编近期冲浪的时候发现一个torch模型架构可视化的神级python库VisualTorch&#xff0c;给各位铁子分享一下doge。 VisualTorch旨在帮助可视化基于Torch的神经网络架构&#xff0c;似乎是今年才上传到github上。它目前支持为PyTorch的Sequential和Cu…

jQuery——jQuery的2把利器

1、jQuery 核心函数 ① 简称&#xff1a;jQuery 函数&#xff0c;即为 $ 或者 jQuery ② jQuery 库向外直接暴露的是 $ 或者 jQuery ③ 引入 jQuery 库后&#xff0c;直接使用 $ 即可 当函数用&#xff1a;$&#xff08;xxx&#xff09; 当对象用&#xff1a;$.xxx&#x…

华为官宣,不支持安卓应用的纯血鸿蒙终于来了

华为前不久与苹果新品发布会撞车的全球首款量产三折叠屏幕手机 Mate XT&#xff0c;本以为已是其下半年狠活儿担当。 但直到看完昨天下午的华为秋季全场景发布会才发现&#xff0c;好家伙&#xff0c;此前那都叫小打小闹&#xff0c;原来大招全搁在后头呢&#xff01; 这场近两…

蒙语学习快速方法,速记蒙语单词怎么学习更高效!

要高效学习蒙古语和速记单词&#xff0c;首先要掌握基础知识&#xff0c;如字母表和发音规则。接着&#xff0c;专注于学习日常用语和基础词汇&#xff0c;并运用记忆技巧如联想、发音和构词法来帮助记忆。利用专门的学习软件&#xff0c;如“蒙语学习通”&#xff0c;可以提供…

进程间通信 (一)【管道通信(上)】

目录 1. 概况2. 管道通信的原理2.1 初步理解2.2 深入理解 1. 概况 是什么&#xff1a;两个及以上的进程实现数据层面的交互&#xff0c;称为进程间的通信。 因为进程独立性的存在&#xff0c;所以一个进程无法直接访问另一个进程的数据&#xff0c;即便是父子进程&#xff0c;子…

数字IC设计\FPGA 职位经典笔试面试整理--基础篇2

1. 卡诺图 逻辑函数表达式可以使用其最小项相加来表示&#xff0c;用所有的最小项可以转换为卡诺图进行逻辑项化简 卡诺图讲解资料1 卡诺图讲解资料2 卡诺图讲解资料3 最小项的定义 一个函数的某个乘积项包含了函数的全部变量&#xff0c;其中每个变量都以原变量或反变量的形…

从传统到智能:低代码平台在生产型企业中的应用实践

在全球数字化浪潮的推动下&#xff0c;生产型企业正面临前所未有的变革压力。为了在激烈的市场竞争中保持竞争力&#xff0c;企业迫切需要通过技术手段实现业务流程的优化和创新。然而&#xff0c;传统的软件开发方式往往耗时耗力&#xff0c;难以快速响应市场需求。低代码平台…

一些依赖库的交叉编译步骤

交叉编译链版本&#xff1a;12.3.0 一、curl-7.43.0库交叉编译 libcurl是一个跨平台的网络协议库&#xff0c;支持http, https, ftp, gopher, telnet, dict, file, 和ldap 协议。libcurl同样支持HTTPS证书授权&#xff0c;HTTP POST, HTTP PUT, FTP 上传, HTTP基本表单上传&a…

Django学习实战篇六(适合略有基础的新手小白学习)(从0开发项目)

前言&#xff1a; 上一章中&#xff0c;我们完成了页面样式的配置&#xff0c;让之前简陋的页面变得漂亮了些。 整理一下目前已经完成的系统&#xff0c;从界面上看&#xff0c;已经完成了以下页面: 首页分类列表页标签列表页口博文详情页 这离我们的需求还有些距离&#xff0…

哪款手机软件适合记事?记事本软件推荐

在这个信息爆炸的时代&#xff0c;手机已经成为我们生活中不可或缺的一部分。它不仅携带方便&#xff0c;而且功能强大&#xff0c;几乎可以完成我们日常所需的所有任务。随着生活节奏的加快&#xff0c;人们越来越需要一个可靠的工具来帮助自己记录重要信息和工作事项。这时候…

德勤校招网申笔试综合能力测试SHL题库与面试真题攻略

德勤的综合能力测试&#xff08;General Ability&#xff09;是其校园招聘在线测评的关键环节&#xff0c;旨在评估应聘者的多项认知能力。以下是对这部分内容的全面整合&#xff1a; 综合能力测试&#xff08;General Ability&#xff09; 测试时长为46分钟&#xff0c;包含…

ORA-12560:TNS:协议适配器错误

今天准备在数据库服务器创建一个用户&#xff0c;使用管理员账号进行登录 sqlplus / as sysdba时&#xff0c;突然报了个ORA-12560&#xff1a;TNS&#xff1a;协议适配器错误&#xff0c;吓的我一激灵&#xff0c;不应该啊&#xff0c;之前一直都是正常的&#xff0c;也是在网…

大漠yolo-数据集标注

参考 【按键精灵】大漠插件yolo环境配置_哔哩哔哩_bilibili 1. 2. 3.启动

MySQL高阶1873-计算特殊奖金

目录 题目 准备数据 分析数据 总结 题目 编写解决方案&#xff0c;计算每个雇员的奖金。如果一个雇员的 id 是 奇数 并且他的名字不是以 M 开头&#xff0c;那么他的奖金是他工资的 100% &#xff0c;否则奖金为 0 。 返回的结果按照 employee_id 排序。 准备数据 Crea…