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脚本的原子性。

相关推荐
黄焖鸡能干四碗5 分钟前
智慧教育,智慧校园,智慧安防学校建设解决方案(PPT+WORD)
java·大数据·开发语言·数据库·人工智能
lssjzmn13 分钟前
Spring Web 异步响应实战:从 CompletableFuture 到 ResponseBodyEmitter 的全链路优化
java·前端·后端·springboot·异步·接口优化
new_daimond26 分钟前
二级缓存在实际项目中的应用
java
一只乔哇噻33 分钟前
java后端工程师进修ing(研一版 || day41)
java·开发语言·学习·算法
User_芊芊君子1 小时前
【Java】设计模式——单例、工厂、代理模式
java·设计模式·代理模式
2301_803554521 小时前
正向代理,反向代理,负载均衡还有nginx
java·nginx·负载均衡
要开心吖ZSH1 小时前
软件设计师备考-(十六)数据结构及算法应用(重要)
java·数据结构·算法·软考·软件设计师
向上的车轮1 小时前
基于Java Spring Boot的云原生TodoList Demo 项目,验证云原生核心特性
java·spring boot·云原生
程序员清风1 小时前
快手一面:为什么要求用Static来修饰ThreadLocal变量?
java·后端·面试
逍遥德1 小时前
Java8 Comparator接口 和 List Steam 排序使用案例
java·spring boot·list·排序算法