使用Lua脚本通过原子减防止超卖

需求

  双十二要搞一个一分钱门票抢购的活动。

分析

  性能分析,抢购时会发生高并发,如果仅仅依靠Mysql数据库,有可能因为大量的请求频繁访问数据库造成服务器雪崩,所以考虑通过Redis减库存,最终的数据落地到DB中。

  在高并发的情况下,还要考虑到超卖的问题,因而打算使用Lua脚本完成原子减的操作。

  在这里,我们只针对减库存的操作进行分析。

实现

  不使用原子操作,出现超卖的情况。第一步:先从redis中查出库存进行判断,第二步:如果库存>0,则进行减库存的操作。

  代码实现:

 1         // 第一步:从redis中查出库存
 2         Integer stock = (Integer) RedisUtils.get("stock");
 3 
 4         // 第二步:如果库存>0,则进行减库存的操作
 5         if (stock > 0) {
 6             long spareStock = RedisUtils.decr("stock", 1);
 7             System.out.println(getName() + "抢到了第" + spareStock + "件");
 8         } else {
 9             System.out.println("库存不足");
10         }

  用多线程模拟并发请求:库存为500,创建505个线程去抢购。

1         for(int i =1;i<=505;i++){
2             MyThread2 thread =new MyThread2("线程"+i);
3             thread.start();
4         }

  执行结果:出现超卖问题,原因是:查询库存及减库存不是原子性操作。

   使用原子性操作:直接减库存。

1     public void run() {
2         long stock = RedisUtils.stock("stock");
3         if (stock > 0) {
4             System.out.println(getName() + "抢到了第" + stock + "件");
5         } else {
6             System.out.println("库存不足");
7         }
8 
9     }

  Lua脚本实现减库存操作:

 /**
     * 库存不足
     */
    public static final int LOW_STOCK = 0;
    /**
     * 不限库存
     */
    public static final long UNINITIALIZED_STOCK = -1L;

    /**
     * 执行扣库存的脚本
     */
    public static final String STOCK_LUA;

    static {
        // 初始化减库存lua脚本
        StringBuilder sb = new StringBuilder();
        sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        sb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        sb.append("    if (stock == -1) then");
        sb.append("        return 1;");
        sb.append("    end;");
        sb.append("    if (stock > 0) then");
        sb.append("        redis.call('incrby', KEYS[1], -1);");
        sb.append("        return stock;");
        sb.append("    end;");
        sb.append("    return 0;");
        sb.append("end;");
        sb.append("return -1;");

        STOCK_LUA = sb.toString();
    }

    /**
     * 扣库存
     *
     * @param key 库存key
     * @return 扣减之前剩余的库存【0:库存不足; -1:库存未初始化; 大于0:扣减库存之前的剩余库存】
     */
    public static Long stock(String key) {
        // 脚本里的KEYS参数
        List<String> keys = new ArrayList<>();
        keys.add(key);
        // 脚本里的ARGV参数
        List<String> args = new ArrayList<>();

        Long result = (Long)redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                // 集群模式
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
                }

                // 单机模式
                else if (nativeConnection instanceof Jedis) {
                    return (Long)((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
                }
                return UNINITIALIZED_STOCK;
            }
        });
        return result;
    }

  执行结果:505个线程去抢500个商品,有五个线程会抢不到,测试结果与预期一致,解决了超卖的问题。

 

参考:https://blog.csdn.net/xiaolyuh123/article/details/79208959

posted @ 2019-12-02 17:56  一马关月  阅读(2233)  评论(1编辑  收藏  举报