2024 年,所在公司的 PDF 報告定製需求不斷增加,業務範圍也逐步擴大,對用戶數據安全和審批流程的要求隨之提高。
原有流程基於飛書多維表格、審批流程和 HTML 轉 PDF 工具搭建。隨著業務深化,這套流程在權限控制、報告管理和系統集成上的能力逐漸難以滿足需求,因此由我負責報告生成與審批系統的架構設計。
系統既需要支持當時的報告業務,也需要保留未來接入 LIMS 系統和移動端應用的可能性。在功能之外,一個需要儘早回答的問題是:當業務操作涉及多個服務時,應該為一致性付出多少實現和運維成本?
一、服務邊界與事務邊界是兩回事
當時從領域建模和系統職責劃分出發,將系統拆為授權、基礎、框架、網關、存儲和報告六類服務,服務間通過 Apache Dubbo 進行 RPC 通信。
回頭看,這一劃分同時包含業務職責與技術職責。網關、框架等技術模塊的獨立部署,並不能直接證明它們構成了 DDD 中的限界上下文;DDD 本身也不要求採用微服務。
未來需要接入 LIMS 和移動端,意味著系統需要清晰的接口和可演進的業務邊界,但並不必然要求在第一階段就把所有模塊拆成獨立進程。這是當時架構選擇中值得繼續復盤的一點。
微服務也不意味著每次跨服務調用都需要分佈式事務。查詢多個服務的數據,或者發送允許稍後完成的通知,與要求多個服務的寫操作共同成功或失敗,是不同的問題。
真正需要處理的是:一個業務操作修改了多個服務擁有的數據,而業務又要求這些修改保持一致。
以報告流程為例,報告狀態更新、文件生成和通知發送可能發生在同一條業務鏈路中。但它們是否必須同時完成,需要由業務規則決定。這個例子用於說明一致性要求的區別,並不代表這些步驟在當時都被放進了同一個事務。
通知失敗或許可以重試;報告未生成時,系統是否允許進入某個審批狀態,則需要更明確的約束。不能僅憑“屬於同一個流程”,就把全部操作納入強一致事務。
二、評估 Seata,先理解它解決什麼
在方案調研中,Seata 是一個重要參考。它提供 AT、TCC、Saga 和 XA 等事務模式,但這些模式對業務侵入、數據庫支持和一致性語義的要求並不相同。
以 AT 模式為例,參與全局事務的服務會在本地事務中修改業務數據,並記錄用於回滾的 undo log,隨後提交本地事務。事務協調器根據全局事務的結果組織提交或回滾;需要回滾時,參與者利用 undo log 撤銷已經提交的修改。
這套機制還涉及全局事務上下文傳播、分支事務註冊和全局鎖,不能簡化為“調用方拋出異常,再通知被調用方回滾”。它的隔離效果也不能直接視為與單數據庫本地事務完全相同。
對於當時的系統,微服務已經拆分,但項目初期的使用量並不大。引入事務協調中間件,除了部署資源,還需要承擔配置、監控、故障恢復和版本兼容的成本。
因此,當時沒有直接引入 Seata。這個選擇應建立在業務一致性要求與團隊運維能力之上,而不是用廠商聲譽判斷產品是否合適,也不能把某次部署的內存佔用當作產品固定的資源需求。
不引入協調器,並不會讓一致性問題消失。系統仍需要明確哪些操作由本地事務保證,哪些失敗允許補償,以及哪些場景尚未獲得跨資源原子提交的保證。
三、當時採用的三類處理方式
當時的實現主要採用了異常記錄、共享數據庫和動態多數據源三類方式。它們分別降低了不同方面的成本,但不能統稱為三種等價的分佈式事務方案。
異常記錄:為人工恢復保留依據
對於當時判斷可以接受暫時不一致、且能夠事後修復的操作,主要通過日誌保留執行結果和錯誤信息,再由人工處理異常。
這種方式適合低頻、可核對、可恢復的業務,但日誌本身並不保證一致性。“非核心功能”也不意味著失敗沒有損失,仍需要判斷失敗影響是否可接受。
要讓人工恢復成為可靠機制,後續還需要明確如何發現失敗、關聯業務對象、識別已經完成的步驟,以及如何避免重複補償。這些能力應單獨驗證,不能因為寫入了錯誤日誌,就默認已經形成恢復閉環。
共享數據庫:減少跨庫訪問
授權與框架服務使用同一個數據庫,減少了跨數據庫訪問和維護多個數據源的複雜度。
但共享數據庫不等於共享事務。兩個服務即使連接同一個數據庫,只要通過各自的連接執行並提交本地事務,就仍然可能出現一個成功、另一個失敗的情況。普通 Dubbo 調用不會自動傳播調用方的數據庫事務。
如果希望通過本地事務保證一組寫操作共同完成,需要把這些操作收斂到同一個事務邊界中,例如由同一個服務負責執行,並通過同一事務管理器、同一事務資源完成提交。
所以,共享數據庫只是減少跨庫複雜度的手段。它還可能增加服務之間的數據耦合;後續若要獨立演進,仍需明確數據歸屬和訪問邊界。
動態多數據源:集中執行多庫操作
對於需要訪問多個數據庫的核心業務,引入 Baomidou 的 Dynamic Datasource,讓一個服務能夠路由到不同數據源,執行相關操作。
這可以減少一部分跨服務調用,但數據源路由與事務協調仍是兩回事。一個進程能夠操作多個數據庫,並不代表這些數據庫可以原子提交。
如果採用多連接的本地事務組合,也需要檢查具體實現的提交機制和失敗邊界。依次提交時,可能出現前一個數據庫已經成功提交,後一個數據庫提交失敗的情況,不能僅靠統一捕獲異常就把所有已提交修改撤銷。
因此,動態多數據源可以作為收斂執行位置的工具,但不能直接當作分佈式事務的替代品。涉及跨庫核心寫操作時,仍需要明確部分提交後的處理方式,或者引入能夠滿足業務要求的協調機制。
四、先把一致性要求寫成業務規則
這次設計更值得提煉的經驗,是先回答“哪些狀態不能出現”,再選擇事務方案。
不同操作可以有不同的一致性要求:
| 業務要求 | 優先考慮的處理方式 | 必須確認的邊界 |
|---|---|---|
| 一組數據庫寫入必須共同成功或失敗 | 收斂到本地事務;無法收斂時評估分佈式協調 | 數據是否確實參與同一個事務,失敗後能否回滾 |
| 允許暫時不一致,但必須最終完成 | 持久化待處理狀態,結合重試和補償 | 任務不能丟失,重複執行需要安全 |
| 低頻且允許人工恢復 | 異常記錄、核對和人工補償 | 失敗能被發現,責任人和恢復步驟明確 |
這張表是後續評估的依據,不代表當時已經完整實現了所有機制。
報告系統還涉及對象存儲等非數據庫資源。數據庫事務回滾,並不會自動刪除已經生成的文件。即使引入 Seata AT,也不能直接假定普通對象存儲操作會被一併回滾。
如果某個流程需要協調數據庫狀態與文件生成,可以考慮將“待生成、生成成功、生成失敗”等狀態顯式建模,再對失敗步驟進行重試或補償。若通過消息驅動後續處理,還要解決業務狀態提交與消息發送之間的可靠性問題,例如評估事務型 Outbox。
這些都是需要結合具體業務驗證的演進方向,不能用一箇中間件名稱替代完整的失敗分析。
五、為演進留空間,不等於已經具備接入能力
當時註冊中心與配置中心選擇了 Nacos,也保留了後續評估 Seata 的方向。但採用 Nacos 本身,並不能證明系統已經具備接入 Seata 的條件。
真正的接入工作還包括事務邊界梳理、數據源代理、事務上下文傳播,以及對 SQL 支持、併發衝突和故障回滾的驗證。涉及數據庫之外的資源時,還需要單獨設計處理方式。
更有價值的準備,是把業務狀態和失敗語義定義清楚:哪些步驟可以重試,哪些操作需要冪等,哪些狀態需要補償,以及失敗後由誰發現和處理。
當跨服務原子性成為明確要求,或者人工恢復的頻率、成本和風險已經無法接受時,再評估 Seata、TCC、Saga 等機制,選擇才有具體依據。
六、架構取捨也需要驗證失敗路徑
李智慧在《大型網站技術架構》中強調,大型網站的架構往往隨著業務規模逐步演化。這一點也適用於業務初期的一致性設計:不必為假設中的規模提前引入所有基礎設施,但必須知道當前方案能夠保證什麼。
這次實踐的價值,是根據當時的業務量和資源約束控制複雜度;它的邊界則是,共享數據庫和動態多數據源降低了部分實現成本,卻沒有自動提供跨服務、跨數據庫的原子性。
驗證這類設計,需要主動檢查失敗路徑:RPC 超時但遠端已經提交、數據庫依次提交時中途失敗、文件生成成功但狀態更新失敗,以及重複重試是否產生副作用。
從現有需求出發,既要避免過度建設,也要避免把尚未驗證的一致性能力當作既成事實。先定義業務必須維持的狀態,再收斂事務邊界;剩餘的跨資源問題,通過可驗證的補償或協調機制處理。