RedisSon分布式锁 源码解析,在 java 中使用 redis + lua 做秒杀

news2025/2/6 18:51:31

1. RedisSon 分布式锁

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.0</version>
</dependency>
spring:
  profiles:
    active: dev
  redis:
    cluster:
      nodes: 192.168.0.150:6379,192.168.0.151:6379,192.168.0.152:6379,192.168.0.153:6379,192.168.0.154:6379,192.168.0.155:6379
@Autowired
private RedisProperties redisProperties;

@Bean
public Redisson redisson(){
    Config config = new Config();
    List<String> nodes = redisProperties.getCluster().getNodes();
    List<String> nodeList = new ArrayList<>();
    for (String node : nodes) {
        nodeList.add("redis://"+node);
    }
    config.useClusterServers().setNodeAddresses(nodeList);
    return (Redisson) Redisson.create(config);
}

1.1 加锁

在这里插入图片描述

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
   //获取线程id
    long threadId = Thread.currentThread().getId();
    //获取锁,有就加锁,有自己的锁就重入,value值+1
    //这个方法在下边文章 1.1.1中会有解析
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // 如果返回null 表示加锁成功,锁续命成功
    if (ttl == null) {
        return;
    }
	//订阅线程id
    CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
    RedissonLockEntry entry;
    if (interruptibly) {
        entry = commandExecutor.getInterrupted(future);
    } else {
        entry = commandExecutor.get(future);
    }

    try {
       //不断循环来获取锁
        while (true) {
            ttl = tryAcquire(-1, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                break;
            }
            if (ttl >= 0) {
                try {
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    if (interruptibly) {
                        throw e;
                    }
                    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
                if (interruptibly) {
                    entry.getLatch().acquire();
                } else {
                    entry.getLatch().acquireUninterruptibly();
                }
            }
        }
    } finally {
        unsubscribe(entry, threadId);
    }
}

1.1.1 尝试获取锁

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    //锁过期时间,如果是-1 采用看门狗的 30*1000 的时间,在 1.1.1.1 中有截图
    if (leaseTime != -1) {
        //尝试获取锁方法,在1.1.1.2部分中解析
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        // lock acquired
        if (ttlRemaining == null) {
            if (leaseTime != -1) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                //增加调度任务,来给锁续命
                //在1.1.1.3 部分解析
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

1.1.1.1 默认 看门狗时间 30*1000

在这里插入图片描述
在这里插入图片描述

1.1.1.2 尝试获取锁

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
     return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
             //进行判断是否存在这个key,如果不存在返回0
             "if (redis.call('exists', KEYS[1]) == 0) then " +
             //设置类型为hashmap类型,查询的key为第一个key(KEYS[1]),hashmap的key是线程id(ARGV[2]),值为1
             "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
             //设置过期时间
             "redis.call('pexpire', KEYS[1], ARGV[1]); " +
             "return nil; " +
             "end; " +
             //通过key还有线程id查询如果有 返回 1
             "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
             //在原基础上在加1,处理重入锁问题
             "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
             //设置过期时间
             "redis.call('pexpire', KEYS[1], ARGV[1]); " +
             "return nil; " +
             "end; " +
             //返回获取的信息,如果是  -1表示没有设置过期时间,-2表示没有这个key,大于0表示剩余的过期时间(单位毫秒)
             "return redis.call('pttl', KEYS[1]);",
             //第一个key                              //第一个参数                   //第二个参数     
             Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
 }

1.1.1.3 锁续命

在这里插入图片描述
在这里插入图片描述

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            //判断这个key 和 线程id存不存在,存在返回 1
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
            //重新设置过期时间
             "redis.call('pexpire', KEYS[1], ARGV[1]); " +
             "return 1; " +
             "end; " +
             "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

1.2 解锁

在这里插入图片描述
在这里插入图片描述

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            //判断有没有这个key 和 线程id的hashmap ,如果是0 表示没有
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
            "return nil;" +
            "end; " +
            //hashmap的这个key和线程id的值 减1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            //如果还大于0,说明重入锁,没有完全解锁
            "if (counter > 0) then " +
            //设置过期时间
            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "return 0; " +
            "else " +
            //删除这个key
            "redis.call('del', KEYS[1]); " +
            //已送给其他争抢锁的线程,告诉他们 可以继续争抢锁了
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; " +
            "end; " +
            "return nil;",
            Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

2. java项目中 实现redis+lua

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.3.7.RELEASE</version>
</dependency>

2.1 请注意 在lua中存在两个重要的形参 KEYS 、ARGV

类型描述
nil无效值,类似于java的null,在条件表达式中表示false
booleanfalse或true
number双精度浮点型
string字符串,有单引号或双引号括起来
function函数,类似于linux的函数一样
table1.数组类型 2.json类型
local name = KEYS[1] -- 第一个key
local value = ARGV[1] -- 第一个参数
if tonumber(redis.call('EXISTS',name)) > 0 then -- 是否存在这个key
else
redis.call('SET',name,value);  -- 不存在 就在创建一个
end;
return redis.call('GET',name);  -- 返回这个key的value

用于java中表示为

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

 @GetMapping("/saveRedis")
 public String saveRedis(){
     StringBuilder builder = new StringBuilder();
     builder.append(" local name = KEYS[1] ");
     builder.append(" local value = ARGV[1] ");
     builder.append(" if tonumber(redis.call('EXISTS',name)) > 0 then ");
     builder.append(" else   ");
     builder.append(" redis.call('SET',name,value);   ");
     builder.append(" end; ");
     builder.append(" return redis.call('GET',name); ");
     RedisScript<String> script = RedisScript.of(builder.toString(), String.class);
     String string = UUID.randomUUID().toString();
     String result = stringRedisTemplate.execute(script, Arrays.asList("b"), string);
     return result;
 }

执行接口,发现已经在redis生成了一个 key为 aaa的数据
在这里插入图片描述
在这里插入图片描述

2.2 模拟秒杀,扣减redis中的库存

不包含其他业务,仅用作讲述lua脚本如何编写

@GetMapping("/seckill")
public String seckill(String shopName,String userName){
    StringBuilder builder = new StringBuilder();
    builder.append(" local shopName = KEYS[1] ");//商品名称
    builder.append(" local shopUser = shopName .. 'User' ");//参与秒杀的用户 集合key
    builder.append(" local userName = ARGV[1] ");//参与秒杀的用户
    //判断如果没有商品秒杀的数量,就初始化一个数量为100的,这一步为了省事才这么做,生产环境不要这样写,切记、切记!
    builder.append(" if (not redis.call('GET',shopName)) then  ");
    builder.append("    redis.call('SET',shopName,100);  ");
    builder.append(" end;   ");
    //创建一个函数,用来秒杀商品数量减1,参数秒杀用户集合增加
    builder.append(" local function seckill(shopUser,userName)   ");
    builder.append("    redis.call('SADD',shopUser,userName);   ");
    builder.append("    redis.call('DECRBY',shopName,1);   ");
    builder.append(" end;   ");
    //判断如果 秒杀的用户集合没有就调用函数创建一下
    builder.append(" if tonumber(redis.call('EXISTS',shopUser)) == 0 then   ");
    builder.append("    if tonumber(redis.call('GET',shopName)) > 0 then ");
    builder.append("       seckill(shopUser,userName)   ");
    builder.append("       return 0 ");
    builder.append("    end;   ");
    builder.append(" end;   ");
    //当前用户已秒杀过,则返回1,提示不允许重复秒杀
    builder.append(" if tonumber(redis.call('SISMEMBER',shopUser,userName)) > 0 then ");
    builder.append("    return 1   ");
    builder.append(" end;   ");
    //如果剩余商品数量大于0,就扣减库存,存储秒杀用户
    builder.append(" if tonumber(redis.call('GET',shopName)) > 0 then ");
    builder.append("    seckill(shopUser,userName)   ");
    builder.append("    return 0 ");
    builder.append(" end; ");
    builder.append(" return redis.call('GET',shopName); ");
    RedisScript<Long> script = RedisScript.of(builder.toString(), Long.class);
    String string = UUID.randomUUID().toString();
    Long result = stringRedisTemplate.execute(script, Arrays.asList(shopName), userName);
    if(result == 1){
        return "请勿重复下单";
    }else {
        return "秒杀成功";
    }
}

2.3 测试脚本

2.3.1 调用秒杀接口
秒杀成功,商品数量减1,用户秒杀集合增加当前用户

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
2.3.2 重复秒杀 直接返回失败
在这里插入图片描述
秒杀失败后,库存不变
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

Mac OSX 安装 MongoDB

1&#xff0c;简介 MongoDB是由C语言编写&#xff0c;开源而且基于分布式文件存储的介于关系数据库和非关系数据库之间的产品&#xff1b;在高负载的情况下&#xff0c;通过添加更多节点保证服务器性能&#xff1b;旨在为WEB应用提供可扩展的高性能数据存储解决方案&#xff1…

Copy-Paste

在2D视觉目标检测领域&#xff0c;由相似目标之间的重叠引起的拥挤是普遍存在的挑战。 文章地址&#xff1a;https://arxiv.org/pdf/2211.12110.pdf 研究者首先强调了拥挤问题的两个主要影响&#xff1a;1&#xff09;IoU置信度相关干扰&#xff08;ICD&#xff09;和2&#…

桥接模式

文章目录桥接模式1.桥接模式的本质2.何时选用桥接模式3.优缺点4.桥接模式的结构5.实现模拟消息发送MVC在桥接模式的体现桥接模式 桥接模式实质就是分离抽象和实现&#xff0c;抽象部分有多种&#xff0c;实现部分有多种&#xff0c;耦合在一起很难扩展&#xff0c;将其分离开来…

excel如何排序?两个方法汇总

排序是Excel中最常用的功能之一&#xff0c;也是数据分类和汇总操作的重要前提。excel如何排序&#xff1f;本文介绍如何给Excel里面的数据进行排序&#xff0c;方法很简单。感兴趣的朋友&#xff0c;赶紧来看看吧&#xff01; 操作环境&#xff1a; 演示机型&#xff1a;Dell …

PostgreSQL 导入 SLS,从业务到监控数据

日志服务SLS数据导入简介 日志服务SLS是云原生观测和分析平台&#xff0c;为Log、Metric、Trace等数据提供大规模、低成本、实时的平台化服务。日志服务是提供一站式数据采集、加工、查询与分析、可视化、告警、消费与投递等功能。全面提升在研发、运维、运营、安全等场景的数…

web常见的攻击方式有哪些,以及如何进行防御?

一、是什么 Web攻击&#xff08;WebAttack&#xff09;是针对用户上网行为或网站服务器等设备进行攻击的行为 如植入恶意代码&#xff0c;修改网站权限&#xff0c;获取网站用户隐私信息等等 Web应用程序的安全性是任何基于Web业务的重要组成部分 确保Web应用程序安全十分重…

python中的模块与包详解

目录 一.什么是模块 二.模块的导入 1.import 模块名 2.from 模块名 import 功能名 3.from 模块名 import * 4.as定义别名 模块导入总结 三.自定义模块 制作自定义模块 用pycharm演示 测试模块_ _main_ _变量的作用 演示 ‘_ _all_ _’变量 自定义模块小结 四.python中的包…

Flink集成Seatunnel

安装包下载 相关包的下载地址 Apache SeaTunnel | Apache SeaTunnel Apache Flink: Downloads 解压&#xff08;注意下载scala_2.11&#xff09; tar -zxvf flink-1.13.6-bin-scala_2.11.tgz -C ../module/ Yarn模式部署 环境准备 sudo vi /etc/profile.d/my_env.sh 修…

中国清洁清洗行业等级资质

中国商业企业管理协会清洁服务商专业委员会——“中清委”&#xff08;以下简称评定单位&#xff09;承担组织等级清洁清洗服务机构评定工作。 申请资料 (1)专业清洁清洗服务机构等级评定申请表&#xff08;附录B&#xff09;&#xff1b; (2)法人代表资格证明&#xff1…

小林Coding阅读笔记:操作系统篇之硬件结构,伪共享问题及CPU的任务执行

前言 参考/导流&#xff1a; 小林coding - 2.5 CPU 是如何执行任务的&#xff1f;学习意义 底层基础知识&#xff0c;了解CPU执行过程&#xff0c;让上层编码有效并发问题处理、思考理解调度策略、思想借鉴分析 相关说明 该篇博文是个人阅读的重要梳理&#xff0c;仅做简单参…

Transformer实现以及Pytorch源码解读(一)-数据输入篇

目标 以词性标注任务为例子&#xff0c;实现Transformer&#xff0c;并分析实现Pytorch的源码解读。 数据准备 所选的数据为nltk数据工具中的treebank数据集。treebank数据集的样子如以下两幅图所示&#xff1a; 该数据集中解释变量为若干句完整的句子&#xff1a; 被解释变…

Docker-DockerFile制定镜像

什么是DockerFile&#xff1f; DockerFile是一个用来编写Docker镜像的文本文件&#xff0c;文本内容包含了一条条构建镜像所需要的指令和说明。DockerFile就想要一个脚本文件一样。把我们想要执行的操作放到文本文件里&#xff0c;一键执行。这样我们就可以复用这个DockerFile…

读论文:Learning to Compare: Relation Network for Few-Shot Learning

Abstract 我们提出了一个概念上简单、灵活且通用的少镜头学习框架&#xff0c;其中分类器必须学习识别每个只给出少量示例的新类。我们的方法称为关系网络(RN)&#xff0c;从头到尾进行训练。在元学习过程中&#xff0c;它学习学习一个深度距离度量来比较插曲中的少量图像&…

RNA-seq 详细教程:时间点分析(14)

学习内容 了解如何使用 DESeq2 进行时间的分析LRT 使用 LRT 进行 Time course 分析尽管基因表达的静态测量很受欢迎&#xff0c;但生物过程的时程捕获对于反映其动态性质至关重要&#xff0c;特别是当模式复杂且不仅仅是上升或下降时。在处理此类数据时&#xff0c;似然比检验 …

doris入门后遇到的几个问题总结

文章目录1. Access denied for user anonymnull (using password: NO)2. timeout when waiting for send fragments RPC. Wait(sec): 5, host: xxx(ip)3. Failed to initialize JNI: Failed to find the library libjvm.so.4. 从mysql库导出的json文件大于100M时报错5. csv格式…

OA办公系统:颠覆企业办公模式,激活组织潜能打造新模式

企业的生命力在于生存&#xff0c;而想要在竞争日益激烈的市场环境下生存&#xff0c;就必须不断革新自己的内部条件&#xff0c;否则将会在发展的洪流中被社会所淘汰。如今社会的发展正在信息化世界中进行&#xff0c;企业搭建信息化平台是一条必经之道&#xff0c;而OA办公自…

太爽了!看酷开系统帮你沉浸式带娃!

现如今&#xff0c;OTT大屏涉及的线上内容与娱乐方式与日俱增&#xff0c;不仅常规的电视节目、网剧影视能够随心选择&#xff0c;还发展出以大屏为载体的短视频、健身、云游戏等丰富内容。在人们的居家生活走向常态化的当下&#xff0c;更长的开机使用时间自然对电视操作系统的…

codeforces:C. Another Array Problem【分类讨论 + 找规律】

目录题目截图题目分析ac code总结题目截图 题目分析 做cf题目别老想着套算法模版 找规律才是正道&#xff0c;这就是所谓的「思维」 n 2很简单 n > 4: # 肯定有一个最大值&#xff0c;不妨设它的位置在第三个或以后的x# 前两个值经过两次操作&#xff0c;都变为0# 第0…

Vue.js 目录结构

当我们初始化一个项目后目录结构是这样的&#xff1a; 目录解析 目录/文件说明build项目构建(webpack)相关代码config配置目录&#xff0c;包括端口号等。我们初学可以使用默认的。node_modulesnpm 加载的项目依赖模块src这里是我们要开发的目录&#xff0c;基本上要做的事情都…

# 关于“table“中更新传参回填form

关于"table"中更新传参回填form 一、id查询数据库回填form 使用阶段&#xff1a;Javaweb/ssm/Springboot出现场景&#xff1a;jsp页面&#xff08;el表达式&#xff09;、thymeleaf页面&#xff08;thymeleaf表达式&#xff0c;具体使用方法请前往百度&#xff09;…