為業(yè)務(wù)系統(tǒng)賦能,攜程機(jī)票最終行程系統(tǒng)架構(gòu)演進(jìn)之路
作者簡介
Stephen,攜程資深后端開發(fā)工程師,專注新技術(shù)挖掘,持續(xù)推動(dòng)業(yè)務(wù)創(chuàng)新
Scott ,攜程資深研發(fā)經(jīng)理,負(fù)責(zé)訂單系統(tǒng)架構(gòu)升級(jí)和優(yōu)化
一、背景
攜程機(jī)票訂單系統(tǒng)是由多個(gè)業(yè)務(wù)子系統(tǒng)組成,包括出票、改簽、航變等等,獲取訂單行程信息復(fù)雜度較高。
例如:用戶預(yù)訂了一個(gè)包含了2個(gè)乘客的機(jī)票訂單,該訂單發(fā)生了航變,其中用戶A選擇了退票,用戶B選擇了改簽。
業(yè)務(wù)系統(tǒng)需要獲得該訂單最新的行程信息以及行程變化軌跡,以進(jìn)行展示和進(jìn)一步處理。

上述例子用戶的最新行程信息為:
- 乘客1:航班號(hào)9C888,SHA-PEK,已退票
- 乘客2:航班號(hào)9C999,SHA-PEK,已改簽
歷史的系統(tǒng)設(shè)計(jì)需要通過API對(duì)各業(yè)務(wù)子系統(tǒng)的數(shù)據(jù)進(jìn)行實(shí)時(shí)的聚合和計(jì)算,如果要獲取上述例子的最終行程與軌跡,需要至少調(diào)用訂單、出票、改期、航變系統(tǒng)等,流程復(fù)雜且耗時(shí)高,并且針對(duì)一些復(fù)雜的業(yè)務(wù)場景還可能導(dǎo)致錯(cuò)匹配、漏匹配等問題。

總結(jié)下來有如下幾個(gè)問題:
- 數(shù)據(jù)私有(分散),數(shù)據(jù)模型不統(tǒng)一
- 按照時(shí)間線進(jìn)行聚合的難度大,需要?jiǎng)討B(tài)計(jì)算,耗時(shí)長
- 數(shù)據(jù)存儲(chǔ)周期不一致,完整性不高
- 數(shù)據(jù)分析困難,報(bào)表邏輯復(fù)雜
二、目標(biāo)
總的來說,我們需要設(shè)計(jì)一個(gè)用戶行程系統(tǒng)來滿足以下要求:
- 完整準(zhǔn)確的行程信息
信息豐富完整,并保證更新及時(shí)、準(zhǔn)確 - 使用便利
一站式獲取,使用方效率提升,方便使用方快速接入 - 性能可靠
系統(tǒng)性能良好,可靠性高 - 提升業(yè)務(wù)系統(tǒng)自動(dòng)化率
提升自動(dòng)化率,上線靈活 - 快速實(shí)現(xiàn)復(fù)雜業(yè)務(wù)流程
對(duì)于大量動(dòng)態(tài)數(shù)據(jù)的分析與過濾需要快速實(shí)現(xiàn)并上線
三、實(shí)施方案
3.1 設(shè)計(jì)思路
Q1:系統(tǒng)需要提供什么樣的能力?
1)提供準(zhǔn)確的用戶最新行程信息
用戶和相關(guān)的業(yè)務(wù)系統(tǒng)需要及時(shí)和方便的獲取到完整、準(zhǔn)確行程信息
2)輸出歷史行程變化軌跡
對(duì)于退票等場景,需要了解用戶完整的行程變化軌跡,以便于自動(dòng)化處理相關(guān)數(shù)據(jù)
3)通過行程信息進(jìn)行模糊匹配
對(duì)于航變場景,航司通知某個(gè)具體航班發(fā)生了變化,系統(tǒng)需要通過這些信息匹配到對(duì)應(yīng)的訂單并進(jìn)行后續(xù)的處理
Q2:如何確保信息的豐富和準(zhǔn)確?
1)在豐富性方面,可以接入大量的數(shù)據(jù)源并提供便捷的接入方式,及時(shí)有效的采集數(shù)據(jù),提升系統(tǒng)數(shù)據(jù)的完整性
2)在準(zhǔn)確性方面,可以采用主動(dòng) + 被動(dòng)等方式,多維度的對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、修復(fù),提升數(shù)據(jù)的準(zhǔn)確性
Q3:如何提升系統(tǒng)的穩(wěn)定性和可擴(kuò)展性?
1)通過分布式緩存、結(jié)構(gòu)化并發(fā)等技術(shù)提升系統(tǒng)的性能與穩(wěn)定性
2)通過數(shù)據(jù)庫的sharding、數(shù)據(jù)倉庫的賦能等方式提供在線和離線的數(shù)據(jù)處理能力,進(jìn)一步擴(kuò)充數(shù)據(jù)的應(yīng)用場景
3.2 系統(tǒng)架構(gòu)圖

最終行程系統(tǒng)主要有以下幾個(gè)方面組成:
1)最終行程數(shù)據(jù)通知與更新系統(tǒng)
即上圖中的Data Collector API,通過收集各種來源,如訂單庫、出票系統(tǒng)、改簽系統(tǒng)等的數(shù)據(jù),更新或者落地在最終行程系統(tǒng)數(shù)據(jù)庫中。同時(shí)在落地的時(shí)候也會(huì)進(jìn)行被動(dòng) + 主動(dòng)相結(jié)合的數(shù)據(jù)校驗(yàn)機(jī)制,保證數(shù)據(jù)的準(zhǔn)確性。
2)最終行程查詢系統(tǒng),即上圖中的Query API,其中包含三大功能與若干個(gè)模塊
- 最終行程查詢,對(duì)外輸出該訂單的最終行程信息,該接口流量最高,包含有緩存組件、熔斷器、限流器等,保障其性能的穩(wěn)定;
- 行程溯源軌跡查詢,對(duì)外輸出該訂單下所有行程變化的歷史軌跡,使用方可以通過該接口拿到這個(gè)訂單的行程關(guān)系圖,感知所有變化軌跡;并且整合了價(jià)格計(jì)算模塊、錯(cuò)誤數(shù)據(jù)修正模塊;
- 行程匹配查詢,通過給定的行程要素條件,匹配能夠?qū)?yīng)上的最終行程記錄,并支持批量查詢;
3)數(shù)據(jù)存儲(chǔ)架構(gòu),通過分庫提升數(shù)據(jù)庫的水平擴(kuò)展能力,并且結(jié)合數(shù)據(jù)倉庫為業(yè)務(wù)賦能
3.3 信息豐富性
支持多種更新機(jī)制,方便接入多種類型的通知方,提升信息的豐富度,目前已經(jīng)接入了出、退、改、航變、票號(hào)中心等22個(gè)數(shù)據(jù)源。
策略1: 系統(tǒng)主動(dòng)通知,適用于對(duì)于數(shù)據(jù)新鮮度要求較高的場景,查詢性能較好
策略2: 消息通知消費(fèi),適用于數(shù)據(jù)新鮮度要求不太高的場景,通過反查保證數(shù)據(jù)最終一致,方便系統(tǒng)解耦
策略3: 實(shí)時(shí)查詢,適用于數(shù)據(jù)變化非常頻繁,新鮮度要求高的場景;減少了數(shù)據(jù)冗余,但是在查詢和使用上存在依賴
策略4: 動(dòng)態(tài)數(shù)據(jù)的過濾通知,適用于存在規(guī)則變更,但變化維度和訂單維度不同,需要掃描海量數(shù)據(jù)來獲取更新記錄的場景
3.4 便利度增加和業(yè)務(wù)提升
3.4.1 降低溯源接口接入復(fù)雜度
溯源軌跡接口對(duì)于行程關(guān)系圖的輸出形式,對(duì)于使用方的便利度影響非常大,比如如下的行程關(guān)系圖。

歷史的輸出形式為一種無限層級(jí)的樹形結(jié)構(gòu),這樣的結(jié)構(gòu)雖然能對(duì)向下的溯源查詢以及對(duì)一變多的行程變化關(guān)系提供支持,但是對(duì)于向上的溯源查詢、多變一、多變多的行程變化關(guān)系不友好,許多使用方都需要使用DFS等算法來解析數(shù)據(jù),不夠簡潔易用,容易出錯(cuò);并且樹形結(jié)構(gòu)已經(jīng)不能直觀的反映出類似二變一(中轉(zhuǎn)變直飛)的行程變化場景,而且這樣的結(jié)構(gòu)還會(huì)出現(xiàn)數(shù)據(jù)的冗余,如下圖所示:

基于以上的情況,新溯源接口選擇了類似圖的鄰接矩陣來表述行程溯源變化關(guān)系,通過TripInfo節(jié)點(diǎn)來表示頂點(diǎn)數(shù)組,平鋪出行程溯源關(guān)系圖中各個(gè)節(jié)點(diǎn)的行程信息;通過ChangeInfo節(jié)點(diǎn)來表示邊數(shù)組,主要描述行程變化關(guān)系。這樣的描述更加通用、結(jié)構(gòu)清晰并且對(duì)使用方更友好。

3.4.2 支持大量動(dòng)態(tài)數(shù)據(jù)的掃描與過濾
在實(shí)際的業(yè)務(wù)場景中需要維護(hù)這樣一部分?jǐn)?shù)據(jù),它會(huì)發(fā)生變化,但引起變化的規(guī)則維度與訂單維度不一致,所以需要掃描海量數(shù)據(jù)來獲取需要被更新的記錄。同時(shí),掃描依賴的數(shù)據(jù)可能還需要跨庫才能拿到,按照現(xiàn)有的數(shù)據(jù)庫結(jié)構(gòu)實(shí)現(xiàn)起來非常復(fù)雜。通過調(diào)研,最終采用數(shù)倉并結(jié)合業(yè)務(wù)SDK過濾的動(dòng)態(tài)數(shù)據(jù)主動(dòng)更新機(jī)制,實(shí)現(xiàn)了業(yè)務(wù)場景主動(dòng)更新與通知的功能,該流程有如下幾個(gè)特點(diǎn):

- 輕松整合所有依賴數(shù)據(jù)項(xiàng),通過數(shù)據(jù)倉庫的大數(shù)據(jù)分析能力,可以輕松整合所有依賴的數(shù)據(jù)項(xiàng)
- 對(duì)數(shù)據(jù)進(jìn)行篩選,在數(shù)據(jù)倉庫處理的流程中,添加了業(yè)務(wù)SDK的過濾機(jī)制進(jìn)行數(shù)據(jù)的初篩,將海量數(shù)據(jù)進(jìn)行過濾,并結(jié)合Double Check機(jī)制進(jìn)行進(jìn)一步的篩選,得到真正受影響的記錄
- 觸發(fā)消息的聚合機(jī)制,同時(shí)考慮到了業(yè)務(wù)誤操作后又修改一次的情況,所以增加了消息聚合機(jī)制,聚合一段時(shí)間的消息后再真正觸發(fā)數(shù)倉進(jìn)行處理
該流程具有很強(qiáng)的通用性,通過簡單替換不同的SQL語句,切換不同的SDK,就可以輕松將該流程移植到其他業(yè)務(wù)項(xiàng)目中,實(shí)現(xiàn)了功能的快速上線。
3.5 性能優(yōu)化
3.5.1 提升數(shù)據(jù)庫的水平擴(kuò)展能力
最終行程系統(tǒng)在之前使用的是單庫存儲(chǔ),但是隨著數(shù)據(jù)量的不斷增加,當(dāng)業(yè)務(wù)信息擴(kuò)充時(shí),新增數(shù)據(jù)字段在數(shù)據(jù)庫層面上變的難以操作;并且如果按照業(yè)務(wù)期望的存儲(chǔ)時(shí)間,硬盤使用率會(huì)過高,造成了存儲(chǔ)瓶頸。
經(jīng)過調(diào)研,決定對(duì)最終行程數(shù)據(jù)庫做 Sharding 處理,將數(shù)據(jù)平均分配到多個(gè)分片就可以滿足存儲(chǔ)要求并兼顧性能指標(biāo)。
1)數(shù)據(jù)切分,基于最終行程數(shù)據(jù)特性,即訂單號(hào)訪問占比較高,同時(shí)在訂單號(hào)分布均勻的前提下,最終采用了訂單號(hào)對(duì)數(shù)據(jù)庫總分片數(shù)取模的方式,以保證數(shù)據(jù)分布的均勻性。
2)數(shù)據(jù)兼容,對(duì)于sharding庫和非sharding庫雙寫新數(shù)據(jù)的操作,并考慮數(shù)據(jù)庫存在異常的情況,需要增加異常補(bǔ)償處理機(jī)制;并且對(duì)于歷史存量數(shù)據(jù),也進(jìn)行了分批次的數(shù)據(jù)遷移以及補(bǔ)償功能,同時(shí)為了保證數(shù)據(jù)一致性,在遷移完成后也進(jìn)行了多批次的數(shù)據(jù)對(duì)比與接口對(duì)比工作,保證 Sharding 數(shù)據(jù)的準(zhǔn)確性和可靠性。
3)查詢性能,多分庫的查詢性能是分庫存在的典型問題,對(duì)于最終行程來說,采用非訂單號(hào)查詢操作,分庫后就涉及到多個(gè)分片的 All Shard 查詢,極大地增加了數(shù)據(jù)庫壓力和影響查詢性能。經(jīng)過數(shù)據(jù)統(tǒng)計(jì),分析得到特定的業(yè)務(wù)字段查詢其實(shí)就涵蓋了非訂單號(hào)查詢的大多數(shù),從而增加其二級(jí)索引表就可以有效解決 All Shard 查詢性能的問題。
3.5.2 接入Redis緩存提升系統(tǒng)性能
總體上采用先操作數(shù)據(jù)庫,后刪除緩存;先查詢緩存,查詢不到緩存則查詢數(shù)據(jù)庫,并回填緩存的方式進(jìn)行處理。
1)提升新鮮度,在行程更新流程時(shí)、接收BinLog消息時(shí)、接收業(yè)務(wù)變更消息時(shí)都會(huì)將緩存刪除。
2)采用分級(jí)儲(chǔ)存查詢的模式,查詢時(shí)根據(jù)調(diào)用方所需的數(shù)據(jù)級(jí)別進(jìn)行獲取,縮小Redis獲取數(shù)據(jù)的大小,減少網(wǎng)絡(luò)開銷。
3)異步回填,啟用專用的線程對(duì)緩存數(shù)據(jù)進(jìn)行異步回填,這樣可以不拖累查詢請(qǐng)求本身的耗時(shí)。
4)優(yōu)化緩存容量,對(duì)Json序列化器定制規(guī)則,不輸出值為null的字段;將序列化對(duì)象中的字段通過@JsonProperty注解取一個(gè)簡短的別名,來簡化Json字符串Key的大?。皇褂肸std壓縮算法對(duì)序列化后的數(shù)據(jù)進(jìn)行壓縮;通過前期調(diào)研命中率和生存時(shí)間的關(guān)系,得出達(dá)到預(yù)期命中率的最小緩存生存時(shí)間,從而進(jìn)一步減少Redis的容量。
3.5.3 結(jié)構(gòu)化并發(fā)在匹單接口中的探索
最終行程匹單接口允許使用方傳入多組條件進(jìn)行匹配,接口內(nèi)部對(duì)于這多組條件采用的是for循環(huán)的方式順序執(zhí)行的,存在并發(fā)改造的空間;且匹單接口操作數(shù)據(jù)庫存在多shard查詢的情況,對(duì)于多shard查詢,Dal底層會(huì)使用線程池并發(fā)調(diào)用,對(duì)線程的開銷較大。綜合上述問題,并結(jié)合近期發(fā)布的新的長期支持版本JDK21,發(fā)現(xiàn)了其預(yù)覽功能中的結(jié)構(gòu)化并發(fā)比較適用于匹單場景的優(yōu)化。
1)簡化多線程編程,增強(qiáng)可觀察性。
一般而言,如果我們想要實(shí)現(xiàn)并發(fā)操作,需要使用異步編程的方式來實(shí)現(xiàn),但是使用這樣的方式對(duì)于代碼閱讀性和調(diào)試來說都比較差。在目前的多線程開發(fā)中,常用的方式是使用CompletableFuture的級(jí)聯(lián)方式編寫。與單線程的代碼相比,這樣的寫法并不直觀,并且“任務(wù)終止不干凈”和“等待超過必要時(shí)間”的問題仍然存在,如果要解決這些問題還需要自己實(shí)現(xiàn)一系列模版代碼,費(fèi)力度大大增加。
而結(jié)構(gòu)化并發(fā)的一大特點(diǎn)就是讓開發(fā)人員以類似單線程的方式來編寫多線程代碼,他引出了一個(gè)結(jié)構(gòu)化任務(wù)作用域(Scope)的概念,在這個(gè)作用域中創(chuàng)建并執(zhí)行任務(wù),這些任務(wù)的生命周期都由作用域來負(fù)責(zé)管理,開發(fā)人員可以不用關(guān)系細(xì)節(jié)問題。對(duì)于作用域的任務(wù)使用try-with-resources塊,如果在執(zhí)行中出現(xiàn)錯(cuò)誤,會(huì)自動(dòng)調(diào)用StructuredTaskScope的shutdown方法來終止執(zhí)行,調(diào)用shutdown方法會(huì)阻止新任務(wù)的執(zhí)行,同時(shí)取消正在運(yùn)行中的任務(wù)。

2)使用虛擬線程解決阻塞問題。
StructuredTaskScope底層默認(rèn)采用了虛擬線程進(jìn)行實(shí)現(xiàn),在我們?cè)瓉淼恼J(rèn)知中,線程的使用都是昂貴的,而虛擬線程是JVM中Thread類的實(shí)現(xiàn),它是輕量級(jí)的,當(dāng)使用虛擬線程進(jìn)行代碼執(zhí)行時(shí),如果遇到阻塞操作,便會(huì)釋放掉載體線程;并當(dāng)該阻塞操作可用時(shí),虛擬線程又將被安排在載體線程上去繼續(xù)處理執(zhí)行。即在虛擬線程中,阻塞不是問題,因?yàn)樽枞麜r(shí)底層的載體線程已經(jīng)被釋放了
虛擬線程和結(jié)構(gòu)化并發(fā)的組合將非常強(qiáng)大,虛擬線程使阻塞不再是一個(gè)問題,而結(jié)構(gòu)化并發(fā)為我們提供了更簡單的多線程編寫方案,以更直觀的方式處理異步編程。
3.6 優(yōu)化前后數(shù)據(jù)支撐
- 數(shù)據(jù)庫QPS降低30%
- 數(shù)據(jù)庫CPU平均利用率下降20%
- 平均響應(yīng)時(shí)間降低40%,P95降低30%
- 減少機(jī)器線程數(shù)41%,CPU利用率降低25%,顯著減少機(jī)器壓力
- 快速支持了業(yè)務(wù)功能,人力成本節(jié)約至少50%以上
四、后續(xù)規(guī)劃
1)易用性優(yōu)化
增加行程變化訂閱通知機(jī)制,進(jìn)一步提升易用性。
2)可靠性與性能提升
- 細(xì)化熔斷和降級(jí)的策略
- 和框架團(tuán)隊(duì)協(xié)作,積極推廣新技術(shù)在生產(chǎn)系統(tǒng)上的規(guī)范化落地
- 探索新的數(shù)據(jù)庫結(jié)構(gòu)與數(shù)據(jù)庫選型,提升關(guān)系鏈路的存儲(chǔ)能力
3)可視化
實(shí)現(xiàn)整體客人行程的可視化界面,依托最終行程數(shù)據(jù)的力量,幫助業(yè)務(wù)/產(chǎn)品開發(fā)更快了解到訂單全貌,幫助提升問題解決效率。



































