Redis+Lua解决库存超卖

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

超卖演示

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

@GetMapping("/seckill")
    public String seckill(){
        int userId = new Random().nextInt(1000);
        ValueOperations ops = redisTemplate.opsForValue(); // 通过 opsForValue() 方法获取到键值对操作对象 ValueOperations
        ListOperations 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名

Redis+Lua解决库存超卖_第1张图片 查看库存,发现库存数量为-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 redisScript = new DefaultRedisScript<>(); //创建一个 DefaultRedisScript 对象,并设置 Lua 脚本的文本、结果类型等属性。
        redisScript.setScriptText(script);
        redisScript.setResultType(Long.class);
        // 创建一个 keyList 列表,用于传递给 Lua 脚本的参数,其中包含了商品库存的键和用户列表的键。
        List 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 ops = redisTemplate.opsForValue();
        ListOperations 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";
        }
    }

测试,开启线程

  ExecutorService pool = Executors.newFixedThreadPool(20);
        OkHttpClient okHttpClient = new OkHttpClient();
        final Request request = new Request.Builder()
                                    .url("http://localhost:7125/test/seckill_lua")
                                    .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+Lua解决库存超卖_第2张图片

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

Redis+Lua解决库存超卖_第3张图片

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

你可能感兴趣的:(工作问题总结,redis,lua,java)