在電商系統中買商品過程,先加入購物車,而後選中商品,點擊結算,即會進入待支付狀態,後續支付。過程須要檢驗庫存是否足夠,保證庫存不被超賣。前端
場景一:買家須要購買數量能夠多件場景二:秒殺活動,到時間點只能購買一件git
主要環節:購物車->結清->支付redis
本文講述結清時,扣庫存環節,分佈式系統產生訂單環節後續文章再詳細分析。sql
備註:挺推薦使用https://www.processon.com/在線來作流程圖的數據庫
用分佈式鎖,是爲了防刷、防止同一個用戶同一秒裏面把購物車裏的商品進行屢次結算,防止前端代碼出問題觸發兩次。利用Jedis客戶端編寫分佈式鎖後端
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);複製代碼
lockKey是redis的Key,爲用戶id+商品id+商品數量組成,這樣同一秒中只能有一次處理邏輯。requestId是redis的value,實際是當前線程id,表示有一條線程佔用。緩存
你們要注意這種分佈式鎖寫法,是同時設定超時時間的。有些分佈式鎖的文章多是比較舊版的redis不支持同時設置超時時間,他就一條語句先設置key value,另外一條語句後設置超時時間。因此你們留意一下。安全
安全扣減庫存方案有不少說法,列一下幾個方案和我推薦的方案。網絡
有的文章會用redis分佈式鎖來作保證扣庫存數量準確的環節,讓點擊結算時,後端邏輯會查詢庫存和扣庫存的update語句同時只有一條線程可以執行,以商品id爲分佈式鎖的key,鎖一個商品。可是這樣,其餘購買相同商品的用戶將會進行等待。併發
也有文章會說借鑑ConcurrenthashMap,分段鎖的機制,把100個商品,分在3個段上,key爲分段名字,value爲庫存數量。用戶下單時對用戶id進行%3計算,看落在哪一個redis的key上,就去取哪一個。
如key1=product-01,value1=33;key2=product-02,value2=33;key3=product-03,value3=33;
其實會有幾個問題:
缺點:
redis隊列的lpush、rpop都是隻能每次進出一個,對於購買多個數量的狀況下不適用,只適用於秒殺狀況購買一個的場景、或者搶紅包的場景,因此以爲不是很通用。
備註:這個搶紅包場景之後再分享。
*樣例場景:*
public void order(OrderReq req) {
String key = "product:" + req.getProductId();
// 第一步:先檢查 庫存是否充足
Integer num = (Integer) redisTemplate.get(key);
if (num == null){
// 去查數據庫的數據
// 而且把數據庫的庫存set進redis,注意使用NX參數表示只有當沒有redis中沒有這個key的時候才set庫存數量到redis
//注意要設置序列化方式爲StringRedisSerializer,否則不能把value作加減操做
// 同時設置超時時間,由於不能讓redis存着全部商品的庫存數,以避免佔用內存。
if (count >=0) {
//設置有效期十分鐘
redisTemplate.expire(key, 60*10+隨機數防止雪崩, TimeUnit.SECONDS);
}
// 減小常常訪問數據庫,由於磁盤比內存訪問速度要慢
}
if (num < req.getNum()) {
logger.info("庫存不足");
}
// 第二步:減小庫存
long value = redisTemplate.increment(key, -req.getNum().longValue());
// 庫存充足
if (value >= 0) {
logger.info("成功購買");
// update 數據庫中商品庫存和訂單系統下單,單的狀態未待支付
// 分開兩個系統處理時,能夠用LCN作分佈式事務,可是也是有機率會訂單系統的網絡超時
// 也可使用最終一致性的方式,更新庫存成功後,發送mq,等待訂單建立生成回調。
boolean res= updateProduct(req);
if (res)
createOrder(req);
} else {
// 減了後小小於0 ,如兩我的同時買這個商品,致使A人第一步時看到還有10個庫存,可是B人買9個先處理完邏輯,
// 致使B人的線程10-9=1, A人的線程1-10=-9,則如今須要增長剛剛減去的庫存,讓別人能夠買1個
redisTemplate.increment(key, req.getNum().longValue());
logger.info("恢復redis庫存");
}
}複製代碼
updateProduct方法中執行的sql以下:
update Product set count = count - #{購買數量} where id = #{id} and count - #{購買數量} >= 0;複製代碼
雖然redis已經防止了超賣,可是數據庫層面,爲了也要防止超賣,以防redis崩潰時沒法使用或者不須要redis處理時,則用樂觀鎖,由於不必定所有商品都用redis。
利用sql每條單條語句都是有事務的,因此兩條sql同時執行,也就只會有其中一條sql先執行成功,另一條後執行,也如上文說起到的場景同樣。
分開兩個系統處理庫存和訂單時,這個時候能夠用LCN框架作分佈式事務,可是由於是http請求的,也是有機率會訂單系統的網絡超時,致使未返回結果。
其實也可使用最終一致性的方式,數據表記錄一條交互流水記錄,更新庫存成功後,更新這個交互流水記錄的庫存操做字段爲已處理,訂單處理字段爲處理中,而後發送mq,等待訂單建立生成回調。也要作定時任務作主動查詢訂單系統的結果,以防沒有結果回來。
個人公衆號 :地藏思惟
掘金:地藏Kelvin
簡書:地藏Kelvin
CSDN:地藏Kelvin
個人Gitee: 地藏Kelvin gitee.com/dizang-kelv…