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