在之前的項目中我使用了Seata分布式事務來保證訂單服務的最終一致性,下面就來看一下Seata的AT模式的原理。
AT模式的整體機制是由兩階段協議演變而來的。先來看看什么是兩階段協議
兩階段協議
兩階段提交協議是協調所有分布式原子事務參與者,並決定提交或取消(回滾)的分布式算法。
(1)協議參與者
在兩階段協議中,系統一般包含兩類機器或節點:一類為協調者(coordinator),類似於系統的控制中心,通常一個系統中只有一個;另一類為事務參與者(participants,cohorts或workers),一般包含多個,在數據存儲系統中可以理解為數據副本的個數。協議中假設每個節點都會記錄寫前日志(write-ahead log)並持久性存儲,即使節點發生故障日志也不會丟失。協議中同時假設節點不會發生永久性故障而且任意兩個節點都可以互相通信。
(2)兩階段的執行
1.請求階段(commit-request phase,或稱表決階段,voting phase)
在請求階段,協調者將通知事務參與者准備提交或取消事務,然后進入表決過程。
在表決過程中,參與者將告知協調者自己的決策:同意(事務參與者本地作業執行成功)或取消(本地作業執行故障)。
2.提交階段(commit phase)
在該階段,協調者將基於第一個階段的投票結果進行決策:提交或取消。
當且僅當所有的參與者同意提交事務協調者才通知所有的參與者提交事務,否則協調者將通知所有的參與者取消事務。
參與者在接收到協調者發來的消息后將執行響應的操作。
(3)兩階段提交的缺點
1.同步阻塞問題
執行過程中,所有參與節點都是事務阻塞型的。當參與者占有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。
2.單點故障
由於協調者的重要性,一旦協調者發生故障,參與者會一直阻塞下去。尤其在第二階段,協調者發生故障,那么所有的參與者還都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。(如果是協調者掛掉,可以重新選舉一個協調者,但是無法解決因為協調者宕機導致的參與者處於阻塞狀態的問題)
3.數據不一致
在二階段提交的階段二中,當協調者向參與者發送commit請求之后,發生了局部網絡異常或者在發送commit請求過程中協調者發生了故障,這回導致只有一部分參與者接受到了commit請求。
而在這部分參與者接到commit請求之后就會執行commit操作。但是其他部分未接到commit請求的機器則無法執行事務提交。於是整個分布式系統便出現了數據部一致性的現象。
(4)兩階段提交無法解決的問題
當協調者出錯,同時參與者也出錯時,兩階段無法保證事務執行的完整性。
考慮協調者再發出commit消息之后宕機,而唯一接收到這條消息的參與者同時也宕機了,那么即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。
接下來看一下Seata的AT模式的實現
Seata——AT模式
術語
TC (Transaction Coordinator) - 事務協調者
維護全局和分支事務的狀態,驅動全局事務提交或回滾。
TM (Transaction Manager) - 事務管理器
定義全局事務的范圍:開始全局事務、提交或回滾全局事務。
RM (Resource Manager) - 資源管理器
管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
整體機制
兩階段提交協議的演變:
一階段:業務數據和回滾日志記錄在同一個本地事務中提交,釋放本地鎖和連接資源。
二階段:
- 提交異步化,非常快速地完成。
- 回滾通過一階段的回滾日志進行反向補償。
第一階段,就是各個階段本地提交操作;第二階段會根據第一階段的情況決定是進行全局提交還是全局回滾操作。
寫隔離
- 一階段本地事務提交前,需要確保先拿到 全局鎖 。
- 拿不到 全局鎖 ,不能提交本地事務。
- 拿 全局鎖 的嘗試被限制在一定范圍內,超出范圍將放棄,並回滾本地事務,釋放本地鎖。
以一個例子來說明:
兩個全局事務 tx1 和 tx2,分別對 a 表的 m 字段進行更新操作,m 的初始值 1000。
tx1 先開始,開啟本地事務,拿到本地鎖,更新操作 m = 1000 - 100 = 900。本地事務提交前,先拿到該記錄的 全局鎖 ,本地提交釋放本地鎖。 tx2 后開始,開啟本地事務,拿到本地鎖,更新操作 m = 900 - 100 = 800。本地事務提交前,嘗試拿該記錄的 全局鎖 ,tx1 全局提交前,該記錄的全局鎖被 tx1 持有,tx2 需要重試等待 全局鎖 。
tx1 二階段全局提交,釋放 全局鎖 。tx2 拿到 全局鎖 提交本地事務。
如果 tx1 的二階段全局回滾,則 tx1 需要重新獲取該數據的本地鎖,進行反向補償的更新操作,實現分支的回滾。
此時,如果 tx2 仍在等待該數據的 全局鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗。分支的回滾會一直重試,直到 tx2 的 全局鎖 等鎖超時,放棄 全局鎖 並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功。
因為整個過程 全局鎖 在 tx1 結束前一直是被 tx1 持有的,所以不會發生 臟寫 的問題。
讀隔離
在數據庫本地事務隔離級別 讀已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的默認全局隔離級別是 讀未提交(Read Uncommitted) 。
這里補充一下事務的四個隔離級別:
- READ-UNCOMMITTED(讀未提交): 最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致臟讀、幻讀或不可重復讀。
- READ-COMMITTED(讀已提交): 允許讀取並發事務已經提交的數據,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生。
- REPEATABLE-READ(可重復讀): 對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生。
- SERIALIZABLE(可串行化): 最高的隔離級別,完全服從ACID的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀。
如果應用在特定場景下,必需要求全局的 讀已提交 ,目前 Seata 的方式是通過 SELECT FOR UPDATE 語句的代理。
SELECT FOR UPDATE 語句的執行會申請 全局鎖 ,如果 全局鎖 被其他事務持有,則釋放本地鎖(回滾 SELECT FOR UPDATE 語句的本地執行)並重試。這個過程中,查詢是被 block 住的,直到 全局鎖 拿到,即讀取的相關數據是 已提交 的,才返回。
出於總體性能上的考慮,Seata 目前的方案並沒有對所有 SELECT 語句都進行代理,僅針對 FOR UPDATE 的 SELECT 語句。
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖。
工作機制
以一個示例來說明整個 AT 分支的工作過程。
業務表:product
AT 分支事務的業務邏輯:
update product set name = 'GTS' where name = 'TXC';
一階段
過程:
- 解析 SQL:得到 SQL 的類型(UPDATE),表(product),條件(where name = 'TXC')等相關的信息。
- 查詢前鏡像:根據解析得到的條件信息,生成查詢語句,定位數據。
select id, name, since from product where name = 'TXC';
得到前鏡像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
- 執行業務 SQL:更新這條記錄的 name 為 'GTS'。
- 查詢后鏡像:根據前鏡像的結果,通過 主鍵 定位數據。
select id, name, since from product where id = 1;
得到后鏡像:
id | name | since |
---|---|---|
1 | GTS | 2014 |
- 插入回滾日志:把前后鏡像數據以及業務 SQL 相關的信息組成一條回滾日志記錄,插入到
UNDO_LOG
表中。
{ "branchId": 641789253, "undoItems": [{ "afterImage": { "rows": [{ "fields": [{ "name": "id", "type": 4, "value": 1 }, { "name": "name", "type": 12, "value": "GTS" }, { "name": "since", "type": 12, "value": "2014" }] }], "tableName": "product" }, "beforeImage": { "rows": [{ "fields": [{ "name": "id", "type": 4, "value": 1 }, { "name": "name", "type": 12, "value": "TXC" }, { "name": "since", "type": 12, "value": "2014" }] }], "tableName": "product" }, "sqlType": "UPDATE" }], "xid": "xid:xxx" }
- 提交前,向 TC 注冊分支:申請
product
表中,主鍵值等於 1 的記錄的 全局鎖 。 - 本地事務提交:業務數據的更新和前面步驟中生成的 UNDO LOG 一並提交。
- 將本地事務提交的結果上報給 TC。
二階段-回滾
- 收到 TC 的分支回滾請求,開啟一個本地事務,執行如下操作。
- 通過 XID 和 Branch ID 查找到相應的 UNDO LOG 記錄。
- 數據校驗:拿 UNDO LOG 中的后鏡與當前數據進行比較,如果有不同,說明數據被當前全局事務之外的動作做了修改。這種情況,需要根據配置策略來做處理、。
- 根據 UNDO LOG 中的前鏡像和業務 SQL 的相關信息生成並執行回滾的語句:
update product set name = 'TXC' where id = 1;
- 提交本地事務。並把本地事務的執行結果(即分支事務回滾的結果)上報給 TC。
二階段-提交
如果所有Branch RM都執行成功了,那么二階段就進行全局Commit。因為“業務 SQL”在一階段已經提交至數據庫,每個Branch本地數據庫操作已經完成了, 所以只需將一階段保存的快照數據和行鎖刪掉,也就是把本地的Undolog刪了完成數據清理即可。
執行步驟:
- 收到 TC 的分支提交請求,把請求放入一個異步任務的隊列中,馬上返回提交成功的結果給 TC。
- 異步任務階段的分支提交請求將異步和批量地刪除相應 UNDO LOG 記錄。
參考鏈接 https://www.cnblogs.com/balfish/p/8658691.html