詳解 Seata AT 模式事務隔離級別與全域性鎖設計
Seata AT 模式是一種非侵入式的分散式事務解決方案,Seata 在內部做了對資料庫操作的代理層,我們使用 Seata AT 模式時,實際上用的是 Seata 自帶的資料來源代理 DataSourceProxy,Seata 在這層代理中加入了很多邏輯,比如插入回滾 undo_log 日誌,檢查全域性鎖等。
為什麼要檢查全域性鎖呢,這是由於 Seata AT 模式的事務隔離是建立在支事務的本地隔離級別基礎之上的,在資料庫本地隔離級別讀已提交或以上的前提下,Seata 設計了由事務協調器維護的全域性寫排他鎖,來保證事務間的寫隔離,同時,將全域性事務預設定義在讀未提交的隔離級別上。
Seata 事務隔離級別解讀
在講 Seata 事務隔離級之前,我們先來回顧一下資料庫事務的隔離級別,目前資料庫事務的隔離級別一共有 4 種,由低到高分別為:
-
Read uncommitted:讀未提交
-
Read committed:讀已提交
-
Repeatable read:可重複讀
-
Serializable:序列化
資料庫一般預設的隔離級別為讀已提交,比如 Oracle,也有一些資料的預設隔離級別為可重複讀,比如 Mysql,一般而言,資料庫的讀已提交能夠滿足業務絕大部分場景了。
我們知道 Seata 的事務是一個全域性事務,它包含了若干個分支本地事務,在全域性事務執行過程中(全域性事務還沒執行完),某個本地事務提交了,如果 Seata 沒有采取任務措施,則會導致已提交的本地事務被讀取,造成髒讀,如果資料在全域性事務提交前已提交的本地事務被修改,則會造成髒寫。
由此可以看出,傳統意義的髒讀是讀到了未提交的資料,Seata 髒讀是讀到了全域性事務下未提交的資料,全域性事務可能包含多個本地事務,某個本地事務提交了不代表全域性事務提交了。
在絕大部分應用在讀已提交的隔離級別下工作是沒有問題的,而實際上,這當中又有絕大多數的應用場景,實際上工作在讀未提交的隔離級別下同樣沒有問題。
在極端場景下,應用如果需要達到全域性的讀已提交,Seata 也提供了全域性鎖機制實現全域性事務讀已提交。但是預設情況下,Seata 的全域性事務是工作在讀未提交隔離級別的,保證絕大多數場景的高效性。
全域性鎖實現
AT 模式下,會使用 Seata 內部資料來源代理 DataSourceProxy,全域性鎖的實現就是隱藏在這個代理中。我們分別在執行、提交的過程都做了什麼。
1、執行過程
執行過程在 StatementProxy 類,在執行過程中,如果執行 SQL 是 select for update
,則會使用 SelectForUpdateExecutor 類,如果執行方法中帶有 @GlobalTransactional
or @GlobalLock
註解,則會檢查是否有全域性鎖,如果當前存在全域性鎖,則會回滾本地事務,通過 while 迴圈不斷地重新競爭獲取本地鎖和全域性鎖。
io.seata.rm.datasource.exec.SelectForUpdateExecutor#doExecute
public T doExecute(Object... args) throws Throwable {
Connection conn = statementProxy.getConnection();
// ... ...
try {
// ... ...
while (true) {
try {
// ... ...
if (RootContext.inGlobalTransaction() || RootContext.requireGlobalLock()) {
// Do the same thing under either @GlobalTransactional or @GlobalLock,
// that only check the global lock here.
statementProxy.getConnectionProxy().checkLock(lockKeys);
} else {
throw new RuntimeException("Unknown situation!");
}
break;
} catch (LockConflictException lce) {
if (sp != null) {
conn.rollback(sp);
} else {
conn.rollback();
}
// trigger retry
lockRetryController.sleep(lce);
}
}
} finally {
// ...
}
2、提交過程
提交過程在 ConnectionProxy#doCommit方法中。
1)如果執行方法中帶有 @GlobalTransactional
註解,則會在註冊分支時候獲取全域性鎖:
-
請求 TC 註冊分支
io.seata.rm.datasource.ConnectionProxy#register
private void register() throws TransactionException {
if (!context.hasUndoLog() || !context.hasLockKey()) {
return;
}
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
null, context.getXid(), null, context.buildLockKeys());
context.setBranchId(branchId);
}
-
TC 註冊分支的時候,獲取全域性鎖
io.seata.server.transaction.at.ATCore#branchSessionLock
protected void branchSessionLock(GlobalSession globalSession, BranchSession branchSession) throws TransactionException {
if (!branchSession.lock()) {
throw new BranchTransactionException(LockKeyConflict, String
.format("Global lock acquire failed xid = %s branchId = %s", globalSession.getXid(),
branchSession.getBranchId()));
}
}
2)如果執行方法中帶有 @GlobalLock
註解,在提交前會查詢全域性鎖是否存在,如果存在則拋異常:
io.seata.rm.datasource.ConnectionProxy#processLocalCommitWithGlobalLocks
private void processLocalCommitWithGlobalLocks() throws SQLException {
checkLock(context.buildLockKeys());
try {
targetConnection.commit();
} catch (Throwable ex) {
throw new SQLException(ex);
}
context.reset();
}
GlobalLock 註解說明
從執行過程和提交過程可以看出,既然開啟全域性事務 @GlobalTransactional
註解可以在事務提交前,查詢全域性鎖是否存在,那為什麼 Seata 還要設計多處一個 @GlobalLock
註解呢?
因為並不是所有的資料庫操作都需要開啟全域性事務,而開啟全域性事務是一個比較重的操作,需要向 TC 發起開啟全域性事務等 RPC 過程,而 @GlobalLock
註解只會在執行過程中查詢全域性鎖是否存在,不會去開啟全域性事務,因此在不需要全域性事務,而又需要檢查全域性鎖避免髒讀髒寫時,使用 @GlobalLock
註解是一個更加輕量的操作。
如何防止髒寫
先來看一下使用 Seata AT 模式是怎麼產生髒寫的:
注:分支事務執行過程省略其它過程。
業務一開啟全域性事務,其中包含分支事務A(修改 A)和分支事務 B(修改 B),業務二修改 A,其中業務一執行分支事務 A 先獲取本地鎖,業務二則等待業務一執行完分支事務 A 之後,獲得本地鎖修改 A 併入庫,業務一在執行分支事務時發生異常了,由於分支事務 A 的資料被業務二修改,導致業務一的全域性事務無法回滾。
如何防止髒寫?
1、業務二執行時加 @GlobalTransactional
註解:
注:分支事務執行過程省略其它過程。
業務二在執行全域性事務過程中,分支事務 A 提交前註冊分支事務獲取全域性鎖時,發現業務業務一全域性鎖還沒執行完,因此業務二提交不了,拋異常回滾,所以不會發生髒寫。
2、業務二執行時加 @GlobalLock
註解:
注:分支事務執行過程省略其它過程。
與 @GlobalTransactional
註解效果類似,只不過不需要開啟全域性事務,只在本地事務提交前,檢查全域性鎖是否存在。
2、業務二執行時加 @GlobalLock
註解 + select for update
語句:
注:分支事務執行過程省略其它過程。
如果加了 select for update
語句,則會在 update 前檢查全域性鎖是否存在,只有當全域性鎖釋放之後,業務二才能開始執行 updateA 操作。
如果單單是 transactional,那麼就有可能會出現髒寫,根本原因是沒有 Globallock 註解時,不會檢查全域性鎖,這可能會導致另外一個全域性事務回滾時,發現某個分支事務被髒寫了。所以加 select for update 也有個好處,就是可以重試。
如何防止髒讀
Seata AT 模式的髒讀是指在全域性事務未提交前,被其它業務讀到已提交的分支事務的資料,本質上是Seata預設的全域性事務是讀未提交。
那麼怎麼避免髒讀現象呢?
業務二查詢 A 時加 @GlobalLock
註解 + select for update
語句:
注:分支事務執行過程省略其它過程。
加 select for update
語句會在執行 SQL 前檢查全域性鎖是否存在,只有當全域性鎖完成之後,才能繼續執行 SQL,這樣就防止了髒讀。
最後,看到這裡的讀者,請安排下一鍵三連(點贊、在看、轉發),這次一定好吧, 原創不易,你的支援是我最大的動力!
關注公眾號,後臺 回覆關鍵字「後端 」 可免費領取一份 後端 相關技術棧電子書!
再次感謝你的閱讀,相遇是緣分,歡迎加我微信:295502545,圍觀朋友圈,做個點贊之交!
Seata 相關文章
- 面試官:Redis分散式鎖超時了,任務還沒執行完怎麼辦?
- 詳解 Seata AT 模式事務隔離級別與全域性鎖設計
- Raft: 尋找一種易於理解的一致性演算法(擴充套件版)
- Netty之Reactor執行緒模型(二)
- Caffeine Cache進階本地快取之王
- 一文徹底吃透 DDD 最全建模落地方法論!(附例項)
- 面試官問:講講你對 Java 執行緒池的理解
- Seata 分散式事務之 TCC 理論及設計實現
- 淺談如何成為技術一號位
- 聊聊 page cache 與 Kafka 之間的事兒
- 5點精髓總結:做到啥程度簡歷才配寫“精通MySQL”
- 程式碼即格式:你用過這些高效工具嗎?
- 四年磨一劍:我是如何拿到螞蟻offer的?
- 位元組、騰訊爭先部署,ClickHouse Doris趕超MySQL810倍
- 中通快取服務平臺基於 Kubernetes Operator 的服務化實踐
- 這可能是一年中進螞蟻最好的時機了
- 後端進階系列:POCV的應用和結果分析
- Sentinel斷路器與熔斷降級【原始碼筆記】
- 中通訊息平臺 Kafka 順序消費執行緒模型的實踐與優化
- 中通訊息平臺 Kafka 順序消費執行緒模型的實踐與優化