seata 业务无侵入AT模式详解
GlobalTransactional注解
1、使用此注解,设置事务名称以及超时时间等,加上此注解,会进入GlobalTransactionalInterceptor,切面中io.seata.tm.api.TransactionalTemplate.execute会根据注解组装事务信息,并开启分布式全局事务
// 2. begin transaction
beginTransaction(txInfo, tx);
Object rs = null;
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3.the needed business exception to rollback.
completeTransactionAfterThrowing(txInfo,tx,ex);
throw ex;
}
// 4. everything is fine, commit.
commitTransaction(tx);
进一步看看如何开启的全局事务
public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
throws TransactionException {
GlobalBeginRequest request = new GlobalBeginRequest();
request.setTransactionName(name);
request.setTimeout(timeout);
GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);
return response.getXid();
}
客户端的实现十分简单,向服务端发送一个同步消息
再看看服务端的实现io.seata.server.coordinator.DefaultCoordinator#doGlobalBegin
GlobalSession session = GlobalSession.
createGlobalSession(
applicationId, transactionServiceGroup, name, timeout);
session.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
session.begin();
return session.getXid();
根据 applicationId,transactionServiceGroup,name,timeout创建分布式事务生成XID
最终把生成的GlobalSession放入到GlobalSession的sessionMap中,向客户端返回XID
客户端在接收到xid后,会将xid与线程绑定,放到threadLocal中
2、事务如何传递到下游服务
以seata整合dubbo为例 io.seata.integration.dubbo.alibaba.TransactionPropagationFilter继承自dubbo的filter
if (xid != null) {
RpcContext.getContext().setAttachment(RootContext.KEY_XID, xid);
}
放入到dubbo的RpcContext中,会在远程调用生产者的时候,透传到生产者。
undoLog记录
io.seata.rm.datasource.PreparedStatementProxy seata又对PreparedStatement进行增强,在PreparedStatementProxy的execute方法中,不仅要执行原生的execute,还执行了其自定义逻辑
@Override
public boolean execute() throws SQLException {
return ExecuteTemplate.execute(this, new StatementCallback<Boolean, PreparedStatement>() {
@Override
public Boolean execute(PreparedStatement statement, Object... args) throws SQLException {
return statement.execute();
}
});
最终进入
io.seata.rm.datasource.exec.BaseTransactionalExecutor#prepareUndoLog
protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
if (beforeImage.getRows().size() == 0 && afterImage.getRows().size() == 0) {
return;
}
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
String lockKeys = buildLockKey(lockKeyRecords);
connectionProxy.appendLockKey(lockKeys);
SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
connectionProxy.appendUndoLog(sqlUndoLog);
}
3、rm如何加入分布式事务
同样是io.seata.integration.dubbo.alibaba.TransactionPropagationFilter
if (rpcXid != null) {
RootContext.bind(rpcXid);
bind = true;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("bind[" + rpcXid + "] to RootContext");
}
}
会把rpcContext种的xid但放到 seata的RootContext中
rm事务提交时,
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}
seata使用DataSourceProxy 在getConnection的时候返回ConnectionProxy包裹真正的数据库connection
@Override
public ConnectionProxy getConnection() throws SQLException {
Connection targetConnection = targetDataSource.getConnection();
return new ConnectionProxy(this, targetConnection);
}
在ConnectionProxy提交事务时,会向seata-server注册分支事务并记在ConnectionContext中
private void register() throws TransactionException {
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
null, context.getXid(), null, context.buildLockKeys());
context.setBranchId(branchId);
}
看下seata服务器注册分支事务的流程
return globalSession.lockAndExcute(() -> {
if (!globalSession.isActive()) {
throw new TransactionException(GlobalTransactionNotActive, "Current Status: " + globalSession.getStatus());
}
if (globalSession.getStatus() != GlobalStatus.Begin) {
throw new TransactionException(GlobalTransactionStatusInvalid,
globalSession.getStatus() + " while expecting " + GlobalStatus.Begin);
}
globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
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);
}
return branchSession.getBranchId();
});
globalSession加锁进行操作,分支事务会话放入到全局事务会话的branchSessions中并返回分支事务id。并在io.seata.server.lock.memory.MemoryLocker#acquireLock尝试加全局锁,如加锁失败,则分支事务提交失败。
seata中使用了spi机制来生成锁,dubbo为了灵活的切换注册中心,以及使用的协议,使用spi机制
spi的作用,spi十分像设计模式中的策略者模式,一接口下有多种不同的实现。使用的时候只需要指定name即可使用。
实现关键类EnhancedServiceLoader,以注册中心的spi举例,
EnhancedServiceLoader.load(RegistryProvider.class, Objects.requireNonNull(registryType).name()).provide();
EnhancedServiceLoader会根据RegistryProvider从指定路径利用ClassLoader加载到jvm中,获取class对象,然后实例化返回对象
利用spi机制,可轻松的实现模块之间的插拔就可以使用不同的实现而不用修改代码
总结一下,策略模式与 SPI 机制有下面几点异同:
从设计思想来看。 策略模式和 SPI 机制其思想是类似的,都是通过一定的设计隔离变化的部分,从而让原有部分更加稳定。
从隔离级别来看。 策略模式的隔离是类级别的隔离,而 SPI 机制是项目级别的隔离。
从应用领域来看。 策略模式更多用在业务代码书写,SPI 机制更多用于框架的设计。
seata利用其审批机制创建出memoryLock,去尝试加全局锁,如过当前lockkey无锁放把lockkey放进去,加锁成功,否则失败,返回冲突报错
seata回滚
全局回滚如何触发?
1、分支事务异常回滚导致全局回滚
2、超时导致回滚io.seata.server.coordinator.DefaultCoordinator#timeoutCheck检测事务是否超时
异常回滚分析,看回io.seata.spring.annotation.GlobalTransactionalInterceptor
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3.the needed business exception to rollback.
completeTransactionAfterThrowing(txInfo,tx,ex);
throw ex;
}
发生异常触发回滚
@Override
public GlobalStatus rollback(String xid) throws TransactionException {
GlobalRollbackRequest globalRollback = new GlobalRollbackRequest();
globalRollback.setXid(xid);
GlobalRollbackResponse response = (GlobalRollbackResponse)syncCall(globalRollback);
return response.getGlobalStatus();
}
向seata-server发送一个回滚请求
seata-server在io.seata.server.coordinator.DefaultCore#rollback处理请求
boolean shouldRollBack = globalSession.lockAndExcute(() -> {
globalSession.close(); // Highlight: Firstly, close the session, then no more branch can be registered.
if (globalSession.getStatus() == GlobalStatus.Begin) {
globalSession.changeStatus(GlobalStatus.Rollbacking);
return true;
}
return false;
});
doGlobalRollback(globalSession, false);
将全局会话的状态改为GlobalStatus.Rollbacking开始全局回滚
服务端遍历所有分支会话,然后发送同步并等待分支事务回滚成功
BranchRollbackResponse response = (BranchRollbackResponse)messageSender.sendSyncRequest(resourceId,
branchSession.getClientId(), request);
客户端回滚:io.seata.rm.datasource.DataSourceManager#branchRollback,执行undo操作
UndoLogManager.undo(dataSourceProxy, xid,
首先查询undo log根据branchid和xid。即分支id和全局事务id查undo_log表,undo_log表中记录了beforeImage和afterImage,即分支事务执行前和执行后的数据,在混滚前会先对数据进行验证io.seata.rm.datasource.undo.AbstractUndoExecutor#dataValidationAndGoOn
beforeImage使用来回滚恢复数据的,afterimage是用来判断数据是否已经dirty,undo之前需要检查afterImage和currentRecord是否相等
if (!DataCompareUtils.isRecordsEquals(afterRecords, currentRecords)) {
XXXXXXX
}
注意在判断是否相等时io.seata.rm.datasource.DataCompareUtils#isFieldEquals,
return f0.getValue().equals(f1.getValue());
用的equals判断,如果数据库设置浮点数,那这里就会一直判断为不等于,导致无法回滚
判断完成后,将会执行update语句将数据更新为beforeImage,至此分支事务回滚完毕
response设置branchid和xid 状态设置为PhaseTwo_Rollbacked返回给服务端
再回到seata-server
服务端收到状态为PhaseTwo_Rollbacked的响应,将branchSession移除
case PhaseTwo_Rollbacked:
globalSession.removeBranch(branchSession);
LOGGER.error("Successfully rolled back branch " + branchSession);
continue;
再看看回滚失败的情况
try {
UndoLogManager.undo(dataSourceProxy, xid, branchId);
} catch (TransactionException te) {
if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;
} else {
return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
}
}
PhaseTwo_RollbackFailed_Unretryable为回滚失败,不再尝试回滚
PhaseTwo_RollbackFailed_Retryable为回滚失败,尝试回滚
catch (Throwable e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException rollbackEx) {
LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx);
}
}
throw new TransactionException(BranchRollbackFailed_Retriable, String.format("%s/%s", branchId, xid),
e);
}
从代码里可以看出来除非时error,Throwable下面的异常都是会返回尝试回滚的回滚失败,数组dirty之后,事务将回滚失败并且会不断重试,直到达到超时时间,才会停止。配置为max.rollback.retry.timeout
服务端代码
switch (branchStatus) {
case PhaseTwo_Rollbacked:
globalSession.removeBranch(branchSession);
LOGGER.error("Successfully rolled back branch " + branchSession);
continue;
case PhaseTwo_RollbackFailed_Unretryable:
SessionHelper.endRollbackFailed(globalSession);
LOGGER.error("Failed to rollback global[" + globalSession.getXid() + "] since branch["
+ branchSession.getBranchId() + "] rollback failed");
return;
default:
LOGGER.info("Failed to rollback branch " + branchSession);
if (!retrying) {
queueToRetryRollback(globalSession);
}
return;
}
除了PhaseTwo_RollbackFailed_Unretryable,状态变为回滚失败,不再回盾,其他的失败都会放入重试队列,再次尝试回滚,会话状态变为重试
retryRollbacking.scheduleAtFixedRate(() -> {
try {
handleRetryRollbacking();
} catch (Exception e) {
LOGGER.info("Exception retry rollbacking ... ", e);
}
}, 0, rollbackingRetryDelay, TimeUnit.SECONDS);
默认5秒每次从回滚重试队列中中取出所有会话进行挨个尝试回滚,如果回滚时间超过最大时间,也会放弃回滚
Collection<GlobalSession> rollbackingSessions = SessionHolder.getRetryRollbackingSessionManager().allSessions();
if(CollectionUtils.isEmpty(rollbackingSessions)){
return;
}
long now = System.currentTimeMillis();
for (GlobalSession rollbackingSession : rollbackingSessions) {
try {
if(isRetryTimeout(now, MAX_ROLLBACK_RETRY_TIMEOUT.toMillis(), rollbackingSession.getBeginTime())){
/**
* Prevent thread safety issues
*/
SessionHolder.getRetryCommittingSessionManager().removeGlobalSession(rollbackingSession);
LOGGER.error("GlobalSession rollback retry timeout [{}]", rollbackingSession.getTransactionId());
continue;
}
rollbackingSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
core.doGlobalRollback(rollbackingSession, true);
} catch (TransactionException ex) {
LOGGER.info("Failed to retry rollbacking [{}] {} {}",
rollbackingSession.getXid(), ex.getCode(), ex.getMessage());
}
}
检测事务超时
定时任务io.seata.server.coordinator.DefaultCoordinator#timeoutCheck,默认5秒检测一次
public boolean isTimeout() {
return (System.currentTimeMillis() - beginTime) > timeout;
}
seata提交
整个事务执行过程中,无异常,没超时,事务将会被正常提交
1、提交操作由谁触发?
事务发起者,如果没有异常,将会提交事务
客户端提交事务即向seata-server发送一个提交事务请求
服务端处理提交事务请求io.seata.server.coordinator.DefaultCore#doGlobalCommit,遍历全局事务的所有分支事务,分别进行提交,同样又是发一个同步消息。
2、客户端提交流程
因为,分支事务的本地事物在执行完成后已经commit了,所以在最终提交分支事务的时候并没有什么操作.
if (!ASYNC_COMMIT_BUFFER.offer(new Phase2Context(branchType, xid, branchId, resourceId, applicationData))) {
LOGGER.warn("Async commit buffer is FULL. Rejected branch [" + branchId + "/" + xid + "] will be handled by housekeeping later.");
}
return BranchStatus.PhaseTwo_Committed;
返回二段已提交,
所有分支事务都提交成功后,结束全局会话.
3 全部提交完成之后,事务发起者清理xid等数据,分布式事务执行完毕
// 4. everything is fine, commit.
commitTransaction(tx);
return rs;
} finally {
//5. clear
triggerAfterCompletion();
cleanUp();
}