金融平臺高并發(fā)場景下的搶購解決方案

搶購——常見于電商網(wǎng)站的商品限量低價銷售,限時秒殺等狞尔,特點是商品數(shù)量有限制,價低于往日正常售價巩掺,顧客在搶到庫存中的商品后會暫時占有這筆庫存沪么,支付完成后庫存中才會減1,如果在規(guī)定時間內(nèi)(例如15分鐘)沒有完成支付锌半,會將庫存放回可售庫存禽车。

搶購模塊因瞬時訪問量會比平時高十幾倍甚至幾十倍,不能讓搶購模塊的大并發(fā)量影響到其他模塊刊殉,造成整體系統(tǒng)響應(yīng)緩慢甚至整個系統(tǒng)的崩潰都有可能殉摔,所以將搶購模塊單獨拆分成一個搶購系統(tǒng)已經(jīng)是必然。

開發(fā)之初我們總結(jié)了搶購系統(tǒng)所需要解決的幾個問題
1.庫存的高實時性和一致性记焊,不可超賣或者未賣完即提示已售完
2.大并發(fā)下須保證系統(tǒng)在可承受范圍內(nèi)逸月,做好限流措施
3.整體異步化,與訂單系統(tǒng)遍膜,資產(chǎn)系統(tǒng)的交互中通過中間表碗硬、緩存、隊列實現(xiàn)數(shù)據(jù)的異步瓢颅,減緩對其他系統(tǒng)造成高并發(fā)沖擊恩尾。

流程圖:
圖片.png

任何系統(tǒng)都有壓力臨界點,為防止系統(tǒng)崩潰挽懦,讓系統(tǒng)保持在可接受的壓力環(huán)境下翰意,我們對搶購系統(tǒng)采取了限流措施 , 通過Guava RateLimiter實現(xiàn)平滑限流
限流偽代碼:

//每秒發(fā)放10個令牌
final RateLimiter limiter = RateLimiter create(10.0);
ThreadPoolExecutor excutor = new ThreadPoolExecutor(availableProcessors, availableProcessors * 5, 60L,
            TimeUnit.MILLISECONDS, workQueue, new NamedThreadFactory("wallet", false));
void submitOrder(List tasks) {
    for (Runnable task : tasks) {
        limiter.acquire();
        excutor.execute(task);
    }
}

理財產(chǎn)品的搶購與電商類商品庫存大致概念一樣,在整體的庫存控制中(產(chǎn)品額度)信柿,產(chǎn)品額度分為總額度(total)冀偶,凍結(jié)額度(freeze),已售額度(sold)渔嚷,整個搶購流程中必然是 total >= freeze + sold的情況进鸠,如何有效的控制住高并發(fā)下的額度不超賣,額度更新的原子性也是系統(tǒng)關(guān)鍵點之一形病。實際系統(tǒng)中的可售額度為 total - freeze - sold客年,具體請看下文。

控制額度單從數(shù)據(jù)庫層面是控制不了窒朋,高并發(fā)情況下數(shù)據(jù)庫壓力增大并且按樂觀鎖控制額度不能有效的控制住額度搀罢,目前我們使用redis 存hash結(jié)構(gòu)來操作額度的增加和扣減蝗岖。整體流程如下:

  • 情況1. 凍結(jié)額度增加
    從用戶下單時做凍結(jié)額度的增加侥猩,當(dāng)發(fā)現(xiàn)凍結(jié)額度 + 已售額度 > 總額度,返回訂單創(chuàng)建失敗抵赢。
RedisCallback<Object> createCallback = connection -> {
                        connection.multi();
                        connection.hIncrBy((productIdKey,soldKey,0);
                        //增加凍結(jié)額度
                        connection.hIncrBy(productIdKey,freezeKey,orderAmount);
                        return connection.exec();
                    };
                    List<Object> list = (List)redisService.excute(createCallback);
                    if (ListUtil.isHave(list)){
                        long soldAmount = (long)list.get(0);
                        long withHoldAmount = (long)list.get(1);
                        if (withHoldAmount+soldAmount > sellAmount){
                            logger.error("額度控制 創(chuàng)建訂單 凍結(jié)金額+售賣金額大于總發(fā)售金額 withHoldAmount={},sellAmount={},productId={}",withHoldAmount,sellAmount,productId);
                            RedisCallback<Object> createFailureCallback = connection -> connection.hIncrBy(productIdKey,(freezeKey,-orderAmount);
                            //額度不足回滾數(shù)據(jù)欺劳。
                            redisService.excute(createFailureCallback);
                            //發(fā)布事件唧取,將額度同步到數(shù)據(jù)庫
                            eventBus.postEvent(new SyncAmountDataEvent(productId));
                            throw new ServiceException("額度不足");
                        }
                    }
  • 情況2. 凍結(jié)額度釋放
    定時任務(wù)定時跑批查詢訂單不是已支付且支付開始時間超過最近15分鐘的會調(diào)用關(guān)閉訂單方法,關(guān)閉訂單方法將訂單狀態(tài)修改為已失效划提,并將redis中的凍結(jié)額度-orderAmount釋放出可用額度枫弟。
    connection.hIncrBy(productIdKey,freezeKey,-orderAmount);
  • 情況3. 已售額度增加并釋放凍結(jié)額度
    與情況2一樣,在訂單支付完成后我們需要將sold + orderAmount 鹏往,freeze - orderAmount淡诗。
  connection.multi();
       connection.hIncrBy(productIdKey,sold,orderAmount);
       connection.hIncrBy(productIdKey,freezeKey,-orderAmount);
  return connection.exec();

在redis 額度更新的同時會啟動異步線程更新mysql的額度表數(shù)據(jù)。
以上三種情況可讓額度在訂單的各個狀態(tài)流轉(zhuǎn)時得到有效的控制伊履。

在高并發(fā)情況下韩容,如果將搶購系統(tǒng)的大量請求與其他核心系統(tǒng)同步處理顯然是不明智的,將搶購系統(tǒng)單獨抽離的一大原因就是為了減緩大請求量對整個系統(tǒng)的沖擊唐瀑,這種情況下我們考慮出了使用臨時表記錄訂單數(shù)據(jù)群凶,訂單創(chuàng)建,更新等都會先將訂單信息刷到redis用戶訂單緩存哄辣,保證用戶能第一時間看到自己的已搶成功的訂單请梢,隨后發(fā)送mq同步到訂單系統(tǒng)中,訂單系統(tǒng)在數(shù)據(jù)落地成功后會再次將庫里數(shù)據(jù)和redis做一次同步力穗。訂單緩沖表比較簡單毅弧,可以只存一個訂單json串來實現(xiàn)。

至此搶購系統(tǒng)的大概流程講解完畢当窗,其中還有一些細(xì)節(jié)問題需要去處理形真,比如redis高并發(fā)下性能問題,多次交互會浪費網(wǎng)絡(luò)資源超全,可以考慮將額度控制的內(nèi)容改為redis + lua的方式更為高效咆霜,同時考慮redis在掛掉時的系統(tǒng)如何運轉(zhuǎn)等情況。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嘶朱,一起剝皮案震驚了整個濱河市蛾坯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌疏遏,老刑警劉巖脉课,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異财异,居然都是意外死亡倘零,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門戳寸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呈驶,“玉大人,你說我怎么就攤上這事疫鹊⌒湔埃” “怎么了司致?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長聋迎。 經(jīng)常有香客問我脂矫,道長,這世上最難降的妖魔是什么霉晕? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任庭再,我火速辦了婚禮,結(jié)果婚禮上牺堰,老公的妹妹穿的比我還像新娘佩微。我一直安慰自己,他們只是感情好萌焰,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布哺眯。 她就那樣靜靜地躺著,像睡著了一般扒俯。 火紅的嫁衣襯著肌膚如雪奶卓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天撼玄,我揣著相機與錄音夺姑,去河邊找鬼。 笑死掌猛,一個胖子當(dāng)著我的面吹牛盏浙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播荔茬,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼废膘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了慕蔚?” 一聲冷哼從身側(cè)響起丐黄,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎孔飒,沒想到半個月后灌闺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡坏瞄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年桂对,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸠匀。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡蕉斜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蛛勉,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布睦柴,位于F島的核電站诽凌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏坦敌。R本人自食惡果不足惜侣诵,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望狱窘。 院中可真熱鬧杜顺,春花似錦、人聲如沸蘸炸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搭儒。三九已至穷当,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淹禾,已是汗流浹背馁菜。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留铃岔,地道東北人汪疮。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓痛单,卻偏偏與公主長得像睁宰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子糠悯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351