傅青阳一脸黑线,这个马仔太能装了。随手甩出一个阴阳转盘,元始措手不及,忽然间天旋地转,整个人被倒吊在半空,面色涨红。
回答转盘的提问,答对可重新转动指针,积累三次白色,可解除封禁。转盘上面的指针不出意外地停留在黑色面板。
元始心想,难道又是世界上最坚硬的东西?硫化碳块!男高中生!
或者是你所知的诗词中,最廉价的东西是什么?依山尽!
全局事务启动处理
收!想什么呢,我们这里可是正经的技术文章。
提问1:Seata Server如何处理全局事务的启动?
元始:首先我们知道,启动全局事务的请求是:GlobalBeginRequest。所以我们顺着Netty服务端的处理入口,最终可以找到2个关键的接口:
TCInboundHandler
:事务相关的事件处理器
RMInboundHandler
:资源相关的事件处理器
在TCInboundHandler
的第一个方法就可以看到全局事务的处理入口:
/**
* Handle global begin response.
*
* @param globalBegin the global begin
* @param rpcContext the rpc context
* @return the global begin response
*/
GlobalBeginResponse handle(GlobalBeginRequest globalBegin, RpcContext rpcContext);
@Override
protected void doGlobalBegin(GlobalBeginRequest request, GlobalBeginResponse response, RpcContext rpcContext)
throws TransactionException {
//生成xid
response.setXid(
// 关键代码
core.begin(rpcContext.getApplicationId(), rpcContext.getTransactionServiceGroup(),
request.getTransactionName(), request.getTimeout()));
//...
}
然后启动全局事务的处理也很简单:
- 创建全局会话
GlobalSession
- 生成唯一事务ID:
ipAddress + ":" + port + ":" + 雪花算法ID
- 增加全局会话的生命周期监听器
- 全局会话状态修改为已启动,触发生命周期监听器的
onBegin
事件 - 发布事务启动事件
- 将事务ID作为返回结果,返回给客户端。
客户端的处理入口在ClientOnResponseProcessor
。客户端收到事务ID后,设置全局事务状态为已启动,将事务ID写入线程缓存。
分支事务启动处理
提问2:Seata Server如何处理分支事务的启动?
元始:首先我们都知道JDBC的执行过程:
- 获取连接
- 创建Statement
- Statement执行SQL语句
- 连接提交事务或回滚事务
在这里我们贴一个最简单的Demo:
public static void main(String[] args) throws Exception {
Connection conn = null;
try{
conn = DriverManager.getConnection(DB_URL, USER, PASS);
Statement stmt = conn.createStatement();
String sql = "UPDATE users SET age = 30 WHERE id in (100, 101)";
stmt.executeUpdate(sql);
conn.commit();
} catch (SQLException e) {
conn.rollback();
}finally {
if (conn!=null){
conn.close();
}
}
}
由于我们的Seata的数据源是一个代理对象DataSourceProxy
,从中获取的连接也是代理对象ConnectionProxy
,按照上面的SQL执行过程,实际会将业务SQL先在数据库执行完成,然后在使用ConnectionProxy
提交或回滚事务。
因此我们需要重点来看看ConnectionProxy#commit()
做了什么事情。
@Override
public void commit() throws SQLException {
try {
LOCK_RETRY_POLICY.execute(() -> {
// 事务提交
doCommit();
return null;
});
} catch (SQLException e) {
// 如果失败了,本地事务回滚
if (targetConnection != null && !getAutoCommit() && !getContext().isAutoCommitChanged()) {
rollback();
}
throw e;
} catch (Exception e) {
throw new SQLException(e);
}
}
private void doCommit() throws SQLException {
if (context.inGlobalTransaction()) {
processGlobalTransactionCommit();
}//...
}
private void processGlobalTransactionCommit() throws SQLException {
try {
// 注册分支事务
register();
} //...
try {
// 解析SQL并写入undolog
UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
targetConnection.commit();
} catch (Throwable ex) {
LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
// 上报分支事务提交出现异常的情况
report(false);
throw new SQLException(ex);
}
if (IS_REPORT_SUCCESS_ENABLE) {
// 上报分支事务提交的情况
report(true);
}
context.reset();
}
private void register() throws TransactionException {
if (!context.hasUndoLog() || !context.hasLockKey()) {
return;
}
// 注册分支事务
// context.buildLockKeys() 包含了需要全局锁的主键列表
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
null, context.getXid(), null, context.buildLockKeys());
context.setBranchId(branchId);
}
@Override
public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid, String applicationData, String lockKeys) throws TransactionException {
try {
BranchRegisterRequest request = new BranchRegisterRequest();
request.setXid(xid);
request.setLockKey(lockKeys);
request.setResourceId(resourceId);
request.setBranchType(branchType);
request.setApplicationData(applicationData);
BranchRegisterResponse response = (BranchRegisterResponse) RmNettyRemotingClient.getInstance().sendSyncRequest(request);
if (response.getResultCode() == ResultCode.Failed) {
throw new RmTransactionException(response.getTransactionExceptionCode(), String.format("Response[ %s ]", response.getMsg()));
}
return response.getBranchId();
} //...
}
上面贴了一大坨代码,其实逻辑还是很简单的:
- 执行事务提交,可重试的;如果事务提交失败则执行事务回滚。
- 当前事务作为分支事务注册到Seata Server端。然后解析SQL并写入undolog(这一步在下一篇进行重点剖析),然后将本地事务直接提交,也就是此时在数据库是可以直接看到执行结果的。
- 如果本地事务提交失败,则上报分支事务提交出现异常的情况到Server端。然后Server端会执行全局事务回滚。
- 组装分支事务注册的参数,其中最重要的参数是将本次业务SQL会改动的数据行的主键集合,因为在Server端会将这些数据行进行锁定,不允许其他分支事务进行修改。
- 最后组装成
BranchRegisterRequest
请求,通过前面建立的网络连接发送到Server端。
然后Server端如何处理BranchRegisterRequest
请求呢?
这里就又回到TCInboundHandler
这个事务相关的事件处理器了。跟进代码之后,我直接将关键部分代码贴出来:
@Override
protected void doBranchRegister(BranchRegisterRequest request, BranchRegisterResponse response,
RpcContext rpcContext) throws TransactionException {
response.setBranchId(
// 关键代码
core.branchRegister(request.getBranchType(), request.getResourceId(), rpcContext.getClientId(),
request.getXid(), request.getApplicationData(), request.getLockKey()));
}
@Override
public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid,
String applicationData, String lockKeys) throws TransactionException {
GlobalSession globalSession = assertGlobalSessionNotNull(xid, false);
return SessionHolder.lockAndExecute(globalSession, () -> {
//...
// 创建分支会话
BranchSession branchSession = SessionHelper.newBranchByGlobal(globalSession, branchType, resourceId,
applicationData, lockKeys, clientId);
MDC.put(RootContext.MDC_KEY_BRANCH_ID, String.valueOf(branchSession.getBranchId()));
// 分支事务需要的全局锁在这里进行锁定
branchSessionLock(globalSession, branchSession);
try {
// 加入全局会话
globalSession.addBranch(branchSession);
} catch (RuntimeException ex) {
branchSessionUnlock(branchSession);
//...
}
//...
return branchSession.getBranchId();
});
}
@Override
public boolean lock() throws TransactionException {
if (this.getBranchType().equals(BranchType.AT)) {
// 申请全局锁
return LockerManagerFactory.getLockManager().acquireLock(this);
}
return true;
}
protected List<RowLock> collectRowLocks(String lockKey, String resourceId, String xid, Long transactionId,
Long branchID) {
List<RowLock> locks = new ArrayList<RowLock>();
String[] tableGroupedLockKeys = lockKey.split(";");
for (String tableGroupedLockKey : tableGroupedLockKeys) {
int idx = tableGroupedLockKey.indexOf(":");
if (idx < 0) {
return locks;
}
String tableName = tableGroupedLockKey.substring(0, idx);
String mergedPKs = tableGroupedLockKey.substring(idx + 1);
if (StringUtils.isBlank(mergedPKs)) {
return locks;
}
String[] pks = mergedPKs.split(",");
if (pks == null || pks.length == 0) {
return locks;
}
for (String pk : pks) {
if (StringUtils.isNotBlank(pk)) {
// 数据行锁,锁住的是主键集合
RowLock rowLock = new RowLock();
rowLock.setXid(xid);
rowLock.setTransactionId(transactionId);
rowLock.setBranchId(branchID);
rowLock.setTableName(tableName);
rowLock.setPk(pk);
rowLock.setResourceId(resourceId);
locks.add(rowLock);
}
}
}
return locks;
}
Server端的处理过程如下:
- 首先创建改分支的会话。
- 然后针对本次要修改的数据行申请全局锁定。
- 所谓的数据行锁,其实就是将表的主键列表记录下来。如果后续的分支事务提交中包含了其中的主键,则返回加锁失败。
- 将分支会话加入到全局会话,如果出现回滚,则可以从全局会话取出所有分支会话进行回调。
SELECT FOR UPDATE的特殊处理
提问3:前面提到了业务SQL在注册分支事务之后,其实将本地的数据库事务提交了的,这时候是可以看到SQL的执行结果的。但是我们站在全局事务来看,这个时候的事务隔离其实是最差的读未提交的。所以为了解决这个问题,Seata对 SELECT FOR UPDATE
做了什么特殊处理呢?
元始:问得好,这里我们就需要回到SelectForUpdateExecutor
这个Select for update 的具体SQL执行器,在执行器提交事务之前有个循环处理
String selectPKSQL = buildSelectSQL(paramAppenderList);
while (true) {
try {
// #870
// execute return Boolean
// executeQuery return ResultSet
rs = statementCallback.execute(statementProxy.getTargetStatement(), args);
// Try to get global lock of those rows selected
// 尝试为选择的行获取全局锁
TableRecords selectPKRows = buildTableRecords(getTableMeta(), selectPKSQL, paramAppenderList);
// 构建锁的key
String lockKeys = buildLockKey(selectPKRows);
if (StringUtils.isNullOrEmpty(lockKeys)) {
break;
}
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);
}
}
简单来说就是不断重试,如果重试一定次数还未拿到全局锁,就会退出循环,宣布此次加锁失败。
元始:来来来,再给你讲个故事:
从前有座山,山里有座庙,庙里有个老和尚,长的真是俏!俏也不争春,只把春来报,待到山花烂漫时,他在丛中笑!哈哈哈!
好了干正事,我来贴一下目前我们了解的分布式事务的全貌: