聊聊TiDB的分布式事務模型
本文轉載自微信公眾號「程序員jinjunzhu」,作者 jinjunzhu 。轉載本文請聯系程序員jinjunzhu公眾號。
在傳統關系型數據庫領域,我們常常通過配置事務的隔離級別來解決臟讀、幻讀、不可重復讀的問題。不同的事務隔離級別對應解決問題的力度是不一樣的,下表是不同事務隔離級別對臟讀、幻讀、不可重復讀的容忍度,我們一起看一下:
注意:
Repeatable read的讀鎖會一直到事務結束才釋放;
Read committed的讀鎖不等到事務結束,而是讀取完成后立即釋放。
當然,傳統數據庫解決并發控制的手段還有mvcc,這里就不展開了。
上面我提到了讀鎖,寫鎖、GAP鎖,實際上鎖的種類遠遠不止這些。對于我們開發者來講,經常會談到樂觀鎖和悲觀鎖。樂觀鎖實際上是不加鎖的,悲觀鎖需要真正的加鎖。而在分布式數據庫領域,同樣需要并發控制,同樣也有樂觀事務和悲觀事務。
就TiDB來說,v3.0版本開始支持悲觀事務,從v3.0.8開始,新搭建的TiDB集群已經默認使用悲觀事務了。
傳統數據庫加鎖
傳統數據庫的樂觀鎖,主要是在表中加入一個版本號字段,在更新的時候根據更新結果來進行判斷是否成功。比如我們有一張表table_a, 我們在其中加一個version字段,下面是table_a表的1條記錄
表格
id | name | version |
---|---|---|
1 | jinjunzhu | 4 |
我們更新這條id=1的記錄,SQL如下:
- update table_a set name='xiaoming',version = version + 1 where id=1 and version=4
這時如果SQL執行結果返回更新行數是0,說明別的事務已經更新了version字段,寫沖突產生,業務代碼必須處理這個沖突。高并發下如果對同一條記錄的修改操作非常多,勢必造成大量寫失敗。所以樂觀鎖更適合讀多寫少的場景。
傳統數據庫的悲觀鎖,是用物理加鎖的方式,還是上面的表,不需要version字段了,假如有2條記錄:
id | name |
---|---|
1 | jinjunzhu |
2 | xiaoming |
這時加入我們要同時更新id=1的記錄和id=2的記錄,如果在一個事務內完成,加鎖sql如下:
- select * from table_a where id in(1, 2) for update;
- update table_a set name = 'zhangsan' where id=1;
- update table_a set name = 'lisi' where id=2;
悲觀鎖的問題是遇到長事務,其他事務需要較長時間的鎖等待,所以oracle提供了下面的優化,即發現待修改數據被鎖定后立刻返回失敗:
- select * from table_a for update nowait
Percolator模型
Percolator模型是Google提出的構建在BigTable之上的分布式事務解決方案。Google的論文如下,文章鏈接見延伸閱讀[1]:
- 《Large-scale Incremental Processing Using Distributed Transactions and Notifications》
我們以經典的電商系統為例,假如系統中有訂單、賬戶和庫存3張表,用戶一次購物需要增加1條訂單記錄,賬戶表需要扣減金額,庫存表需要扣減庫存,而這3張表要操作的記錄分別在分布式數據庫的3個切片上,這時就需要應對分布式事務了。
我們看一下Percolator算法模型:
初始階段
初始階段,我們假設訂單表記錄訂單數量是0,賬戶表記錄賬戶金額1000,庫存表記錄商品數量是100,客戶下了1個訂單后,訂單表增加1個訂單,賬戶表扣除金額100,庫存表扣減商品數量1。各個表的初始數據如下表:
上面表格中,":"前面是用時間戳表示的數據版本,后面是數據值。第一列是表名,第二列的低版本保存了數據,第三列列保存了事務操作給數據加的鎖。第四列的高版本保存了指向保存數據版本的指針,比如6這個版本保存了指向了5這個版本數據的指針 6:data@5。
Prewrite階段
在Prewrite階段,協調節點向每個切片發送Prewrite命令。Percolator定義了 primary lock 即主鎖的概念,Prewrite階段,每個分布式事務只能有一個要修改的數據行可以獲得主鎖,本案例假如訂單表獲得了主鎖,其他表的鎖是指向這個主鎖的指針,叫做 secondary lock,如下表:
Prewrite階段,每個要修改的數據行會寫日志,并且根據時間戳記錄事務的私有版本,這里的私有版本就是7,這樣其他事務就不能操作這三條數據了。
注意,獲取主鎖時,如果出現了下面的情況,就會加鎖失敗:
1.其他事務已經加鎖;
2.本次事務開始之后,要更新的數據被其他數據更新了。
commit階段
在commit階段,協調節點只需要跟擁有primary lock的切片進行通信,所以本案例只需要跟訂單表所在切片通信。這時數據如下表:
我們注意到order表的鎖沒有了,而且增加了版本8指向版本7。說明訂單表已經提交成功,沒有私有版本了,但是賬戶表和庫存表的私有版本還在。這是因為Percolator模型并不會同步commit賬戶表和庫存表,而是啟動異步線程來commit這兩張表并清理鎖。如果訂單表提交失敗,賬戶表和庫存表也都需要回滾。
提交成功后,最終數據如下表:
commit階段,因為協調節點只需要跟擁有主鎖的切片(這里是訂單表所在切片)進行通信,保證了原子性,這樣就避免了commit時節點不能全部成功導致的數據不一致問題。
而Prewrite階段記錄了日志和私有版本,如果賬戶表和庫存表所在切片commit失敗,可以根據日志進行再次commit,這樣就保證了數據最終一致。
這里要注意2點:
1.主鎖的選擇是隨機的,比如本例中并不一定會選擇訂單表;
2.協調節點發送commit后訂單表先提交成功,這時如果其他事務要讀取賬戶服務和庫存服務的2條數據,雖然2條數據上面還有lock,但是查找primary@order.bal發現已提交,所以是可以讀取的。不過讀取時需要做一下secondary lock清理工作。
TiDB樂觀事務模型
上面我們分析了Percolator模型,TiDB的樂觀事務正是使用了Percolator模型。
TiDB支持MVCC,事務啟動的時候,會使用一個時間戳start_ts作為當前事務ID,同時作為MVCC的快照版本,之后的讀請求會讀取當前快照版本下的數據,數據校驗成功后客戶端進行兩階段commit,我們看一下下面的時序圖:
第一階段,TiDB收到客戶端請求后,首先會從緩存的待修改key中找出第一個發送prewrite請求,這個key加primary lock后返回成功。然后TiDB會對這個事務其他的所有的key發送prewrite請求,這些key加secondary lock后返回成功。
第二階段,prewrite成功后,TiDB首先會從PD獲取一個時間戳作為當前事務的commit_ts,然后向primary lock key發送commit請求,primary lock key提交數據成功后清理掉primary lock返回成功。TiDB收到primary lock key的成功消息后給客戶端返回成功。
樂觀事務的沖突檢測主要是在prewrite階段,如果檢測到當前的key已經加鎖,會有一個等待時間,這個時間過后如果還沒有獲取到鎖,就返回失敗。因此當多個事務修改同一個key時,必然導致大量的鎖沖突。
注意:TiDB也有重試機制,默認是關閉的。TiDB的重試會重新獲取start_ts,但是不會重新讀取數據,因此不能保證可重復讀的隔離級別。詳細參考TiDB官方文檔。
TiDB悲觀事務模型
TiDB從v3.0 版本開始,引入了悲觀事務。
注意:v3.0.7及之前版本創建的集群升級到更高版本后,默認還是采用樂觀事務,只有新創建集群才會默認使用悲觀事務。我們也可以采用下面命令來開啟悲觀事務。下面第1個語句會修改TiDB系統參數,后面2個語句會忽略系統參數,優先級更高:
SET GLOBAL tidb_txn_mode = 'pessimistic';
BEGIN PESSIMISTIC;
BEGIN OPTIMISTIC;
為了兼容mysql,TiDB的悲觀事務和mysql很類似。悲觀事務支持可重復讀和讀已提交兩種隔離級別,默認使用可重復讀。TiDB中樂觀事務和悲觀事務可以共存,會優先會采用樂觀事務,只有鎖沖突時,才會使用悲觀事務。
使用悲觀事務的語句如下:
- UPDATE、DELETE、INSERT、SELECT FOR UPDATE
TiDB的悲觀事務有幾點需要注意:
- SELECT FOR UPDATE語句會對已提交的最新的數據而非所修改的行加上悲觀鎖
- TiDB不支持GAP鎖,所以在FOR UPDATE語句的WHERE條件使用范圍條件時,還是可以插入的,比如下面的sql如果id不沖突,還是可以插入成功的:
- SELECT * FROM t1 WHERE id BETWEEN 1 AND 10 FOR UPDATE;
- 可以通過innodb_lock_wait_timeout變量設置等待鎖超時時間,默認是50s
- 不支持支持FOR UPDATE NOWAIT語法
- 如果Point Get和Batch Point Get算子沒有讀到數據,依然會對給定的主鍵或者唯一鍵加鎖,阻塞其他事務對相同主鍵加鎖或者進行寫入操作
- 在悲觀事務執行期間,如果執行DDL操作,是可以成功的,但之后事務會提交失敗
- 悲觀事務的執行時間有上限,默認為10分鐘,可以通過參數配置
總結
業務場景的復雜化,必然導致樂觀事務沖突變多,這也是TiDB后續版本轉向悲觀事務的重要原因。TiDB中樂觀事務和悲觀事務可以共存。
延伸閱讀:
[1].https://www.cs.princeton.edu/courses/archive/fall10/cos597B/papers/percolator-osdi10.pdf
[2].https://docs.pingcap.com/zh/tidb/stable/pessimistic-transaction
[3].https://docs.pingcap.com/zh/tidb/stable/optimistic-transaction
[4].https://pingcap.com/blog-cn/percolator-and-txn/