由於框架一開始的定位就是需要支援強一致性分散式儲存,所以如何實現分散式事務成為一個大挑戰。作者學習了CockroachDB及TiDB等資料庫的實現方式後,決定參考TiDB的實現方式,但不同於使用樂觀方式而是採用悲觀鎖方式,遇到事務衝突採用排隊的方式而不是重啟事務。
一、二階段(2PC)遞交流程:
參考下圖舉例說明一下流程:
- 業務服務開始事務,其所在的節點作為事務協調者新建一個事務例項(使用HLC作為事務開始時間戳);
- 協調者將命令1加入事務命令列表(如果是第一個命令則作為事務主記錄),同時向表1的RaftLeader傳送命令,表1狀態機處理命令時上鎖成功後返回(包含當前節點的HLC);
- 同步驟2,但記錄事務主記錄是哪個,用於檢測事務狀態;
- 業務服務處理完後通知協調者遞交事務,協調者選取所有事務參與者節點最大的HLC作為事務遞交時間戳;
- 協調者傳送事務遞交命令至事務主記錄所在表1的RaftLeader,在表1RaftGroup持久化事務主記錄狀態後通知協調者立即向業務服務返回事務是否成功遞交;
- 事務主記錄所在的RaftLeader向其他參與者的RaftLeader非同步通知事務遞交。
每個RaftGroup's Leader都有定時器進行事務狀態檢測:
- 如果檢測到掛起的命令是事務主記錄,則與協調者通訊檢測其是否存活;
- 如果檢測到掛起的命令非事務主記錄,則與事務主記錄所在的RaftLeader通訊檢測事務狀態。
二、上鎖及衝突檢測流程:
簡單說明一下每個Raft節點的狀態機的處理流程:
- Apply事務命令的RaftLog時,先檢測當前鎖列表是否存在衝突,如果沒有衝突上鎖成功持久化鎖資訊後返回;如果存在衝突則排入已上鎖佇列並持久化鎖資訊,等待上級鎖事務遞交後再返回;
- Apply事務遞交命令時,從當前鎖列表內找到對應的命令,持久化寫入命令對應的KV資料至底層的RocksDB內,如果當前鎖有等待佇列,則依次將佇列重新嘗試上鎖;
- 接收到讀命令時,如果與當前鎖衝突,則根據事務開始時間戳判斷是通知衝突事務向後更新遞交時間戳,還是忽略該衝突。
如果Raft節點意外崩潰後重新啟動時,會先從儲存載入鎖資訊恢復當前鎖列表。
三、效能測試:
根據作者的經驗,鎖並不是影響併發效能的原因,衝突才是,所以做了個簡單的併發測試。
1. 併發更新同一條記錄(衝突激烈):
虛擬機器(I74C8G) wrk -t2 -c120 測試約3900tps
對比參照(MacbookPro13 I74C16G):
- PostgreSql: 64執行緒: 呼叫640000次共耗時: 179295毫秒 平均每秒呼叫: 3569
- Cockroach: 64執行緒: 呼叫6400次共耗時: 90784毫秒 平均每秒呼叫: 70
2. 事務插入兩條記錄(無衝突):
64執行緒: 呼叫64000次共耗時: 4008毫秒 平均每秒呼叫: 15968
四、小結:
本篇介紹了框架整合的分散式儲存引擎是如何實現分散式事務的,當然還有很多優化待做(如單分割槽事務遞交優化等)。如果您有問題或Bug報告,請留言或在[GitHub]提交Issue,另外您的關注與點贊將是作者最大的動力。