CMU15-445 數(shù)據(jù)庫(kù)系統(tǒng)播客:OLTP 與分布式事務(wù)
在互聯(lián)網(wǎng)浪潮席卷全球的今天,從電商購(gòu)物到社交分享,從在線支付到流媒體服務(wù),數(shù)據(jù)以前所未有的規(guī)模和速度產(chǎn)生。傳統(tǒng)的單體數(shù)據(jù)庫(kù),盡管穩(wěn)定可靠,卻早已在海量并發(fā)和高可用性的要求面前顯得力不從心。分布式數(shù)據(jù)庫(kù),通過(guò)將數(shù)據(jù)和計(jì)算負(fù)載分散到多臺(tái)機(jī)器上,成為了支撐現(xiàn)代應(yīng)用不可或缺的基石。
然而,將數(shù)據(jù)“分而治之”的同時(shí),也引入了前所未有的復(fù)雜性。當(dāng)一次操作需要跨越多個(gè)網(wǎng)絡(luò)節(jié)點(diǎn)時(shí),我們?nèi)绾未_保它像在單臺(tái)機(jī)器上一樣可靠?當(dāng)某個(gè)節(jié)點(diǎn)突然宕機(jī)時(shí),系統(tǒng)如何繼續(xù)提供服務(wù)而不丟失數(shù)據(jù)?
本文將帶你深入分布式在線事務(wù)處理(OLTP)數(shù)據(jù)庫(kù)的核心,從最基礎(chǔ)的事務(wù)概念出發(fā),層層剖析分布式環(huán)境下的原子性、一致性與可用性挑戰(zhàn),探討從二階段提交(2PC)、共識(shí)協(xié)議(Paxos/Raft)到最終指導(dǎo)所有設(shè)計(jì)的 CAP 定理,理解構(gòu)建一個(gè)健壯分布式系統(tǒng)背后所必須做出的深刻權(quán)衡。
為什么我們離不開(kāi)事務(wù)(Transaction)?
在深入“分布式”之前,我們必須先回到原點(diǎn): 事務(wù)(Transaction) 。事務(wù)是數(shù)據(jù)庫(kù)管理系統(tǒng)執(zhí)行過(guò)程中的一個(gè)邏輯單位,它將一系列操作打包,確保它們要么 全部成功 ,要么 全部失敗 。這種“全有或全無(wú)”的特性,就是我們熟知的 原子性(Atomicity) ,也是 ACID(原子性、一致性、隔離性、持久性)四大特性的基石。
這種保障在 在線事務(wù)處理(OLTP) 場(chǎng)景中至關(guān)重要。OLTP 系統(tǒng)的特點(diǎn)是處理大量、短促、高頻的讀寫(xiě)請(qǐng)求。想象一下你在電商網(wǎng)站下單的場(chǎng)景:
- 創(chuàng)建訂單
- 扣減商品庫(kù)存
- 從你的賬戶扣款
- 增加商家賬戶余額
這是一個(gè)典型的 OLTP 事務(wù)。如果系統(tǒng)在第 3 步成功后、第 4 步執(zhí)行前崩潰,你的錢(qián)被扣了,但商家沒(méi)收到,這將是一場(chǎng)災(zāi)難。事務(wù)確保了這四個(gè)步驟被視為一個(gè)不可分割的整體,從而維護(hù)了系統(tǒng)的數(shù)據(jù)一致性。
在分布式環(huán)境中,這四個(gè)步驟可能發(fā)生在不同的服務(wù)器上(訂單服務(wù)、庫(kù)存服務(wù)、支付服務(wù))。挑戰(zhàn)也因此升級(jí):我們?nèi)绾螀f(xié)調(diào)散布在各地的節(jié)點(diǎn),讓它們對(duì)同一個(gè)事務(wù)的最終結(jié)果(提交或中止)達(dá)成一致的決定?
用二階段提交(2PC)實(shí)現(xiàn)分布式原子性
為了解決跨節(jié)點(diǎn)的原子性問(wèn)題, 原子提交協(xié)議(Atomic Commit Protocol) 應(yīng)運(yùn)而生,其中最經(jīng)典、最廣為人知的就是 二階段提交(Two-Phase Commit, 2PC) 。
2PC 引入了一個(gè) 協(xié)調(diào)者(Coordinator) 的角色,來(lái)統(tǒng)一指揮所有參與該事務(wù)的節(jié)點(diǎn),即 參與者(Participants) 。顧名思義,整個(gè)過(guò)程分為兩個(gè)階段:
階段一:準(zhǔn)備階段(投票階段)
- 詢問(wèn)(Request) : 協(xié)調(diào)者向所有參與者發(fā)送一個(gè)“準(zhǔn)備提交(Prepare)”的消息,詢問(wèn)它們是否可以提交事務(wù)。
- 表態(tài)(Vote) : 每個(gè)參與者會(huì)檢查本地的事務(wù)是否可以成功(例如,是否滿足約束、日志是否已寫(xiě)入持久化存儲(chǔ))。
- 如果可以,它就鎖定相關(guān)資源,并將“可以提交(VOTE-COMMIT)”的響應(yīng)發(fā)給協(xié)調(diào)者。
- 如果不行(例如,發(fā)生本地錯(cuò)誤),它就直接發(fā)送“請(qǐng)求中止(VOTE-ABORT)”的響應(yīng)。
階段二:提交階段(決策階段)
- 決策(Decision) : 協(xié)調(diào)者收集所有參與者的投票。
- 如果所有參與者 都回復(fù)“可以提交”,協(xié)調(diào)者就做出“全局提交(GLOBAL-COMMIT)”的決定,并向所有參與者發(fā)送提交消息。
- 只要有一個(gè)參與者 回復(fù)“請(qǐng)求中止”,或者有參與者超時(shí)未響應(yīng),協(xié)調(diào)者就做出“全局中止(GLOBAL-ABORT)”的決定,并向所有參與者發(fā)送中止消息。
- 執(zhí)行(Execution) : 參與者根據(jù)協(xié)調(diào)者的最終決定,完成本地事務(wù)的提交或回滾,并釋放資源。
2PC 的致命缺陷:阻塞
2PC 用一種簡(jiǎn)單民主的方式(一票否決)保證了原子性,但它存在一個(gè)致命的缺陷: 同步阻塞 。
- 協(xié)調(diào)者單點(diǎn)故障 :如果在第二階段,協(xié)調(diào)者發(fā)出決策后、所有參與者確認(rèn)前宕機(jī),那么所有參與者都會(huì)被“卡住”。它們不知道最終的決定是提交還是中止,只能無(wú)限期地等待協(xié)調(diào)者恢復(fù)。這期間,事務(wù)所占有的資源無(wú)法釋放,整個(gè)系統(tǒng)可能因此癱瘓。
- 參與者故障 :如果某個(gè)參與者在第一階段后宕機(jī),協(xié)調(diào)者將無(wú)法收齊所有投票,導(dǎo)致整個(gè)事務(wù)超時(shí)并最終中止。即使只是網(wǎng)絡(luò)緩慢,也會(huì)導(dǎo)致協(xié)調(diào)者長(zhǎng)時(shí)間等待,拖慢整個(gè)系統(tǒng)的性能。
正是因?yàn)?2PC 的脆弱性,它在對(duì)可用性要求極高的現(xiàn)代系統(tǒng)中已逐漸被更健壯的協(xié)議所取代。
從 Paxos 共識(shí)到 Raft 的普及
2PC 的問(wèn)題在于,決策權(quán)過(guò)于集中且需要所有節(jié)點(diǎn)達(dá)成一致。有沒(méi)有一種方法,即使少數(shù)節(jié)點(diǎn)失聯(lián),系統(tǒng)也能繼續(xù)運(yùn)轉(zhuǎn)?答案就是 共識(shí)協(xié)議(Consensus Protocol) 。
與 2PC 的“全票通過(guò)”不同,像 Paxos 和 Raft 這樣的共識(shí)協(xié)議遵循 “少數(shù)服從多數(shù)” 的原則。它們的目標(biāo)是讓分布式節(jié)點(diǎn)集群就某個(gè)值(例如,事務(wù)是提交還是中止)達(dá)成不可撤銷(xiāo)的一致。
與 2PC 的核心區(qū)別在于:共識(shí)協(xié)議只需要集群中超過(guò)半數(shù)(Majority)的節(jié)點(diǎn)達(dá)成一致,就可以做出決策。
這意味著,在一個(gè)由 5 個(gè)節(jié)點(diǎn)組成的集群中,只要有 3 個(gè)節(jié)點(diǎn)正常工作并達(dá)成一致,系統(tǒng)就能繼續(xù)處理請(qǐng)求,它可以容忍最多 2 個(gè)節(jié)點(diǎn)的失效。這極大地提高了系統(tǒng)的容錯(cuò)能力和可用性,徹底解決了 2PC 的阻塞問(wèn)題。
- Paxos : 由萊斯利·蘭波特提出,是共識(shí)協(xié)議的鼻祖,以嚴(yán)謹(jǐn)?shù)y以理解著稱(chēng)。為了解決多個(gè)提議者可能導(dǎo)致“活鎖”(livelock)的問(wèn)題,工程實(shí)踐中通常采用其變種 Multi-Paxos ,即選舉一個(gè) 領(lǐng)導(dǎo)者(Leader) 來(lái)統(tǒng)一發(fā)起提議,從而提高效率。
- Raft : 由斯坦福大學(xué)的學(xué)者設(shè)計(jì),其目標(biāo)就是提供與 Paxos 相同的容錯(cuò)保證,但更容易被理解和實(shí)現(xiàn)。Raft 通過(guò)明確的領(lǐng)導(dǎo)者選舉、日志復(fù)制等步驟,使得共識(shí)過(guò)程更加清晰,如今已在 TiDB、etcd、CockroachDB 等眾多知名項(xiàng)目中得到廣泛應(yīng)用。
高可用的基石:數(shù)據(jù)復(fù)制策略
解決了事務(wù)的決策問(wèn)題后,我們還需要考慮數(shù)據(jù)的物理安全。如果存儲(chǔ)數(shù)據(jù)的唯一節(jié)點(diǎn)宕機(jī),數(shù)據(jù)就會(huì)永久丟失。為此, 數(shù)據(jù)復(fù)制(Replication) 成為分布式系統(tǒng)的標(biāo)配。通過(guò)將數(shù)據(jù)存儲(chǔ)多個(gè)副本,即使部分節(jié)點(diǎn)失效,系統(tǒng)依然可以依賴其他副本繼續(xù)提供服務(wù)。
最常見(jiàn)的復(fù)制模型是 主從復(fù)制(Leader-Follower Replication) :
- 寫(xiě)入流程 : 所有寫(xiě)入請(qǐng)求都必須發(fā)送到 主節(jié)點(diǎn)(Leader) 。主節(jié)點(diǎn)完成寫(xiě)入后,負(fù)責(zé)將數(shù)據(jù)變更日志同步給所有 從節(jié)點(diǎn)(Followers) 。
- 讀取流程 : 讀取請(qǐng)求既可以由主節(jié)點(diǎn)處理(保證讀到最新數(shù)據(jù)),也可以為了分流而發(fā)送到從節(jié)點(diǎn)(可能讀到稍有延遲的數(shù)據(jù))。
- 故障轉(zhuǎn)移 : 當(dāng)主節(jié)點(diǎn)宕機(jī)時(shí),系統(tǒng)會(huì)通過(guò)共識(shí)協(xié)議(如 Raft)在從節(jié)點(diǎn)中選舉出一個(gè)新的主節(jié)點(diǎn),接管所有寫(xiě)入流量,實(shí)現(xiàn)自動(dòng)故障恢復(fù)。
在這個(gè)復(fù)制過(guò)程中,一個(gè)關(guān)鍵的權(quán)衡點(diǎn)在于:主節(jié)點(diǎn)何時(shí)向客戶端確認(rèn)“寫(xiě)入成功”?這直接決定了系統(tǒng)的一致性模型。
- 同步復(fù)制(Synchronous Replication)
- 機(jī)制 : 主節(jié)點(diǎn)必須等待 所有 (或指定數(shù)量的)從節(jié)點(diǎn)確認(rèn)已成功接收并持久化日志后,才向客戶端返回成功。
- 保障 : 強(qiáng)一致性(Strong Consistency) 。一旦寫(xiě)入成功,任何后續(xù)的讀取請(qǐng)求(無(wú)論訪問(wèn)哪個(gè)節(jié)點(diǎn))都保證能看到最新的數(shù)據(jù)。不會(huì)有數(shù)據(jù)丟失的風(fēng)險(xiǎn)。
- 代價(jià) : 性能較低,因?yàn)槭聞?wù)的延遲取決于最慢的那個(gè)從節(jié)點(diǎn)。
- 場(chǎng)景 : 金融交易、核心訂單系統(tǒng)等對(duì)數(shù)據(jù)正確性要求零容忍的場(chǎng)景。
- 異步復(fù)制(Asynchronous Replication)
- 機(jī)制 : 主節(jié)點(diǎn)將日志發(fā)送出去后, 無(wú)需等待 從節(jié)點(diǎn)的確認(rèn),立即向客戶端返回成功。
- 保障 : 最終一致性(Eventual Consistency) 。數(shù)據(jù)最終會(huì)同步到所有副本,但在主節(jié)點(diǎn)宕機(jī)且數(shù)據(jù)尚未同步完成的極端情況下,可能會(huì)丟失少量“已提交”的數(shù)據(jù)。
- 代價(jià) : 數(shù)據(jù)安全性稍弱,但寫(xiě)入延遲極低,吞吐量高。
- 場(chǎng)景 : 社交媒體點(diǎn)贊、發(fā)布評(píng)論等對(duì)性能和可用性要求高,但能容忍秒級(jí)數(shù)據(jù)延遲的場(chǎng)景。
CAP 定理的權(quán)衡藝術(shù)
至此,我們討論了原子性、可用性、一致性等多個(gè)維度。而將這些概念統(tǒng)一在一個(gè)框架下的,就是分布式系統(tǒng)領(lǐng)域著名的 CAP 定理 。
該定理指出,一個(gè)分布式系統(tǒng)在以下三個(gè)核心特性中, 最多只能同時(shí)滿足兩個(gè) :
- 一致性 (Consistency, C) : 所有節(jié)點(diǎn)在同一時(shí)間訪問(wèn)到的數(shù)據(jù)是完全一致的。這里的一致性通常指最嚴(yán)格的線性一致性(Linearizability),即所有讀操作都能獲取到最近一次寫(xiě)入的最新數(shù)據(jù)。
- 可用性 (Availability, A) : 任何來(lái)自客戶端的請(qǐng)求,無(wú)論成功或失敗,系統(tǒng)都能在有限時(shí)間內(nèi)給出響應(yīng)。簡(jiǎn)單說(shuō),就是系統(tǒng)永遠(yuǎn)是“活的”。
- 分區(qū)容錯(cuò)性 (Partition Tolerance, P) : 系統(tǒng)在遇到網(wǎng)絡(luò)分區(qū)(即節(jié)點(diǎn)間的網(wǎng)絡(luò)連接中斷,導(dǎo)致集群被分割成多個(gè)孤島)時(shí),仍然能夠繼續(xù)運(yùn)行。
在現(xiàn)代分布式系統(tǒng)中,網(wǎng)絡(luò)故障是常態(tài)而非例外,因此 P(分區(qū)容錯(cuò)性)是必選項(xiàng) 。這意味著,我們必須在 C(一致性) 和 A(可用性) 之間做出選擇。
選擇 CP (Consistency / Partition Tolerance)
- 當(dāng)網(wǎng)絡(luò)分區(qū)發(fā)生時(shí),為了保證數(shù)據(jù)的一致性,系統(tǒng)會(huì) 犧牲可用性 。
- 通常的做法是,被分割出去的少數(shù)節(jié)點(diǎn)會(huì)停止服務(wù),只有擁有“法定人數(shù)”(Majority)的主分區(qū)會(huì)繼續(xù)處理請(qǐng)求。這避免了“腦裂”(Split-Brain)問(wèn)題,即不同分區(qū)產(chǎn)生相互沖突的數(shù)據(jù)。
- 代表系統(tǒng) : 大多數(shù)關(guān)系型數(shù)據(jù)庫(kù)、NewSQL 數(shù)據(jù)庫(kù)如 Google Spanner, TiDB, CockroachDB。它們優(yōu)先保證數(shù)據(jù)絕不出錯(cuò)。
選擇 AP (Availability / Partition Tolerance)
- 當(dāng)網(wǎng)絡(luò)分區(qū)發(fā)生時(shí),為了保證系統(tǒng)的可用性(每個(gè)分區(qū)都能響應(yīng)請(qǐng)求),系統(tǒng)會(huì) 犧牲一致性 。
- 所有分區(qū)會(huì)繼續(xù)獨(dú)立處理讀寫(xiě)請(qǐng)求。這意味著在分區(qū)期間,你寫(xiě)入一個(gè)分區(qū)的數(shù)據(jù)對(duì)另一個(gè)分區(qū)是不可見(jiàn)的。當(dāng)網(wǎng)絡(luò)恢復(fù)后,系統(tǒng)需要通過(guò)復(fù)雜的沖突解決機(jī)制來(lái)合并這些差異數(shù)據(jù),最終達(dá)到一致。
- 代表系統(tǒng) : 大多數(shù) NoSQL 數(shù)據(jù)庫(kù)如 Amazon DynamoDB, Cassandra。它們優(yōu)先保證服務(wù)永遠(yuǎn)在線,即使數(shù)據(jù)可能暫時(shí)不一致。
深度拓展(一):MySQL 內(nèi)部的“二階段提交”—— redo log 與 binlog 的雙重奏
讀到這里,經(jīng)驗(yàn)豐富的后端工程師可能會(huì)聯(lián)想到一個(gè)非常熟悉的場(chǎng)景:MySQL 中 redo log 和 binlog 的協(xié)同工作。這其實(shí)就是二階段提交(2PC)思想在 單機(jī)系統(tǒng)內(nèi)部、跨組件協(xié)調(diào) 中的一個(gè)絕佳應(yīng)用。它與我們之前討論的、用于多臺(tái)機(jī)器間的分布式事務(wù) 2PC,在思想上同源,但在應(yīng)用范疇和目標(biāo)上有所不同。
為何需要這個(gè)內(nèi)部 2PC?
首先,我們必須明確兩個(gè)日志的核心職責(zé):
redo log(重做日志) : 這是 InnoDB 存儲(chǔ)引擎層面的日志。它記錄了對(duì)數(shù)據(jù)頁(yè)(Page)的物理修改,保證了事務(wù)的 持久性(Durability) 和 崩潰安全(Crash Safety) 。即使數(shù)據(jù)庫(kù)異常宕機(jī),InnoDB 也可以通過(guò)redo log恢復(fù)到宕機(jī)前的狀態(tài)。binlog(二進(jìn)制日志) : 這是 MySQL Server 層面的日志。它記錄了所有修改數(shù)據(jù)的邏輯操作(SQL 語(yǔ)句或行的變更),主要用于 主從復(fù)制(Replication) 和 數(shù)據(jù)恢復(fù) 。從庫(kù)通過(guò)拉取并回放主庫(kù)的binlog來(lái)實(shí)現(xiàn)數(shù)據(jù)同步。
核心矛盾在于 :一個(gè)事務(wù)的提交,既要確保在 InnoDB 內(nèi)部是持久的(redo log 寫(xiě)入),也要確保能被從庫(kù)正確復(fù)制(binlog 寫(xiě)入)。如果這兩個(gè)操作不是原子的,一旦在它們之間發(fā)生宕機(jī),就會(huì)導(dǎo)致主從數(shù)據(jù)不一致的災(zāi)難:
- 場(chǎng)景A:先寫(xiě)
redo log,再寫(xiě)binlog。如果redo log寫(xiě)完后、binlog寫(xiě)入前宕機(jī)。主庫(kù)重啟后通過(guò)redo log恢復(fù)了數(shù)據(jù),但binlog里沒(méi)有這次變更。結(jié)果就是,從庫(kù)永遠(yuǎn)收不到這次更新,主從數(shù)據(jù)產(chǎn)生永久性差異。 - 場(chǎng)景B:先寫(xiě)
binlog,再寫(xiě)redo log。如果binlog寫(xiě)完后、redo log寫(xiě)入前宕機(jī)。主庫(kù)重啟后由于沒(méi)有redo log記錄,會(huì)回滾這個(gè)事務(wù),數(shù)據(jù)沒(méi)有變化。但binlog已經(jīng)記錄了這次變更并可能已傳給從庫(kù)。結(jié)果是,從庫(kù)比主庫(kù)“多”了一次更新,數(shù)據(jù)再次不一致。
MySQL 的解決方案:內(nèi)部 2PC
為了解決這個(gè)“跨組件原子寫(xiě)入”的問(wèn)題,MySQL 巧妙地引入了內(nèi)部 2PC:
準(zhǔn)備階段 (Prepare Phase)
- 當(dāng)客戶端執(zhí)行
COMMIT時(shí),InnoDB 引擎會(huì)將事務(wù)的所有變更寫(xiě)入redo log,并將該事務(wù)標(biāo)記為 “準(zhǔn)備(prepare)” 狀態(tài)。此時(shí),事務(wù)并未真正提交。
提交階段 (Commit Phase)
- MySQL Server 層接收到 InnoDB 的“準(zhǔn)備好了”信號(hào)后,會(huì)將該事務(wù)的變更寫(xiě)入
binlog。 binlog成功寫(xiě)入磁盤(pán)后,Server 層會(huì)調(diào)用 InnoDB 的接口,通知其完成事務(wù)的最終提交。InnoDB 收到指令后,將redo log中對(duì)應(yīng)的事務(wù)狀態(tài)從“準(zhǔn)備”修改為 “提交(commit)” 。
這個(gè)過(guò)程確保了 redo log 和 binlog 的內(nèi)容是邏輯一致的。即使在任何一個(gè)步驟之間發(fā)生崩潰,MySQL 在重啟時(shí)都有明確的恢復(fù)策略:
- 如果一個(gè)事務(wù)在
redo log中是 “prepare” 狀態(tài),但在binlog中 找不到 ,說(shuō)明崩潰發(fā)生在第一階段之后、第二階段之前?;謴?fù)時(shí), 回滾 該事務(wù)。 - 如果一個(gè)事務(wù)在
redo log中是 “prepare” 狀態(tài),并且在binlog中 能找到 ,說(shuō)明崩潰發(fā)生在第二階段binlog寫(xiě)完之后、redo logcommit 之前?;謴?fù)時(shí), 提交 該事務(wù)。
通過(guò)這種方式,MySQL 保證了任何一個(gè)成功提交的事務(wù),其 redo log 和 binlog 必然是同時(shí)存在的、完整的,從而為主從復(fù)制的正確性提供了堅(jiān)實(shí)的基礎(chǔ)。這與分布式 2PC 用于協(xié)調(diào)多個(gè)獨(dú)立節(jié)點(diǎn)的思路異曲同工,都是為了確保一個(gè)邏輯操作在不同參與方之間的原子性。
深度拓展(二):現(xiàn)代高并發(fā)架構(gòu) —— MySQL、Raft 與分片的“三位一體”
了解了 MySQL 的內(nèi)部機(jī)制后,我們?cè)賹⒁暯抢睾暧^架構(gòu)。大型互聯(lián)網(wǎng)公司面對(duì)每秒數(shù)萬(wàn)甚至數(shù)十萬(wàn)的請(qǐng)求,是如何基于 MySQL 構(gòu)建高可用、高并發(fā)的存儲(chǔ)服務(wù)的?答案并非單一技術(shù),而是一個(gè)由 數(shù)據(jù)復(fù)制、共識(shí)算法和智能代理 組合而成的精密體系。
傳統(tǒng)的 MySQL 主從復(fù)制(一主多從)雖然能通過(guò)讀寫(xiě)分離分?jǐn)傌?fù)載,但其“軟肋”也十分明顯:
- 故障轉(zhuǎn)移(Failover)是手動(dòng)的或依賴腳本 :主庫(kù)宕機(jī)后,需要 DBA 或自動(dòng)化腳本介入,選擇一個(gè)從庫(kù)提升為新主庫(kù),并修改應(yīng)用配置。這個(gè)過(guò)程耗時(shí)且容易出錯(cuò)。
- 存在“腦裂”風(fēng)險(xiǎn) :在自動(dòng)切換的方案中,如果因?yàn)榫W(wǎng)絡(luò)問(wèn)題導(dǎo)致主庫(kù)“假死”,可能會(huì)產(chǎn)生兩個(gè)主庫(kù),造成數(shù)據(jù)沖突。
為了解決這些問(wèn)題,現(xiàn)代架構(gòu)通常采用 基于 Raft 共識(shí)的 MySQL 高可用方案 ,如官方的 MySQL Group Replication (MGR) 或開(kāi)源的 Orchestrator ,以及在此之上的代理層(如 ProxySQL )。
架構(gòu)藍(lán)圖:共識(shí)算法賦能的 MySQL 集群
一個(gè)典型的高可用 MySQL 集群架構(gòu)如下:
- 數(shù)據(jù)層 : 由一組(通常至少 3 個(gè))安裝了 MySQL 實(shí)例的服務(wù)器組成。它們之間通過(guò) Group Replication 插件進(jìn)行通信。
- 共識(shí)層 : Group Replication 內(nèi)部集成了一個(gè) Raft 變種的共識(shí)協(xié)議 。這個(gè)協(xié)議不負(fù)責(zé)同步業(yè)務(wù)數(shù)據(jù)(業(yè)務(wù)數(shù)據(jù)同步依然依賴
binlog),而是專(zhuān)門(mén)負(fù)責(zé) 集群成員管理、主節(jié)點(diǎn)選舉和維護(hù)集群元數(shù)據(jù)的一致性 。 - 代理/路由層 : 應(yīng)用并不會(huì)直接連接 MySQL 實(shí)例,而是連接一個(gè)輕量級(jí)的智能代理(如 ProxySQL)。這個(gè)代理了解整個(gè) MySQL 集群的拓?fù)浣Y(jié)構(gòu)和節(jié)點(diǎn)角色(誰(shuí)是主,誰(shuí)是從),并負(fù)責(zé):
- 自動(dòng)讀寫(xiě)分離 : 將所有寫(xiě)請(qǐng)求(
INSERT,UPDATE,DELETE)自動(dòng)路由到唯一的主節(jié)點(diǎn)。 - 讀負(fù)載均衡 : 將讀請(qǐng)求(
SELECT)根據(jù)策略分發(fā)到多個(gè)從節(jié)點(diǎn)。 - 故障感知與無(wú)縫切換 : 實(shí)時(shí)監(jiān)控集群狀態(tài)。一旦共識(shí)層選舉出新的主節(jié)點(diǎn),代理會(huì)立刻感知到變化,并將新的寫(xiě)請(qǐng)求無(wú)縫地轉(zhuǎn)發(fā)到新主庫(kù),對(duì)應(yīng)用層完全透明。
工作流程與高并發(fā)應(yīng)對(duì)
正常運(yùn)行 : Raft 協(xié)議確保集群中有且僅有一個(gè)主節(jié)點(diǎn)。所有寫(xiě)操作都經(jīng)過(guò)代理,匯集到主節(jié)點(diǎn)。主節(jié)點(diǎn)通過(guò) 半同步復(fù)制(Semi-Synchronous Replication) (一種強(qiáng)化的復(fù)制模式,要求至少一個(gè)從庫(kù)確認(rèn)收到日志后才向客戶端返回成功)將 binlog 同步給從庫(kù),保證了數(shù)據(jù)的高一致性(CP 傾向)。同時(shí),海量的讀請(qǐng)求被代理分發(fā)到所有從庫(kù),實(shí)現(xiàn)了讀能力的水平擴(kuò)展。
主庫(kù)故障 :
- 集群內(nèi)的節(jié)點(diǎn)通過(guò)心跳檢測(cè)發(fā)現(xiàn)主庫(kù)失聯(lián)。
- 共識(shí)模塊立即觸發(fā)新一輪的 領(lǐng)導(dǎo)者選舉 。
- 存活的從庫(kù)節(jié)點(diǎn)通過(guò) Raft 協(xié)議,在數(shù)秒內(nèi)投票選舉出一個(gè)數(shù)據(jù)最新的從庫(kù)成為新主庫(kù)。
- 代理層檢測(cè)到主庫(kù)變更,立即將流量切換到新主庫(kù)。整個(gè)過(guò)程 快速、自動(dòng),無(wú)需人工干預(yù) ,極大地提升了系統(tǒng)的可用性(Availability)。
應(yīng)對(duì)極致并發(fā)寫(xiě)入:分庫(kù)分表(Sharding)
當(dāng)單一主庫(kù)的寫(xiě)入性能達(dá)到瓶頸時(shí),上述架構(gòu)將作為“一個(gè)單元”被水平復(fù)制。這就是 分庫(kù)分表(Sharding) 。數(shù)據(jù)被按照某個(gè)維度(如用戶 ID、訂單 ID)切分成多個(gè)分片(Shard),每個(gè)分片都是一個(gè)獨(dú)立的、由 Raft 管理的高可用 MySQL 集群。代理層(或更上層的服務(wù)治理框架)會(huì)根據(jù)請(qǐng)求中的 Sharding Key,將其路由到對(duì)應(yīng)的集群。通過(guò)這種方式,系統(tǒng)的寫(xiě)入能力可以隨著分片的增加而近乎線性地?cái)U(kuò)展。
綜上所述,現(xiàn)代 MySQL 高并發(fā)架構(gòu),正是我們博文中所述理論的完美實(shí)踐:
- 它使用 Raft 共識(shí)算法 解決了集群的 選主和故障轉(zhuǎn)移 問(wèn)題,提供了 CP 級(jí)別的元數(shù)據(jù)一致性保障。
- 它使用 主從復(fù)制 作為 數(shù)據(jù)同步 的手段,并可在 強(qiáng)一致(半同步) 和 高性能(異步) 之間靈活配置。
- 它通過(guò) 讀寫(xiě)分離 和 分庫(kù)分表 ,將負(fù)載分散到龐大的集群中,實(shí)現(xiàn)了強(qiáng)大的 橫向擴(kuò)展 能力。
這是一個(gè)將共識(shí)理論、數(shù)據(jù)復(fù)制模型和業(yè)務(wù)擴(kuò)展策略有機(jī)結(jié)合的、優(yōu)雅而強(qiáng)大的工程范例。
分布式事務(wù)解析(一):為何分布式事務(wù)不可避免?
當(dāng)提到微服務(wù)、分庫(kù)分表等架構(gòu)時(shí),必然會(huì)追問(wèn):“那如何處理跨服務(wù)/跨庫(kù)的數(shù)據(jù)一致性問(wèn)題?”這個(gè)問(wèn)題直接引出了分布式事務(wù)。我們首先要知道這個(gè)問(wèn)題的根源在何處。
分布式事務(wù)并非憑空產(chǎn)生,它幾乎總是我們?yōu)榱俗非?nbsp;系統(tǒng)可擴(kuò)展性(Scalability) 而做出的架構(gòu)決策所帶來(lái)的“甜蜜的負(fù)擔(dān)”。主要源于以下三大經(jīng)典場(chǎng)景:
場(chǎng)景一:微服務(wù)拆分(業(yè)務(wù)維度)
這是最常見(jiàn)的場(chǎng)景。隨著業(yè)務(wù)變得復(fù)雜,單體應(yīng)用被拆分成多個(gè)獨(dú)立的微服務(wù),每個(gè)服務(wù)都有自己的專(zhuān)屬數(shù)據(jù)庫(kù)。
案例:電商下單
- 業(yè)務(wù)流程 :用戶下單,需要同時(shí)操作 訂單服務(wù) (創(chuàng)建訂單)、 庫(kù)存服務(wù) (扣減庫(kù)存)和 賬戶服務(wù) (扣減余額)。
- 痛點(diǎn) :這三個(gè)服務(wù)部署在不同節(jié)點(diǎn),訪問(wèn)各自獨(dú)立的數(shù)據(jù)庫(kù)。任何一個(gè)環(huán)節(jié)失敗,都必須撤銷(xiāo)已經(jīng)成功的操作,否則就會(huì)出現(xiàn)“創(chuàng)建了訂單但庫(kù)存沒(méi)扣”或“錢(qián)扣了但訂單創(chuàng)建失敗”等嚴(yán)重業(yè)務(wù)錯(cuò)誤。單個(gè)數(shù)據(jù)庫(kù)的本地 ACID 事務(wù)在此已無(wú)能為力。
- 結(jié)論 : 微服務(wù)化將單個(gè)業(yè)務(wù)流程內(nèi)的多個(gè)數(shù)據(jù)操作,從“庫(kù)內(nèi)跨表”變成了“跨庫(kù)跨服務(wù)” 。為了保證業(yè)務(wù)流程的原子性,分布式事務(wù)成為剛需。
場(chǎng)景二:數(shù)據(jù)分片/分庫(kù)分表(數(shù)據(jù)維度)
當(dāng)單一數(shù)據(jù)庫(kù)無(wú)法承受海量數(shù)據(jù)的存儲(chǔ)和訪問(wèn)壓力時(shí),我們會(huì)對(duì)其進(jìn)行水平拆分(Sharding)。
案例:朋友圈/社交轉(zhuǎn)賬
- 業(yè)務(wù)流程 :用戶 A 給用戶 B 轉(zhuǎn)賬。為了存儲(chǔ)海量用戶數(shù)據(jù),用戶表和賬戶表早已根據(jù)
user_id進(jìn)行了分片,用戶 A 的數(shù)據(jù)在 DB1,用戶 B 的數(shù)據(jù)在 DB2。 - 痛點(diǎn) :一個(gè)簡(jiǎn)單的轉(zhuǎn)賬操作,現(xiàn)在變成了對(duì) DB1 的
UPDATE(A 扣款)和對(duì) DB2 的UPDATE(B 收款)。這實(shí)質(zhì)上已經(jīng)是一個(gè)跨兩個(gè)數(shù)據(jù)庫(kù)實(shí)例的事務(wù)。 - 結(jié)論 : 數(shù)據(jù)分片將原本可能在同一個(gè)數(shù)據(jù)庫(kù)內(nèi)完成的事務(wù),強(qiáng)制拆分成了跨庫(kù)事務(wù) 。只要業(yè)務(wù)邏輯需要同時(shí)修改落在不同分片上的數(shù)據(jù),就必然會(huì)遇到分布式事務(wù)問(wèn)題。
場(chǎng)景三:異構(gòu)系統(tǒng)數(shù)據(jù)同步
在復(fù)雜的系統(tǒng)中,數(shù)據(jù)往往需要在不同類(lèi)型的存儲(chǔ)或系統(tǒng)間保持一致。
案例:數(shù)據(jù)庫(kù)與緩存/搜索引擎雙寫(xiě)
- 業(yè)務(wù)流程 :用戶修改了商品信息。操作需要 先更新 MySQL 數(shù)據(jù)庫(kù),然后更新 Redis 緩存和 Elasticsearch 中的商品索引 ,以確保用戶能立即看到并搜到最新的信息。
- 痛點(diǎn) :如何保證數(shù)據(jù)庫(kù)寫(xiě)入成功后,緩存和 ES 也一定能更新成功?如果更新緩存或 ES 失敗,就會(huì)導(dǎo)致用戶看到舊數(shù)據(jù),產(chǎn)生數(shù)據(jù)不一致。
- 結(jié)論 :只要一個(gè)業(yè)務(wù)操作需要原子化地修改多個(gè)異構(gòu)數(shù)據(jù)源,就產(chǎn)生了分布式事務(wù)的需求 。
分布式事務(wù)解析(二):解決方案從“剛性”到“柔性”的權(quán)衡
分布式事務(wù)的解決方案,本質(zhì)上是在 一致性、性能、可用性和實(shí)現(xiàn)復(fù)雜度 之間做選擇。我們可以將其劃分為兩大陣營(yíng): 剛性事務(wù) 和 柔性事務(wù) 。
陣營(yíng)一:剛性事務(wù)(追求強(qiáng)一致性)
這類(lèi)方案嚴(yán)格遵循 ACID 原則,特別是原子性(Atomicity)和隔離性(Isolation),試圖在分布式環(huán)境下復(fù)制單機(jī)事務(wù)的體驗(yàn)。
代表:兩階段提交 (2PC) / 三階段提交 (3PC)
- 核心思想 :引入一個(gè)“協(xié)調(diào)者”來(lái)統(tǒng)一指揮所有“參與者”,通過(guò)“投票(準(zhǔn)備)”和“執(zhí)行(提交/回滾)”兩個(gè)階段,強(qiáng)制所有節(jié)點(diǎn)達(dá)成一致。
- 致命缺陷 : 2PC 的三大問(wèn)題: 同步阻塞 (資源鎖定時(shí)間長(zhǎng),嚴(yán)重影響并發(fā)性能)、 協(xié)調(diào)者單點(diǎn)故障(協(xié)調(diào)者宕機(jī)導(dǎo)致整個(gè)事務(wù)阻塞甚至不一致)、極端情況下的數(shù)據(jù)不一致 (協(xié)調(diào)者在第二階段部分通知后宕機(jī))。
三階段提交(3PC)相比 2PC 做了哪些改進(jìn)?
三階段提交(Three-Phase Commit, 3PC)是在 2PC 的基礎(chǔ)上為了解決 2PC 在協(xié)調(diào)者故障時(shí)導(dǎo)致參與者長(zhǎng)時(shí)間阻塞 的問(wèn)題而提出的改進(jìn)。3PC 通過(guò)把“做出提交決定”拆成三個(gè)階段,引入一個(gè)中間的 預(yù)提交(pre-commit / can-commit → pre-commit → do-commit) 階段,目的是讓參與者在協(xié)調(diào)者失聯(lián)時(shí)也能做出安全的局部決策,從而盡量避免無(wú)限等待。但必須強(qiáng)調(diào):3PC 的安全性依賴于網(wǎng)絡(luò)延遲有界(bounded delay)和不存在長(zhǎng)期網(wǎng)絡(luò)分區(qū)的假設(shè)——在實(shí)際互聯(lián)網(wǎng)環(huán)境中,這個(gè)假設(shè)往往不成立,所以 3PC 并沒(méi)有像 Raft 那樣在工程實(shí)踐中廣泛替代 2PC。
3PC 的三個(gè)階段(簡(jiǎn)要)
- CanCommit(詢問(wèn) / 準(zhǔn)備投票) :協(xié)調(diào)者詢問(wèn)參與者是否能準(zhǔn)備提交(與 2PC 的 Prepare 類(lèi)似)。參與者檢查本地條件并返回
YES/NO(可以/不可以)。 - PreCommit(預(yù)提交 / 鎖定) :如果所有參與者都返回
YES,協(xié)調(diào)者發(fā)送PRE-COMMIT。參與者在收到PRE-COMMIT后進(jìn)入“已準(zhǔn)備但尚未最終提交”的狀態(tài),并在本地做必要的持久化(但仍可安全回滾)。 - DoCommit(最終提交 / 決策) :協(xié)調(diào)者廣播
DO-COMMIT(或ABORT),參與者據(jù)此完成提交或回滾。
3PC 相對(duì) 2PC 的改進(jìn)點(diǎn) :
- 在協(xié)調(diào)者在第二階段宕機(jī)的情況下,參與者在
PRE-COMMIT狀態(tài)下能通過(guò)與其他參與者交換信息,確定是否可以安全地自主完成提交,從而 減少長(zhǎng)期阻塞的概率 。 - 通過(guò)增加一個(gè)全局“預(yù)提交”步驟,讓參與者在進(jìn)入真正提交之前把狀態(tài)寫(xiě)到可恢復(fù)的持久化日志,這樣在協(xié)調(diào)者短時(shí)失聯(lián)時(shí),參與者能依據(jù)已知的“預(yù)提交”信息做出更安全的判斷。
局限與現(xiàn)實(shí)原因(為什么不常用) :
- 3PC 依賴于 網(wǎng)絡(luò)延遲有界且不存在持久分區(qū) 的假設(shè)。在真實(shí)分布式系統(tǒng)(互聯(lián)網(wǎng))中,這個(gè)假設(shè)通常無(wú)法保證,因此 3PC 在分區(qū)發(fā)生時(shí)可能導(dǎo)致不安全或仍然阻塞。
- 相比之下,共識(shí)算法(Paxos/Raft)以“多數(shù)派可決”為核心,不依賴網(wǎng)絡(luò)延遲有界的強(qiáng)假設(shè),并且在網(wǎng)絡(luò)分區(qū)與領(lǐng)導(dǎo)選舉上表現(xiàn)更穩(wěn)健,所以工程實(shí)踐中更傾向于用共識(shí)協(xié)議配合其他模式解決分布式一致性問(wèn)題。
總結(jié)一句話:3PC 在理論上通過(guò)引入“預(yù)提交”緩解了 2PC 的一些阻塞場(chǎng)景,但其對(duì)網(wǎng)絡(luò)假設(shè)的依賴使它在不可靠網(wǎng)絡(luò)環(huán)境中并非通用解,因此在互聯(lián)網(wǎng)級(jí)別的系統(tǒng)中并未廣泛替代 2PC/被普遍采用。
正因?yàn)檫@些嚴(yán)重缺陷,2PC/3PC 這類(lèi)方案 性能極差、對(duì)網(wǎng)絡(luò)容錯(cuò)性低 ,在現(xiàn)代互聯(lián)網(wǎng)高并發(fā)、高可用的微服務(wù)架構(gòu)中 幾乎不被采用 。它更多是理論基礎(chǔ)和傳統(tǒng)單體應(yīng)用集成領(lǐng)域的方案(如 XA 規(guī)范)。
陣營(yíng)二:柔性事務(wù)(追求最終一致性)
這類(lèi)方案放寬了對(duì)強(qiáng)一致性的要求,不要求數(shù)據(jù)在事務(wù)執(zhí)行過(guò)程中時(shí)刻一致,而是承諾在經(jīng)歷一個(gè)短暫的延遲后,數(shù)據(jù) 最終 會(huì)達(dá)到一致?tīng)顟B(tài)。這是基于 BASE 理論(Basically Available, Soft state, Eventually consistent) 的設(shè)計(jì),是當(dāng)前互聯(lián)網(wǎng)架構(gòu)的主流選擇。
代表一:TCC (Try-Confirm-Cancel)
核心思想 :將業(yè)務(wù)邏輯分為三個(gè)階段: 資源預(yù)留 (Try) 、 確認(rèn)執(zhí)行 (Confirm) 、 取消預(yù)留 (Cancel) 。
舉一個(gè)形象的例子 :比如“預(yù)訂機(jī)票”。Try 階段是“凍結(jié)”機(jī)票庫(kù)存和用戶資金,但并未實(shí)際扣減;Confirm 階段是實(shí)際扣減庫(kù)存和資金,完成出票;Cancel 階段是釋放被凍結(jié)的庫(kù)存和資金。
下面給出一個(gè)簡(jiǎn)化的 TCC 偽代碼示例,演示預(yù)留(Try)、確認(rèn)(Confirm)和取消(Cancel)三個(gè)接口的調(diào)用關(guān)系與冪等性要求。示例以“下單 -> 凍結(jié)庫(kù)存 -> 凍結(jié)余額 -> 確認(rèn)支付/取消”為場(chǎng)景。
# 服務(wù) A (Order service) 調(diào)用示例(偽代碼)
def place_order(user_id, item_id, amount):
# 1. Try 階段:各服務(wù)做資源預(yù)留
ok1 = inventory.try_reserve(item_id, qty=1, tx_id=tx_id)
ok2 = account.try_freeze(user_id, amount, tx_id=tx_id)
if not (ok1 and ok2):
# 如果任一預(yù)留失敗,執(zhí)行 Cancel
inventory.cancel(item_id, tx_id=tx_id)
account.cancel(user_id, tx_id=tx_id)
return False
# 2. 如果預(yù)留都成功,調(diào)用 Confirm(通常協(xié)調(diào)者在確認(rèn)業(yè)務(wù)可以完成后觸發(fā))
confirm1 = inventory.confirm(item_id, tx_id=tx_id)
confirm2 = account.confirm(user_id, tx_id=tx_id)
if not (confirm1 and confirm2):
# 若 Confirm 任一失敗,需要調(diào)用 Cancel 作為補(bǔ)償(或重試策略)
inventory.cancel(item_id, tx_id=tx_id)
account.cancel(user_id, tx_id=tx_id)
return False
return True
# 單個(gè)參與者(庫(kù)存服務(wù))內(nèi)部邏輯示意
class InventoryService:
def try_reserve(self, item_id, qty, tx_id):
# 寫(xiě)入本地預(yù)留表,鎖定庫(kù)存(應(yīng)冪等)
with db.tx() as cur:
if already_tried(tx_id):
return True
if available(item_id) < qty:
return False
insert_reservation(tx_id, item_id, qty)
# 不做最終扣減,只標(biāo)記已預(yù)留
cur.commit()
return True
def confirm(self, item_id, tx_id):
# 將預(yù)留轉(zhuǎn)化為實(shí)際扣減:冪等且持久
with db.tx() as cur:
if already_confirmed(tx_id):
return True
apply_deduct(item_id, get_reserved_qty(tx_id))
mark_confirmed(tx_id)
cur.commit()
return True
def cancel(self, item_id, tx_id):
# 釋放預(yù)留:冪等
with db.tx() as cur:
if already_cancelled_or_confirmed(tx_id):
return True
release_reservation(tx_id)
mark_cancelled(tx_id)
cur.commit()
return True要點(diǎn)提醒:
- 每個(gè)接口必須 冪等 ,以應(yīng)對(duì)重試和網(wǎng)絡(luò)不確定性。
- Try 階段應(yīng)盡量只做 資源預(yù)留 (輕量),避免長(zhǎng)時(shí)間持有不可釋放的鎖,否則影響并發(fā)。
- Confirm/Cancel 必須能根據(jù)
tx_id判斷狀態(tài)并安全執(zhí)行。
優(yōu)點(diǎn) :一致性很高,因?yàn)樗跇I(yè)務(wù)提交前已經(jīng)確保所有資源都可用,可以實(shí)現(xiàn)“準(zhǔn)強(qiáng)一致性”。回滾(Cancel)邏輯通常很清晰。
缺點(diǎn) : 對(duì)業(yè)務(wù)代碼侵入性極強(qiáng) 。你需要為每個(gè)TCC服務(wù)都編寫(xiě) Try, Confirm, Cancel 三個(gè)接口,開(kāi)發(fā)和維護(hù)成本高。
代表二:SAGA 模式
核心思想 :將一個(gè)長(zhǎng)事務(wù)拆分為多個(gè) 本地事務(wù) 的序列,每個(gè)本地事務(wù)都有一個(gè)對(duì)應(yīng)的 補(bǔ)償事務(wù) 。如果正向流程中的任何一步失敗,系統(tǒng)會(huì)反向調(diào)用前面已成功步驟的補(bǔ)償事務(wù),以達(dá)到回滾的目的。
對(duì)比 TCC :SAGA 沒(méi)有“預(yù)留”階段,是“先斬后奏”。TCC 是預(yù)留-執(zhí)行,SAGA 是執(zhí)行-補(bǔ)償。
協(xié)調(diào)方式 :兩種協(xié)調(diào)方式。
- 編排式(Orchestration) :由一個(gè)中央?yún)f(xié)調(diào)器(Saga Coordinator)統(tǒng)一調(diào)度。
- 協(xié)同式(Choreography) :各個(gè)服務(wù)通過(guò)訂閱/發(fā)布事件(通常借助消息隊(duì)列)來(lái)驅(qū)動(dòng)流程。
編排式 SAGA(Centralized Orchestrator)
# Saga 協(xié)調(diào)器(中央編排)
def saga_place_order(order_id, user_id, item_id, amount):
try:
# Step 1: 創(chuàng)建訂單(本地事務(wù))
svc_order.create_order(order_id, user_id, item_id)
# Step 2: 調(diào)用庫(kù)存服務(wù)
svc_inventory.decrease(item_id, qty=1) # 本地事務(wù)
# Step 3: 調(diào)用支付服務(wù)
svc_payment.charge(user_id, amount) # 本地事務(wù)
# Step 4: 發(fā)貨
svc_shipping.ship(order_id, item_id) # 本地事務(wù)
# 全部成功,Saga 結(jié)束
return True
except Exception as e:
# 出現(xiàn)任何錯(cuò)誤,按已完成步驟依次執(zhí)行補(bǔ)償
# 注意:補(bǔ)償順序通常是反序
try: svc_shipping.cancel_ship(order_id)
except: pass
try: svc_payment.refund(user_id, amount)
except: pass
try: svc_inventory.increase(item_id, qty=1)
except: pass
try: svc_order.cancel_order(order_id)
except: pass
return False協(xié)同式 SAGA(Event-driven Choreography)
# 服務(wù)間通過(guò)事件驅(qū)動(dòng),每個(gè)服務(wù)在處理完自己的本地事務(wù)后發(fā)布事件
# 示例:Order Service 創(chuàng)建訂單后,發(fā)布 OrderCreated 事件
# Order Service
def create_order(order_id, user_id, item_id):
with db.tx():
insert_order(order_id, user_id, item_id, status='CREATED')
publish_event('OrderCreated', {'order_id': order_id, 'item_id': item_id})
# Inventory Service (訂閱 OrderCreated)
def on_order_created(event):
try:
with db.tx():
decrease_stock(event.item_id, 1)
publish_event('InventoryReserved', {'order_id': event.order_id})
except:
publish_event('InventoryFailed', {'order_id': event.order_id})
# Payment Service (訂閱 InventoryReserved)
def on_inventory_reserved(event):
try:
with db.tx():
charge_user(order.user_id, amount)
publish_event('PaymentSucceeded', {'order_id': event.order_id})
except:
publish_event('PaymentFailed', {'order_id': event.order_id})
# 下游服務(wù)根據(jù)成功/失敗事件決定下一步或觸發(fā)補(bǔ)償
# 例如:如果 PaymentFailed,發(fā)布 PaymentFailed 事件,Order Service 或其它服務(wù)會(huì)收到并觸發(fā) refund/rollback要點(diǎn)說(shuō)明:
- 編排式 可控性更強(qiáng),易于監(jiān)控與補(bǔ)償(中央?yún)f(xié)調(diào)器負(fù)責(zé)全局狀態(tài)),但集中化會(huì)增加單點(diǎn)邏輯復(fù)雜度。
- 協(xié)同式 更松耦合、彈性好,但容易變得分散且難以跟蹤全局狀態(tài),補(bǔ)償和異常處理需要更復(fù)雜的事件治理與冪等設(shè)計(jì)。
- 無(wú)論哪種方式, 補(bǔ)償事務(wù)必須設(shè)計(jì)為冪等 ,并且要能部分或逐步恢復(fù)系統(tǒng)一致性。
SAGA 優(yōu)點(diǎn) :耦合度低,實(shí)現(xiàn)相對(duì) TCC 簡(jiǎn)單(只需關(guān)心補(bǔ)償邏輯),性能好,特別適合流程長(zhǎng)、需要異步執(zhí)行的業(yè)務(wù)。
SAGA 缺點(diǎn) : 不保證隔離性 。在事務(wù)提交到一半時(shí),其他請(qǐng)求可能會(huì)讀到中間狀態(tài)(例如,訂單已創(chuàng)建,但庫(kù)存還未扣減)。補(bǔ)償邏輯的設(shè)計(jì)可能非常復(fù)雜。
代表三:基于可靠消息的最終一致性 (Transactional Outbox)
核心思想 :這是目前應(yīng)用最廣泛的方案之一。其精髓在于 利用本地事務(wù)的原子性,來(lái)保證“業(yè)務(wù)操作”和“發(fā)送消息”這兩個(gè)動(dòng)作的原子性 。
流程 :
- 開(kāi)啟 數(shù)據(jù)庫(kù)本地事務(wù) 。
- 執(zhí)行業(yè)務(wù)操作(如扣庫(kù)存)。
- 將要發(fā)送的消息(如“庫(kù)存已扣減”)寫(xiě)入到 同一個(gè)數(shù)據(jù)庫(kù) 的一張“本地消息表”中。
- 提交本地事務(wù) 。此時(shí),業(yè)務(wù)數(shù)據(jù)和消息數(shù)據(jù)要么都成功,要么都失敗。
- 一個(gè)獨(dú)立的后臺(tái)任務(wù)/服務(wù),輪詢?cè)撓⒈?,將消息投遞到真正的消息隊(duì)列(MQ)中。
- 下游服務(wù)(如訂單服務(wù))消費(fèi) MQ 中的消息,執(zhí)行自己的本地事務(wù)。
Transactional Outbox 的核心在于: 把要發(fā)送的事件/消息先寫(xiě)入與業(yè)務(wù)數(shù)據(jù)相同的數(shù)據(jù)庫(kù)事務(wù)中(outbox 表) ,保證寫(xiě)業(yè)務(wù)數(shù)據(jù)和寫(xiě) outbox 的原子性;隨后由獨(dú)立的投遞器將 outbox 的消息可靠地投遞給消息隊(duì)列(MQ),并將其標(biāo)記為已發(fā)送。
# 生產(chǎn)者:在同一個(gè)本地事務(wù)中寫(xiě)業(yè)務(wù)數(shù)據(jù)和 outbox
def deduct_inventory_and_emit_event(item_id, qty):
with db.tx() as cur:
# 1. 本地業(yè)務(wù)修改
update_inventory(item_id, -qty)
# 2. 寫(xiě)入 outbox 表(消息尚未發(fā)送)
outbox_msg = {
'id': gen_uuid(),
'topic': 'inventory.deducted',
'payload': json.dumps({'item_id': item_id, 'qty': qty}),
'status': 'PENDING',
'created_at': now()
}
insert_outbox(outbox_msg)
cur.commit()
# 投遞器(異步后臺(tái)任務(wù))
def outbox_dispatcher_loop():
while True:
msgs = select_pending_outbox(limit=100)
for msg in msgs:
try:
mq.publish(msg.topic, msg.payload) # 將消息發(fā)到 MQ(需保證冪等/至少一次)
mark_outbox_sent(msg.id, sent_at=now())
except Exception as e:
# 發(fā)布失敗:記錄日志,稍后重試(冪等性在消費(fèi)端保證)
log.error("publish failed", e)
sleep(poll_interval)消費(fèi)端需保證 冪等性 以應(yīng)對(duì)消息可能的重復(fù)投遞。優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單、與現(xiàn)有關(guān)系型數(shù)據(jù)庫(kù)天然集成,且能利用數(shù)據(jù)庫(kù)事務(wù)保證本地原子性;缺點(diǎn)是需要一個(gè)額外的投遞進(jìn)程和 outbox 表的運(yùn)維,且消息發(fā)出存在短時(shí)延遲(取決于投遞器的輪詢頻率或觸發(fā)策略)。
優(yōu)點(diǎn) :與業(yè)務(wù)邏輯解耦,實(shí)現(xiàn)相對(duì)簡(jiǎn)單(依賴數(shù)據(jù)庫(kù)和 MQ),性能好,可靠性高。
缺點(diǎn) :一致性有時(shí)效性延遲;需要額外維護(hù)消息表和輪詢服務(wù);下游服務(wù)需要保證消費(fèi)的冪等性。
為了簡(jiǎn)化本地消息表的模式,像 RocketMQ 這樣的消息中間件提供了內(nèi)置的 事務(wù)消息 功能,它通過(guò) 半消息 和 事務(wù)回查 機(jī)制,在 MQ 層面實(shí)現(xiàn)了業(yè)務(wù)操作與消息發(fā)送的原子性,開(kāi)發(fā)者只需關(guān)注業(yè)務(wù)邏輯和回查接口的實(shí)現(xiàn)。
分布式事務(wù)解析(三):如何在具體場(chǎng)景中做技術(shù)選型?
比如:“如果要你設(shè)計(jì)一個(gè)電商平臺(tái)的下單到支付流程,你會(huì)選擇哪種分布式事務(wù)方案?”
分析業(yè)務(wù)場(chǎng)景,明確一致性要求
下單扣庫(kù)存 :這個(gè)環(huán)節(jié),用戶對(duì)延遲不敏感,但庫(kù)存數(shù)據(jù)必須準(zhǔn)確。這是一個(gè)典型的異步、長(zhǎng)流程場(chǎng)景。
支付扣款 :這個(gè)環(huán)節(jié)涉及到真實(shí)的資金,對(duì)一致性要求極高。用戶不能容忍錢(qián)扣了但訂單狀態(tài)沒(méi)更新,或者訂單顯示支付成功但錢(qián)沒(méi)扣。
方案選型與組合
一個(gè)復(fù)雜的業(yè)務(wù)流程,通常不是單一方案能搞定的,而是 組合拳 。
對(duì)于“下單 -> 扣庫(kù)存 -> 通知發(fā)貨”這類(lèi)非核心資金鏈路
- 首選:基于可靠消息的最終一致性 或 SAGA 模式。
- 理由 :將“創(chuàng)建訂單”作為核心的上游本地事務(wù)。訂單創(chuàng)建成功后,通過(guò)“本地消息表”模式,可靠地向 MQ 發(fā)送一個(gè)
OrderCreated事件。下游的“庫(kù)存服務(wù)”、“物流服務(wù)”、“積分服務(wù)”等各自訂閱這個(gè)消息,并執(zhí)行自己的業(yè)務(wù)邏輯。這種方式 松耦合 ,允許各服務(wù)異步執(zhí)行, 系統(tǒng)吞吐量高 。即使某個(gè)下游服務(wù)暫時(shí)失敗,重試機(jī)制也能保證數(shù)據(jù) 最終一致 ,完全滿足這類(lèi)場(chǎng)景的需求。
對(duì)于“支付 -> 更新訂單狀態(tài)”這類(lèi)核心資金鏈路
- 首選:TCC 模式 ,或者通過(guò)巧妙的業(yè)務(wù)設(shè)計(jì)規(guī)避。
- 理由 :支付環(huán)節(jié),優(yōu)先考慮 TCC。當(dāng)用戶點(diǎn)擊支付時(shí),支付網(wǎng)關(guān)會(huì)調(diào)用我們的支付服務(wù)。
- Try 階段 :支付服務(wù)調(diào)用訂單服務(wù),嘗試將訂單狀態(tài)置為“支付中”(預(yù)留狀態(tài)),并調(diào)用賬戶服務(wù)凍結(jié)用戶余額。
- Confirm 階段 :如果支付渠道返回成功,則調(diào)用訂單服務(wù)的 Confirm 接口,將狀態(tài)改為“已支付”,并調(diào)用賬戶服務(wù)真正扣款。
- Cancel 階段 :如果支付失敗或超時(shí),則調(diào)用 Cancel 接口,將訂單狀態(tài)回滾,并解凍用戶余額。
TCC 方案雖然開(kāi)發(fā)復(fù)雜,但它能提供 準(zhǔn)強(qiáng)一致性 的保障,防止資金類(lèi)業(yè)務(wù)出現(xiàn)差錯(cuò),這是業(yè)務(wù)的底線。
如下是更貼近支付場(chǎng)景的具體偽代碼,展示 Try/Confirm/Cancel 的典型調(diào)用順序與冪等性保障。
# 支付協(xié)調(diào)器(偽代碼)
def process_payment(order_id, user_id, amount):
tx_id = f"pay-{order_id}-{now_ts()}"
# 1. Try 階段:訂單服務(wù)和賬戶服務(wù)進(jìn)行預(yù)留/凍結(jié)
ok_order = order_service.try_mark_paying(order_id, tx_id) # 將訂單置為 PAYING(預(yù)留)
ok_account = account_service.try_freeze(user_id, amount, tx_id)
if not (ok_order and ok_account):
# 如果任一 Try 失敗,調(diào)用 Cancel
order_service.cancel_mark_paying(order_id, tx_id)
account_service.cancel_freeze(user_id, tx_id)
return False
# 2. 調(diào)用第三方支付渠道(同步或異步)
success = payment_gateway.charge(user_id, amount)
# 3. 根據(jù)渠道結(jié)果調(diào)用 Confirm 或 Cancel
if success:
order_service.confirm_paid(order_id, tx_id)
account_service.confirm_charge(user_id, tx_id)
return True
else:
order_service.cancel_mark_paying(order_id, tx_id)
account_service.cancel_freeze(user_id, tx_id)
return False
# 賬戶服務(wù)內(nèi)部(示意)
class AccountService:
def try_freeze(self, user_id, amount, tx_id):
with db.tx():
if already_tried(tx_id): return True
if get_balance(user_id) < amount: return False
insert_freeze_record(tx_id, user_id, amount)
# 不實(shí)際扣款,只凍結(jié)
cur.commit()
return True
def confirm_charge(self, user_id, tx_id):
with db.tx():
if already_confirmed(tx_id): return True
deduct_balance(user_id, get_freeze_amount(tx_id))
mark_confirmed(tx_id)
cur.commit()
return True
def cancel_freeze(self, user_id, tx_id):
with db.tx():
if already_cancelled_or_confirmed(tx_id): return True
remove_freeze_record(tx_id)
mark_cancelled(tx_id)
cur.commit()
return True要點(diǎn)回顧:
- 支付相關(guān)的 Try 必須保證對(duì)外部渠道調(diào)用之前,資金/狀態(tài)已被“鎖住”或標(biāo)記,否則會(huì)產(chǎn)生競(jìng)態(tài)。
- Confirm/Cancel 都必須是冪等的,以便在網(wǎng)絡(luò)或重試下不會(huì)重復(fù)扣款或錯(cuò)誤釋放資金。
- 實(shí)際工程中,外部支付渠道通常是異步回調(diào),協(xié)調(diào)器需要設(shè)計(jì)回調(diào)處理邏輯,并把回調(diào)與原始
tx_id關(guān)聯(lián)起來(lái)以完成 Confirm/Cancel。
對(duì) 2PC 的態(tài)度 :在這個(gè)場(chǎng)景中,一般不會(huì)考慮使用 2PC。因?yàn)樗鼘?duì)性能的損耗和對(duì)可用性的影響,對(duì)于面向海量用戶的互聯(lián)網(wǎng)應(yīng)用是不可接受的。
結(jié)語(yǔ)
分布式數(shù)據(jù)庫(kù)的世界,本質(zhì)上是一個(gè)充滿權(quán)衡的藝術(shù)。
- 為了實(shí)現(xiàn) 分布式原子性 ,我們從有阻塞風(fēng)險(xiǎn)的 2PC 演進(jìn)到了基于多數(shù)共識(shí)的 Paxos/Raft 。
- 為了實(shí)現(xiàn) 高可用和數(shù)據(jù)安全 ,我們采用 主從復(fù)制 ,并在 同步(強(qiáng)一致) 與 異步(最終一致) 之間權(quán)衡。
- 而這一切頂層設(shè)計(jì)的指導(dǎo)原則,都落在了 CAP 定理 上——在網(wǎng)絡(luò)分區(qū)不可避免的前提下,我們究竟是選擇數(shù)據(jù)的絕對(duì)正確(CP),還是選擇服務(wù)的永不中斷(AP)。
理解這些核心概念與它們背后的取舍,不僅能幫助我們更好地選擇和使用數(shù)據(jù)庫(kù),更能讓我們?cè)谠O(shè)計(jì)任何分布式系統(tǒng)時(shí),都能做出更明智、更貼合業(yè)務(wù)需求的決策。





































