使用过Redis事务的应该清楚,Redis事务实现是通过打包多条命令,单独的隔离操作,事务中的所有命令都会按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务中的命令要么全部被执行,要么全部都不执行(原子操作)。但其中有命令因业务原因执行失败并不会阻断后续命令的执行,且也无法回滚已经执行过的命令。如果想要实现和MySQL一样的事务处理可以使用Lua脚本来实现,Lua脚本中可实现简单的逻辑判断,执行中止等操作。

1 初始Lua脚本

Lua是一个小巧的脚本语言,Redis 脚本使用 Lua 解释器来执行脚本。 Reids 2.6 版本通过内嵌支持 Lua 环境。执行脚本的常用命令为 EVAL。编写Lua脚本就和编写shell脚本一样的简单。Lua语言详细教程参见

示例:

  1. --[[
  2. version:1.0
  3. 检测key是否存在,如果存在并设置过期时间
  4. 入参列表:
  5. 参数个数量:1
  6. KEYS[1]:goodsKey 商品Key
  7. 返回列表code:
  8. +0:不存在
  9. +1:存在
  10. --]]
  11. local usableKey = KEYS[1]
  12. --[ 判断usableKeyRedis中是否存在 存在将过期时间延长1分钟 并返回是否存在结果--]
  13. local usableExists = redis.call('EXISTS', usableKey)
  14. if (1 == usableExists) then
  15. redis.call('PEXPIRE', usableKey, 60000)
  16. end
  17. return { usableExists }
  1. 示例代码中redis.call(), 是Redis内置方法,用与执行redis命令
  2. if () then end 是Lua语言基本分支语法
  3. KEYS 为Redis环境执行Lua脚本时Redis Key 参数,如果使用变量入参使用ARGV接收
  4. “—”代表单行注释 “—[[ 多行注释 —]]”

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脚本事务方案实现

初始化商品库存量:

  1. //利用Watch 命令乐观乐特性,减少锁竞争所损耗的性能
  2. public boolean init(InitStockCallback initStockCallback, InitOperationData initOperationData) {
  3. //SessionCallback 会话级Rdis事务回调接口 针对于operations所有操作将在同一个Redis tcp连接上完成
  4. List<Object> result = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
  5. public List<Object> execute(RedisOperations operations) {
  6. Assert.notNull(operations, "operations must not be null");
  7. //Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
  8. //当出前并发初始化同一个商品库存量时,只有一个能成功
  9. operations.watch(initOperationData.getWatchKeys());
  10. int initQuantity;
  11. try {
  12. //查询DB商品库存量
  13. initQuantity = initStockCallback.getInitQuantity(initOperationData);
  14. } catch (Exception e) {
  15. //异常后释放watch
  16. operations.unwatch();
  17. throw e;
  18. }
  19. //开启Reids事务
  20. operations.multi();
  21. //setNx设置商品库存量
  22. operations.opsForValue().setIfAbsent(initOperationData.getGoodsKey(), String.valueOf(initQuantity));
  23. //设置商品库存量 key 过期时间
  24. operations.expire(initOperationData.getGoodsKey(), Duration.ofMinutes(60000L));
  25. ///执行事事务
  26. return operations.exec();
  27. }
  28. });
  29. //判断事务执行结果
  30. if (!CollectionUtils.isEmpty(result) && result.get(0) instanceof Boolean) {
  31. return (Boolean) result.get(0);
  32. }
  33. return false;
  34. }

库存扣减逻辑

  1. --[[
  2. version:1.0
  3. 减可用库存
  4. 入参列表:
  5. 参数个数量:
  6. KEYS[1]:usableKey 商品可用量Key
  7. KEYS[3]:usableSubtractKey 减量记录key
  8. KEYS[4]:operateKey 操作防重Key
  9. KEYS[5]:hSetRecord 记录操作单号信息
  10. ARGV[1]:quantity操作数量
  11. ARGV[2]:version 操作版本号
  12. ARGV[5]:serialNumber 单据流水编码
  13. ARGV[6]:record 是否记录过程量
  14. 返回列表:
  15. +1:操作成功
  16. 0: 操作失败
  17. -1: KEY不存在
  18. -2:重复操作
  19. -3: 库存不足
  20. -4:过期操作
  21. -5:缺量库存不足
  22. -6:可用负库存
  23. --]]
  24. local usableKey = KEYS[1];
  25. local usableSubtractKey = KEYS[3]
  26. local operateKey = KEYS[4]
  27. local hSetRecord = KEYS[5]
  28. local quantity = tonumber(ARGV[1])
  29. local version = ARGV[2]
  30. local serialNumber = ARGV[5]
  31. --[ 判断商品库存key是否存在 不存在返回-1 --]
  32. local usableExists = redis.call('EXISTS', usableKey);
  33. if (0 == usableExists) then
  34. return { -1, version, 0, 0 };
  35. end
  36. --[ 设置防重key 设置失败说明操作重复返回-2 --]
  37. local isNotRepeat = redis.call('SETNX', operateKey, version);
  38. if (0 == isNotRepeat) then
  39. redis.call('SET', operateKey, version);
  40. return { -2, version, quantity, 0 };
  41. end
  42. --[ 商品库存量扣减后小0 说明库存不足 回滚扣减数量 并清除防重key立即过期 返回-3 --]
  43. local usableResult = redis.call('DECRBY', usableKey, quantity);
  44. if ( usableResult < 0) then
  45. redis.call('INCRBY', usableKey, quantity);
  46. redis.call('PEXPIRE', operateKey, 0);
  47. return { -3, version, 0, usableResult };
  48. end
  49. --[ 记录扣减量并设置防重key 30天后过期 返回 1--]
  50. -- [ 需要记录过程量与过程单据信息 --]
  51. local usableSubtractResult = redis.call('INCRBY', usableSubtractKey, quantity);
  52. redis.call('HSET', hSetRecord, serialNumber, quantity)
  53. redis.call('PEXPIRE', hSetRecord, 3600000)
  54. redis.call('PEXPIRE', operateKey, 2592000000)
  55. redis.call('PEXPIRE', usableKey, 3600000)
  56. return { 1, version, quantity, 0, usableResult ,usableSubtractResult}

初始化Lua脚本到Redis服务器

  1. //读取Lua脚本文件
  2. private String readLua(File file) {
  3. StringBuilder sbf = new StringBuilder();
  4. try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
  5. String temp;
  6. while (Objects.nonNull(temp = reader.readLine())) {
  7. sbf.append(temp);
  8. sbf.append('\n');
  9. }
  10. return sbf.toString();
  11. } catch (FileNotFoundException e) {
  12. LOGGER.error("[{}]文件不存在", file.getPath());
  13. } catch (IOException e) {
  14. LOGGER.error("[{}]文件读取异常", file.getPath());
  15. }
  16. return null;
  17. }
  18. //初始化Lua脚本到Redis服务器 成功后会返回脚本对应的sha1码,系统缓存脚本sha1码,
  19. //通过sha1码可以在Redis服务器执行对应的脚本
  20. public String scriptLoad(File file) {
  21. String script = readLua(file)
  22. return stringRedisTemplate.execute((RedisCallback<String>) connection -> connection.scriptLoad(script.getBytes()));
  23. }

脚本执行

  1. public OperationResult evalSha(String redisScriptSha1,OperationData operationData) {
  2. List<String> keys = operationData.getKeys();
  3. String[] args = operationData.getArgs();
  4. //执行Lua脚本 keys 为Lua脚本中使用到的KEYS args为Lua脚本中使用到的ARGV参数
  5. //如果是在Redis集群模式下,同一个脚本中的多个key,要满足多个key在同一个分片
  6. //服务器开启hash tag功能,多个key 使用{}将相同部分包裹
  7. //例:usableKey:{EMG123} operateKey:operate:{EMG123}
  8. Object result = stringRedisTemplate.execute(redisScriptSha1, keys, args);
  9. //解析执行结果
  10. return parseResult(operationData, result);
  11. }

3 总结

Redis在小数据操作并发可达到10W,针对与业务中对资源强校验且高并发场景下使用Redis配合Lua脚本完成简单逻辑处理抗并发量是个不错的选择。

注:Lua脚本逻辑尽量简单,Lua脚本实用于耗时短且原子操作。耗时长影响Redis服务器性能,非原子操作或逻辑复杂会增加于脚本调试与维度难度。理想状态是将业务用Lua脚本包装成一个如Redis命令一样的操作。


作者:王纯

Lua脚本在Redis事务中的应用实践的更多相关文章

  1. Lua脚本在redis分布式锁场景的运用

    目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...

  2. c#中用lua脚本执行redis命令

    直接贴出代码,实现执行lua脚本的方法,用到的第三方类库是 StackExchange.Redis(nuget上有) 注:下面的代码是简化后的,实际使用要修改, using System; using ...

  3. Redis进阶之使用Lua脚本自定义Redis命令

    [本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 1.在Redis ...

  4. 运维实践-最新Nginx二进制构建编译lua-nginx-module动态链接Lua脚本访问Redis数据库读取静态资源隐式展现

    关注「WeiyiGeek」公众号 设为「特别关注」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 0x0n 前言 ...

  5. nginx插入lua脚本访问redis

    目标:收集用户日志 流程: 浏览器端get方法将数据传到nginx服务 nginx收集到数据,执行内嵌lua脚本,访问redis,根据token获得用户id 将日志信息存入文件 1.nginx安装,参 ...

  6. redis事务中的WATCH命令和基于CAS的乐观锁

    转自:http://blog.sina.com.cn/s/blog_ae8441630101cgy3.html 在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能. ...

  7. 使用Lua 脚本实现redis 分布式锁,报错:ERR Error running script (call to f_8ea1e266485534d17ddba5af05c1b61273c30467): @user_script:10: @user_script: 10: Lua redis() command arguments must be strings or integers .

    在使用SpringBoot开发时,使用RedisTemplate执行 redisTemplate.execute(lockScript, redisList); 发现报错: ERR Error run ...

  8. 使用nginx+lua脚本读写redis缓存

    配置 新建spring boot项目增加redis配置 <dependency> <groupId>org.springframework.boot</groupId&g ...

  9. Redis篇:事务和lua脚本的使用

    现在多数秒杀,抽奖,抢红包等大并发高流量的功能一般都是基于 redis 实现,然而在选择 redis 的时候,我们也要了解 redis 如何保证服务正确运行的原理 前言 redis 如何实现高性能和高 ...

随机推荐

  1. MongoDB 的安装和基本操作

    MongoDB 的安装 使用 docker 安装 下载镜像: docker pull mongo:4.4.8(推荐,下载指定版本) docker pull mongo:latest (默认下载最新版本 ...

  2. go-zero微服务实战系列(九、极致优化秒杀性能)

    上一篇文章中引入了消息队列对秒杀流量做削峰的处理,我们使用的是Kafka,看起来似乎工作的不错,但其实还是有很多隐患存在,如果这些隐患不优化处理掉,那么秒杀抢购活动开始后可能会出现消息堆积.消费延迟. ...

  3. 实时数据引擎系列(五): 关于 SQL Server 与 SQL Server CDC

      摘要:在企业客户里, SQL Server 在传统的制造业依然散发着持久的生命力,SQL Server 的 CDC 复杂度相比 Oracle 较低, 因此标准的官方派做法就是直接使用这个 CDC ...

  4. 配置SSM公钥及创建远程仓库和在IEDA中集成git操作

    3.将.ssh下的id_rsa.pub公钥copy到gitee工作台中 4.创建个人仓库 5.设置开源许可证:开源是否可以随意转载,开源但是不能商业使用,不能转载,- 限制! 6.克隆到本地! IDE ...

  5. Trie 树总结

    Trie,又经常叫前缀树,字典树等等.它有很多变种,如后缀树,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree.当然很多名字的意义其实有交 ...

  6. 关于分组查询的一道sql题

    背景:想做一道sql的测试题,题目为: 按照角色分组算出每个角色按有办公室和没办公室的统计人数(列出角色,数量,有无办公室,注意一个角色如果部分有办公室,部分没有需分开统计) 如下,构造测试环境与对应 ...

  7. 【机器学习基础】——另一个视角解释SVM

    SVM的另一种解释 前面已经较为详细地对SVM进行了推导,前面有提到SVM可以利用梯度下降来进行求解,但并未进行详细的解释,本节主要从另一个视角对SVM进行解释,首先先回顾之前有关SVM的有关内容,然 ...

  8. .Net之时间轮算法(终极版)定时任务

    TimeWheelDemo 一个基于时间轮原理的定时器 对时间轮的理解 其实我是有一篇文章(.Net 之时间轮算法(终极版))针对时间轮的理论理解的,但是,我想,为啥我看完时间轮原理后,会采用这样的方 ...

  9. 从零开始Blazor Server(8)--增加菜单以及调整位置

    这篇干啥 这篇文章主要是把前面的一些东西稍微调整一下,使其更适合后面的内容. 主要是两个事,一个是把原来的PermissionEntity直接变成MenuEntity,直接让最后一级是菜单,这样后面就 ...

  10. DolphinScheduler 线上 Meetup 视频回放(07.25)

    上周六下午 DolphinScheduler 社区联合 Doris 社区进行了 2020 年首次线上 Meetup,各位讲师都做了非常精彩的分享,也吸引了 1900 多位技术伙伴观看. 其中 Dolp ...