Lua 脚本在 Redis 事务中的应用实践

Lua 脚本在 Redis 事务中的应用实践

使用过 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 8 返回列表code: 9 +0:不存在 10 +1:存在 11--]] 12local usableKey = KEYS[1] 13 14--[ 判断usableKey在Redis中是否存在 存在将过期时间延长1分钟 并返回是否存在结果--] 15local usableExists = redis.call('EXISTS', usableKey) 16if (1 == usableExists) then 17 redis.call('PEXPIRE', usableKey, 60000) 18end 19return { 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连接上完成 4List<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--]] 24local usableKey = KEYS[1]; 25local usableSubtractKey = KEYS[3] 26local operateKey = KEYS[4] 27local hSetRecord = KEYS[5] 28 29local quantity = tonumber(ARGV[1]) 30local version = ARGV[2] 31local serialNumber = ARGV[5] 32 33--[ 判断商品库存key是否存在 不存在返回-1 --] 34local usableExists = redis.call('EXISTS', usableKey); 35if (0 == usableExists) then 36 return { -1, version, 0, 0 }; 37end 38 39--[ 设置防重key 设置失败说明操作重复返回-2 --] 40local isNotRepeat = redis.call('SETNX', operateKey, version); 41if (0 == isNotRepeat) then 42 redis.call('SET', operateKey, version); 43 return { -2, version, quantity, 0 }; 44end 45 46 47--[ 商品库存量扣减后小0 说明库存不足 回滚扣减数量 并清除防重key立即过期 返回-3 --] 48local usableResult = redis.call('DECRBY', usableKey, quantity); 49if ( usableResult < 0) then 50 redis.call('INCRBY', usableKey, quantity); 51 redis.call('PEXPIRE', operateKey, 0); 52 return { -3, version, 0, usableResult }; 53end 54 55--[ 记录扣减量并设置防重key 30天后过期 返回 1--] 56-- [ 需要记录过程量与过程单据信息 --] 57local usableSubtractResult = redis.call('INCRBY', usableSubtractKey, quantity); 58redis.call('HSET', hSetRecord, serialNumber, quantity) 59redis.call('PEXPIRE', hSetRecord, 3600000) 60redis.call('PEXPIRE', operateKey, 2592000000) 61redis.call('PEXPIRE', usableKey, 3600000) 62return { 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服务器执行对应的脚本 20public String scriptLoad(File file) { 21String 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} 8Object result = stringRedisTemplate.execute(redisScriptSha1, keys, args); 9//解析执行结果 10return parseResult(operationData, result); 11 }

3 总结

Redis 在小数据操作并发可达到 10W, 针对与业务中对资源强校验且高并发场景下使用 Redis 配合 Lua 脚本完成简单逻辑处理抗并发量是个不错的选择,博主一般都是部署在cnaaa服务器上的,感兴趣的朋友可以自己部署一套尝试练习下。

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

点赞
收藏

评论区

加载中...

相关推荐

Redis事务,持久化,哨兵机制

1Redis事务基本事务指令Redis提供了一定的事务支持,可以保证一组操作原子执行不被打断,但是如果执行中出现错误,事务不能回滚,Redis未提供回滚支持。multi 开启事务exec 执行事务127.0.0.1:6379multiOK127.0.0.

Redis+Lua——他叫了外援

    Redis从2.6版本开始引入对Lua脚本的支持,通过在Redis服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令。Lua    Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。为

Redis执行Lua脚本示例

Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:1.减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。2.原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。3.

Redis笔记总结

四、事务  Redis中的事务是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务的命令要么全部执行,要么全部不执行。  事务的原理是先将一个事务的命令发给Redis,然后再让Redis依次执行这些命令。  需要注意的是Redis并没有提过像关系型数据库那样的回滚功能!不过由于Redis不支持回滚,这也使得Redis在事

Mysql系列第十五讲 事务详解

Mysql系列第十五讲什么是事务?事务的几个特性(ACID)Mysql中事务操作savepoint关键字只读事务事务中的一些问题事务的隔离级别关于隔离级别的选择什么是事务?数据库中的事务是指对数据库执行一批操作,这些操作最终要么

Redis高级特性

redis的事务(transaction)转载:https://blog.csdn.net/fmwind/article/details/78065236redis中的事务是一组命令的集合。事务同命令一样都是redis的最小执行单元。一组事务中的命令要么都执行,要么都不执行。(例如:转账)原理:先