使用 Apache Seata 進行分散式事務管理
1. 引言
在本教程中,我們將了解Apache Seata ,它最初由阿里巴巴開發,現在是Apache Incubator專案的一部分。我們將了解它是什麼,如何使用它以及我們可以用它做什麼。
2. 為什麼需要分散式事務?
為了編寫健全的應用程序,我們通常會使用資料庫事務來確保對資料的任何更改都是原子性的。也就是說,要嘛全部更改都執行,要嘛全部不執行。這有助於確保我們的數據始終保持有效狀態。
當我們使用單一服務管理資料時,這很容易實現。當系統收到請求時,我們會啟動一個新的事務。所有資料變更都在此事務中完成,並且只有當整個請求成功時,我們才會提交。
在這裡,如果在為用戶記錄帳單時出現錯誤,訂單和庫存變更將被撤銷,系統仍保持正確的狀態。
如果我們轉向以多個分散式服務的方式運行這些服務,那麼我們的交易也會變成分散式的:
流程完全相同,但透過將庫存、訂單和計費服務拆分到不同的應用程式中,我們也將它們拆分成了不同的事務。現在,如果帳單輸入失敗,庫存和訂單的變更已經提交,無法輕易撤銷。
這就是分散式事務的用武之地。如果我們有一種方法可以跨多個應用程式維護資料庫事務,我們既可以獲得系統拆分帶來的好處,也可以獲得整個使用者操作只需一個事務的好處。
3. 什麼是 Apache Seata?
Apache Seata 是一個開源項目,最初是阿里巴巴集團的一部分,它幫助我們在 Java 微服務應用程式中管理分散式事務。
使用 Seata 時,我們會執行一個額外的服務作為事務協調器。當應用程式收到請求時,發起請求的服務(作為事務管理器)會在事務協調器內部啟動一個新的分散式事務。所有其他服務都會參與這個事務中,直到交易被持久化或回滾為止。
在這裡,我們的流程基本上相同,但我們新增了事務協調器,並將所有內容封裝在一個分散式事務中。這將確保所有三個資料庫同時提交或回滾,從而使我們的整個系統保持有效狀態。
4. Seata 伺服器
在使用 Seata 之前,我們需要確保 Seata 伺服器正在運行。它在我們整個系統中扮演事務協調器的角色。
最簡單的實作方法是將其作為Docker 容器運行在我們的環境中。例如,我們可以將其新增至 Docker Compose 檔案中,如下所示:
services:
seata-server:
image: apache/seata-server:2.6.0
預設情況下,它會監聽 8091 端口,並使用容器內的本機檔案系統來追蹤分散式交易。
接下來,我們就可以設定應用程式與 Seata 配合使用了。
5. 使用 Spring Boot
Seata 提供了一個Spring Boot starter ,我們可以用它來設定。如果我們使用 Maven,可以將這個依賴項加入到pom.xml檔中:
<dependency>
<groupId>org.apache.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.6.0</version>
</dependency>
5.1. 配置 Seata
接下來,我們需要為 Seata 提供一個設定檔。該檔案必須位於類別路徑中,因此我們將它建立為src/main/resources/seata.conf :
transport {
type = "TCP"
server = "NIO"
heartbeat = true
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-size = 100
share-boss-worker = false
client-selector-thread-size = 1
client-selector-thread-prefix = "NettyClientSelector"
client-worker-thread-prefix = "NettyClientWorkerThread"
}
shutdown {
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
vgroupMapping.my_tx_group = "default"
default.grouplist = "seata-server:8091"
enableDegrade = false
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
sagaBranchRegisterEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
defaultGlobalTransactionTimeout = 60000
degradeCheck = false
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
compress {
enable = true
type = "zip"
threshold = "64k"
}
}
log {
exceptionRate = 100
}
}
大部分都是標準的,但請注意,我們必須在service.default.grouplist欄位中設定 Seata 伺服器的主機和連接埠。
我們還需要對 Spring 進行一些配置,使其能夠與 Seata 協同工作。我們在application.properties檔案中進行這些配置:
seata.enabled=true
seata.application-id=${spring.application.name}
seata.tx-service-group=my_tx_group
seata.registry.type=file
seata.registry.file.name=seata.conf
seata.config.type=file
seata.config.file.name=seata.conf
seata.service.vgroup-mapping.my_tx_group=default
seata.service.grouplist.default=seata-server:8091
seata.data-source-proxy-mode=AT
seata.enable-auto-data-source-proxy=true
這其中也包含了 Seata 伺服器的主機和端口,位於seata.service.grouplist.default屬性中。我們還需要確保其中幾個屬性與 Seata 設定檔匹配,並且seata.registry.file.name和seata.config.file.name屬性指向我們的seata.conf檔案。
最後,如果我們使用此處配置的 AT 模式,則需要在服務資料庫中建立一個特殊的undo_log表:
CREATE TABLE IF NOT EXISTS undo_log (
id BIGSERIAL NOT NULL,
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info BYTEA NOT NULL,
log_status INT NOT NULL,
log_created TIMESTAMP(0) NOT NULL,
log_modified TIMESTAMP(0) NOT NULL,
CONSTRAINT pk_undo_log PRIMARY KEY (id),
CONSTRAINT ux_undo_log UNIQUE (xid, branch_id)
);
我們在seata.conf檔案中配置確切的表名。
此時,Seata 已與我們的服務整合。如果我們現在啟動項目,將會看到幾個日誌訊息表明這一點:
2026-03-14T07:53:37.728Z INFO 1 --- [apache-seata-a] [ main] oassbaSeataAutoConfiguration : Automatically configure Seata
2026-03-14T07:53:37.802Z INFO 1 --- [apache-seata-a] [ main] ServiceLoader$InnerEnhancedServiceLoader : Load compatible class io.seata.spring.annotation.ScannerChecker
2026-03-14T07:53:37.984Z INFO 1 --- [apache-seata-a] [ main] ServiceLoader$InnerEnhancedServiceLoader : Load compatible class io.seata.integration.tx.api.remoting.RemotingParser
2026-03-14T07:53:37.996Z INFO 1 --- [apache-seata-a] [ main] oassaGlobalTransactionScanner : Initializing Global Transaction Clients ...
.....
2026-03-14T07:53:45.533Z INFO 1 --- [apache-seata-a] [ main] oascrpc.netty.RmNettyRemotingClient : RM will register :jdbc:postgresql://postgres:5432/seata
2026-03-14T07:53:45.540Z INFO 1 --- [apache-seata-a] [ main] oascrpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:172.18.0.2:8091,msg:< RegisterRMRequest{resourceIds='jdbc:postgresql://postgres:5432/seata', version='2.6.0', applicationId='apache-seata-a', transactionServiceGroup='my_tx_group', extraData='null'} >
2026-03-14T07:53:45.586Z INFO 1 --- [apache-seata-a] [ main] oascrpc.netty.RmNettyRemotingClient : register RM success. client version:2.6.0, server version:2.6.0,channel:[id: 0x0a28dceb, L:/172.18.0.6:39884 - R:172.18.0.2/172.18.0.2:8091]
2026-03-14T07:53:45.590Z INFO 1 --- [apache-seata-a] [ main] oascrpc.netty.NettyPoolableFactory : register success, cost 34 ms, version:2.6.0,role:RMROLE,channel:[id: 0x0a28dceb, L:/172.18.0.6:39884 - R:172.18.0.2/172.18.0.2:8091]
2026-03-14T07:53:45.634Z INFO 1 --- [apache-seata-a] [ main] .ssadSeataAutoDataSourceProxyCreator : Auto proxy data source 'dataSource' by 'AT' mode.
5.2 全球交易
Spring 與 Seata 完全整合後,我們就可以開始使用它了。我們使用@GlobalTransaction註解來實現這一點,該註解用於標記需要在服務之間分發的事務的開始:
@PostMapping("/a/{mode}")
@GlobalTransactional
public void handle() {
// Controller logic here
}
我們可以在通常使用@Transactional註解的地方使用它,它會啟動一個新的資料庫事務。該事務會向 Seata 註冊,並且可以跨越多個服務,而不僅限於本地。
請注意,我們僅在全域事務開始時添加此註解。同一事務中的後續服務無需新增此註解。稍後我們將以不同的方式處理它們。
如果需要,我們也可以使用類似於標準@Transactional註解的方式為交易提供一些配置:
@GlobalTransactional(rollbackFor = MyException.class, timeoutMills = 10000)
在這裡,我們指出,對於MyException的任何子類,事務都應該回滾,逾時時間為 10 秒。
5.3 事務傳播
遺憾的是,如果我們現在嘗試這樣做,就會發現事務無法正確傳播。我們會在服務日誌中看到已向 Seata 註冊的提示訊息,但後續服務將不會執行任何操作。
Seata 透過在服務之間傳遞一個特殊的XID值來管理這一點。通常,這個值會放在服務間呼叫時的 HTTP 頭部TX_XID中。
如果我們使用的是標準 Spring,那麼我們需要自行管理這些。這包括將其添加到所有傳出的 HTTP 呼叫中,以及在所有傳入的呼叫中接收它。
如果我們使用 Spring RestClient,那麼我們可以編寫一個ClientHttpRequestInterceptor實作來幫我們完成這項工作:
public class SeataXidClientInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
String xid = RootContext.getXID();
if (StringUtils.hasText(xid)) {
request.getHeaders().add(RootContext.KEY_XID, xid);
}
return execution.execute(request, body);
}
}
這只是簡單地將我們的 XID 值添加到傳出的 HTTP 請求中。
因此,我們必須確保我們的RestClient始終使用此方法:
@Bean
public RestClient restClient() {
return RestClient.builder()
.requestInterceptor(new SeataXidClientInterceptor())
.build();
}
我們也可以使用任何其他 HTTP 用戶端(例如 WebClient 或 RestTemplate)來執行完全相同的操作。
此時,我們所有的出站呼叫都會指示全域事務的 XID。但是,我們仍然需要在下游服務中使用這些事務。我們可以使用 servlet 過濾器來實現這一點:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SeataXidFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) req;
String xid = httpRequest.getHeader(RootContext.KEY_XID);
boolean bound = false;
if (StringUtils.hasText(xid) && !xid.equals(RootContext.getXID())) {
RootContext.bind(xid);
bound = true;
}
try {
chain.doFilter(req, res);
} finally {
if (bound) {
RootContext.unbind();
}
}
}
}
這樣做正好相反——如果傳入的 HTTP 請求中存在 XID,則在繼續處理請求之前將其綁定到本機服務,並確保在最後將其解綁。
至此,我們的交易已正確涵蓋我們的服務,整個交易集將一起提交或回溯。
6. 使用 Spring Cloud
與 Spring Boot 不同,Spring Cloud 可以自動為我們處理其中的一些事情。
在 Spring Cloud 環境中,我們需要在專案中使用 不同的依賴項。此外,我們還需要注意版本號碼-最新的2025.1.0.0 版本僅適用於 Spring Boot 4,而2025.0.0.0 版本則需要 Spring Boot 3。
這個依賴項以 BOM 的形式提供,我們可以將其匯入到依賴項管理部分以管理版本,然後作為實際的啟動依賴項:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2025.0.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
我們仍然需要像以前一樣進行配置,使用seata.conf和application.properties檔案。不過,框架會為我們處理大部分事務傳播。
Spring Cloud Starter 會自動設定我們的服務,以便在需要時將所有傳入的 HTTP 請求加入全域事務。這樣就無需寫 servlet 過濾器了。
此啟動器也會配置RestTemplate bean,使其自動將XID值轉送給下游服務,因此如果我們使用它,則無需在此處進行額外設定。遺憾的是,它不適用於RestClient或WebClient ,因此如果我們使用它們,則仍然需要手動配置。
7. 總結
本文簡要介紹了 Apache Seata。我們了解了它的定義以及如何在應用程式中使用它。下次編寫事務服務時,不妨試試看。
與往常一樣,本文中的所有範例都可以在 GitHub 上找到。