Seata是阿里巴巴开源的分布式事务中间件,以高效并且对业务0侵入的方式,解决微服务场景下面临的分布式事务问题。
Seata中的几个组建
- Server,维护全局事务和各个分支的会话信息并保证全局事务之间的隔离性,协调分支事务提交或者回滚
- TM,全局事务的实例,定义全局事务的范围,事务的传递,并发起全局事务的提交与回滚
- RM,分支事务,维护整个链路中一环的提交与回滚,并上报分支事务状态
Seata设计思路
Seata的设计思路继承自二阶段提交,即全局事务的提交分成两个阶段,第一阶段,注册分支事务,执行SQL,上报分支事务状态,第二阶段,根据TC Commit/Rollback指令,提交/回滚本地事务。
Seata的设计与典型的二阶段提交有所不同,在第一阶段,本地SQL事务执行成功,就会直接commit,并不会推迟到第二阶段才提交,全局事务仍然是到第二阶段才进行提交。 本地事务在第一阶段,会根据事务执行前后的数据库快照,生成一个undolog。
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;
}
第二阶段,如果全局事务执行Commit操作,分支事务接收到指令,则无需再做任何本地提交操作,直接异步返回成功即可;如果全局事务执行Rollback操作,分支事务接收到指令,根据undolog的数据,对本地事务进行回滚操作。
默认情况下,Seata的本地事务隔离级别依据数据库配置决定,如读已提交,而全局事务的隔离级别为读未提交。如果业务上强制要求全局事务同样是读已提交,Seata已经对select for update
做了处理,使用该语法,会要求获取全局事务锁,利用全局事务锁来保证达到读已提交。
对Seata认识
有利的方面
- 对二阶段提交进行了优化,在一阶段就进行本地事务提交操作,减少了对资源的占用。
- 设计方案未以来数据库本身的支持,为更多的场景的接入提供可能性
- 全局事务锁的设计依赖主键,即行级锁,相比于mysql InnoDB的锁索引,锁的粒度更小
/**
* build lockKey
*
* @param rowsIncludingPK the records
* @return the string
*/
protected String buildLockKey(TableRecords rowsIncludingPK) {
if (rowsIncludingPK.size() == 0) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append(rowsIncludingPK.getTableMeta().getTableName());
sb.append(":");
int filedSequence = 0;
for (Field field : rowsIncludingPK.pkRows()) {
sb.append(field.getValue());
filedSequence++;
if (filedSequence < rowsIncludingPK.pkRows().size()) {
sb.append(",");
}
}
return sb.toString();
}
一些限制
- 默认情况下的全局事务隔离级别,可能会导致业务上的脏数据的产生。
- TM严格依赖分支的失败异常通知,对于某些依赖错误码通知的场景不友好。
- 全局事务锁在某些场景下仍然会限制本地分支资源的释放,如:事务B完成一阶段的前提是获取全局事务锁, 如果事务A和事务B有相同的行操作并且事务A二阶段暂未完成,则全局事务锁不会被释放,事务B可能在一阶段被挂起或者获取不到全局事务锁而产生异常。