Lua脚本在Redis事务中的应用实践(redistemplate lua脚本)
mhr18 2024-11-05 10:24 25 浏览 0 评论
使用过Redis事务的应该清楚,Redis事务实现是通过打包多条命令,单独的隔离操作,事务中的所有命令都会按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务中的命令要么全部被执行,要么全部都不执行(原子操作)。但其中有命令因业务原因执行失败并不会阻断后续命令的执行,且也无法回滚已经执行过的命令。如果想要实现和MySQL一样的事务处理可以使用Lua脚本来实现,Lua脚本中可实现简单的逻辑判断,执行中止等操作。
1 初始Lua脚本
Lua是一个小巧的脚本语言,Redis 脚本使用 Lua 解释器来执行脚本。 Reids 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为 EVAL。编写Lua脚本就和编写shell脚本一样的简单。Lua语言详细教程参见
示例:
--[[
version:1.0
检测key是否存在,如果存在并设置过期时间
入参列表:
参数个数量:1
KEYS[1]:goodsKey 商品Key
返回列表code:
+0:不存在
+1:存在
--]]
local usableKey = KEYS[1]
--[ 判断usableKey在Redis中是否存在 存在将过期时间延长1分钟 并返回是否存在结果--]
local usableExists = redis.call('EXISTS', usableKey)
if (1 == usableExists) then
redis.call('PEXPIRE', usableKey, 60000)
end
return { usableExists }
- 示例代码中redis.call(), 是Redis内置方法,用与执行redis命令
- if () then end 是Lua语言基本分支语法
- KEYS 为Redis环境执行Lua脚本时Redis Key 参数,如果使用变量入参使用ARGV接收
- “—”代表单行注释 “—[[ 多行注释 —]]”
2 实践应用
2.1 需求分析
经典案例需求:库存量扣减并检测库存量是否充足。
基础需求分析:商品当前库存量>=扣减数量时,执行扣减。商品当前库存量<扣减数量时,返回库存不足
实现方案分析:
1)MySQL事务实现:
- 利用DB行级锁,锁定要扣减商品库存量数据,再判断库存量是否充足,充足执行扣减,否则返回库存不足。
- 执行库存扣减,再判断扣减后结果是否小于0,小于0说明库存不足,事务回滚,否则提交事务。
2)方案优缺点分析:
- 优点:MySQL天然支持事务,实现难度低。
- 缺点:不考虑热点商品场景,当业务量达到一定量级时会达到MySQL性能瓶颈,单库无法支持业务时扩展问题成为难点,分表、分库等方案对功能开发、业务运维、数据运维都须要有针对于分表、分库方案所配套的系统或方案。对于系统改造实现难度较高。
Redis Lua脚本事务实现:将库存扣减判断库存量最小原子操作逻辑编写为Lua脚本。
- 从DB中初始化商品库存数量,利用Redis WATCH命令。
- 判断商品库存量是否充足,充足执行扣减,否则返回库存不足。
- 执行库存扣减,再判断扣减后结果是否小于0,小于0说明库存不足,反向操作增加减少库存量,返回操作结果
方案优缺点分析:
- 优点:Redis命令执行单线程特性,无须考虑并发锁竟争所带来的实现复杂度。Redis天然支持Lua脚本,Lua语言学习难度低,实现与MySQL方案难度相当。Redis同一时间单位支持的并发量比MySQL大,执行耗时更小。对于业务量的增长可以扩容Redis集群分片。
- 缺点:暂无
2.2 Redis Lua脚本事务方案实现
初始化商品库存量:
//利用Watch 命令乐观乐特性,减少锁竞争所损耗的性能
public boolean init(InitStockCallback initStockCallback, InitOperationData initOperationData) {
//SessionCallback 会话级Rdis事务回调接口 针对于operations所有操作将在同一个Redis tcp连接上完成
List<Object> result = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
public List<Object> execute(RedisOperations operations) {
Assert.notNull(operations, "operations must not be null");
//Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
//当出前并发初始化同一个商品库存量时,只有一个能成功
operations.watch(initOperationData.getWatchKeys());
int initQuantity;
try {
//查询DB商品库存量
initQuantity = initStockCallback.getInitQuantity(initOperationData);
} catch (Exception e) {
//异常后释放watch
operations.unwatch();
throw e;
}
//开启Reids事务
operations.multi();
//setNx设置商品库存量
operations.opsForValue().setIfAbsent(initOperationData.getGoodsKey(), String.valueOf(initQuantity));
//设置商品库存量 key 过期时间
operations.expire(initOperationData.getGoodsKey(), Duration.ofMinutes(60000L));
///执行事事务
return operations.exec();
}
});
//判断事务执行结果
if (!CollectionUtils.isEmpty(result) && result.get(0) instanceof Boolean) {
return (Boolean) result.get(0);
}
return false;
}
库存扣减逻辑
--[[
version:1.0
减可用库存
入参列表:
参数个数量:
KEYS[1]:usableKey 商品可用量Key
KEYS[3]:usableSubtractKey 减量记录key
KEYS[4]:operateKey 操作防重Key
KEYS[5]:hSetRecord 记录操作单号信息
ARGV[1]:quantity操作数量
ARGV[2]:version 操作版本号
ARGV[5]:serialNumber 单据流水编码
ARGV[6]:record 是否记录过程量
返回列表:
+1:操作成功
0: 操作失败
-1: KEY不存在
-2:重复操作
-3: 库存不足
-4:过期操作
-5:缺量库存不足
-6:可用负库存
--]]
local usableKey = KEYS[1];
local usableSubtractKey = KEYS[3]
local operateKey = KEYS[4]
local hSetRecord = KEYS[5]
local quantity = tonumber(ARGV[1])
local version = ARGV[2]
local serialNumber = ARGV[5]
--[ 判断商品库存key是否存在 不存在返回-1 --]
local usableExists = redis.call('EXISTS', usableKey);
if (0 == usableExists) then
return { -1, version, 0, 0 };
end
--[ 设置防重key 设置失败说明操作重复返回-2 --]
local isNotRepeat = redis.call('SETNX', operateKey, version);
if (0 == isNotRepeat) then
redis.call('SET', operateKey, version);
return { -2, version, quantity, 0 };
end
--[ 商品库存量扣减后小0 说明库存不足 回滚扣减数量 并清除防重key立即过期 返回-3 --]
local usableResult = redis.call('DECRBY', usableKey, quantity);
if ( usableResult < 0) then
redis.call('INCRBY', usableKey, quantity);
redis.call('PEXPIRE', operateKey, 0);
return { -3, version, 0, usableResult };
end
--[ 记录扣减量并设置防重key 30天后过期 返回 1--]
-- [ 需要记录过程量与过程单据信息 --]
local usableSubtractResult = redis.call('INCRBY', usableSubtractKey, quantity);
redis.call('HSET', hSetRecord, serialNumber, quantity)
redis.call('PEXPIRE', hSetRecord, 3600000)
redis.call('PEXPIRE', operateKey, 2592000000)
redis.call('PEXPIRE', usableKey, 3600000)
return { 1, version, quantity, 0, usableResult ,usableSubtractResult}
初始化Lua脚本到Redis服务器
//读取Lua脚本文件
private String readLua(File file) {
StringBuilder sbf = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String temp;
while (Objects.nonNull(temp = reader.readLine())) {
sbf.append(temp);
sbf.append('\n');
}
return sbf.toString();
} catch (FileNotFoundException e) {
LOGGER.error("[{}]文件不存在", file.getPath());
} catch (IOException e) {
LOGGER.error("[{}]文件读取异常", file.getPath());
}
return null;
}
//初始化Lua脚本到Redis服务器 成功后会返回脚本对应的sha1码,系统缓存脚本sha1码,
//通过sha1码可以在Redis服务器执行对应的脚本
public String scriptLoad(File file) {
String script = readLua(file)
return stringRedisTemplate.execute((RedisCallback<String>) connection -> connection.scriptLoad(script.getBytes()));
}
脚本执行
public OperationResult evalSha(String redisScriptSha1,OperationData operationData) {
List<String> keys = operationData.getKeys();
String[] args = operationData.getArgs();
//执行Lua脚本 keys 为Lua脚本中使用到的KEYS args为Lua脚本中使用到的ARGV参数
//如果是在Redis集群模式下,同一个脚本中的多个key,要满足多个key在同一个分片
//服务器开启hash tag功能,多个key 使用{}将相同部分包裹
//例:usableKey:{EMG123} operateKey:operate:{EMG123}
Object result = stringRedisTemplate.execute(redisScriptSha1, keys, args);
//解析执行结果
return parseResult(operationData, result);
}
3 总结
Redis在小数据操作并发可达到10W,针对与业务中对资源强校验且高并发场景下使用Redis配合Lua脚本完成简单逻辑处理抗并发量是个不错的选择。
注:Lua脚本逻辑尽量简单,Lua脚本实用于耗时短且原子操作。耗时长影响Redis服务器性能,非原子操作或逻辑复杂会增加于脚本调试与维度难度。理想状态是将业务用Lua脚本包装成一个如Redis命令一样的操作。
作者:王纯
相关推荐
- 【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...
- Pure Storage推出统一数据管理云平台及新闪存阵列
-
PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...
- 对Java学习的10条建议(对java课程的建议)
-
不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...
- SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!
-
官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...
- JDK21有没有什么稳定、简单又强势的特性?
-
佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...
- 「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了
-
在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...
- Java面试题及答案最全总结(2025版)
-
大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...
- 数据库日常运维工作内容(数据库日常运维 工作内容)
-
#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...
- 分布式之系统底层原理(上)(底层分布式技术)
-
作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...
- oracle 死锁了怎么办?kill 进程 直接上干货
-
1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...
- SpringBoot 各种分页查询方式详解(全网最全)
-
一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...
- 《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略
-
《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...
- LoadRunner(loadrunner录制不到脚本)
-
一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...
- Redis数据类型介绍(redis 数据类型)
-
介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...
- RMAN备份监控及优化总结(rman备份原理)
-
今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)