做者:Shirlyios
TiDB 最佳實踐系列是面向廣大 TiDB 用戶的系列教程,旨在深刻淺出介紹 TiDB 的架構與原理,幫助用戶在生產環境中最大限度發揮 TiDB 的優點。咱們將分享一系列典型場景下的最佳實踐路徑,便於你們快速上手,迅速定位並解決問題。算法
在前兩篇的文章中,咱們分別介紹了 TiDB 高併發寫入常見熱點問題及規避方法 和 PD 調度策略最佳實踐,本文咱們將深刻淺出介紹 TiDB 樂觀事務原理,並給出多種場景下的最佳實踐,但願你們可以從中收益。同時,也歡迎你們給咱們提供相關的優化建議,參與到咱們的優化工做中來。服務器
建議你們在閱讀以前先了解 TiDB 的總體架構 和 Percollator 事務模型。另外,本文重點關注原理及最佳實踐路徑,具體的 TiDB 事務語句你們能夠在 官方文檔 中查閱。網絡
TiDB 使用 Percolator 事務模型,實現了分佈式事務(建議未讀過該論文的同窗先瀏覽一下 論文 中事務部份內容)。session
說到事務,不得不先拋出事務的基本概念。一般咱們用 ACID 來定義事務(ACID 概念定義)。下面咱們簡單說一下 TiDB 是怎麼實現 ACID 的:架構
截止本文發稿時,TiDB 一共提供了兩種事務模式:樂觀事務和悲觀事務。那麼樂觀事務和悲觀事務有什麼區別呢?最本質的區別就是何時檢測衝突:併發
下面咱們將着重介紹樂觀事務在 TiDB 中的實現。另外,想要了解 TiDB 悲觀事務更多細節的同窗,能夠先閱讀本文,思考一下在 TiDB 中如何實現悲觀事務,咱們後續也會提供《悲觀鎖事務最佳實踐》給你們參考。異步
有了 Percolator 基礎後,下面咱們來介紹 TiDB 樂觀鎖事務處理流程。分佈式
TiDB 在處理一個事務時,處理流程以下:高併發
客戶端 begin 了一個事務。
a. TiDB 從 PD 獲取一個全局惟一遞增的版本號做爲當前事務的開始版本號,這裏咱們定義爲該事務的 start_ts
。
客戶端發起讀請求。
a. TiDB 從 PD 獲取數據路由信息,數據具體存在哪一個 TiKV 上。
b. TiDB 向 TiKV 獲取 start_ts
版本下對應的數據信息。
客戶端發起寫請求。
a. TiDB 對寫入數據進行校驗,如數據類型是否正確、是否符合惟一索引約束等,確保新寫入數據事務符合一致性約束,將檢查經過的數據存放在內存裏。
客戶端發起 commit。
TiDB 開始兩階段提交將事務原子地提交,數據真正落盤。
a. TiDB 從當前要寫入的數據中選擇一個 Key 做爲當前事務的 Primary Key。
b. TiDB 從 PD 獲取全部數據的寫入路由信息,並將全部的 Key 按照全部的路由進行分類。
c. TiDB 併發向全部涉及的 TiKV 發起 prewrite 請求,TiKV 收到 prewrite 數據後,檢查數據版本信息是否存在衝突、過時,符合條件給數據加鎖。
d. TiDB 收到全部的 prewrite 成功。
e. TiDB 向 PD 獲取第二個全局惟一遞增版本,做爲本次事務的 commit_ts
。
f. TiDB 向 Primary Key 所在 TiKV 發起第二階段提交 commit 操做,TiKV 收到 commit 操做後,檢查數據合法性,清理 prewrite 階段留下的鎖。
g. TiDB 收到 f 成功信息。
TiDB 向客戶端返回事務提交成功。
TiDB 異步清理本次事務遺留的鎖信息。
從上面這個過程能夠看到, TiDB 事務存在如下優勢:
缺點以下:
基於以上缺點的分析,咱們有了一些實踐建議,將在下文詳細介紹。
爲了下降網絡交互對於小事務的影響,咱們建議小事務打包來作。如在 auto commit 模式下,下面每條語句成爲了一個事務:
# original version with auto_commit UPDATE my_table SET a='new_value' WHERE id = 1; UPDATE my_table SET a='newer_value' WHERE id = 2; UPDATE my_table SET a='newest_value' WHERE id = 3;
以上每一條語句,都須要通過兩階段提交,網絡交互就直接 *3, 若是咱們可以打包成一個事務提交,性能上會有一個顯著的提高,以下:
# improved version START TRANSACTION; UPDATE my_table SET a='new_value' WHERE id = 1; UPDATE my_table SET a='newer_value' WHERE id = 2; UPDATE my_table SET a='newest_value' WHERE id = 3; COMMIT;
同理,對於 insert 語句也建議打包成事務來處理。
既然小事務有問題,咱們的事務是否是越大越好呢?
咱們回過頭來分析兩階段提交的過程,聰明如你,很容易就能夠發現,當事務過大時,會有如下問題:
爲了解決這個問題,咱們對事務的大小作了一些限制:
所以,對於 TiDB 樂觀事務而言,事務太大或者過小,都會出現性能上的問題。咱們建議每 100~500 行寫入一個事務,能夠達到一個比較優的性能。
事務的衝突,主要指事務併發執行時,對相同的 Key 有讀寫操做,主要分兩種:
在 TiDB 的樂觀鎖機制中,由於是在客戶端對事務 commit 時,纔會觸發兩階段提交,檢測是否存在寫寫衝突。因此,在樂觀鎖中,存在寫寫衝突時,很容易在事務提交時暴露,於是更容易被用戶感知。
由於咱們本文着重將樂觀鎖的最佳實踐,那麼咱們這邊來分析一下樂觀事務下,TiDB 的行爲。
默認配置下,如下併發事務存在衝突時,結果以下:
在這個 case 中,現象分析以下:
t1
開始事務,事務 B 在事務 t1
以後的 t2
開始。t4
時,事務 A 想要更新 id = 1
的這一行數據,雖然此時這行數據在 t3
這個時間點被事務 B 已經更新了,可是由於 TiDB 樂觀事務只有在事務 commit 時才檢測衝突,因此時間點 t4
的執行成功了。t5
,事務 B 成功提交,數據落盤。t6
,事務 A 嘗試提交,檢測衝突時發現 t1
以後有新的數據寫入,返回衝突,事務 A 提交失敗,提示客戶端進行重試。根據樂觀鎖的定義,這樣作徹底符合邏輯。
咱們知道了樂觀鎖下事務的默認行爲,能夠知道在衝突比較大的時候,Commit 很容易出現失敗。然而,TiDB 的大部分用戶,都是來自於 MySQL;而 MySQL 內部使用的是悲觀鎖。對應到這個 case,就是事務 A 在 t4
更新時就會報失敗,客戶端就會根據需求去重試。
換言之,MySQL 的衝突檢測在 SQL 執行過程當中執行,因此 commit 時很難出現異常。而 TiDB 使用樂觀鎖機制形成的兩邊行爲不一致,則須要客戶端修改大量的代碼。 爲了解決廣大 MySQL 用戶的這個問題,TiDB 提供了內部默認重試機制,這裏,也就是當事務 A commit 發現衝突時,TiDB 內部從新回放帶寫入的 SQL。爲此 TiDB 提供瞭如下參數,
tidb_disable_txn_auto_retry
:這個參數控制是否自動重試,默認爲 1
,即不重試。
tidb_retry_limit
:用來控制重試次數,注意只有第一個參數啓用時該參數纔會生效。
如何設置以上參數呢?推薦兩種方式設置:
session 級別設置:
set @@tidb_disable_txn_auto_retry = 0; set @@tidb_retry_limit = 10;
全局設置:
set @@global.tidb_disable_txn_auto_retry = 0; set @@global.tidb_retry_limit = 10;
那麼重試是否是萬能的呢?這要從重試的原理出發,重試的步驟:
start_ts
。細心如你可能會發現,咱們這邊只對寫入的 SQL 進行回放,並無說起讀取 SQL。這個行爲看似很合理,可是這個會引起其餘問題:
start_ts
發生了變動,當前這個事務中,讀到的數據與事務真正開始的那個時間發生了變化,寫入的版本也是同理變成了重試時獲取的 start_ts
而不是事務一開始時的那個。打開了重試後,咱們來看下面的例子:
咱們來詳細分析如下這個 case:
如圖,在 session B 在 t2
開始事務 2,t5
提交成功。session A 的事務 1 在事務 2 以前開始,在事務 n2 提交完成後提交。
事務 一、事務 2 會同時去更新同一行數據。
session A 提交事務 1 時,發現衝突,tidb 內部重試事務 1。
重試時,從新取得新的 start_ts
爲 t8’
。
回放更新語句 update tidb set name='pd' where id =1 and status=1
。
i. 發現當前版本 t8’
下並不存在符合條件的語句,不須要更新。
ii. 沒有數據更新,返回上層成功。
tidb 認爲事務 1 重試成功,返回客戶端成功。
session A 認爲事務執行成功,查詢結果,在不存在其餘更新的狀況下,發現數據與預想的不一致。
這裏咱們能夠看到,對於重試事務,若是自己事務中更新語句須要依賴查詢結果時,由於重試時會從新取版本號做爲 start_ts
,於是沒法保證事務本來的 ReadRepeatable
隔離型,結果與預測可能出現不一致。
綜上所述,若是存在依賴查詢結果來更新 SQL 語句的事務,建議不要打開 TiDB 樂觀鎖的重試機制。
從上文咱們能夠知道,檢測底層數據是否存在寫寫衝突是一個很重的操做,由於要讀取到數據進行檢測,這個操做在 prewrite 時 TiKV 中具體執行。爲了優化這一塊性能,TiDB 集羣會在內存裏面進行一次衝突預檢測。
TiDB 做爲一個分佈式系統,咱們在內存中的衝突檢測主要在兩個模塊進行:
TiDB 層,若是在 TiDB 實例自己發現存在寫寫衝突,那麼第一個寫入發出去後,後面的寫入就已經能清楚地知道本身衝突了,不必再往下層 TiKV 發送請求去檢測衝突。
TiKV 層,主要發生在 prewrite 階段。由於 TiDB 集羣是一個分佈式系統,TiDB 實例自己無狀態,實例之間沒法感知到彼此的存在,也就沒法確認本身的寫入與別的 TiDB 實例是否存在衝突,因此會在 TiKV 這一層檢測具體的數據是否有衝突。
其中 TiDB 層的衝突檢測能夠關閉,配置項能夠啓用:
txn-local-latches:事務內存鎖相關配置,當本地事務衝突比較多時建議開啓。
- enable
- 開啓
- 默認值:false
- capacity
- Hash 對應的 slot 數,會自動向上調整爲 2 的指數倍。每一個 slot 佔 32 Bytes 內存。當寫入數據的範圍比較廣時(如導數據),設置太小會致使變慢,性能降低。
- 默認值:1024000
細心的朋友可能又注意到,這邊有個 capacity 的配置,它的設置主要會影響到衝突判斷的正確性。在實現衝突檢測時,咱們不可能把全部的 Key 都存到內存裏,佔空間太大,得不償失。因此,真正存下來的是每一個 Key 的 hash 值,有 hash 算法就有碰撞也就是誤判的機率,這裏咱們經過 capacity 來控制 hash 取模的值:
capacity 值越小,佔用內存小,誤判機率越大。
capacity 值越大,佔用內存大,誤判機率越小。
在真實使用時,若是業務場景可以預判斷寫入不存在衝突,如導入數據操做,建議關閉。
相應地,TiKV 內存中的衝突檢測也有一套相似的東西。不一樣的是,TiKV 的檢測會更嚴格,不容許關閉,只提供了一個 hash 取模值的配置項:
- scheduler-concurrency
- scheduler 內置一個內存鎖機制,防止同時對一個 Key 進行操做。每一個 Key hash 到不一樣的槽。
- 默認值:2048000
此外,TiKV 提供了監控查看具體消耗在 latch 等待的時間:
若是發現這個 wait duration 特別高,說明耗在等待鎖的請求上比較久,若是不存在底層寫入慢問題的話,基本上能夠判斷這段時間內衝突比較多。
綜上所述,Percolator 樂觀事務實現原理簡單,可是缺點諸多,爲了優化這些缺陷帶來的性能上和功能上的開銷,咱們作了諸多努力。可是誰也不敢自信滿滿地說:這一塊的性能已經達到了極致。
時至今日,咱們還在持續努力將這一塊作得更好更遠,但願能讓更多使用 TiDB 的小夥伴能從中受益。與此同時,咱們也很是期待你們在使用過程當中的反饋,若是你們對 TiDB 事務有更多優化建議,歡迎聯繫我 wuxuelian@pingcap.com 。您看似不經意的一個舉動,都有可能使更多飽受折磨的互聯網同窗們從中享受到分佈式事務的樂趣。
原文閱讀:https://pingcap.com/blog-cn/best-practice-optimistic-transaction/