使用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM