高并發(fā)場(chǎng)景-訂單庫(kù)存防止超賣

背景

在電商系統(tǒng)中買商品過(guò)程楞陷,先加入購(gòu)物車怔鳖,然后選中商品,點(diǎn)擊結(jié)算固蛾,即會(huì)進(jìn)入待支付狀態(tài)结执,后續(xù)支付度陆。
過(guò)程需要檢驗(yàn)庫(kù)存是否足夠,保證庫(kù)存不被超賣献幔。

場(chǎng)景一:買家需要購(gòu)買數(shù)量可以多件
場(chǎng)景二:秒殺活動(dòng)懂傀,到時(shí)間點(diǎn)只能購(gòu)買一件

目的

  • 防止相同用戶重復(fù)下單
  • 檢查庫(kù)存準(zhǔn)確數(shù)量
  • 防止扣錯(cuò)庫(kù)存數(shù)量
  • 扣庫(kù)存時(shí)性能效率提升、不阻塞用戶

主要解決手段

  • 利用redis的incr斜姥、decr的原子性做操作
  • redis的lpush鸿竖、rpop的原子性做操作,但是這個(gè)只能一個(gè)一個(gè)的扣铸敏,但不能原子地同時(shí)扣多個(gè)
  • sql樂(lè)觀鎖

交互流程

商品扣庫(kù)存.jpg

主要環(huán)節(jié):購(gòu)物車->結(jié)清->支付

本文講述結(jié)清時(shí)缚忧,扣庫(kù)存環(huán)節(jié),分布式系統(tǒng)產(chǎn)生訂單環(huán)節(jié)后續(xù)文章再詳細(xì)分析杈笔。

備注:挺推薦使用https://www.processon.com/在線來(lái)做流程圖的

一闪水、防止重復(fù)

利用redis分布式鎖

用分布式鎖,是為了防刷蒙具、防止同一個(gè)用戶同一秒里面把購(gòu)物車?yán)锏纳唐愤M(jìn)行多次結(jié)算球榆,防止前端代碼出問(wèn)題觸發(fā)兩次。
利用Jedis客戶端編寫分布式鎖

  String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

lockKey是redis的Key禁筏,為用戶id+商品id+商品數(shù)量組成持钉,這樣同一秒中只能有一次處理邏輯。
requestId是redis的value篱昔,實(shí)際是當(dāng)前線程id每强,表示有一條線程占用。

大家要注意這種分布式鎖寫法州刽,是同時(shí)設(shè)定超時(shí)時(shí)間的空执。有些分布式鎖的文章可能是比較舊版的redis不支持同時(shí)設(shè)置超時(shí)時(shí)間,他就一條語(yǔ)句先設(shè)置key value穗椅,另一條語(yǔ)句后設(shè)置超時(shí)時(shí)間辨绊。所以大家留意一下。

二匹表、扣減庫(kù)存

安全扣減庫(kù)存方案有很多說(shuō)法门坷,列一下幾個(gè)方案和我推薦的方案。

方案一:分布式鎖

有的文章會(huì)用redis分布式鎖來(lái)做保證扣庫(kù)存數(shù)量準(zhǔn)確的環(huán)節(jié)桑孩,讓點(diǎn)擊結(jié)算時(shí)拜鹤,后端邏輯會(huì)查詢庫(kù)存和扣庫(kù)存的update語(yǔ)句同時(shí)只有一條線程能夠執(zhí)行,以商品id為分布式鎖的key流椒,鎖一個(gè)商品敏簿。但是這樣,其他購(gòu)買相同商品的用戶將會(huì)進(jìn)行等待。

  • 優(yōu)點(diǎn):這樣做雖然安全
  • 缺點(diǎn):但是失去的是性能問(wèn)題惯裕。

方案二:分布式鎖+分段緩存

也有文章會(huì)說(shuō)借鑒ConcurrenthashMap温数,分段鎖的機(jī)制,把100個(gè)商品蜻势,分在3個(gè)段上撑刺,key為分段名字,value為庫(kù)存數(shù)量握玛。用戶下單時(shí)對(duì)用戶id進(jìn)行%3計(jì)算够傍,看落在哪個(gè)redis的key上,就去取哪個(gè)挠铲。

如key1=product-01,value1=33;key2=product-02,value2=33;key3=product-03,value3=33;

其實(shí)會(huì)有幾個(gè)問(wèn)題:

  • 一個(gè)是用戶想買34件的時(shí)候冕屯,要去兩個(gè)片查
  • 一個(gè)片上賣完了為0,又要去另外一個(gè)片查
  • 取余方式計(jì)算每一片數(shù)量拂苹,除不盡時(shí)安聘,讓最后一片補(bǔ),如100/3=33.33瓢棒。

缺點(diǎn):

  • 方案復(fù)雜
  • 有遺留問(wèn)題

方案三: redis的lpush rpop

redis隊(duì)列的lpush浴韭、rpop都是只能每次進(jìn)出一個(gè),對(duì)于購(gòu)買多個(gè)數(shù)量的情況下不適用脯宿,只適用于秒殺情況購(gòu)買一個(gè)的場(chǎng)景念颈、或者搶紅包的場(chǎng)景,所以覺(jué)得不是很通用连霉。

備注:這個(gè)搶紅包場(chǎng)景以后再分享舍肠。

方案四:推薦使用redis原子操作+sql樂(lè)觀鎖

利用Redis increment 的原子操作,保證庫(kù)存數(shù)安全

  1. 先查詢r(jià)edis中是否有庫(kù)存信息窘面,如果沒(méi)有就去數(shù)據(jù)庫(kù)查,這樣就可以減少訪問(wèn)數(shù)據(jù)庫(kù)的次數(shù)叽躯。
    獲取到后把數(shù)值填入redis财边,以商品id為key,數(shù)量為value点骑。
    注意要設(shè)置序列化方式為StringRedisSerializer酣难,不然不能把value做加減操作。
    還需要設(shè)置redis對(duì)應(yīng)這個(gè)key的超時(shí)時(shí)間黑滴,以防所有商品庫(kù)存數(shù)據(jù)都在redis中憨募。

  2. 比較下單數(shù)量的大小,如果夠就做后續(xù)邏輯袁辈。

  3. 執(zhí)行redis客戶端的increment菜谣,參數(shù)為負(fù)數(shù),則做減法。因?yàn)閞edis是單線程處理尾膊,并且因?yàn)?strong>increment讓key對(duì)應(yīng)的value 減少后返回的是修改后的值媳危。
    有的人會(huì)不做第一步查詢直接減,其實(shí)這樣不太好冈敛,因?yàn)楫?dāng)庫(kù)存為1時(shí)待笑,很多做減3,或者減30情況抓谴,其實(shí)都是不夠暮蹂,這樣就白減。

  4. 扣減數(shù)據(jù)庫(kù)的庫(kù)存癌压,這個(gè)時(shí)候就不需要再select查詢仰泻,直接樂(lè)觀鎖update,把庫(kù)存字段值減1 措拇。

  5. 做完扣庫(kù)存就在訂單系統(tǒng)做下單我纪。

樣例場(chǎng)景:

  1. 假設(shè)兩個(gè)用戶在第一步查詢得到庫(kù)存等于10,A用戶走到第二步扣10件丐吓,同時(shí)一秒內(nèi)B用戶走到第二部扣3件浅悉。
  2. 因?yàn)閞edis單線程處理,若A用戶線程先執(zhí)行redis語(yǔ)句券犁,那么現(xiàn)在庫(kù)存等于0术健,B就只能失敗,就不會(huì)出更新數(shù)據(jù)庫(kù)了粘衬。
    public void order(OrderReq req) {
        String key = "product:" + req.getProductId();
        // 第一步:先檢查 庫(kù)存是否充足
        Integer num = (Integer) redisTemplate.get(key);
        if (num == null){
          // 去查數(shù)據(jù)庫(kù)的數(shù)據(jù)
          // 并且把數(shù)據(jù)庫(kù)的庫(kù)存set進(jìn)redis荞估,注意使用NX參數(shù)表示只有當(dāng)沒(méi)有redis中沒(méi)有這個(gè)key的時(shí)候才set庫(kù)存數(shù)量到redis
          //注意要設(shè)置序列化方式為StringRedisSerializer,不然不能把value做加減操作
          // 同時(shí)設(shè)置超時(shí)時(shí)間稚新,因?yàn)椴荒茏宺edis存著所有商品的庫(kù)存數(shù)勘伺,以免占用內(nèi)存。
           if (count >=0) {
            //設(shè)置有效期十分鐘
            redisTemplate.expire(key, 60*10+隨機(jī)數(shù)防止雪崩, TimeUnit.SECONDS);
        }
          // 減少經(jīng)常訪問(wèn)數(shù)據(jù)庫(kù)褂删,因?yàn)榇疟P比內(nèi)存訪問(wèn)速度要慢
        }
        if (num < req.getNum()) {
            logger.info("庫(kù)存不足");
        }
        // 第二步:減少庫(kù)存
        long value = redisTemplate.increment(key, -req.getNum().longValue());
        // 庫(kù)存充足
        if (value >= 0) {
            logger.info("成功購(gòu)買");
            // update 數(shù)據(jù)庫(kù)中商品庫(kù)存和訂單系統(tǒng)下單飞醉,單的狀態(tài)未待支付
            // 分開(kāi)兩個(gè)系統(tǒng)處理時(shí),可以用LCN做分布式事務(wù)屯阀,但是也是有概率會(huì)訂單系統(tǒng)的網(wǎng)絡(luò)超時(shí)
            // 也可以使用最終一致性的方式缅帘,更新庫(kù)存成功后,發(fā)送mq难衰,等待訂單創(chuàng)建生成回調(diào)钦无。
            boolean res= updateProduct(req);
            if (res)
                createOrder(req);
        } else {
            // 減了后小小于0 ,如兩個(gè)人同時(shí)買這個(gè)商品盖袭,導(dǎo)致A人第一步時(shí)看到還有10個(gè)庫(kù)存失暂,但是B人買9個(gè)先處理完邏輯彼宠,
            // 導(dǎo)致B人的線程10-9=1, A人的線程1-10=-9,則現(xiàn)在需要增加剛剛減去的庫(kù)存趣席,讓別人可以買1個(gè)
            redisTemplate.increment(key, req.getNum().longValue());
            logger.info("恢復(fù)redis庫(kù)存");
        }
    }

update使用樂(lè)觀鎖

updateProduct方法中執(zhí)行的sql如下:

update Product set count = count - #{購(gòu)買數(shù)量} where id = #{id} and count - #{購(gòu)買數(shù)量} >= 0;

雖然redis已經(jīng)防止了超賣兵志,但是數(shù)據(jù)庫(kù)層面,為了也要防止超賣宣肚,以防redis崩潰時(shí)無(wú)法使用或者不需要redis處理時(shí)想罕,則用樂(lè)觀鎖,因?yàn)椴灰欢ㄈ可唐范加胷edis霉涨。

利用sql每條單條語(yǔ)句都是有事務(wù)的按价,所以兩條sql同時(shí)執(zhí)行,也就只會(huì)有其中一條sql先執(zhí)行成功笙瑟,另外一條后執(zhí)行楼镐,也如上文提及到的場(chǎng)景一樣。

簡(jiǎn)單說(shuō)一下分布式事務(wù):

分開(kāi)兩個(gè)系統(tǒng)處理庫(kù)存和訂單時(shí)往枷,這個(gè)時(shí)候可以用LCN框架做分布式事務(wù)框产,但是因?yàn)槭莌ttp請(qǐng)求的,也是有概率會(huì)訂單系統(tǒng)的網(wǎng)絡(luò)超時(shí)错洁,導(dǎo)致未返回結(jié)果秉宿。

其實(shí)也可以使用最終一致性的方式,數(shù)據(jù)表記錄一條交互流水記錄屯碴,更新庫(kù)存成功后描睦,更新這個(gè)交互流水記錄的庫(kù)存操作字段為已處理,訂單處理字段為處理中导而,然后發(fā)送mq忱叭,等待訂單創(chuàng)建生成回調(diào)。也要做定時(shí)任務(wù)做主動(dòng)查詢訂單系統(tǒng)的結(jié)果今艺,以防沒(méi)有結(jié)果回來(lái)韵丑。

方案優(yōu)勢(shì)

  • 不需要頻繁訪問(wèn)數(shù)據(jù)庫(kù)商品庫(kù)存還有多少
  • 不阻塞其他用戶
  • 安全扣減庫(kù)存量
  • 內(nèi)存訪問(wèn)庫(kù)存數(shù)量,減少數(shù)據(jù)庫(kù)交互

高并發(fā)額外優(yōu)化

  • 用戶訪問(wèn)下單是虚缎,前端ui可以讓用戶觸發(fā)結(jié)算后埂息,把按鈕置灰色,防止重復(fù)觸發(fā)遥巴。
  • 可以按照庫(kù)存數(shù)量來(lái)選定是否要用redis,因?yàn)槿绻麕?kù)存數(shù)量少享幽,或者說(shuō)最近下單次數(shù)少的商品铲掐,就不用放redis,因?yàn)樯偃丝春唾I的情況下值桩,不必放redis導(dǎo)致占用內(nèi)存摆霉。
  • 如果到時(shí)間點(diǎn)搶購(gòu)時(shí),可以使用mq隊(duì)列形式,用戶觸發(fā)購(gòu)買商品后携栋,進(jìn)入隊(duì)列搭盾,讓用戶的頁(yè)面一直在轉(zhuǎn)圈圈,等輪到他買的時(shí)候再進(jìn)入結(jié)算頁(yè)面婉支,結(jié)算頁(yè)面的后續(xù)流程和本文一致鸯隅。

歡迎關(guān)注

我的公眾號(hào) :地藏思維

掘金:地藏Kelvin

簡(jiǎn)書:地藏Kelvin

我的Gitee: 地藏Kelvin https://gitee.com/kelvin-cai

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者向挖。
  • 序言:七十年代末蝌以,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子何之,更是在濱河造成了極大的恐慌跟畅,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溶推,死亡現(xiàn)場(chǎng)離奇詭異徊件,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蒜危,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門虱痕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人舰褪,你說(shuō)我怎么就攤上這事皆疹。” “怎么了占拍?”我有些...
    開(kāi)封第一講書人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵略就,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我晃酒,道長(zhǎng)表牢,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任贝次,我火速辦了婚禮崔兴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蛔翅。我一直安慰自己敲茄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開(kāi)白布山析。 她就那樣靜靜地躺著堰燎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪笋轨。 梳的紋絲不亂的頭發(fā)上秆剪,一...
    開(kāi)封第一講書人閱讀 51,215評(píng)論 1 299
  • 那天赊淑,我揣著相機(jī)與錄音,去河邊找鬼仅讽。 笑死陶缺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的洁灵。 我是一名探鬼主播饱岸,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼处渣!你這毒婦竟也來(lái)了伶贰?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤罐栈,失蹤者是張志新(化名)和其女友劉穎黍衙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荠诬,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琅翻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了柑贞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片方椎。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖钧嘶,靈堂內(nèi)的尸體忽然破棺而出棠众,到底是詐尸還是另有隱情,我是刑警寧澤有决,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布闸拿,位于F島的核電站,受9級(jí)特大地震影響书幕,放射性物質(zhì)發(fā)生泄漏新荤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一台汇、第九天 我趴在偏房一處隱蔽的房頂上張望苛骨。 院中可真熱鬧,春花似錦苟呐、人聲如沸痒芝。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)严衬。三九已至,卻和暖如春两波,著一層夾襖步出監(jiān)牢的瞬間瞳步,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工腰奋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留单起,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓劣坊,卻偏偏與公主長(zhǎng)得像嘀倒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子局冰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容