1. 程式人生 > >高併發下的訂單與庫存的處理

高併發下的訂單與庫存的處理

  • 問題:一件商品只有100個庫存,現在有1000或者更多的使用者來購買,每個使用者計劃同時購買1個到幾個不等商品。如何保證庫存在高併發的場景下是安全的。
    1.不多發
    2.不少發

  • 下單涉及的一些步驟
    1.下單
    2.下單同時預佔庫存
    3.支付
    4.支付成功真正減扣庫存
    5.取消訂單
    6.回退預佔庫存

  • 什麼時候進行預佔庫存
    方案一:加入購物車的時候去預佔庫存。
    方案二:下單的時候去預佔庫存。
    方案三:支付的時候去預佔庫存。
    分析:
    方案一:加入購物車並不代表使用者一定會購買,如果這個時候開始預佔庫存,會導致想購買的無法加入購物車。而不想購買的人一直佔用庫存。顯然這種做法是不可取的。
    方案二:商品加入購物車後,選擇下單,這個時候去預佔庫存。使用者選擇去支付說明了,使用者購買慾望是比 方案一 要強烈的。訂單也有一個時效,例如半個小時。超過半個小時後,系統自動取消訂單,回退預佔庫存。
    方案三:下單成功去支付的時候去預佔庫存。只有100個使用者能支付成功,900個使用者支付失敗。使用者體驗不好,就像你走了一條光明大道,一路通暢,突然被告知此處不通行。而且支付流程也是一個比較複雜的流程,如果和減庫存放在一起,將會變的更復雜。

所以綜上所述:
選擇方案二比較合理。

  • 重複下單問題

    1. 使用者點選過快,重複提交兩次。

    2. 網路延時,使用者重新整理或者點選下單重複提交。

    3. 網路框架重複請求,某些網路框架,在延時比較高的情況下會自動重複請求。

    4. 使用者惡意行為。

解決辦法

  1. 在UI攔截,點選後按鈕置灰,不能繼續點選,防止使用者,連續點選造成的重複下單。

  2. 在下單前獲取一個下單的唯一token,下單的時候需要這個token。後臺系統校驗這個 token是否有效,才繼續進行下單操作。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

    /**

     * 先生成 token 儲存到 Redis

     * token 作為 key , 並設定過期時間 時間長度 根據任務需求

     * value 為數字 自增判斷 是否使用過

     *

     * @param user

     * @return

     */

    public String createToken(User user) {

        String key = "placeOrder:token:" + user.getId();

        String token = UUID.randomUUID().toString();

        //儲存到Redis

        redisService.set(key + token, 0, 1000L);

        return token;

    }

 

    /**

     * 校驗下單的token是否有效

     * @param user

     * @param token

     * @return

     */

    public boolean checkToken(User user, String token) {

        String key = "placeOrder:token:" + user.getId();

        if (null != redisService.get(key + token)) {

            long times = redisService.increment(key + token, 1);

            if (times == 1) {

                //利用increment 原子性 判斷是否 該token 是否使用

                return true;

            else {

                // 已經使用過了

            }

            //刪除

            redisService.remove(key + token);

        }

        return false;

    }

  • 如何安全的減扣庫存

同一個使用者或者多個使用者同時搶購一個商品的時候,我們如何做到併發安全減扣庫存?

資料庫操作商品庫存:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

/**

 * Created by Administrator on 2017/9/8.

 */

public interface ProductDao extends JpaRepository<Product, Integer> {

 

    /**

     * @param pid 商品ID

     * @param num 購買數量

     * @return

     */

 

    @Transactional

    @Modifying

    @Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1")

    int reduceStock1(Integer pid, Integer num);

 

    /**

     * @param pid 商品ID

     * @param num 購買數量

     * @return

     */

 

    @Transactional

    @Modifying

    @Query("update Product set availableNum = availableNum - ?2 , reserveNum = reserveNum + ?2 where id = ?1 and  availableNum - ?2 >= 0")

    int reduceStock2(Integer pid, Integer num);

 

}

下單:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

    /**

     * 下單操作1

     *

     * @param req

     */

    private int place(PlaceOrderReq req) {

        User user = userDao.findOne(req.getUserId());

        Product product = productDao.findOne(req.getProductId());

        //下單數量

        Integer num = req.getNum();

        //可用庫存

        Integer availableNum = product.getAvailableNum();

        //可用預定

        if (availableNum >= num) {

            //減庫存

            int count = productDao.reduceStock1(product.getId(), num);

            if (count == 1) {

                //生成訂單

                createOrders(user, product, num);

            else {

                logger.info("庫存不足 3");

            }

            return 1;

        else {

            logger.info("庫存不足 4");

            return -1;

        }

    }

 

     /**

     * 下單操作2

     *

     * @param req

     */

    private int place2(PlaceOrderReq req) {

        User user = userDao.findOne(req.getUserId());

        Product product = productDao.findOne(req.getProductId());

        //下單數量

        Integer num = req.getNum();

        //可用庫存

        Integer availableNum = product.getAvailableNum();

        //可用預定

        if (availableNum >= num) {

            //減庫存

            int count = productDao.reduceStock2(product.getId(), num);

            if (count == 1) {

                //生成訂單

                createOrders(user, product, num);

            else {

                logger.info("庫存不足 3");

            }

            return 1;

        else {

            logger.info("庫存不足 4");

            return -1;

        }

    }

方法1 :

不考慮庫存安全的寫法:

1

2

3

4

5

6

7

8

9

10

11

12

13

       /**

         * 方法 1

         * 減可用

         * 加預佔

         * 庫存資料不安全

         *

         * @param req

         */

        @Override

        @Transactional

        public void placeOrder(PlaceOrderReq req) {

            place1(req);

        }

分析:
在高並的場景下,假設庫存只有 2 件 ,兩個請求同時進來,搶購改商品,購買數量都是 2.
A請求 此時去獲取庫存,發現庫存剛好足夠,執行扣庫存下單操作。
在 A 請求為完成的時候(事務未提交),B請求 此時也去獲取庫存,發現庫存還有2. 此時也去執行扣庫存,下單操作。

庫存剩 2  ,但是賣出了 4 。最終資料庫庫存數量將變為 -2 ,所以庫存是不安全的。

方法2 :

這個操作可以保證庫存資料是安全的。

1

2

3

4

5

6

7

8

9

10

11

12

13

       /**

         * 方法 2

         * 減可用

         * 加預佔

         * 庫存資料不安全

         *

         * @param req

         */

        @Override

        @Transactional

        public void placeOrder(PlaceOrderReq req) {

            place2(req);

        }

分析: 在方法1 的基礎上 ,更新庫存的語句,增加了可用庫存數量 大於 0, availableNum - num >= 0 ;實質是使用了資料庫的樂觀鎖來控制庫存安全,在併發量不是很大的情況下可以這麼做。但是如果是秒殺,搶購,瞬時流量很高的話,壓力會都到資料庫,可能拖垮資料庫。

方法3:

該方法也可以保證庫存數量安全。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/**

     * 方法 3

     * 採用 Redis 鎖  通一個時間 只能一個 請求修改 同一個商品的數量

     * <p>

     * 缺點併發不高,同時只能一個使用者搶佔操作,使用者體驗不好!

     *

     * @param req

     */

    @Override

    public void placeOrder2(PlaceOrderReq req) {

        String lockKey = "placeOrder:" + req.getProductId();

        boolean isLock = redisService.lock(lockKey);

        if (!isLock) {

            logger.info("系統繁忙稍後再試!");

            return 2;

        }

        //place2(req);

        place1(req);

        //這兩個方法都可以

        redisService.unLock(lockKey);

    }

分析:

利用Redis 分散式鎖, 強制控制 同一個商品,同時只能一個請求處理下單。 其他請求返回 ‘系統繁忙稍後再試!’;
強制把處理請求序列化,缺點併發不高 ,處理比較慢,不適合搶購等方案 。
使用者體驗也不好,明明看到庫存是充足的,就是強不到。
相比方案2減輕了資料庫的壓力。

方法4 :

可以保證庫存安全,滿足高併發處理,但是相對複雜一點。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

        /**

         * 方法 4

         * 商品的數量 等其他資訊 先儲存 到 Redis

         * 檢查庫存 與 減少庫存 不是原子性,  以 increment > 0 為準

         *

         * @param req

         */

        @Override

        public void placeOrder3(PlaceOrderReq req) {

            String key = "product:" + req.getProductId();

            // 先檢查 庫存是否充足

            Integer num = (Integer) redisService.get(key);

            if (num < req.getNum()) {

                logger.info("庫存不足 1");

            }else{

            //不可在這裡下單減庫存,否則導致資料不安全, 情況類似 方法1;

            }

            //減少庫存

            Long value = redisService.increment(key, -req.getNum().longValue());

            //庫存充足

            if (value >= 0) {

                logger.info("成功搶購 ! ");

                //TODO 真正減 扣 庫存 等操作 下單等操作  ,這些操作可用通過 MQ 或 其他方式

                place2(req);

            else {

                //庫存不足,需要增加剛剛減去的庫存

                redisService.increment(key, req.getNum().longValue());

                logger.info("庫存不足 2 ");

            }

        }

分析:
利用Redis increment 的原子操作,保證庫存安全。 事先需要把庫存的數量等其他資訊儲存到Redis,並保證更新庫存的時候,更新Redis。
進來的時候 先 get 庫存數量是否充足,再執行 increment。以 increment > 0 為準。
檢查庫存 與 減少庫存 不是原子性的。
檢查庫存的時候技術庫存充足也不可下單;否則造成庫存不安全,原來類似 方法1.
increment 是個原子操作,已這個為準。

redisService.increment(key, -req.getNum().longValue()) >= 0 說明庫存充足,可以下單。

redisService.increment(key, -req.getNum().longValue()) < 0 的時候 不能下單,次數庫存不足。並且需要 回加剛剛減去的庫存數量,否則會導致剛才減扣的數量 一直買不出去。資料庫與快取的庫存不一致。

次方法可以滿足 高並搶購等一些方案,真正減扣庫存和下單可以非同步執行。

  • 訂單時效問題,訂單取消等
    為保證商家利益,同時把商品賣給有需要的人,訂單下單成功後,往往會有個有效時間。超過這個時間,訂單取消,庫存回滾。

為每筆訂單設定 有效時間 可用參考這個:http://jblog.top/article/details/254951   

訂單取消後,可利用MQ 回退庫存等,參考:http://jblog.top/article/details/255453

要更多幹貨、技術猛料的孩子,快點拿起手機掃碼關注我,我在這裡等你哦~