天天看點

TDSQL XA的事務隔離級别

本文是我最初于2017年發表在我的個人微信公衆号裡面,現釋出在這裡。

1.1. 概述

TDSQL XA全局事務(global transaction)是指使用者用戶端連接配接到TDSQL XA分布式資料庫系統後發起和執行的事務,也就是TDSQL XA處理的分布式事務。一個全局事務可能會寫入資料到多個後端mysql 資料庫執行個體,每個執行個體上面的本地事務都是這個全局事務的事務分支(transaction branch)。用戶端發起全局事務送出時,運作在TDSQL XA的網關子產品中的全局事務管理器會控制該事務通路的所有後端mysql資料庫執行個體完成兩階段送出。

TDSQL XA的全局事務的隔離級别最高可以達到serializable級别,條件是網關與後端MySQL連接配接中設定隔離級别為serializable。在select 語句總是使用事務鎖做并發控制的情況下(本文全部内容均假設mysql使用innodb存儲引擎,後文不再贅述),網關與後端mysql的連接配接中設定的隔離級别就是全局事務的隔離級别。 根據使用者在資料庫會話中設定的隔離級别的不同,TDSQL XA的全局事務可以達到read committed, repeatable read, 或者serializable隔離級别。

一個TDSQL XA叢集中可以有任意多個網關執行個體,并且連接配接到每個網關上的用戶端連接配接都可以發起分布式事務。并且我們為了避免網關執行個體之間的通信開銷和是以導緻的脆弱性,網關被設計成不做任何執行個體間通信。但即便如此,隻要所有連接配接都是用serializable隔離級别那麼TDSQL XA執行的事務仍然可以達到可串行化隔離級别。

1.2. 全局可串行化

為什麼本地事務以serializable隔離級别運作就可以確定全局事務的serializable隔離級别?在介紹如何做到這一點之前,先介紹一下TDSQL XA的網關的兩點重要的内部設計:

  1. 獨立的後端連接配接

對于連接配接到網關的每個用戶端連接配接,網關會向這個連接配接當中的語句通路的每一個後端DB發起一個獨立的連接配接。并且每一個變量設定會傳播到後端的所有連接配接中。比如,如果你在用戶端設定了set tx_isolation=”serializable”; 那麼這個設定會被網關設定到你的用戶端連接配接對應的每一個網關與後端DB的連接配接當中。

  1. 語句串行執行

網關解析一條SQL語句後如果決定發送處理後的語句到多個後端DB,那麼這個發送操作是并行的,也就是不需要等待每個DB傳回結果就會發到下一個DB。但是,隻有收到全部本條語句的執行結果并且彙集後,才傳回結果給用戶端。然後,網關才會接收和處理下一條用戶端發過來的語句。

上面兩點看似簡單平常,但是它們是網關能夠保障每個後端DB的局部事務排程結果産生相同的全局事務排程結果的關鍵。

舉例來說,假設有并發執行的全局事務GT1和GT2,它們的隔離級别都被設定為serializable,根據#1,網關與後端的連接配接上面隔離級别也都設定為serializable了。GT1和GT2在set1上面更新同一行,并且在set2上各自插入不同的插入一行,事務分支: GT1 {T11, T12},GT2{T21, T22}。

Set1Set2GT1T11:update t1 set a=100 where pk=1T12:insert into t2 values(100,200);GT2T21:update t1 set a=200 where pk=1T22:insert into t2 values(10,20);

由于T11與T21并發更新同一行,如果T11先更新,那麼T11會拿到pk=1的那行(标注為R1)的事務鎖直到GT1結束送出才釋放,然後T21才能拿到R1的行事務鎖開始執行,然後T22執行。是以執行順序就是 GT1->GT2(T11->T12->T21->T22);類似地,假如是T21先拿到了R1的行鎖,那麼執行順序就将是 GT2->GT1(T21->T22->T11->T12)。也就是說,運作在每個MySQL執行個體上的事務鎖排程機制可以確定全局事務的串行執行。

我們可以得出這個推論:全局事務的本地事務分支的依賴關系也是全局事務的依賴關系。這裡的依賴關系就是事務鎖的等待關系。按照上例來說,T11先拿到R1的事務鎖,那麼T21就依賴于T11,于是也能夠導緻GT2依賴于GT1。也就是說這點是成立的:

T11-> T21 ==> GT1->GT2 (1)

網關内部設計“#2 語句串行執行”正是確定這點的關鍵。

根據資料庫事務處理的基本理論,如果某個并發事務排程機制可以讓具有依賴關系的事務構成一個有向無環圖(DAG),那麼這個排程就是可串行化的排程。由于每個後端DB都在使用serializable隔離級别,是以每個後端DB上面并發執行的事務分支構成的依賴關系圖一定是DAG。使用上面的推論(1),每個後端DB上面并發執行的事務分支的依賴關系圖通過圖的合并操作就自然形成了TDSQL XA所處理的并發執行的全局事務的依賴關系圖GTG;如果這個圖GTG是一個有向無環圖,那麼這些全局事務一定是在可串行化隔離級别下運作的;如果GTG有環,那麼在serializable隔離級别下一定則會發生死鎖,并且很可能是全局死鎖,那麼innodb死鎖處理機制和TDSQL XA的全局死鎖處理機制就會解除這些死鎖。我會另外撰文講解TDSQL XA的全局死鎖處理機制。

如果一個事務在read-committed隔離級别下運作,它的讀鎖在目前select語句結束後就釋放;在repeatable-read隔離級别下,其讀鎖在事務結束時刻才釋放。根據上面的推論(1),可以輕易得出:對TDSQL XA來說,其本地mysql事務在read-committed/repeatable-read隔離級别下運作時,TDSQL XA的全局事務也是在read-committed/repeatable-read隔離級别下運作。

1.2.1. 假想基于MVCC的全局可串行化機制

使用serializable隔離級别時innodb就不再使用MVCC做查詢了,而是基于鎖,即使你不在select語句中加上for updates/lock in share mode。如果要基于MVCC實作TDSQL XA 的可串行化隔離級别是有巨大代價的,這個代價主要是叢集的性能損失以及可靠性損失。是以我們并沒有這樣做。不過出于

QQ号買号平台

技術探索的興趣,我們可以想想假如要做到這一點應該怎麼做,有什麼問題。

為了實作可串行化隔離級别,我們就需要一個全局事務id分發機制或者隊列産生全局順序。比如PGXL的GTM就是全局事務ID生成器,而這個GTM執行個體就是一個單點故障源。即使再為它實作容災,它也仍然是一個性能和可擴充性瓶頸。

同時,由于mysql innodb使用MVCC做select(除了serializable和for update/lock in share mode子句),還需要将這個全局事務id給予innodb做事務id,同時,還需要TDSQL XA叢集的多個set的innodb 共享各自的本地事務狀态給所有其他innodb(這也是PGXL 所做的),任何一個innodb的本地事務的啟動,prepare,commit,abort都需要通知給所有其他innodb執行個體。隻有這樣做,叢集中的每個innodb執行個體才能夠建立全局完全有一緻的、目前叢集中正在處理的所有事務的狀态,以便做多版本并發控制。這本身都會造成極大的性能開銷,并且導緻set之間的嚴重依賴,降低系統可靠性。這些都是我們要極力避免的。

1.3. Select語句的資料一緻性

如果mysql 的連接配接上面隔離級别不是serializable并且select 語句不使用for update/lock ins hare mode子句的話,其實mysql(innodb)使用的是多版本并發控制(MVCC) 。但是在使用分布式事務的情況下,使用MVCC是有問題的。

這是因為innodb 的MVCC隻針對同一個mysqld程序内的事務有效,innodb并不能知道一個本地事務分支所屬的全局事務在其他innodb執行個體當中的事務分支的狀态(active, committed, prepared, etc),是以有可能查詢到一個未完全送出的全局事務的改動---隻有本地事務分支完成了送出,其他mysql執行個體上面還沒有完成送出。歸根到底的原因是無法得到全局一緻性快照,但是如上節所述,全局一緻性快照的維護代價極其昂貴,并不适合OLTP系統。

舉例來說,假設有并發執行的全局事務GT1和GT2,它們在set1和set2的分支分别是GT1 {T11, T12},GT2{T21, T22},并且在set1和set2上面GT1查詢的行和GT2更新的行都有交集。GT1 做跨set 的select時,GT2正在送出。如果select使用mvcc的話,網關發送GT1的select語句到set1上面時,T11的快照包含T21的改動,因為T11在set1上面啟動的時候T21此刻已經完成本地送出;網關發送GT1的select語句到set2上面時,T12的快照不包含T22的改動,因為T12在T22完成送出之前就已經啟動了。這樣的話,GT1的這個select語句就會查詢到GT2的T21的更新,而GT2還沒有送出完成因為GT2的T22還未完成送出,是以GT1的這個select語句不包含GT2的T22的更新。也就是說GT1讀取到了GT2的一部分更新但是本應該讀到的另外一部分更新結果卻因為T22尚未送出而沒有讀取到。這就是一個一緻性問題。在使用事務鎖做select的情況下,這個問題就不會出現了。

本例中如果GT1的select語句本來也隻會通路到set1上面的資料,那麼盡管GT2.T22并未完成送出,那麼對于TDSQL XA來說也不算是一緻性問題,這是因為在TDSQL XA中,隻要commit log中記錄好了要送出的事務就一定會完成送出,是以盡管GT1讀取到GT2.T21的改動時GT2還沒有完成送出,但是GT2一定會送出,并且GT1也并不需要(不可能)讀取到GT2當中未完成送出的事務分支的更新,也就是說GT1讀取到的是完整的穩定的可靠的結果。在使用事務鎖做select的情況下這個現象仍然會出現,無論使用的是哪個隔離級别,但是這是完全沒問題的。以本例具體來說,如果GT1隻在set1上面運作,并且GT1與GT2.T21有事務鎖沖突,那麼隻要GT2.T21完成送出,那麼GT1就可以繼續執行,即使此刻GT2.T22還沒有完成送出。由于GT2必然會完成送出并且GT1所使用的GT2的更新是穩定可靠的并且時完整的,是以GT1可以正确地執行。

在使用MVCC做select查詢的情況下,做兩階段送出的全局事務隻能做到最終一緻性,MVCC有可能讀取到沒有完全送出(i.e.在所有參與的set上都完成送出)的GT的部分改動。一般情況下,這個不一緻的時間視窗很小;需要agent送出時會時間視窗會比較長。對一緻性要求高的話,就是用serializable隔離級别。

1.3.1. 解決方案

為了解決上述問題,XA事務做select就不能使用MVCC。這需要每一個select語句使用鎖來做并發控制,具體由兩種辦法,最簡單的就是在網關與後端的連接配接當中設定隔離級别是serializable,這樣所有的select自動都是加共享鎖的。或者用戶端對每個select語句都顯示使用加鎖子句: select ... lock in share mode/for update也可以。這樣就可以讀取一緻性的資料,并且這樣的好處是你可以在read committed 或者repeatable read隔離級别下對select使用事務鎖以便確定select的資料一緻性,缺點是對于現有應用來說需要修改所有的select查詢語句。對上面的例子來說,這麼做以後,T12到set2上面做select查詢時候會被T22阻塞,直到T22也完成送出,也就是GT2完成送出。這樣GT1查詢到的結果就是已經送出的事務的結果。如果GT1隻會查詢到set1上面的資料也就是說T12不存在,那麼GT1盡管在GT2送出完成之前就有可能讀取到它在set1上面的改動,但是如前所述這是沒問題的。

1.4. 結論

在select 語句總是使用事務鎖做并發控制的情況下,網關與後端mysql的連接配接中設定的隔離級别就是全局事務的隔離級别。TDSQL XA的全局事務可以達到read committed, repeatable read, 或者serializable隔離級别。

如果select使用MVCC的話那麼可能會有查詢結果的資料一緻性問題,這些問題可以通過讓select擷取事務鎖來避免;如果select使用事務鎖會更多地增加死鎖的發送幾率,并且一定程度上降低事務并發性能。這些都是為了資料一緻性必須付出的代價。