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结构)。添加两种水果的库存。

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

查看结果:

2.2 业务代码

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

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

java 复制代码
@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 若获取锁超时,则会出现扣减失败

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

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

相关推荐
FQNmxDG4S8 小时前
Java多线程编程:Thread与Runnable的并发控制
java·开发语言
虹科网络安全8 小时前
艾体宝干货|数据复制详解:类型、原理与适用场景
java·开发语言·数据库
axng pmje9 小时前
Java语法进阶
java·开发语言·jvm
HackTorjan9 小时前
深度神经网络的反向传播与梯度优化原理
人工智能·spring boot·神经网络·机器学习·dnn
rKWP8gKv79 小时前
Java微服务性能监控:Prometheus与Grafana集成方案
java·微服务·prometheus
老前端的功夫9 小时前
【Java从入门到入土】28:Stream API:告别for循环的新时代
java·开发语言·python
qq_435287929 小时前
第9章 夸父逐日与后羿射日:死循环与进程终止?十个太阳同时值班的并行冲突
java·开发语言·git·死循环·进程终止·并行冲突·夸父逐日
小江的记录本9 小时前
【Kafka核心】架构模型:Producer、Broker、Consumer、Consumer Group、Topic、Partition、Replica
java·数据库·分布式·后端·搜索引擎·架构·kafka
yaoxin5211239 小时前
397. Java 文件操作基础 - 创建常规文件与临时文件
java·开发语言·python
极客先躯12 小时前
高级java每日一道面试题-2025年11月24日-容器与虚拟化题[Dockerj]-runc 的作用是什么?
java·oci 的命令行工具·最小可用·无守护进程·完全标准·创建容器的核心流程·runc 核心职责思维导图