Seata 是什麼
Seata
是阿里近期開源的分散式事務框架,地址:github.com/seata/seata。框架包括了集團的 TXC
(雲版本叫 GTS
)和螞蟻金服的 TCC
兩種模式,短短數月 Github
上的 star
數已經接近一萬,算是目前唯一有大廠背書的分散式事務解決方案。
TXC
在 Seata
中又叫 AT
模式,意為補償方法是框架自動生成的,對使用者完全遮蔽,使用者可以向使用本地事務那樣使用分散式事務,缺點是僅支援關係型資料庫(目前支援 MySQL
),引入 Seata AT
的服務需要本地建表儲存 rollback_info
,隔離級別預設 RU
適用場景有限。
TCC
不算是新概念,很早就有了,使用者通過定義 try/confirm/cancel
三個方法在應用層面模擬兩階段提交,區別在於 TCC 中 try
方法也需要運算元據庫進行資源鎖定,後續兩個補償方法由框架自動呼叫,分別進行資源提交和回滾,這點同單純的儲存層 2PC
不太一樣。螞蟻金服向 Seata
貢獻了自己的 TCC
實現,據說已經演化了十多年,大量應用在在金融、交易、倉儲等領域。
分散式事務的誕生背景
早期應用都是單一架構,例如支付服務涉及到的賬戶、金額、訂單系統等都由單一應用負責,底層訪問同一個資料庫例項,自然事務操作也是本地事務,藉助 Spring
可以輕鬆實現;但是由於量級越來越大,單一服務需要進行職責拆分變為三個獨立的服務,通過 RPC
進行呼叫,資料也存在不同的資料庫例項中,由於這時一次業務操作涉及對多個資料庫資料的修改,無法再依靠本地事務,只能通過分散式事務框架來解決。
TCC 就是分散式事務的一種解決方案,屬於柔性補償型,優點在於理解簡單、僅 try
階段加鎖併發效能較好,缺點在於程式碼改造成本。
什麼是 TCC 本文就不再贅述了,TCC 的概念本身並不複雜
Seata TCC 使用方法
在分析原始碼之前,我們先簡要提及下 Seata TCC
模式的使用方法,有助於後續理解整個 TCC
流程。
Seata TCC 參與方
Seata
中的 TCC
模式要求 TCC
服務的參與方在介面上增加 @TwoPhaseBusinessAction
註解,註明 TCC
介面的名稱(全域性唯一),TCC
介面的 confirm
和 cancel
方法的名稱,用於後續框架反射呼叫,下面是一個 TCC
介面的案例:
public interface TccAction {
@TwoPhaseBusinessAction(name = "yourTccActionName", commitMethod = "confirm", rollbackMethod = "cancel")
public boolean try(BusinessActionContext businessActionContext, int a, int b);
public boolean confirm(BusinessActionContext businessActionContext);
public boolean cancel(BusinessActionContext businessActionContext);
}
複製程式碼
緊接著定義實現類 Impl
實現這個介面,為三個方法提供具體實現。最後將參與方服務進行釋出,註冊到遠端,主要為了後續能讓 Seata
框架呼叫到參與方的 confirm
或者 cancel
方法閉環整個 TCC
事務。
Seata TCC 發起方
Seata TCC
的發起方類似於我們上圖中的 payment service
,參與方需要在業務方法上增加 @GlobalTransactional
註解,用於開啟切面註冊全域性事務,業務方法中呼叫 TCC
參與方的若干 try
方法,一旦業務方法呼叫成功,Seata
框架會通知 TC
回撥這些參與方的 confirm
和 cancel
方法。
原始碼分析
Seata
中 TCC
模式的原始碼並不複雜,主要集中於:
module | class | 功能 |
---|---|---|
seata-spring | GlobalTransactionalInterceptor.class | 全域性事務切面邏輯,包括註冊全域性事務,拿到 xid |
seata-spring | TccActionInterceptor.class | TCC 參與方切面邏輯 |
seata-tcc | TCCResourceManager.class | 解析 TCC Bean,儲存 TCC Resources,便於後續回撥 |
seata-tcc | ActionInterceptorHandler.class | TCC 分支事務註冊實現 |
seata-server | DefaultCoordinator.class、FileTransactionStoreManager.class | 主要是 TC 的實現、事務儲存等實現 |
註冊 TCC Resources
Seata
中一個 TCC
介面被稱作一個 TCC Resources
,其結構如下:
public class TCCResource implements Resource {
private String resourceGroupId = "DEFAULT";
private String appName;
private String actionName; // TCC 介面名稱
private Object targetBean; // TCC Bean
private Method prepareMethod; // try 方法
private String commitMethodName;
private Method commitMethod; // confirm 方法
private String rollbackMethodName;
private Method rollbackMethod; // cancel 方法
// …… 省略
}
複製程式碼
Seata
解析到應用中存在 TCC Bean
,則通過 parserRemotingServiceInfo
方法生成一個 TCCResource
物件,進而呼叫 TCCResourceManager
類的 registerResource
方法,將 TCCResource
物件儲存到本地的 tccResourceCache
中,它是一個 ConcurrentHashMap
結構,同時通過 RmRpcClient
將該 TCCResource
的 resourceId
、address
等資訊註冊到服務端,便於後續 TC
通過 RPC
回撥到正確的地址。
// 解析 TCCResource 的部分程式碼
Class<?> interfaceClass = remotingBeanDesc.getInterfaceClass();
Method[] methods = interfaceClass.getMethods();
if(isService(bean, beanName)){
try {
// 如果是 TCC service Bean,解析並註冊該 resource
Object targetBean = remotingBeanDesc.getTargetBean();
for(Method m : methods){
TwoPhaseBusinessAction twoPhaseBusinessAction = m.getAnnotation(TwoPhaseBusinessAction.class);
if(twoPhaseBusinessAction != null){
// 如果有 TCC 參與方註解,定義一個 TCCResource,
TCCResource tccResource = new TCCResource();
tccResource.setActionName(twoPhaseBusinessAction.name());
// TCC Bean
tccResource.setTargetBean(targetBean);
// try 方法
tccResource.setPrepareMethod(m);
// confirm 方法名稱
tccResource.setCommitMethodName(twoPhaseBusinessAction.commitMethod());
// confirm 方法物件
tccResource.setCommitMethod(ReflectionUtil.getMethod(interfaceClass, twoPhaseBusinessAction.commitMethod(), new Class[]{BusinessActionContext.class}));
// cancel 方法名稱
tccResource.setRollbackMethodName(twoPhaseBusinessAction.rollbackMethod());
// cancel 方法物件
tccResource.setRollbackMethod(ReflectionUtil.getMethod(interfaceClass, twoPhaseBusinessAction.rollbackMethod(), new Class[]{BusinessActionContext.class}));
// 呼叫到 TCCResourceManager 的 registerResource 方法
DefaultResourceManager.get().registerResource(tccResource);
}
}
}catch (Throwable t){
throw new FrameworkException(t, "parser remting service error");
}
}
複製程式碼
我們看一下 TCCResourceManager
的 registerResource
方法的實現:
// 記憶體中儲存的 resourceId 和 TCCResource 的對映關係
private Map<String, Resource> tccResourceCache = new ConcurrentHashMap<String, Resource>();
@Override
public void registerResource(Resource resource) {
TCCResource tccResource = (TCCResource) resource;
tccResourceCache.put(tccResource.getResourceId(), tccResource);
// 呼叫父類的方法通過 RPC 註冊到遠端
super.registerResource(tccResource);
}
複製程式碼
我們看下 TCCResource
是如何註冊到服務端的:
public void registerResource(Resource resource) {
// 拿到 RmRpcClient 例項,呼叫其 registerResource 方法
RmRpcClient.getInstance().registerResource(resource.getResourceGroupId(), resource.getResourceId());
}
public void registerResource(String resourceGroupId, String resourceId) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("register to RM resourceId:" + resourceId);
}
synchronized (channels) {
for (Map.Entry<String, Channel> entry : channels.entrySet()) {
String serverAddress = entry.getKey();
Channel rmChannel = entry.getValue();
if (LOGGER.isInfoEnabled()) {
LOGGER.info("register resource, resourceId:" + resourceId);
}
// 註冊 resourceId,遠端將其解析為一個 RpcContext 儲存在記憶體中
sendRegisterMessage(serverAddress, rmChannel, resourceId);
}
}
}
複製程式碼
GlobalTransaction 註冊全域性事務
GlobalTransaction
註解是全域性事務的入口,其切面邏輯實現在 GlobalTransactionalInterceptor
類中。如果判斷進入 @GlobalTransaction
修飾的方法,會呼叫 handleGlobalTransaction
方法進入切面邏輯,其中關鍵方法是 transactionalTemplate
的 execute
方法。
public Object execute(TransactionalExecutor business) throws Throwable {
// 如果上游已經有 xid 傳過來說明自己是下游,直接參與到這個全域性事務中就可以,不必新開一個,角色是 Participant
// 如果上游沒有 xid 傳遞過來,說明自己是發起方,新開啟一個全域性事務,角色是 Launcher
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// …… …… 省略
try {
// 開啟全域性事務
beginTransaction(txInfo, tx);
Object rs = null;
try {
// 呼叫業務方法
rs = business.execute();
} catch (Throwable ex) {
// 如果拋異常,通知 TC 回滾全域性事務
completeTransactionAfterThrowing(txInfo,tx,ex);
throw ex;
}
// 如果不拋異常,通知 TC 提交全域性事務
commitTransaction(tx);
return rs;
}
// …… …… 省略
}
複製程式碼
beginTransaction
方法呼叫了 transactionManager
的 begin
方法:
// 客戶端
@Override
public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
throws TransactionException {
GlobalBeginRequest request = new GlobalBeginRequest();
request.setTransactionName(name);
request.setTimeout(timeout);
// 傳送 RPC,獲取 TC 下發的 xid
GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);
return response.getXid();
}
// 服務端
@Override
public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
throws TransactionException {
// 全域性事務用 GlobalSession 來表示
GlobalSession session = GlobalSession.createGlobalSession(
applicationId, transactionServiceGroup, name, timeout);
session.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
// 將 GlobalSession 寫入檔案儲存
session.begin();
// 返回 UUID 作為全域性事務 ID
return XID.generateXID(session.getTransactionId());
}
複製程式碼
TwoPhaseBusinessAction 註冊分支事務
全域性事務呼叫業務方法時,會進入 TCC
參與方的切面邏輯,主要實現在 TccActionInterceptor
類中,關鍵方法是 actionInterceptorHandler
的 proceed
方法。
public Map<String, Object> proceed(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction, Callback<Object> targetCallback) throws Throwable {
// …… …… 省略
// 建立分支事務
String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext);
actionContext.setBranchId(branchId);
// 記錄方法引數
Class<?>[] types = method.getParameterTypes();
int argIndex = 0;
for (Class<?> cls : types) {
if (cls.getName().equals(BusinessActionContext.class.getName())) {
arguments[argIndex] = actionContext;
break;
}
argIndex++;
}
// …… …… 省略
}
複製程式碼
doTccActionLogStore
方法負責註冊分支事務:
// 客戶端
protected String doTccActionLogStore(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction, BusinessActionContext actionContext) {
String actionName = actionContext.getActionName();
// 拿到全域性事務 ID
String xid = actionContext.getXid();
// …… …… 省略
try {
// resourceManager 通過 RPC 向 TC 註冊分支事務
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.TCC, actionName, null, xid, applicationContextStr, null);
// 拿到 TC 返回的分支事務 ID
return String.valueOf(branchId);
}
// …… …… 省略
}
// 服務端
@Override
public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid,
String applicationData, String lockKeys) throws TransactionException {
GlobalSession globalSession = assertGlobalSession(XID.getTransactionId(xid), GlobalStatus.Begin);
// 分支事務用 BranchSession 表示,新建一個 BranchSession
BranchSession branchSession = SessionHelper.newBranchByGlobal(globalSession, branchType, resourceId,
applicationData, lockKeys, clientId);
if (!branchSession.lock()) {
throw new TransactionException(LockKeyConflict);
}
try {
// 將分支事務加入全域性事務中,也會寫檔案
globalSession.addBranch(branchSession);
} catch (RuntimeException ex) {
throw new TransactionException(FailedToAddBranch);
}
// 返回分支事務 ID
return branchSession.getBranchId();
}
複製程式碼
TC 回撥參與方補償方法
分支事務註冊完畢,業務方法呼叫成功則通知 TC
提交全域性事務。
@Override
public void commit() throws TransactionException {
// 如果是參與者,無需發起提交請求
if (role == GlobalTransactionRole.Participant) {
return;
}
// 由 TM 向 TC 發出提交全域性事務的請求
status = transactionManager.commit(xid);
}
複製程式碼
TC
收到客戶端 TM
的 commit
請求後:
@Override
public GlobalStatus commit(String xid) throws TransactionException {
// 根據 xid 找出 GlobalSession
GlobalSession globalSession = SessionHolder.findGlobalSession(XID.getTransactionId(xid));
if (globalSession == null) {
return GlobalStatus.Finished;
}
GlobalStatus status = globalSession.getStatus();
// 關閉這個 GlobalSession,不讓後續的分支事務再註冊上來
globalSession.closeAndClean();
if (status == GlobalStatus.Begin) {
// 修改狀態為提交進行中
globalSession.changeStatus(GlobalStatus.Committing);
// 一旦分支事務中存在 TCC,做同步提交,其實 TCC 分支也可以非同步提交,要求高效能時可以選擇非同步
if (globalSession.canBeCommittedAsync()) {
asyncCommit(globalSession);
} else {
doGlobalCommit(globalSession, false);
}
}
return globalSession.getStatus();
}
複製程式碼
doGlobalCommit
是我們關注的關鍵方法,我們忽略其中的次要邏輯:
@Override
public void doGlobalCommit(GlobalSession globalSession, boolean retrying) throws TransactionException {
for (BranchSession branchSession : globalSession.getSortedBranches()) {
// …… …… 省略
try {
// 呼叫 DefaultCoordinator 的 branchCommit 方法做分支提交
// 引數有分支事務 id,resourceId 用來尋找對應的 TCCResource 和補償方法引數資訊
BranchStatus branchStatus = resourceManagerInbound.branchCommit(branchSession.getBranchType(),
XID.generateXID(branchSession.getTransactionId()), branchSession.getBranchId(),
branchSession.getResourceId(), branchSession.getApplicationData());
}
}
// …… …… 省略
}
複製程式碼
服務端的 DefaultCoordinator
類中的 branchCommit
方法發出 RPC
請求,呼叫對應 TCCResource
提供方:
@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
String applicationData)
throws TransactionException {
// …… …… 省略
// 獲取全域性事務和分支事務
GlobalSession globalSession = SessionHolder.findGlobalSession(XID.getTransactionId(xid));
BranchSession branchSession = globalSession.getBranch(branchId);
// 根據 resourceId 找到對應的 channel 和 RpcContext
BranchCommitResponse response = (BranchCommitResponse)messageSender.sendSyncRequest(resourceId,
branchSession.getClientId(), request);
// 返回分支事務提交狀態
return response.getBranchStatus();
// …… …… 省略
}
複製程式碼
客戶端自然是接收到分支提交的 RPC
請求,然後本地找出之前解析並保持下來的 TCCResource
進行補償方法的反射呼叫,下面我們擷取其中的關鍵步驟進行分析。
@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
// 根據 resourceId 找出記憶體中保留的 TCCResource 物件
TCCResource tccResource = (TCCResource) tccResourceCache.get(resourceId);
if(tccResource == null){
throw new ShouldNeverHappenException("TCC resource is not exist, resourceId:" + resourceId);
}
// 獲取 targetBean 和相應的 method 物件
Object targetTCCBean = tccResource.getTargetBean();
Method commitMethod = tccResource.getCommitMethod();
try {
boolean result = false;
// 取出補償方法引數資訊
BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId, applicationData);
// 反射呼叫補償方法
Object ret = commitMethod.invoke(targetTCCBean, businessActionContext);
// 返回狀態
return result ? BranchStatus.PhaseTwo_Committed:BranchStatus.PhaseTwo_CommitFailed_Retryable;
}
// …… …… 省略
}
複製程式碼
事務儲存
關於 Seata TC 模組如何進行事務儲存,網上有的文章已經講得很詳細,例如 深度剖析一站式分散式事務方案 Seata-Server,因此這裡不再贅述。
需要提及的一點是,TC
有可能成為整個分散式事務服務的效能瓶頸,因此如何做到高效能
和高可用
很重要,目前的儲存方式是 File
,程式碼中也有關於 DB Store Mode
的 TODO
項,檔案相比於 DB
效能肯定好一些但是可用性會差一點,這塊怎麼保證要等到後續 HA Cluster
釋出之後再看。
總結
整個 Seata
框架中關於 TCC
部分的原始碼並不複雜,本文只選取了部分類中的關鍵程式碼進行展示,忽略了一些判斷邏輯和異常處理,筆者認為 Seata TCC
中關於 TCC
異常的封裝和自定義處理、還有各種使用者擴充套件埋點的設計也值得一看。
螞蟻 SOFA Channel
之前做過一個關於 Seata TCC
Seata TCC 分享 的講解裡也提到,TCC
框架的難點不在於本身,而在於如何寫好一個 TCC
介面,如果對這部分內容感興趣,可以點選連結進行詳細瞭解。
寫在最後
這是一個不定時更新的、披著程式設計師外衣的文青小號。既分享極客技術,也記錄人間煙火。