seata事务【提交】原理及源码分析

回顾

在本篇文章开始之前,我们回顾一下seata在AT模式下提交回滚的二个阶段(摘录官网)

一阶段

Seata通过其JDBC数据源代理对业务SQL进行解析,然后把业务数据在更新前后的数据镜像组织成回滚日志,利用本地事务的ACID特性,将业务数据的更新和回滚日志的写入在同一个本地事务中提交
1、解析SQL
2、查询前镜像
3、执行业务SQL
4、查询后镜像
5、生成undolog并插入表undo_log
注:第3步和第5步在同一个本地事务中

二阶段提交

收到TC协调器的提交请求,异步清理回滚日志

二阶段回滚

收到TC协调器的分支回滚请求
1、开启本地事务
2、通过XID和Branch ID查询undo_log表记录
3、解析rollback_info字段信息,比较前镜像、后镜像和当前记录值来确定是否回滚
4、生成回滚SQL并执行
5、提交本地事务

抛出疑问

在高并发场景下,第一阶段是如何保证前后镜像的数据不会被其它线程修改呢?如图:
在这里插入图片描述
说明:
1、A线程所在的服务接入seata,执行的业务SQL如下
update product set name = 'GTS' where name = 'TXC'
2、B线程所在的服务未接入seata,只是一个普通的业务操作
update product set version = '2015' where name = 'TXC'

梳理一下执行过程:
A线程业务SQL开始执行之前,首先通过解析的SQL查询前镜像

//前镜像为:1 | TXC | 2014
select id,name,version  where name = 'TXC'

在高并发场景下,A线程在执行业务SQL前,这时B线程修改了同一行记录的version字段

//修改后的值:1 | TXC | 2015
update product set version = '2015' where name = 'TXC'

A线程继续执行业务SQL

//修改后的值:1 | GTS | 2015
update product set name = 'GTS' where name = 'TXC'

A线程根据ID查询后镜像

//后镜像为:1 | GTS | 2015
select id,name,version from product where id = 1

整个过程执行完毕!这里假设A线程的下游服务业务异常,则最终的决议是全局回滚,那么A线程执行回滚的数据版本是前镜像,也就是1 TXC 2014,这将导致B线程执行的数据1 TXC 2015被“忽略”了?

这种情况会出现吗?答案是否,而且B线程会被阻塞,直到A线程的本地事务提交后才开始执行,那么seata是怎么处理的?

源码

第一阶段涉及的主要类图:
在这里插入图片描述

如何保证UndoLog和业务SQL在同一个事务中完成

父类AbstractDMLBaseExecuot主要提供了的事务开启、提交及回滚操作,内部使用了模板方法模式,暴露beforeImage()和afterImage()两个方法,由具体的子类完成实现

protected T executeAutoCommitTrue(Object[] args) throws Throwable {
    T result = null;
    AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
    LockRetryController lockRetryController = new LockRetryController();
    try {
    	//开启事务
        connectionProxy.setAutoCommit(false);
        while (true) {
            try {
            	//
                result = executeAutoCommitFalse(args);
                //提交事务
                connectionProxy.commit();
                break;
            } catch (LockConflictException lockConflict) {
            	//目标Connection回滚
                connectionProxy.getTargetConnection().rollback();
                lockRetryController.sleep(lockConflict);
            }
        }
    } catch (Exception e) {
        LOGGER.error("exception occur", e);
        throw e;
    } finally {
    	//还原事务提交状态
        connectionProxy.setAutoCommit(true);
    }
    return result;
}

其中executeAutoCommitFalse方法主要完成前后镜像的获取、执行业务SQL以及生成undo_log

protected T executeAutoCommitFalse(Object[] args) throws Throwable {
	TableRecords beforeImage = beforeImage();
	T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
	TableRecords afterImage = afterImage(beforeImage);
	prepareUndoLog(beforeImage, afterImage);
	return result;
}

最终事务的提交是由ConnectionProxy代理来完成的,在执行目标connection的commit之前完成对undo_log的插入操作

if (context.hasUndoLog()) {
	//插入undolog日志数据
	UndoLogManager.flushUndoLogs(this);
}
//本地事务提交
targetConnection.commit();

A线程查询某行的前镜像并且未释放本地锁之前,其它线程必须等待

seata提供了3种基于DML操作的执行器策略,分别是DeleteExecutor、UpdateExecutor、InsertExecutor

上面我们已经讲过beforeImage()和afterImage()方法由子类完成,这里以UpdateExecutor为例展开讨论

前镜像是根据业务SQL解析出查询条件,再通过Select语句查询出前镜像,代码中我们可以看到在SQL语句最后加上了"for update"关键字,也就是说在查询前镜像时已经对表数据加上了排他锁(表锁或行锁取决于条件是否是索引字段)

protected TableRecords beforeImage() throws SQLException {
	...
	StringBuffer selectSQLAppender = new StringBuffer("SELECT ");
	if (!tmeta.containsPK(updateColumns)) {
		selectSQLAppender.append(this.getColumnNameInSQL(tmeta.getPkName()) + ", ");
	}
	...
	selectSQLAppender.append(" FROM " + this.getFromTableInSQL());
	if (StringUtils.isNotBlank(whereCondition)) {
		selectSQLAppender.append(" WHERE " + whereCondition);
	}
	
	selectSQLAppender.append(" FOR UPDATE");
	String selectSQL = selectSQLAppender.toString();
	...
	return beforeImage;
}

后镜像的查询逻辑比较好理解,seata也充分考虑到性能问题,因为前后镜像的主键不会被修改,所以seata使用前镜像的主键来查询后镜像数据

 selectSQLAppender.append(" FROM " + this.getFromTableInSQL() + " WHERE " + this.buildWhereConditionByPKs(pkRows));

对于本篇开始提出的疑问,看到这里已经有了答案。但是我们必须注意另外一个问题,如果查询条件未使用索引字段,那么for update会锁住整张表,对系统性能有一定的影响,所以我们在设计表索引字段时,应该要考虑到这一点

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值