当前位置: 首页 > news >正文

springboot整合lua脚本在Redis实现商品库存扣减

1、目的

        使用lua脚本,可以保证多条命令的操作原子性;同时可以减少操作IO(比如说判断redis对应数据是否小于0,小于0就重置为100,这个场景一般是取出来再判断,再存放进行,就至少存在2次IO,用lua脚本一条命令1次IO就解决了,在批量扣减情况存在多次IO,lua脚本1次也可以解决),提高速度,降低IO.

2、使用案列

        根据传入的产品标识及数量扣减该产品数量;此处为单个产品扣减,可优化为批量产品传入,lua内部用table处理。

2.1 初始化redis参数

添加产品及库存(hash结构)。添加两种水果的库存。

// 向Redis中添加数据
redisTemplate.opsForHash().put("productMap", "pro1", "{\"name\":\"苹果\",\"stock\":100}");
redisTemplate.opsForHash().put("productMap", "pro2", "{\"name\":\"西瓜\",\"stock\":1200}");

查看结果:

2.2 业务代码

传入lua脚本,实现对应产品库存数量扣减。(可优化为多产品批量扣减)

通过setnx加锁,防止死锁设置锁超时时间,同时业务执行完手动释放锁。设置锁等待时间、及锁等待轮询获取锁。(eg:自旋释放cpu资源重新抢占资源)

@Test
public void tete(){// 向Redis中添加数据redisTemplate.opsForHash().put("productMap", "pro1", "{\"name\":\"苹果\",\"stock\":100}");redisTemplate.opsForHash().put("productMap", "pro2", "{\"name\":\"西瓜\",\"stock\":1200}");// Lua 脚本字符串    String luaScript = "local productKey = KEYS[1]; " +"local pro = KEYS[2]; " +"local lockKey = KEYS[3]; " +"local lockTimeout = tonumber(ARGV[1]); " +"local deductAmount = tonumber(ARGV[2]); " +"local spinIntervalMs = tonumber(ARGV[3]); " +"local maxSpinCount = tonumber(ARGV[4]); " +"local lockAcquired = redis.call('setnx', lockKey, 1); " +"if lockAcquired == 1 then " +"    redis.call('pexpire', lockKey, lockTimeout); " +"    local currentValue = redis.call('hget', productKey, pro); " +"    if currentValue then " +"        local dbObj = cjson.decode(currentValue);" +"        local currentStock = tonumber(dbObj.stock); " +"        if currentStock >= deductAmount then " +"            dbObj.stock = currentStock - deductAmount; " +"            local updatedValue = cjson.encode(dbObj); " +"            redis.call('hset', productKey, pro, updatedValue); " +"            redis.call('del', lockKey); " +  // 释放锁"            return true; " +"        else " +"            return false; " +"        end " +"    else " +"        return false; " +"    end " +"else " +"    local spinCount = 0; " +"    while spinCount < maxSpinCount do " +"        local lockValue = redis.call('get', lockKey); " +"        if not lockValue then " +"            lockAcquired = redis.call('setnx', lockKey, 1); " +"            if lockAcquired == 1 then " +"                redis.call('pexpire', lockKey, lockTimeout); " +"                local currentValue = redis.call('hget', productKey, pro); " +"                if currentValue then " +"                   local dbObj = cjson.decode(currentValue);" +"                   local currentStock = tonumber(dbObj.stock); " +"                    if currentStock >= deductAmount then " +"                        dbObj.stock = currentStock - deductAmount; " +"                        local updatedValue = cjson.encode(dbObj); " +"                        redis.call('hset', productKey, pro, updatedValue); " +"                        redis.call('del', lockKey); " +  "                        return true; " +"                    else " +"                        return false; " +"                    end " +"                else " +"                    return false; " +"                end " +"            end " +"            break; " +"        end " +"        spinCount = spinCount + 1; " +"    end " +"    return false; " +"end";// 创建DefaultRedisScript对象DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();script.setScriptText(luaScript);script.setResultType(Boolean.class); // 设置返回类型为Boolean// 执行脚本Boolean result = (Boolean) redisTemplate.execute(script,Collections.unmodifiableList(List.of("productMap","pro1","lock:pro1")), // KEYS参数"50000", // ARGV参数第一个:锁过期时间(毫秒)"10", // ARGV参数第二个:扣减数量"1000",// ARGV参数第3个:等待时间"5");// ARGV参数第4个:轮询次数System.out.println("result1:"+result);Boolean result2 = (Boolean) redisTemplate.execute(script,Collections.unmodifiableList(List.of("productMap","pro1","lock:pro1")), // KEYS参数"50000", // ARGV参数第一个:锁过期时间(毫秒)"10", // ARGV参数第二个:扣减数量"1000","5");System.out.println("result2:"+result2);Boolean result3 = (Boolean) redisTemplate.execute(script,Collections.unmodifiableList(List.of("productMap","pro1","lock:pro1")), // KEYS参数"50000", // ARGV参数第一个:锁过期时间(毫秒)"10", // ARGV参数第二个:扣减数量"1000","5");System.out.println("result3:"+result3);if (result2) {System.out.println("Stock deduction successful.");} else {System.out.println("Insufficient stock or lock already acquired.");}// 验证库存是否正确扣减Object updatedValue = redisTemplate.opsForHash().get("productMap", "pro1");System.out.println(updatedValue);Boolean result5 = (Boolean) redisTemplate.execute(script,Collections.unmodifiableList(List.of("productMap","pro2","lock:pro2")), // KEYS参数"5000", // ARGV参数第一个:锁过期时间(毫秒)"500", // ARGV参数第二个:扣减数量"1000","5");
}

2.3 正常执行结果

2.4 若获取锁超时,则会出现扣减失败

脚本执行时间过长会导致。(此处可通过删除手动释放锁实现:模拟业务耗时过长没办法手动释放锁需等待锁国企时间)

第二个扣减等待超时。可通过设置调整添加自旋时间重试或业务代码判断重试机制

相关文章:

springboot整合lua脚本在Redis实现商品库存扣减

1、目的 使用lua脚本&#xff0c;可以保证多条命令的操作原子性&#xff1b;同时可以减少操作IO&#xff08;比如说判断redis对应数据是否小于0&#xff0c;小于0就重置为100&#xff0c;这个场景一般是取出来再判断&#xff0c;再存放进行&#xff0c;就至少存在2次IO,用lua脚…...

MySQL ON DUPLICATE KEY UPDATE影响行数

目录 分析为什么Updates返回7 总结 数据库更新日志如下 insertOrUpdateList|> Preparing: INSERT INTO clue_user_tag (vuid, tag_id, tag_type, content) VALUES (?, ?, ?, ?) , (?, ?, ?, ?) , (?, ?, ?, ?) , (?, ?, ?, ?) ON DUPLICATE KEY UPDATE …...

uniapp小程序 slot中无法传递外部参数的解决方案

最近在封装一个List组件&#xff0c;外部传给我数据&#xff0c;我循环后将每个Item部分slot到外部&#xff0c;由调用者自己去写item布局&#xff0c;类似ElementUI、iView的Tabe列表。 List: <view v-if"list.length > 0" class"list-scroll__item&quo…...

umi实现动态获取菜单权限

文章目录 前景登录组件编写登录逻辑菜单的时机动态路由页面刷新手动修改地址 前景 不同用户拥有不同的菜单权限&#xff0c;现在我们实现登录动态获取权限菜单。 登录组件编写 //当我们需要使用dva的dispatch函数时&#xff0c;除了通过connect函数包裹组件还可以使用这种方…...

Pytest-Bdd-Playwright 系列教程(14):Docstring 参数

Pytest-Bdd-Playwright 系列教程&#xff08;14&#xff09;&#xff1a;Docstring 参数 前言一、什么是docstring?二、基本语法三、主要特点四、实际例子五、注意事项六、使用建议总结 前言 在自动化测试的过程中&#xff0c;我们经常需要处理复杂的测试数据或需要输入多行文…...

交互开发---测量工具(适用VTK或OpenGL开发的应用程序)

简介&#xff1a; 经常使用RadiAnt DICOM Viewer来查看DICOM数据&#xff0c;该软件中的测量工具比较好用&#xff0c;就想着仿照其交互方式自己实现下。后采用VTK开发应用程序时&#xff0c;经常需要开发各种各样的测量工具&#xff0c;如果沿用VTK的widgets的思路&#xff0c…...

Qt 一个简单的QChart 绘图

Qt 一个简单的QChart 绘图 先上程序运行结果图&#xff1a; “sample9_1QChart.h” 文件代码如下&#xff1a; #pragma once#include <QtWidgets/QMainWindow> #include "ui_sample9_1QChart.h"#include <QtCharts> //必须这么设置 QT_CHARTS_USE_NAME…...

【Java笔记】LinkedList 底层结构

一、LinkedList 的全面说明 LinkedList底层实现了双向链表和双端队列特点可以添加任意元素(元素可以重复)&#xff0c;包括null线程不安全&#xff0c;没有实现同步 二、LinkedList 的底层操作机制 三、LinkedList的增删改查案例 public class LinkedListCRUD { public stati…...

el-table组件树形数据修改展开箭头

<style lang"scss" scoped> ::v-deep .el-table__expand-icon .el-icon-arrow-right:before {content: ">"; // 箭头样式font-size: 16px; }::v-deep .el-table__expand-icon{ // 没有展开的状态background-color: rgba(241, 242, 245, 1);color:…...

太速科技-FMC154-基于FMC 八路SFP+万兆光纤子卡

FMC154-基于FMC 八路SFP万兆光纤子卡 一、板卡概述 本卡是一个FPGA夹层卡&#xff08;FMC&#xff09;模块&#xff0c;可提供高达8个SFP / SFP 模块接口&#xff0c;直接插入千兆位级收发器&#xff08;MGT&#xff09;的赛灵思FPGA。支持业界标准的小型可插拔&#xff0…...

记:排查设备web时慢时快问题,速度提升100%

问题描述 问题1&#xff1a; 发现web登录界面刷新和登录功能都比较卡&#xff0c;开浏览器控制台看了下&#xff0c;让我很惊讶&#xff0c;居然能这么慢&#xff1a; 公司2个局域网内的表现不同&#xff0c;局域网A中的都比较卡&#xff0c;局域网B中的又不存在该现象。 问…...

音视频入门基础:MPEG2-TS专题(13)——FFmpeg源码中,解析Section Header的实现

一、引言 在《音视频入门基础&#xff1a;MPEG2-TS专题&#xff08;11&#xff09;—— TS中的Section》中讲述了Section Header的基本概念&#xff0c;本文讲述FFmpeg源码中是怎样解析Section Header的。 二、parse_section_header函数的定义 FFmpeg源码中通过parse_section…...

根据PDF模板单个PDF导出到浏览器和多个PDF打包ZIP导出到浏览器

一、单个PDF导出到浏览器 /*** * param templatePath 模板路径* param fileName 文件名称* param data 填充文本* param images 填充图片* param response* throws IOException*/public static void generateTempPDF(String templatePath, String fileName, Map<String, S…...

如何创建一个基本的Spring Boot应用程序

以下是一个简单的Spring Boot应用开发代码示例&#xff0c;它展示了如何创建一个基本的Spring Boot应用程序&#xff0c;并实现一个简单的RESTful API服务。 步骤1&#xff1a;创建项目 使用Spring Initializr或您喜欢的IDE&#xff08;如IntelliJ IDEA或Eclipse&#xff09;…...

1.2 计算机网络的分类和应用(重要知识点)

1.2.1 计算机网络的分类 计算机网络的定义&#xff1a; 由通信线路互相连接的、能自主工作的计算机构成&#xff0c;强调各计算机&#xff08;工作站&#xff09;拥有独立的计算资源和任务能力。与多终端分时系统不同&#xff0c;后者终端仅作为主机接口&#xff0c;不具备计…...

@JsonSerialize失效解决

当在实体类中加入这个注解时&#xff0c;本意是想如果是空值则返回0给页面&#xff0c;但是发现使用 JsonSerialize(using BigSerializer.class)无效&#xff0c;因为如果是null值会不走序列化的接口实现类&#xff0c;需要使用nullUsing 需要这样使用...

Docker部署WebRTC-Streamer

文章目录 WebRTC-Streamer概述Docker部署WebRTC-StreamerVue使用WebRTC-Streamer一些问题 WebRTC-Streamer概述 WebRTC-Streamer是一个基于WebRTC技术的流媒体传输工具&#xff0c;它可以通过Web浏览器实现实时音视频流的传输和播放。它提供了一种简单而强大的方式&#xff…...

2025年的大模型计划重点在于跨领域智能、工作流自动化、多模态能力强化

明年的计划和大模型发展方向可以围绕以下几个方面展开&#xff0c;结合实际应用场景和技术趋势&#xff0c;明确可执行的目标和期待的成果&#xff1a; 2025 年计划与展望&#xff1a;大模型能做些什么&#xff1f; 1. 更深层次的跨领域能力融合 目标&#xff1a;构建更强的跨…...

day12 接口测试 ——入门→精通→实战(1)

【没有所谓的运气&#x1f36c;&#xff0c;只有绝对的努力✊】 目录 1、接口测试分类 1.1 内部接口&#xff1a; 1.2 外部接口&#xff1a; 2、目前接口架构设计 2.1、基于SOAP架构&#xff0c; 2.2、基于RPC架构&#xff0c; 2.3、基于RestFul架构&#xff0c; 2.3.1…...

伏羲0.07(文生图)

为了使0.06代码能够有效运行并输出项目目录及所有文件&#xff0c;我们在代码中添加一些额外的功能。 项目目录结构 项目目录结构如下&#xff1a; text_to_image_project/ │ ├── config.yaml ├── data/ │ ├── train_data.csv │ └── test_data.txt ├── mod…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

Netty从入门到进阶(二)

二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架&#xff0c;用于…...

Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成

一个面向 Java 开发者的 Sring-Ai 示例工程项目&#xff0c;该项目是一个 Spring AI 快速入门的样例工程项目&#xff0c;旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计&#xff0c;每个模块都专注于特定的功能领域&#xff0c;便于学习和…...

提升移动端网页调试效率:WebDebugX 与常见工具组合实践

在日常移动端开发中&#xff0c;网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时&#xff0c;开发者迫切需要一套高效、可靠且跨平台的调试方案。过去&#xff0c;我们或多或少使用过 Chrome DevTools、Remote Debug…...

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器

一、原理介绍 传统滑模观测器采用如下结构&#xff1a; 传统SMO中LPF会带来相位延迟和幅值衰减&#xff0c;并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF)&#xff0c;可以去除高次谐波&#xff0c;并且不用相位补偿就可以获得一个误差较小的转子位…...

Visual Studio Code 扩展

Visual Studio Code 扩展 change-case 大小写转换EmmyLua for VSCode 调试插件Bookmarks 书签 change-case 大小写转换 https://marketplace.visualstudio.com/items?itemNamewmaurer.change-case 选中单词后&#xff0c;命令 changeCase.commands 可预览转换效果 EmmyLua…...

Neko虚拟浏览器远程协作方案:Docker+内网穿透技术部署实践

前言&#xff1a;本文将向开发者介绍一款创新性协作工具——Neko虚拟浏览器。在数字化协作场景中&#xff0c;跨地域的团队常需面对实时共享屏幕、协同编辑文档等需求。通过本指南&#xff0c;你将掌握在Ubuntu系统中使用容器化技术部署该工具的具体方案&#xff0c;并结合内网…...