12306系統深度優化之預扣庫存,異步下單,MySQL高可用
一、12306深度優化整體架構
在節假日和春節時候,火車票提前預售。在預售的點會有大量人們搶購車票。由于高并發,導致服務癱瘓。
1.1 解決方案
內存計算余票
異步交易系統(削峰方案)
數據庫進行高可用搭建(讀寫分離)
1.1.1 削峰解決方案
1. 削峰方案:
- 對于瞬時流量我們最先想到的是中間件進行削峰,把直接調用轉化為間接異步推送。中間隊列在一瞬間接受流量鋒,在另外一端平滑的將消息推送。
2. 答題:
在下單時候,我們需要答題。因為每個人的答題速度不一樣,錯開搶票時間。
3. 分時間段發放票:
將票分多個時間段進行發放
1.1.2 數據同步方案架構
- 后臺管理員按照日期生成乘車計劃、座位信息、車次信息,并通過logstash將數據同步到ES和redis中。
二、用戶下單分析
用戶在下單時候,用戶經歷下單、扣庫存、支付。在高并發場景下保證,車票不多買也不少賣,且支付后車票真實有效。
2.1 對下單操作分析(異步下單的優勢)
1. 方案一
用戶在下單完,立馬扣除庫存,等待用戶支付。且創建訂單和扣除庫存是原子操作。能保證不超賣問題。方案問題:
- 在極度并發請求下,每次創建訂單對內存操作對性能影響很大。訂單數據需要保存到數據庫,對數據庫壓力很大。如果很多人下了訂單,但是不支付。會導致很多票沒有賣掉。
2. 方案二
在極度并發場景下,庫存減為0時候,很多用戶搶到訂單卻不能支付。而且也不能避免數據的IO操作。
3. 方案三
- 用戶選擇乘車計劃,點擊下單-->進入下單服務集群中,--》判斷redis庫存是否充足,否(直接響應票已售空)、是(預扣庫存)-》把下單信息發送給mq,es同步庫存信息-》如果用戶支付,訂單處理服務進行訂單信息入庫,并響應成功信息。如果超時未支付,redis進行庫存回退,es進行庫存回退。
2.1 下單操作
2.1.1 nginx進行限流配置
為了防止一些搶票助手發送無用請求,采用nginx進行限流操作。
1. 限制訪問頻率:limit_req_zone:單位時間內請求數,采用漏斗算法。
2. 限制并發連接數:limit_conn_zone:同一時間的連接數。
2.1.2 下單流程
1. 訂單生成:
預扣庫存
找對應日期對應車次對應座位類型的乘車計劃庫存(站票,坐票,硬臥、軟臥等),進行庫存扣減。
分配座位
遍歷所有指定座位類型車廂的key(集合)。
遍歷集合獲取每個車廂的座位(集合)。遍歷集合獲取每一個座位對應狀態。如果座位沒有售出,標記座位并更改座位的狀態。
生成訂單
- 在創建訂單時候,我們可以采用一個線程完成。且線程執行完畢,我們要獲取線程的執行結果,采用Callable執行訂單創建。
在線程中創建訂單對象。
將指定日期的乘車計劃封裝到一個對象中。
生成訂單。
Redis排隊
采用 Redis 中的 ZSet 集合存儲排隊信息,使用:列車編號 + 乘車日期 + 用戶 id 作為 key,使用當前系統時間的納秒值作為 value。
2. 同步ES庫存:
下單服務發送同步信息(乘車日期、座位性質、列車車次)到mq中。es同步服務監聽mq,獲取發送信息。根據搜索條件查詢到數據,并進行庫存扣減。
3. 發送訂單數據:
創建訂單和用戶信息,并設置交換機和隊列。發送訂單信息,發送完訂單數據,跳轉到下單成功界面。下單成功界面調用排隊接口,顯示排隊信息。
2.1.3 訂單處理流程
1. 環境準備:
因為下單,寫的操作遠大于讀的操作,因此mysql采用雙主雙從搭建模式。
2. 保存訂單:
下單服務監聽mq,如果有訂單生成,對訂單信息進行入庫。
3. 刪除排隊信息:
訂單保存成功后,刪除排隊信息。
4. 將下單結果通過websocket發送給客戶端:
保存完訂單數據以后,調用 WebSocketServer ,完成消息推送。
2.2 下單優化
用戶已經下單了,再次提交訂單是否扣除訂單?用戶雖然下單了但是一直沒有支付。預扣減庫存是否存在線程安全問題。
2.2.1 預扣庫存優化
當該用戶已經購買了指定列車的火車票,那么我們就不能再進行預扣庫.
根據用戶信息和乘車計劃id查詢訂單信息,查詢到信息,不進行庫存扣減。
2.2.2 庫存回退
1. 方案一(延遲隊列):
延遲隊列:消息發送后,特定時間后消費者才能拿到消息進行消費。
2. 方案二(死信隊列):
死信隊列:一個消息在隊列中變成死信隊列之后,消息會被從新發送到另外一個交換機中,這個交換機就是死信隊列。一個消息成為死信隊列情況:
- 消息被拒絕,并且設置 requeue 參數為 false
- 消息過期
- 隊列達到最大長度
2.2.3 庫存安全性判斷(分布式鎖)
在進行庫存扣減時候,加鎖。因為扣除庫存是多服務,因此需要用分布式鎖來解決(mysql實現,zookeeper實現,redis實現)。