分支事务的注册是在seata的一个很重要的数据源DataSourceProxy中去做的,简单来说就是通过数据源这一层去干预了数据库的执行而达到分支事务的注册。
所以我们直接进入DataSourceProxy的源码看获取连接的地方,我就来模拟一个过程就是我们如果在分支事务中要向数据库插入一条数据,那么jdbc的过程如下:
1.获取连接Connection,开始事务;
2.通过连接创建一个预处理对象,比如PreparedStatement = Connection.prepareStatement(sql);
3.通过PreparedStatement 的execute方法得到执行结果;
4.提交本地事务。
带着上面的sql的一个普通事务的执行过程我们来分析下分支事务的执行。
io.seata.rm.datasource.DataSourceProxy#getConnection()
public ConnectionProxy getConnection() throws SQLException {
/**
* 获取连接的时候来调用的数据源连接,这里将数据源连接包了一层
* 这肯定要包一层,因为因为seata需要注册分支事务ID,注册分支事务需要敢于sql的执行
* 执行的前后需要插入自己的逻辑,所以这里需要将Connection包装一层
* 这里包装的是ConnectionProxy
*/
Connection targetConnection = targetDataSource.getConnection();
return new ConnectionProxy(this, targetConnection);
}
sql的预处理肯定是ConnectionProxy中执行的,所以直接定位到ConnectionProxy中
io.seata.rm.datasource.AbstractConnectionProxy#prepareStatement(java.lang.String)
/**
* sql的预处理,就是执行那个preparing.prepareStatement(sql)就是类似于
* 我们jdbc的操作的时候的sql预处理,所以这里就是执行业务sql的逻辑入口
* @param sql
* @return
* @throws SQLException
*/
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
String dbType = getDbType();
// support oracle 10.2+
PreparedStatement targetPreparedStatement = null;
if (BranchType.AT == RootContext.getBranchType()) {
List<SQLRecognizer> sqlRecognizers = SQLVisitorFactory.get(sql, dbType);
if (sqlRecognizers != null && sqlRecognizers.size() == 1) {
SQLRecognizer sqlRecognizer = sqlRecognizers.get(0);
if (sqlRecognizer != null && sqlRecognizer.getSQLType() == SQLType.INSERT) {
TableMeta tableMeta = TableMetaCacheFactory.getTableMetaCache(dbType).getTableMeta(getTargetConnection(),
sqlRecognizer.getTableName(), getDataSourceProxy().getResourceId());
String[] pkNameArray = new String[tableMeta.getPrimaryKeyOnlyName().size()];
tableMeta.getPrimaryKeyOnlyName().toArray(pkNameArray);
targetPreparedStatement = getTargetConnection().prepareStatement(sql,pkNameArray);
}
}
}
if (targetPreparedStatement == null) {
//进行sql预处理
targetPreparedStatement = getTargetConnection().prepareStatement(sql);
}
//预处理完成以后返回预处理对象,和jdbc操作sql的预处理一样的,执行完预处理需要返回一个预处理对象
//我们拿到这个预处理对象需要进行execute得到结果集,所以这里先返回一个预处理对象,只是seata这里
//给我们返回了一个sql预处理对象的封装而已,因为它要做一些事情,比如分支事务的处理,所以这个对象返回以后
//下面就可以直接看PreparedStatementProxy的execute方法了
return new PreparedStatementProxy(this, targetPreparedStatement, sql);
}
上面的ConnectionProxy得到了PreparedStatementProxy,也就是执行了sql的预处理,这个时候就需要到PreparedStatementProxy里面去看它的execute方法了,这个方法就是得到执行结果,得到执行结果就意味着所有的分支事务逻辑都里面了。
io.seata.rm.datasource.PreparedStatementProxy#execute
/**
* sql的真正的执行逻辑
* @return
* @throws SQLException
*/
@Override
public boolean execute() throws SQLException {
//调用ExecuteTemplate模板方法执行得到结果集
return ExecuteTemplate.execute(this, (statement, args) -> statement.execute());
}
io.seata.rm.datasource.exec.ExecuteTemplate#execute
public static <T, S extends Statement> T execute(List<SQLRecognizer> sqlRecognizers,
StatementProxy<S> statementProxy,
StatementCallback<T, S> statementCallback,
Object... args) throws SQLException {
//判断是否是AT模式,如果不是AT模式直接当成普通的sql执行
if (!RootContext.requireGlobalLock() && BranchType.AT != RootContext.getBranchType()) {
// Just work as original statement
return statementCallback.execute(statementProxy.getTargetStatement(), args);
}
String dbType = statementProxy.getConnectionProxy().getDbType();
if (CollectionUtils.isEmpty(sqlRecognizers)) {
sqlRecognizers = SQLVisitorFactory.get(
statementProxy.getTargetSQL(),
dbType);
}
Executor<T> executor;
if (CollectionUtils.isEmpty(sqlRecognizers)) {
executor = new PlainExecutor<>(statementProxy, statementCallback);
} else {
//得到sql的分析结果
if (sqlRecognizers.size() == 1) {
SQLRecognizer sqlRecognizer = sqlRecognizers.get(0);
//根据的sql类型得到不同的sql执行器
switch (sqlRecognizer.getSQLType()) {
case INSERT:
//通过spi机制得到插入的执行器,这里spi提供的有mysql、oracle和POSTGRE
executor = EnhancedServiceLoader.load(InsertExecutor.class, dbType,
new Class[]{StatementProxy.class, StatementCallback.class, SQLRecognizer.class},
new Object[]{statementProxy, statementCallback, sqlRecognizer});
break;
case UPDATE:
executor = new UpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
break;
case DELETE:
executor = new DeleteExecutor<>(statementProxy, statementCallback, sqlRecognizer);
break;
case SELECT_FOR_UPDATE:
executor = new SelectForUpdateExecutor<>(statementProxy, statementCallback, sqlRecognizer);
break;
default:
executor = new PlainExecutor<>(statementProxy, statementCallback);
break;
}
} else {
executor = new MultiExecutor<>(statementProxy, statementCallback, sqlRecognizers);
}
}
T rs;
try {
//真正的执行io.seata.rm.datasource.exec.BaseTransactionalExecutor.execute
rs = executor.execute(args);
} catch (Throwable ex) {
if (!(ex instanceof SQLException)) {
// Turn other exception into SQLException
ex = new SQLException(ex);
}
throw (SQLException) ex;
}
return rs;
}
io.seata.rm.datasource.exec.BaseTransactionalExecutor#execute
public T execute(Object... args) throws Throwable {
String xid = RootContext.getXID();
if (xid != null) {
//如果全局事务ID不为空,则绑定到statementProxy
statementProxy.getConnectionProxy().bind(xid);
}
//是否开启全局所绑定到statementProxy中
statementProxy.getConnectionProxy().setGlobalLockRequire(RootContext.requireGlobalLock());
//执行
return doExecute(args);
}
io.seata.rm.datasource.exec.AbstractDMLBaseExecutor#doExecute
@Override
public T doExecute(Object... args) throws Throwable {
//简单理解就是通过Statement拿到一个Connection,只是说这里拿到的一个seata的代理
AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
//如果连接对象的自动提交是true,进入executeAutoCommitTrue,否则进入executeAutoCommitFalse
//这边一般进入的executeAutoCommitTrue,因为一路跟下来还没有看到那么设置了连接的自动提交为false
//事务的肯定要在sql执行前将自动提交设置为false,事务完成以后手动提交
if (connectionProxy.getAutoCommit()) {
return executeAutoCommitTrue(args);
} else {
return executeAutoCommitFalse(args);
}
}
io.seata.rm.datasource.exec.AbstractDMLBaseExecutor#executeAutoCommitTrue
protected T executeAutoCommitTrue(Object[] args) throws Throwable {
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
try {
//设置事务的提交模式为手动
connectionProxy.setAutoCommit(false);
return new LockRetryPolicy(connectionProxy).execute(() -> {
//这里就是执行真正的业务sql,但是在执行真正的业务sql需要插入前后镜像信息
T result = executeAutoCommitFalse(args);
//本地事务的提交,到这里本地事务的业务sql都已经执行完成了,在mysql中,这里是开启的一个本地事务,事务符合隔离性的,所以也是可重复度
//那么其他事务是读取不到已经执行的sql结果的,所以在本地事务提交的时候,那么分布式事务就需要做点事情,做什么呢?首先executeAutoCommitFalse
//已经生成了本地事务的前后镜像,所以在本地事务提交前需要插入镜像到undolog表中,如果到时候分布式事务TC告诉需要回滚的时候要从undolog中恢复数据,
//就是要具有一个补偿机制,所以commit()方法里面就需要做的事情:
//1.调用TC服务器注册分支事务(xid 和分支id进行绑定,插入到tc服务器的表branch_table);
//2.将前后置镜像插入到undo log日志表;
//3.真正的提交本地事务;
connectionProxy.commit();
return result;
});
} catch (Exception e) {
// when exception occur in finally,this exception will lost, so just print it here
LOGGER.error("execute executeAutoCommitTrue error:{}", e.getMessage(), e);
if (!LockRetryPolicy.isLockRetryPolicyBranchRollbackOnConflict()) {
connectionProxy.getTargetConnection().rollback();
}
throw e;
} finally {
//重置数据库连接
connectionProxy.getContext().reset();
//设置自动提交为true,还原
connectionProxy.setAutoCommit(true);
}
}
executeAutoCommitFalse这个方法中真正的执行了我们的业务sql,比如insert操作,但是在执行这个sql的前后做了一些事情,比如前置镜像和后置镜像,因为seata是通过undulog日志来进行回滚的,如果说在整个事务过程中出现了需要回滚的,那么seata就会从undo 日志里面取出前置镜像进行恢复,如果说整个事务是成功的,那么会将这个undo日志删除。
io.seata.rm.datasource.exec.AbstractDMLBaseExecutor#executeAutoCommitFalse
protected T executeAutoCommitFalse(Object[] args) throws Exception {
if (!JdbcConstants.MYSQL.equalsIgnoreCase(getDbType()) && isMultiPk()) {
throw new NotSupportYetException("multi pk only support mysql!");
}
//查询前置镜像,查询前置镜像这里做的事情就是
//如果是插入:那么会查询这条插入语句之前的数据,肯定是空的嘛
//如果是更新:那么会将查询出更新前的数据作为前置镜像
//但是这里需要注意的是,seta的读隔离是如何做到的呢?就是在这里做的,如果说是一条更新的语句
//那么在beforeImage中会进行行锁,简单来说就是如果这个业务是更新order表的数据,那么
//前置镜像做的事情就是select tableCloumn from order where xxxxx for update
//通过for update进行读隔离,也就是说如果对于相同的数据来说,第一个事务已经开始了没有结束,
//那么第二个事务是被阻塞到for update上
//类似与我们之前没有分布式事务组件的时候自己通过for update来控制行锁,但是很容易死锁,需要合理控制粒度
TableRecords beforeImage = beforeImage();
//执行真正的业务逻辑sql
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
//执行后置镜像
TableRecords afterImage = afterImage(beforeImage);
//将前置镜像和后置镜像的信息封装到缓存sqlUndoItemsBuffer中,并且里面还生成了lockkey
prepareUndoLog(beforeImage, afterImage);
return result;
}
protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
if (beforeImage.getRows().isEmpty() && afterImage.getRows().isEmpty()) {
return;
}
ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
//这里生成lockkey,lock key的生成规则是 tableName+key1_key2,比如t_user:1_a,2_b
String lockKeys = buildLockKey(lockKeyRecords);
//将lock key放入缓存lockKeysBuffer,因为注册分支事务需要将lock key传过去
connectionProxy.appendLockKey(lockKeys);
//封装前后置镜像到SQLUndoLog
SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
//将前后置镜像放入缓存sqlUndoItemsBuffer
connectionProxy.appendUndoLog(sqlUndoLog);
}
在上面的executeAutoCommitTrue中有一行代码LockRetryPolicy(connectionProxy).execute就是处理重试机制的,也就是说在执行真正的executeAutoCommitFalse的时候是可以允许失败的,可以重试
io.seata.rm.datasource.exec.AbstractDMLBaseExecutor.LockRetryPolicy#execute
public <T> T execute(Callable<T> callable) throws Exception {
/**
* LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT表示是否开始调用重试机制
* 默认为true,就代表就是说在真正执行call逻辑的时候,如果失败了是否需要重试
*/
if (LOCK_RETRY_POLICY_BRANCH_ROLLBACK_ON_CONFLICT) {
//调用失败了可重试的方法
return doRetryOnLockConflict(callable);
} else {
//调用call的业务逻辑方法
return callable.call();
}
}
@Override
protected void onException(Exception e) throws Exception {
ConnectionContext context = connection.getContext();
//UndoItems can't use the Set collection class to prevent ABA
//清空undolog日志缓存,下次要重新生成
context.getUndoItems().clear();
//清空lockkey的缓存
context.getLockKeysBuffer().clear();
//连接的回滚
connection.getTargetConnection().rollback();
}
protected <T> T doRetryOnLockConflict(Callable<T> callable) throws Exception {
LockRetryController lockRetryController = new LockRetryController();
//这里是一个死循环,每次调用如果失败了可重试,因为异常是吃掉了的
while (true) {
try {
return callable.call();
} catch (LockConflictException lockConflict) {
//失败了清空缓存和连接回滚
onException(lockConflict);
//失败了睡眠10ms,然后重试,默认重试次数是30次,每次-1,如果次数用完了还是异常直接抛出异常
lockRetryController.sleep(lockConflict);
} catch (Exception e) {
onException(e);
throw e;
}
}
}
上面已经完成了它的逻辑,但是现在还有两件事没有做,就是分支事务的注册和本地事务的提交,而本地事务的提交之前会将上面生成的前置镜像和后置镜像保存到undo日志表里面,undo日志是为了回滚的时候进行补偿的,所以经过上面的过程还剩3个过程:
1、分支事务的远程注册;
2、undo log日志的记录;
3、本地事务的提交;
带着这三个问题,我们来分析下
io.seata.rm.datasource.ConnectionProxy#doCommit
private void doCommit() throws SQLException {
//判断是否是全局事务
if (context.inGlobalTransaction()) {
//全局事务的处理
processGlobalTransactionCommit();
} else if (context.isGlobalLockRequire()) {
processLocalCommitWithGlobalLocks();
} else {
//普通的事务
targetConnection.commit();
}
}
private void processGlobalTransactionCommit() throws SQLException {
try {
//1.注册分支事务,调用远程的TC服务器注册分支事务
register();
} catch (TransactionException e) {
//出现异常,判断是否是写隔离没有获取到行锁
recognizeLockKeyConflictException(e, context.buildLockKeys());
}
try {
//2.保存undo日志
UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
//3.真正提交本地事务
targetConnection.commit();
} catch (Throwable ex) {
LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
//事务失败了,向TC汇报事务状态
report(false);
throw new SQLException(ex);
}
if (IS_REPORT_SUCCESS_ENABLE) {
report(true);
}
context.reset();
}
io.seata.rm.datasource.ConnectionProxy#register
/**
* 注册分支事务
* @throws TransactionException
*/
private void register() throws TransactionException {
//判断 undo日志是否已经生成或者lock key是否已经生成,如果没有就不能进行分支事务的注册
if (!context.hasUndoLog() || context.getLockKeysBuffer().isEmpty()) {
return;
}
//resourceid=jdbc:mysql://localhost:3306/seata_order
//lock key:order_tbl:12(表名:主键值)
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
null, context.getXid(), null, context.buildLockKeys());
context.setBranchId(branchId);
}
客户端的操作已经完成了,并且客户端已经完成了本地事务主要执行过程,开始注册分支事务,注册分支事务是通过netty发送rcp到server端,server端的处理是在协调者对象中
io.seata.server.coordinator.DefaultCoordinator#doBranchRegister
/**
* 分支事务的注册
* @param request the request
* @param response the response
* @param rpcContext the rpc context
* @throws TransactionException
*/
@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()));
}
io.seata.server.coordinator.AbstractCore#branchRegister
public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid,
String applicationData, String lockKeys) throws TransactionException {
//注册分支事务,先通过全局事xid找到全局事务和分支事务
//1.先通过全局事务XID找到去global_table查询到全局事务对象
//2.通过查询出来的xid对象去查询branch_talbe是否有分支事务,最后将这两个事务对象封装到GlobalSession中
GlobalSession globalSession = assertGlobalSessionNotNull(xid, false);
return SessionHolder.lockAndExecute(globalSession, () -> {
//事务状态检查,全局事务状态必须是开启状态并且是激活的状态,否则直接抛出异常
globalSessionStatusCheck(globalSession);
//添加一个监听器
globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
//创建分支事务对象
BranchSession branchSession = SessionHelper.newBranchByGlobal(globalSession, branchType, resourceId,
applicationData, lockKeys, clientId);
//获取分支事务的全局锁,如果获取到锁则正常执行,如果获取锁失败,则报错
//这里获取锁就是往lock_table插入一条全局锁数据,如果插入成功则获取锁,如果插入失败,根据主键冲突则获取失败
//这里插入的lock_table就是为了获取锁,主键是row_key,生成规则是 resourceId^^^tablename^^^pk
//比如jdbc:mysql://localhost:3306/seata_account^^^account_tbl^^^12,所以对于同一条记录来说获取锁就很简单了
//如果两个线程都过来获取锁,只有最先记录到lock_table的能获取到全局锁,其他线程就会直接报错,
//所以简单理解就是:注册分支事务时,先获取全局锁(lock_table),根据lock_table的主键冲突来决定谁能获取到锁
//所以简单来说branchSessionLock做的事情如下:
//1.根据lockey来获取要收集的行锁记录;
//2.生成行锁需要的rowKey;
//3.添加收集到的行锁到lock_table;
//4.那么就代表了本次分支事务锁持有的记录有哪些,也就是是对这些记录进行写隔离。
branchSessionLock(globalSession, branchSession);
try {
//真正的注册分支事务
globalSession.addBranch(branchSession);
} catch (RuntimeException ex) {
branchSessionUnlock(branchSession);
throw new BranchTransactionException(FailedToAddBranch, String
.format("Failed to store branch xid = %s branchId = %s", globalSession.getXid(),
branchSession.getBranchId()), ex);
}
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Register branch successfully, xid = {}, branchId = {}, resourceId = {} ,lockKeys = {}",
globalSession.getXid(), branchSession.getBranchId(), resourceId, lockKeys);
}
//返回注册成功的分支事务ID,这个分支事务ID就是通过雪花算法生成的
return branchSession.getBranchId();
});
}
io.seata.server.session.BranchSession#lock
public boolean lock() throws TransactionException {
if (this.getBranchType().equals(BranchType.AT)) {
//如果是AT模式,尝试获取锁
return LockerManagerFactory.getLockManager().acquireLock(this);
}
return true;
}
io.seata.server.lock.AbstractLockManager#acquireLock
public boolean acquireLock(BranchSession branchSession) throws TransactionException {
if (branchSession == null) {
throw new IllegalArgumentException("branchSession can't be null for memory/file locker.");
}
String lockKey = branchSession.getLockKey();
if (StringUtils.isNullOrEmpty(lockKey)) {
// no lock
return true;
}
// get locks of branch
//收集所有的分支事务的行锁,返回的locks就是表示本次分支事务要执行多少条行锁的注册,简单来说就是要隔离多少条数据
List<RowLock> locks = collectRowLocks(branchSession);
if (CollectionUtils.isEmpty(locks)) {
// no lock
return true;
}
//添加行锁记录到lock_table
return getLocker(branchSession).acquireLock(locks);
}
io.seata.server.lock.AbstractLockManager#collectRowLocks
protected List<RowLock> collectRowLocks(String lockKey, String resourceId, String xid, Long transactionId,
Long branchID) {
List<RowLock> locks = new ArrayList<RowLock>();
//这里是收集行锁,什么意思呢?就是说在注册分支事务的时候要看下是否已经有线程已经注册了这个行锁,也就是说写隔离,别人
//正在做这个事务的时候,你不能做,因为两个事务做的事情是一样的,seata实现的是读未提交的隔离级别,就是说分支事务在
//在完成了本地事务的时候,别的线程是能够看到这个数据的,但是不能操作(读隔离),而这里做的 写隔离,写隔离的意思就是说
//相同的数据只能有一个事务来做,而其他的事务是不能做的,seata这里使用的是locak_table的行锁来做的
//lockKey是客户端传过来的,它的格式是:表名:主键1,主键2。。。
//“:”后面的主键是表示本次要操作的主键有几个,也就是要获取几个行锁,简单来说就是一个业务操作可能会修改多条数据
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;
}
io.seata.server.storage.db.lock.DataBaseLocker#acquireLock
public boolean acquireLock(List<RowLock> locks) {
if (CollectionUtils.isEmpty(locks)) {
// no lock
return true;
}
try {
//lockStore是要调用jdbc添加行锁记录到数据库,其中将RowLock转换成了LockDO
//这里面转换就要涉及到生成行锁lock_table的rowKey的生成
//rowKey的生成规则是:资源id(jdbcUrl)^^^表名^^^主键
return lockStore.acquireLock(convertToLockDO(locks));
} catch (StoreException e) {
throw e;
} catch (Exception t) {
LOGGER.error("AcquireLock error, locks:{}", CollectionUtils.toString(locks), t);
return false;
}
}