白話TiDB原理

1、為什麼要搞TiDB

當資料庫大到一定程度的時候,查詢會變慢。作為一個傳統資料庫比如MySQL,這個時候就需要分表、水平擴展。並且沒有辦法進行跨節點的join或者分散式事務。

而另外一些NoSQL,比如HBase, MongoDB等,雖然能很好地水平擴展,不用分庫分表了,卻又不支持人見人愛的SQL以及一致性事務。而以 Google Spanner (thenewstack.io/google-c ) 和F1為代表的NewSQL,不僅能夠很好地水平擴展,還能支持ACID。那麼,怎麼才能做到呢?

2、TiDB的原理簡介

整個TiDB包括幾個核心部分:MySQL協議、分散式事務、Raft、本地存儲。

2.1 MySQL協議

TiDB有一個協議層,兼容了MySQL協議。MySQL協議下面是TiKV。那麼MySQL和TiKV是如何關聯起來的呢?每一個TiDB實例對應多個TiDB Server,每個TiDB Server有一個關係型資料庫schema,描述表、列、索引。TiDB把表結構和KV存儲做了一個映射。

舉個例子,比如:

INSERT INTO t(id, name, address) values (1, "Jack", "Sunnyvale")

與Mysql的schema及表結構不同,TiDB會把這條記錄轉變成KV的形式,key=t_r1,value={jack,Sunnyvale}。

如果除了主鍵之外還有其他列做索引,那麼除了row-id到row-content的映射之外,還有一個index-id到row-id的映射。

2.2 水平擴展

為了提升數據的安全性、可用性以及性能,存儲被分為若干個region,每一個region都包含一個區段的key值,比如[A-H), [H-N), [O-T), [U-Z)。這樣整體的寫吞吐就能得到提升。而每一個region對應一個Raft組,也就意味著每一個region都有幾個副本。這樣每個region的數據都能保持強一致性。

另外,TiDB servers沒有數據分片、也無狀態。任何一個數據中心的任何一臺TiDB Server都可以訪問到所有數據。

2.3 ACID的分散式事務

TiDB分散式事務的實現是受Percolator的啟發。我們看看Google Percolator基於MVCC(Multi-Version Concurrency Control 多版本並發控制)的事務是怎麼做到分散式事務ACID的。

由於Percolator是基於Bigtable的,所以數據結構直接使用了Bigtable的Tablet。

每個Tablet可以想像成大的key-value map,按照key排序。每一個row相當於一個key-value對,每個row有個key,value則對應幾個關鍵列,timestamp,data,lock,write,對應的是該列的生成時間,數據,是否上鎖,寫入的數據版本(時間戳)。

如:

{
"Will" : {
"balance:data" : {
1517543541533: "$10"
},
"balance:lock" : {
1517543541533: "primary-lock"
}
"balance:write": {
1517543541533: "data@timestamp=1517543541533"
}
},
"Jean" : {
"balance:data" : {
1517543541534: "$12"
},
"balance:lock" : {
1517543541534: "[email protected]"
}
"balance:write": {
1517543541534: "data@timestamp=1517543541534"
}
}
}

接下來我們來通過例子說明原理。

比如will有10元,jean有2元。will向jean轉賬7元。

0 初始狀態。

1 Will上主行鎖,並且更新數據。

2 Jean上次行鎖,並且更新數據。

3 Will檢查是否有主行鎖,如果沒鎖則失敗;有鎖則寫入新值,並清理之前的主行鎖。從這一刻開始,後續的讀Will行操作都將看到新的值$3。

4 Jean行寫入新值,並清理次行鎖

整個事務分成2階段。

第一階段,上鎖,記錄新數據。如果事務看到有任何其他write列記錄的時間戳大於它的時間戳,該事務就取消。如果事務看到有鎖存在,不管什麼時間戳,也會中止。這裡有可能出現一種情況:lock存在,但是時間戳小於當前事務時間戳(之前事務還沒提交完),但是因為可能性不大,所以不另外考慮。

第二階段,去掉鎖的同時寫新數據。

我們來看看異常情況:

如果客戶端在提交的時候掛了,那麼鎖就有可能一直留在那,如何清理和處理讀寫呢?

這個清理的動作交給後續的讀寫操作來處理。如果後來的事務看到鎖,它怎麼判斷這個鎖要不要被清理呢?這很難。假設事務A先發生,事務B後發生。那麼事務B看到A在某列上的鎖時,非常難判斷它是掛掉了導致lock沒有清理遺留在那裡還是事務A準備提交並清除lock。

因此,我們能做的就是避免A和B對鎖的競爭。即:

  1. 要麼A檢測到鎖,commit並釋放鎖
  2. 要麼B的讀操作檢測到鎖,清理鎖。如果是B的寫操作,直接返回失敗。此時A如果提交就會因為拿不到鎖而中止,提交失敗,重試即可。

如果在第二階段primary提交清除了lock,但是次行鎖沒清除,那麼後續的事務在讀操作會試圖roll forward,把沒完成的部分完成,即write並清除lock。

Raft和RocksDB的部分讀者有興趣自行了解。


推薦閱讀:
相關文章