高併發核心技術 - 訂單與庫存

問題:

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

下單涉及的一些步驟

1.下單 2.下單同時預佔庫存 3.支付 4.支付成功真正減扣庫存 5.取消訂單 6.回退預佔庫存redis

何時進行預佔庫存

方案一:加入購物車的時候去預佔庫存。 方案二:下單的時候去預佔庫存。 方案三:支付的時候去預佔庫存。數據庫

分析:

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

因此綜上所述: 選擇方案二比較合理。安全

重複下單問題

  1. 用戶點擊過快,重複提交兩次。
  2. 網絡延時,用戶刷新或者點擊下單重複提交。
  3. 網絡框架重複請求,某些網絡框架,在延時比較高的狀況下會自動重複請求。
  4. 用戶惡意行爲。

解決辦法

  1. 在UI攔截,點擊後按鈕置灰,不能繼續點擊,防止用戶,連續點擊形成的重複下單。
  2. 在下單前獲取一個下單的惟一token,下單的時候須要這個token。後臺系統校驗這個 token是否有效,才繼續進行下單操做。
/**
     * 先生成 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;
}
複製代碼
  • 如何安全的減扣庫存

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

數據庫操做商品庫存:網絡

/**
 * 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
     *
     * @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框架

不考慮庫存安全的寫法:dom

/**
     * 方法 1
     * 減可用
     * 加預佔
     * 庫存數據不安全
     *
     * @param req
     */
    @Override
    @Transactional
    public void placeOrder(PlaceOrderReq req) {
        place1(req);
    }
複製代碼

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

庫存剩 2 ,可是賣出了 4 。最終數據庫庫存數量將變爲 -2 ,因此庫存是不安全的。

方法2

這個操做能夠保證庫存數據是安全的。

/**
     * 方法 2
     * 減可用
     * 加預佔
     * 庫存數據不安全
     *
     * @param req
     */
@Override
    @Transactional
    public void placeOrder(PlaceOrderReq req) {
	place2(req);
}
複製代碼

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

方法3

該方法也能夠保證庫存數量安全。

/**
     * 方法 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

能夠保證庫存安全,知足高併發處理,可是相對複雜一點。

/**
     * 方法 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 的時候 不能下單,次數庫存不足。而且須要 回加剛剛減去的庫存數量,不然會致使剛纔減扣的數量 一直買不出去。數據庫與緩存的庫存不一致。

次方法能夠知足 高並搶購等一些方案,真正減扣庫存和下單能夠異步執行。

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

訂單取消後,可利用MQ 回退庫存等。

點關注,不迷路,這是一個程序員都想要關注的公衆號

相關文章
相關標籤/搜索