Redis+Lua解决库存超卖

库存超卖是指在多个用户同时进行购买时,由于并发量大或程序设计不当,导致最终实际售出的商品数量超过了库存数量,从而引发了一系列的问题。

超卖演示

它通过获取 goods:1001 对应的值来检查商品库存是否充足 ,如果充足则执行减一和记录用户的操作,最后输出用户操作列表并返回成功;否则直接返回失败。

@GetMapping("/seckill")
    public String seckill(){
        int userId = new Random().nextInt(1000);
        ValueOperations<String,Object> ops = redisTemplate.opsForValue(); // 通过 opsForValue() 方法获取到键值对操作对象 ValueOperations
        ListOperations<String,Object> listOperations = redisTemplate.opsForList(); // 通过 opsForValue() 方法获取到键值对操作对象 opsForList
        // 它通过获取 goods:1001 对应的值来检查商品库存是否充足
        if (Integer.parseInt(ops.get("goods:1001").toString()) > 0) {
            // 减库存
            ops.decrement("goods:1001");
            // 买下的用户Id
            listOperations.leftPush("user:1001",String.valueOf(userId));
            // 输出用户名单
            System.out.println(listOperations.range("user:1001", 0, -1));
            return "success";
        }else {
            return "fail";
        }
    }

这段代码使用了 Java 的 ExecutorService 框架和 OkHttpClient 发起了一个简单的 HTTP GET 请求。在一个固定线程池中创建了 20 个线程,然后循环 100 次,每次都向指定的 URL 发起请求,并输出响应结果。

 ExecutorService pool = Executors.newFixedThreadPool(20);
        OkHttpClient okHttpClient = new OkHttpClient();
        final Request request = new Request.Builder()
                                    .url("http://localhost:7125/test/seckill")
                                    .get()
                                    .build();
        for (int i = 0; i < 100; i++) {
            final  int idx=i;
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    Call call = okHttpClient.newCall(request);
                    try {
                        Response resp = call.execute();
                        System.out.println(idx + ":" + resp.body().string());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

现在redis有个商品1001的库存为10

开始测试秒杀,发现用户名单有20名

查看库存,发现库存数量为-10,明显超卖了

redis+lua

这段代码是对上段代码的优化,创建一个 DefaultRedisScript 对象,并设置 Lua 脚本的文本和结果类型等属性。并把lua脚本加入到DefaultRedisScript 对象

/**
     * 使用 Lua 脚本可以将多个 Redis 命令封装在一个原子操作中,确保操作的一致性。这在多线程并发环境下特别有用。
     * @return
     */
    @GetMapping("/seckill_lua")
    public String seckillWithLua(){
        int userId = new Random().nextInt(1000);

        /*
            脚本流程如下:
            首先,通过 redis.call('get', KEYS[1]) 获取名为 KEYS[1] 的键对应的值,这里 KEYS[1] 是商品库存的键。
            然后,使用 tonumber() 函数将获取到的值转换成数字类型。
            接着,判断获取到的值是否大于0,如果大于0,表示商品库存充足,可以进行购买。
            如果商品库存充足,执行以下操作:
            使用 redis.call('decr', KEYS[1]) 命令将 KEYS[1] 对应的值减1,表示购买了一个商品。
            使用 redis.call('lpush', KEYS[2], ARGV[1]) 命令将 ARGV[1](用户编号)添加到名为 KEYS[2] 的列表中,表示用户的操作记录。
            返回数字1,表示购买成功。
            如果商品库存不足(获取到的值不大于0),返回数字0,表示购买失败。
         */
        String script = "if tonumber(redis.call('get', KEYS[1])) > 0 then redis.call('decr', KEYS[1]) redis.call('lpush', KEYS[2], ARGV[1]) return 1 else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); //创建一个 DefaultRedisScript 对象,并设置 Lua 脚本的文本、结果类型等属性。
        redisScript.setScriptText(script);
        redisScript.setResultType(Long.class);
        // 创建一个 keyList 列表,用于传递给 Lua 脚本的参数,其中包含了商品库存的键和用户列表的键。
        List<String> keyList = new ArrayList<>();
        keyList.add("goods:1001"); //KEYS[1] 代表商品的库存key
        keyList.add("user:1001");  //KEYS[2] 代表用户的列表key
        //ARGV[1] 代表用户的随机编号
        // 使用 redisTemplate.execute() 方法执行 Lua 脚本,传入 Lua 脚本对象、keyList 和用户随机编号作为参数,得到执行结果。
        Long result = (Long) redisTemplate.execute(redisScript, keyList, String.valueOf(userId));
        ValueOperations<String, Object> ops = redisTemplate.opsForValue();
        ListOperations<String, Object> listOperations = redisTemplate.opsForList();

        //对返回值结果的判断
        if(result == 1){
            System.out.println(ops.get("goods:1001"));
            System.out.println(listOperations.range("user:1001", 0, -1));
            return "success";
        }else{
            return "false";
        }
    }

Redis 的单线程模型是指 Redis 服务器只有一个主线程,负责接收客户端连接、解析命令、执行命令等操作。因为 Redis 是使用内存作为数据存储介质,所以单线程模型可以避免多线程操作同一块内存时的并发问题,同时也避免了线程切换带来的开销和锁粒度的问题。

redis在命令上的单线程保证了lua脚本的原子性。

相关推荐
ok!ko3 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰4 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没5 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
编程、小哥哥5 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程6 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇6 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码7 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
郭二哈7 小时前
C++——模板进阶、继承
java·服务器·c++
A尘埃7 小时前
SpringBoot的数据访问
java·spring boot·后端