突破關係型資料庫桎梏:雲原生資料庫中介軟體核心剖析
作者介紹
張亮, 京東數科資料研發負責人。熱愛開源,目前主導兩個開源專案Elastic-Job和Sharding-Sphere(Sharding-JDBC)。擅長以Java為主分散式架構以及以Kubernetes和Mesos為主的雲平臺方向,推崇優雅程式碼,對如何寫出具有展現力的程式碼有較多研究。2018年初加入京東數科,現擔任資料研發負責人。目前主要精力投入在將Sharding-Sphere打造為業界一流的金融級資料解決方案之上。
資料庫技術的發展與變革方興未艾,NewSQL的出現,只是將各種所需技術組合在一起,而這些技術組合在一起所實現的核心功能,推動著雲原生資料庫的發展。在上一篇文章《關係型資料庫尚能飯否?NoSQL、NewSQL誰能接棒?》中我們已經瞭解了雲原生資料庫的發展背景,所以本文會有針對性地深入解讀雲原生資料庫的相關內容。
NewSQL的三種分類中,新架構和雲資料庫涉及了太多與資料庫相關的底層實現,為了保證本文的範圍不至太過發散,我們重點介紹透明化分片資料庫中介軟體的核心功能與實現原理,另外兩種型別的NewSQL在核心功能上類似,但實現原理會有所差別。
一、資料分片
傳統的將資料集中儲存至單一資料節點的解決方案,在效能和可用性兩方面已經難於滿足網際網路的海量資料場景。由於關係型資料庫大多采用B+樹型別的索引,在資料量超過閾值的情況下,索引深度的增加也將使得磁碟訪問的IO次數增加,進而導致查詢效能的大幅下降;同時高併發訪問請求也使得集中式資料庫成為系統的最大瓶頸。
在傳統關係型資料庫無法滿足網際網路場景需要的情況下,將資料儲存至原生支援分散式的NoSQL的嘗試越來越多。但NoSQL對SQL的不相容性以及生態圈的不完善,使得它們在與關係型資料庫的博弈中始終無法完成致命一擊,關係型資料庫的地位依然不可撼動。
資料分片,指按照某個維度將存放在單一資料庫中的資料分散地存放至多個數據庫或表中,以達到提升效能瓶頸及可用性的效果。資料分片的有效手段是對關係型資料庫進行分庫或分表。分庫和分表均可以有效避免因為資料量超過可承受閾值而產生的查詢瓶頸。
除此之外,分庫還能夠用於有效分散對資料庫單點的訪問量;而分表則能夠提供儘量將分散式事務轉化為本地事務的可能。使用多主多從的分片方式,可以有效避免資料單點,從而提升資料架構的可用性。
1、垂直分片
垂直分片又稱為縱向拆分,它的核心理念是專庫專用。在拆分之前,一個數據庫由多個數據表構成,每個表對應著不同的業務。而拆分之後,則按照業務將表進行歸類,分佈到不同的資料庫中,從而將壓力分擔到不同的資料庫之上,如圖:
2、水平分片
水平分片又稱為橫向拆分。相對於垂直分片,水平分片不是將資料根據業務邏輯分類,而是按照某個欄位的某種規則將資料分散到多個庫或表中,每個分片僅包含其中的一部分資料。
例如,根據ID的最後一位以10取餘,尾數是0的放入0庫(表),尾數是1的放入1庫(表)。如圖:
為了解決關係型資料庫面對海量資料時因資料量過大而導致的效能問題,將資料進行分片是行之有效的解決方案。
將集中於單一節點的資料拆分並分別儲存到多個數據庫或表,稱為分庫分表。分庫可以有效分散由高併發所帶來的對資料庫訪問的壓力。分表雖然無法緩解資料庫壓力,但僅跨分表的更新操作,依然能使用資料庫原生的ACID事務;而一旦涉及到跨庫的更新操作,分散式事務的問題就會變得無比複雜。
通過分庫和分表拆分資料使得各個表的資料量保持在閾值以下。垂直分片往往需要對架構和設計進行調整,通常來講,是來不及應對網際網路快速變化的業務需求的,而且它也無法真正解決單點瓶頸。而水平分片從理論上突破了單機資料量處理的瓶頸,並且擴充套件相對自由,是分庫分表的標準解決方案。
分庫和讀寫分離疏導流量是應對高訪問量的常見手段。分表雖然可以解決海量資料導致的效能問題,但無法解決過多請求訪問同一資料庫導致的響應變慢問題。所以水平分片通常採取分庫的方式,一併解決資料量和訪問量巨大的問題。讀寫分離是另一個疏導流量的辦法,但讀寫資料間的延遲是架構設計時需要考慮的問題。
雖然分庫可以解決上述問題,但分散式架構在獲得了收益的同時,也帶來了新的問題。面對如此散亂的分庫分表之後的資料,應用開發和運維人員對資料庫的操作變得異常繁重就是其中的重要挑戰之一。他們需要知道什麼樣的資料需要從哪個具體的資料庫的分表中去獲取。
新架構的NewSQL與資料分片中介軟體在這個功能的處理方式上是不同的:
- 新架構的NewSQL會重新設計資料庫儲存引擎,將同一表中的資料儲存在分散式檔案系統中。
- 資料分片中介軟體則是儘量透明化分庫分表所帶來的影響,讓使用方儘量像使用一個數據庫一樣使用水平分片之後的資料庫。
跨庫事務是分散式資料庫要面對的棘手事情。合理採用分表,可以在降低單表資料量的情況下,儘量使用本地事務,善於使用同庫不同表可有效避免分散式事務帶來的麻煩。在不能避免跨庫事務的場景,有些業務仍需保持事務的一致性。而基於XA的分散式事務由於效能低下,無法被網際網路公司所採納,大多采用最終一致性的柔性事務代替分散式事務。
3、讀寫分離
面對日益增加的系統訪問量,資料庫的吞吐量面臨著巨大瓶頸。對於同一時間有大量併發讀操作和較少寫操作型別的應用系統來說,將單一的資料庫拆分為主庫和從庫,主庫負責處理事務性的增刪改操作,從庫負責處理查詢操作,能夠有效的避免由資料更新導致的行鎖,使得整個系統的查詢效能得到極大改善。
通過一主多從的配置方式,可以將查詢請求均勻分散到多個數據副本,能夠進一步提升系統的處理能力。
使用多主多從的方式,不但能夠提升系統的吞吐量,還能夠提升系統的可用性,可以達到在任何一個數據庫宕機,甚至磁碟物理損壞的情況下仍然不影響系統的正常執行。
讀寫分離本質上是資料分片的一種。與將資料根據分片鍵打散至各個資料節點的水平分片不同,讀寫分離則是根據SQL語義的分析,將讀和寫請求分別路由至主庫與從庫。讀寫分離的資料節點中的資料是一致的,而水平分片每個資料節點的資料內容卻並不相同。將水平分片和讀寫分離聯合使用,能夠更加有效的提升系統性能,但同時也讓系統維護更復雜。
雖然讀寫分離可以提升系統的吞吐量和可用性,但同時也帶來了資料不一致的問題,這包括多個主庫之間的資料一致性及主庫與從庫之間的資料一致性問題。並且,讀寫分離也帶來了與資料分片同樣的問題,它也會使得應用開發和運維人員對資料庫的操作和運維變得更加複雜。
透明化讀寫分離所帶來的影響,讓使用方儘量像使用一個數據庫一樣使用主從資料庫,是讀寫分離的主要功能。
4、核心流程
資料分片核心是由SQL解析、SQL路由、SQL改寫、SQL執行及結果歸併的流程組成。為了保持原有的應用程式實現低接入成本,則需相容對資料庫的訪問,因此需要進行資料庫協議的適配。
協議適配
NewSQL對傳統關係型資料庫的相容性,除了SQL之外,相容資料庫的協議可以降低使用方的接入成本。開源的關係型資料庫均能通過實現它的協議標準,將自己的產品裝扮成原生的關係型資料庫。
由於MySQL和PostgreSQL流行度較高,很多NewSQL會實現它們的傳輸協議,讓使用MySQL和PostgreSQL的使用者能夠無需修改業務程式碼就自動接入NewSQL產品。
MySQL協議
MySQL是當前最為流行的開源資料庫。要了解它的協議,可以通過MySQL的基本資料型別、協議包結構、連線階段和命令階段這4方面入手。
基本資料型別
MySQL協議包中所有的內容均由MySQL所定義的基本資料型別組成,具體資料型別參見下表:
MySQL基本資料型別
在需要將二進位制資料轉換為MySQL可理解的資料時,MySQL協議包將根據資料型別預先定義的位數讀取,並轉換為相應的數字或字串;反之亦然,MySQL會將每個欄位按照規範中規定的長度寫入協議包。
協議包結構
MySQL協議由一個或多個MySQL協議包(MySQL Packet)組成。無論型別如何,它均由訊息長度(Payload Length)、序列主鍵(Sequence ID)和訊息體(Payload)這3部分組成:
- 訊息長度為int<3>型別。它表示隨後的訊息體所佔用的位元組總數。需要注意的是,訊息長度並不包含序列主鍵的佔位在內。
- 序列主鍵為int<1>型別。它表示一次請求後返回的多個MySQL協議包中,每個協議包的序號。佔位為1位元組的序列主鍵最大值為0xff,即十進位制的255,但這並非表示每次請求最多隻能包含255個MySQL協議包,超過255的序列主鍵將再次從0開始計數。例如一次查詢可能返回幾十萬的記錄,那麼MySQL協議包只需保證其序列主鍵連續,將大於255的序列主鍵重置為0,重新開始計數即可。
- 訊息體的長度為訊息長度所宣告的位元組數。它是MySQL協議包中真正的業務資料,根據不同的協議包型別,訊息體的內容也不同。
連線階段
連線階段用於建立MySQL的客戶端與服務端的通訊管道。該階段主要執行交換並匹配MySQL客戶端與服務端的版本功能描述(Capability Negotiation)、建立SSL通訊管道及驗證授權這3個任務。下圖以MySQL服務端為視角繪製了連線建立流程圖:
MySQL連線階段流程圖
該圖並未包含MySQL服務端與客戶端的互動。實際上,MySQL的連線建立是由客戶端發起的。
MySQL服務端在接收到客戶端的連線請求後,先進行服務端和客戶端版本間所具有的功能資訊的交換和匹配(Capability Negotiation),然後根據兩端的協商結果生成不同格式的初始化握手協議包,並向客戶端寫入改協議包。協議包中包括由MySQL服務端分配的連線主鍵、服務端當前版本功能描述(Capabilities)以及為驗證授權生成的密文。
MySQL客戶端在接收到服務端傳送的握手協議包後,將傳送握手協議響應包。該協議包中主要包含的資訊是用於資料庫訪問的使用者名稱及加密後的密碼密文。
MySQL服務端接收到握手協議響應包之後,即進行授權校驗,並將校驗結果返回至客戶端。
命令階段
連線階段成功之後,則進入命令執行的互動階段。MySQL一共有32個命令協議包,具體型別參見下圖:
MySQL命令包
MySQL的命令協議包分為4個大類,分別是:文字協議、二進位制協議、儲存過程及資料複製協議。
協議包訊息體中的首位用於標識命令型別。協議包根據名稱即可望文生義,在這裡無需一一解釋它們的具體用途,下文會解析幾個重點的MySQL命令協議包:
-
COM_QUERY
COM_QUERY是MySQL用於以明文格式查詢的重要命令,它對應JDBC中的java.sql.Statement。COM_QUERY命令本身較為簡單,它由識別符號和SQL組成:
1 [03] COM_QUERY
string[EOF] the query the server shall execute
COM_QUERY的響應協議包則較為複雜,見下圖:
MySQL查詢命令流程圖
COM_QUERY根據其場景有可能返回4種類型,它們是:查詢結果、更新結果、檔案執行結果及錯誤結果。
當執行過程中出現如網路斷開、SQL語法不正確等錯誤時,MySQL協議要求將協議包首位設定為0xff,並將錯誤資訊封裝至ErrPacket協議包返回。
通過檔案執行COM_QUERY的情況並不常見,此處不再過多說明。
對於更新請求,MySQL協議要求將協議包首位設定為0x00,並返回OkPacket協議包。OkPacket協議包需要包含本次更新操作所影響的行記錄數及最後插入的主鍵值資訊。
查詢請求最為複雜,它需要將讀取int<lenenc>的方式獲得結果集欄位的數目建立為獨立的FIELD_COUNT協議包返回。然後再依次將返回欄位的每一列詳細資訊分別生成獨立的COLUMN_DEFINITION協議包,查詢欄位的元資料資訊最終以一個EofPacket結束。之後便可以開始逐行生成資料協議包Text Protocol Resultset Row,它本身並不關注資料的具體型別,會統一將其轉換為string<lenenc>格式。資料協議包最終依然以一個EofPacket結束。
對應於JDBC中java.sql.PreparedStatement的操作,則是由MySQL協議包中的二進位制協議組成,它們由COM_STMT_PREPARE、COM_STMT_EXECUTE、COM_STMT_ CLOSE、COM_STMT_RESET和COM_ STMT_SEND_LONG_DATA這5個協議包組成。其中最為重要的是COM_STMT_PREPARE和COM_STMT_ EXECUTE,它們分別對應JDBC中的connection.prepareStatement方法以及connection.execute&connection.executeQuery&connection.executeUpdate方法。
-
COM_STMT_PREPARE
COM_STMT_PREPARE協議包與COM_QUERY協議包類似,同樣是由命令識別符號和SQL組成:
1 [16] COM_STMT_PREPARE
string[EOF] the query to prepare
COM_STMT_PREPARE協議包的返回值並非查詢結果,而是由statement_id、列數目和引數數目等資訊組成的響應協議包。statement_id是由MySQL分配給完成預編譯之後的SQL的唯一標識,通過statement_id即可從MySQL中獲取相應的SQL。
由COM_STMT_PREPARE命令註冊過的SQL,只需將statement_id傳給COM_STMT_EXECUTE命令即可,無需將SQL本身再次傳入,節省了無謂的網路頻寬消耗。
而且MySQL可以根據COM_STMT_PREPARE傳入的SQL預編譯為抽象語法樹以供複用,進而提升SQL的執行效率。採用COM_QUERY的方式執行SQL,則需要將每條SQL重新編譯。這也是PreparedStatement比Statement效率更佳的原因所在。
-
COM_STMT_EXECUTE
COM_STMT_EXECUTE協議包主要由statement-id和與SQL的配對的引數組成。它使用了一個名為-bitmap的資料結構,用於標識引數中的空值。
COM_STMT_EXECUTE命令的響應協議包與COM_QUERY命令的響應協議包類似,都是採用欄位元資料和查詢結果集的格式返回,中間依然使用EofPacket間隔。
有所不同的是,COM_STMT_EXECUTE命令的響應協議包使用Binary Protocol Resultset Row來代替Text Protocol Resultset Row,它不會無視資料的型別統一轉換為字串,而是根據返回資料的型別,寫入相應的MySQL基本資料型別,進一步節省網路傳輸的頻寬。
其他協議
除了MySQL協議,PostgreSQL協議和SQLServer協議也是完全開源的,可以通過同樣的方式實現。而另一個常用的資料庫Oracle協議並不開源,無法通過這種方式實現。
SQL解析
相對於其他程式語言,SQL是比較簡單的。不過,它依然是一門完善的程式語言,因此解析SQL語法與解析其他程式語言(如:Java語言、C語言、Go語言等)並無本質區別。
解析過程分為詞法解析和語法解析。先通過詞法解析將SQL拆分為一個個不可再分的單詞。再使用語法解析器將SQL轉換為抽象語法樹。最後通過訪問抽象語法樹,提煉出解析上下文。
解析上下文包括表、選擇項、排序項、分組項、聚合函式、分頁資訊、查詢條件。如果是分片中介軟體型別的NewSQL還需要記錄可能修改的佔位符標記。
將SQL:select username, ismale from userinfo where age > 20 and level > 5 and 1 = 1解析為抽象語法樹:
抽象語法樹
生成抽象語法樹的第三方工具有很多,ANTLR是不錯的選擇。它可以通過開發者定義的規則生成抽象語法樹的Java程式碼並提供訪問者介面。相比於程式碼生成,手寫抽象語法樹在執行效率方面會更加高效,但是工作量也比較大。對效能要求高的場景中,可以考慮定製化抽象語法樹。
請求路由
根據解析上下文匹配資料分片策略,並生成路由路徑。對於攜帶分片鍵的SQL路由,根據分片鍵的不同可以劃分為單片路由(分片操作符是等號)、多片路由(分片操作符是IN)和範圍路由(分片操作符是BETWEEN)。不攜帶分片鍵的SQL則採用廣播路由。
分片策略通常可由資料庫內建或由使用者方配置。資料庫內建的方案較為簡單,內建的分片策略大致可分為尾數取模、雜湊、範圍、標籤、時間等;由使用者方配置的分片策略則更加靈活,可以根據使用方需求定製複合分片策略。
SQL改寫
新架構的NewSQL無需SQL改寫,這部分主要是針對分片中介軟體型別的NewSQL。它用於將SQL改寫為在真實資料庫中可以正確執行的語句。包括將邏輯表名稱替換為真實表名稱,將分頁資訊的起始取值和結束取值改寫,增加為排序、分組和自增主鍵使用的補列,將AVG改寫為SUM/COUNT等。
結果歸併
將多個執行結果集歸併並統一對應用端輸出。結果歸併包括流式歸併和記憶體歸併:
- 流式歸併 用於簡單查詢、排序查詢、分組查詢及排序和分組但排序項和分組項完全一致的場景,流式歸併結果集的遍歷方式是通過每一次呼叫next方法取出,無需佔用額外的記憶體。
- 記憶體歸併 則需要將結果集中所有資料載入至記憶體處理,如果結果集資料過多,會佔用大量記憶體。
二、分散式事務
前文提到過,資料庫事務是需要滿足ACID(原子性、一致性、隔離性、永續性)這四個特性的:
- 原子性(Atomicity)指事務作為整體來執行,要麼全部執行,要麼全不執行。
- 一致性(Consistency)指事務應確保資料從一個一致的狀態轉變為另一個一致的狀態。
- 隔離性(Isolation)指多個事務併發執行時,一個事務的執行不應影響其他事務的執行。
- 永續性(Durability)指已提交的事務修改資料會被持久儲存。
在單一資料節點中,事務僅限於對單一資料庫資源的訪問控制,稱之為本地事務。但在基於SOA的分散式應用環境下,越來越多的應用要求對多個數據庫資源、多個服務的訪問都能納入到同一個事務當中,分散式事務應運而生。
關係型資料庫雖然對本地事務提供了完美的ACID原生支援。但在分散式的場景下,它卻成為系統性能的桎梏。如何讓資料庫在分散式場景下滿足ACID的特性或找尋相應的替代方案,是分散式事務的重點工作。
1、XA協議
最早的分散式事務模型是由X/Open國際聯盟提出的X/Open Distributed Transaction Processing(DTP)模型,簡稱XA協議。
DTP模型中通過一個全域性事務管理器與多個資源管理器進行互動。全域性事務管理器負責管理全域性事務狀態和參與事務的資源,資源管理器則負責具體的資源操作,DTP模型與應用程式的關係見下圖:
DTP模型
XA協議使用兩階段提交來保證分散式事務原子性。它將提交過程分為準備階段和提交階段。
- 在準備階段時,全域性事務管理器向每個資源管理器傳送準備訊息,用於確認本地事務操作的成功與否;
- 在提交階段時,若全域性事務管理器收到了所有資源管理器回覆的成功訊息,則向每個資源管理器傳送提交訊息,否則傳送回滾訊息。資源管理器根據接收到的訊息對本地事務進行提交或回滾操作。
下圖展示了XA協議的事務流程:
XA事務流程
二階段提交是XA協議的標準實現。它將分散式事務的提交拆分為兩階段:prepare和commit/rollback。
開啟XA全域性事務後,所有子事務會按照本地預設的隔離級別鎖定資源,並記錄undo和redo日誌,然後由TM發起prepare投票,詢問所有的子事務是否可以進行提交:當所有子事務反饋的結果為“yes”時,TM再發起commit;若其中任何一個子事務反饋的結果為“no”,TM則發起rollback;如果在prepare階段的反饋結果為yes,而commit的過程中出現宕機等異常時,則在節點服務重啟後,可根據XA recover再次進行commit補償,以保證資料的一致性。
基於XA協議實現的分散式事務對業務侵入很小,它最大優勢就是對使用方透明,使用者可以像使用本地事務一樣使用基於XA協議的分散式事務。XA協議能夠嚴格保障事務ACID特性。
但嚴格保障事務ACID特性是一把雙刃劍。
事務執行在過程中需要將所需資源全部鎖定,它更加適用於執行時間確定的短事務,對於長事務來說,整個事務進行期間對資料的獨佔,將導致對熱點資料依賴的業務系統併發效能衰退明顯。因此,在高併發的效能至上場景中,基於XA協議的分散式事務並不是最佳選擇。
2、柔性事務
如果將實現了ACID事務要素的事務稱為剛性事務的話,那麼基於BASE事務要素的事務則稱為柔性事務。BASE是基本可用(Basically Available)、柔性狀態(Soft state)和最終一致性(Eventually consistent)這三個要素的縮寫:
- 基本可用保證分散式事務參與方不一定同時線上;
- 柔性狀態允許系統狀態更新有一定的延時,這個延時對客戶來說不一定能夠察覺;
- 最終一致性通常是通過訊息可達的方式保證系統的最終一致性。
在ACID事務中對隔離性的要求很高,在事務執行過程中,必須將所有的資源鎖定。柔性事務的理念則是通過業務邏輯將互斥鎖操作從資源層面上移至業務層面。通過放寬對強一致性要求,來換取系統吞吐量的提升。
由於在分散式系統中,可能會出現超時重試的情況,因此柔性事務中的操作必須是冪等的,需要通過冪等來避免多次請求所帶來的問題。實現柔性事務的方案主要有最大努力送達、Saga和TCC。
最大努力送達
是最簡單的一種柔性事務,它適合對於資料庫的操作最終一定能夠成功的場景。由NewSQL自動記錄執行失敗的SQL,並反覆嘗試,直至執行成功。使用最大努力送達型的柔性事務是沒有回滾功能的。
這種型別的柔性事務實現最為簡單,但是對場景的要求十分苛刻。這種策略的優點是無鎖定資源時間,效能損耗小。缺點是嘗試多次提交失敗後,無法回滾,它僅適用於事務最終一定能夠成功的業務場景。因此它是通過事務回滾功能上的妥協,來換取效能的提升。
Saga
Saga源於1987年由Hector Garcaa-Molrna和Kenneth Salem發表的論文。
論文參考連結:
www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf
Saga事務更適合使用長事務的場景。它由多個本地事務所組成,每個本地事務有相應的執行模組和補償模組,任何一個本地事務出錯時,可以通過呼叫相關的補充方法達到事務的最終一致性。
Saga模型將一個分散式事務拆分為多個本地事務,每個本地事務都有相應的執行模組(Transaction)和補償模組(Compensation)。當Saga事務中的任一本地事務執行失敗時,可以通過呼叫其相關補償方法恢復之前的事務,以達到事務最終的一致性。
當每個Saga子事務T1,T2,…,Tn都有對應的補償定義C1,C2,…,Cn-1,那麼Saga系統可以保證:
- 子事務序列T1,T2,…,Tn得以完成 。這是事務的最佳情況,即無需回滾的情況。
- 或者序列T1,T2,…,Tx, Cx,…,C2,C1,(其中x小於n)得以完成。它能夠保證當回滾發生時,補償操作按照正向操作相反的順序依次執行。
Saga模型同時支援正向恢復以及逆向恢復。正向恢復是指重試當前失敗的事務,它的實現前提是每個子事務都能夠最終執行成功;向後恢復則是前文提及的,在任一子事務失敗時,補償所有已完成的事務。
顯然,正向恢復沒有必要提供補償事務,如果在業務中的子事務最終總會成功,那麼向前恢復則能夠降低Saga模型的使用複雜度。另外,如果補償事務難以實現,則正向恢復也是不錯的選擇。
雖然在理論上來講,補償事務永不失敗。然而,在分散式的世界中,伺服器可能會宕機、網路可能會失敗,甚至資料中心也可能會停電。因此,需要提供故障恢復後回退的機制,比如人工干預。
Saga模型沒有XA協議中的準備階段,因此事務沒有實現隔離性。如果兩個Saga事務同時操作同一資源則會產生更新丟失,髒資料讀取等問題。這就需要使用Saga事務的應用程式需要在應用層面加入資源鎖定的邏輯。
TCC
TCC(Try-Confirm-Cancel)分散式事務模型通過對業務邏輯的分解來實現分散式事務。顧名思義,TCC事務模型需要業務系統提供以下三段業務邏輯:
- Try。 完成業務檢查,預留業務所需資源。Try操作是整個TCC的精髓所在,可靈活選擇業務資源鎖的粒度。
- Confirm。 執行業務邏輯,直接使用Try階段預留的業務資源,無需再次做業務檢查。
- Cancel。 釋放Try階段預留的業務資源。
TCC模型僅提供兩階段原子提交協議,保證分散式事務原子性。事務的隔離交給業務邏輯來實現。TCC模型的隔離性思想就是通過業務的改造,從資料庫資源層面加鎖上移至業務層面加鎖,從而釋放底層資料庫鎖資源,放寬分散式事務鎖協議,提高系統的併發性。
雖然在柔性事務中,TCC事務模型的功能最強,但需要應用方負責提供實現Try、Confirm和Cancel操作的三個介面,供事務管理器呼叫。因此業務方改造的成本較高。
以A賬戶向B賬戶匯款100元為例,下圖展示了TCC對業務的改造:
匯款服務和收款服務分別需要實現,Try-Confirm-Cancel介面,並在業務初始化階段將其注入到TCC事務管理器中。
匯款服務
Try
- 檢查A賬戶有效性,即檢視A賬戶的狀態是否為“轉帳中”或者“凍結”;
- 檢查A賬戶餘額是否充足;
- 從A賬戶中扣減100元,並將狀態置為“轉賬中”;
- 預留扣減資源,將從A往B賬戶轉賬100元這個事件存入訊息或者日誌中。
Confirm
- 不做任何操作。
Cancel
- A賬戶增加100元;
- 從日誌或者訊息中,釋放扣減資源。
收款服務
Try
- 檢查B賬戶賬戶是否有效。
Confirm
- 讀取日誌或者訊息,B賬戶增加100元;
- 從日誌或者訊息中,釋放扣減資源。
由此可以看出,TCC模型對業務的侵入較強,改造的難度較大。
訊息驅動
訊息一致性方案是通過訊息中介軟體保證上下游應用資料操作的一致性。基本思路是將本地操作和傳送訊息放在一個本地事務中,下游應用向訊息系統訂閱該訊息,收到訊息後執行相應操作。本質上是依靠訊息的重試機制,達到最終一致性。下圖是訊息驅動的事務模型:
訊息驅動的缺點是:耦合度高,需要在業務系統中引入訊息中介軟體,導致系統複雜度增加。
總的來說,基於ACID的強一致性事務和基於BASE的最終一致性事務都不是銀彈,只有在最適合的場景中才能發揮它們的最大長處。詳細對比一下它們之前的區別,以幫助開發者進行技術選型。由於訊息驅動與業務系統的耦合度較高,因此不列入對比表格:
一味的追求強一致性未必是最合理的解決方案。對於分散式系統來說,建議使用“外柔內剛”的設計方案。外柔指的是在跨資料分片的情況下使用柔性事務,保證資料最終一致即可,並且換取最佳效能;內剛則是指在同一資料分片內使用本地事務,以達到ACID的效果。
三、資料庫治理
1、基礎治理
前文講述的服務治理,在資料庫的基礎治理部分大都是通用的。主要包括配置中心、註冊中心、限流、熔斷、失效轉移、呼叫鏈路追蹤等:
- 配置中心 用於配置集中化以及動態配置更新及通知下發;
- 註冊中心 用於服務發現,這裡的服務是指資料庫中間層例項本身,通過它可以實現狀態監測及自動通知,進而使得資料庫中介軟體具備高可用和自我治癒能力;
- 限流 用於流量的過載保護,分為資料庫中介軟體本身的流量過載保護和對資料庫的流量過載保護;
- 熔斷 也是流量過載的保護措施之一,它的不同之處在於熔斷整個客戶端對資料庫的訪問,以保護資料庫能夠為其他流量正常的系統繼續提供服務,可以通過前文講的熔斷器模式實現自動熔斷機制;
- 失效轉移 用於多資料副本的情況,在資料完全一致的多資料節點中,當某一節點不可用後,可通過失效轉移的機制讓資料庫中介軟體訪問至另外有效的資料節點操作資料;
- 呼叫鏈路追蹤 則是將對資料庫訪問的呼叫鏈路、效能、拓撲關係等指標以視覺化的方式展現出來。
2、彈性伸縮
資料庫治理與服務治理不同的關鍵點在於,資料庫是有狀態的,每個資料節點都有自己持久化的資料,因此很難像服務化一樣做到彈性伸縮。
當系統的訪問量和資料量超過之前評估的預期時,往往涉及到對資料庫的重新分片。雖然使用日期分片等策略時,可以在無需遷移遺留資料的情況下直接擴容,但在大部分場景中,資料庫中的遺留資料往往無法直接對映到新的分片策略中。分片策略的修改則需要進行資料的遷移。
在傳統的系統中,停止服務進行資料遷移,遷移結束之後再重啟服務是行之有效的解決方案。但這種方案使得業務方的資料遷移成本非常高,需要業務方工程師精準的評估資料量。
在網際網路場景中,系統可用性要求極高,而且業務爆發性增長的可能性較傳統行業也更加常見。在雲原生的服務架構模型中,彈性伸縮是常見的需求,並且可以比較輕鬆的實現。因此與服務對等的資料彈性伸縮功能,是雲原生資料庫的重要能力。
除了系統預分片之外,彈性伸縮的另一個實現方案是線上資料遷移。線上資料遷移經常被比喻為“在飛行過程中給飛機換引擎”,它最大的挑戰是如何保證遷移過程使服務不受影響。線上資料遷移可以在修改了資料庫的分片策略之後(比如將根據主鍵%4分為4個庫的分片方式改為根據主鍵%16的16個庫的分片方式),通過一系列的系統化操作,保證資料正確的遷移到新的資料節點的同時,讓依賴資料庫的服務完全無需感知。它可以分為以下4個步驟:
- 同步線上雙寫。 即同時將資料寫入分片策略修改前的原資料節點及分片策略修改後的新資料節點。可以通過一致性演算法來保證雙寫的一致性,如前文介紹過的Paxos或Raft演算法;
- 歷史資料遷移。 以離線的方式,將需要遷移到新資料節點部分的歷史存量資料從原有資料節點遷移過去。可以通過SQL的方式,也可以通過binlog等二進位制方式進行處理;
- 資料來源切換。 將讀寫請求切換至新的資料來源,並停止對原資料節點的雙寫;
- 清理冗餘資料。 在舊資料節點中,清理已遷移至新資料節點的相關資料。
線上資料遷移不僅可以做資料擴容,也可以通過同樣的方式線上進行DDL操作。由於資料庫原生的DDL操作是不支援事務的,而且在對包含大量資料表做DDL時會導致長時間鎖表,因此,通過線上資料遷移的方式,是能夠支援線上DDL操作的。線上DDL操作與資料遷移步驟是一致的,只需要在遷移之前新建一個DDL修改後的空表,然後根據上述4步驟進行即可。