一、概要
阿里最近开源了分布式事务的解决方案fescar。
Fescar 是 阿里巴巴 开源的 分布式事务中间件,以 高效 并且对业务 0 侵入 的方式,解决 微服务 场景下面临的分布式事务问题。
具体介绍可以详见wiki地址
分布式事务一直是微服务的一个痛点。性能问题和业务的接入成本一直是困扰开发者的难题。阿里能够实现业务0入侵并且保证性能,不得不觉得很惊讶。
二、解决的概要思路
- Transaction Coordinator(TC): 全局和分支事务的状态的保持,驱动这个全局的提交和回滚.
- Transaction Manager(TM): 明确全局事务的范围:开始一个全局事务,提交或者回滚一个全局事务.
- Resource Manager(RM): 管理分支事务工作资源,告诉TC,注册这个分支事务和上报分支事务的状态,驱动分支事务的的提交和回滚。
三、回滚方案
接下来我们重点看下fescar是如何做到对代码无入侵的。
Phase1:
Fescar 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。
这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。
Phase2:
如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
简单来说,是通过生成自动生成回滚sql来实现自动回滚的。
三、疑问
解决方案好像很美好,但是事实上会不会有问题呢?
1.回滚sql生成的正确性如何保证
fescar的做法是类似mysql binlog的做法,把更新前后的每个字段都保存下来,用于生成回滚的sql。
2.即便回滚的sql能正确生成,如何保证一定能回滚成功?
3.并发的场景数据的一致性如何保证
前面提到的高性能的前提是不会对资源做锁,也就是在phase1的时候就已经释放锁了。那么在并发场景下会不会有数据不一致的情况?
四、验证
MySQLUndoUpdateExecutor.java
protected String buildUndoSQL() {
TableRecords beforeImage = sqlUndoLog.getBeforeImage();
List<Row> beforeImageRows = beforeImage.getRows();
if (beforeImageRows == null || beforeImageRows.size() == 0) {
throw new ShouldNeverHappenException("Invalid UNDO LOG"); // TODO
}
Row row = beforeImageRows.get(0);
StringBuffer mainSQL = new StringBuffer("UPDATE " + sqlUndoLog.getTableName() + " SET ");
StringBuffer where = new StringBuffer(" WHERE ");
boolean first = true;
for (Field field : row.getFields()) {
if (field.getKeyType() == KeyType.PrimaryKey) {
where.append(field.getName() + " = ?");
} else {
if (first) {
first = false;
} else {
mainSQL.append(", ");
}
mainSQL.append(field.getName() + " = ?");
}
}
return mainSQL.append(where).toString();
}
从代码可以看得出来,这里采用的是类似binlog的row方式,把更新前后的值都保存下来。然后在更新的时候尝试还原回去。
下面是我本地拿下来的一条undo_log的记录
mysql> select * from undo_log\G
*************************** 1. row ***************************
id: 165
branch_id: 201285990
xid: 172.19.5.94:8091:201285989
rollback_info: {"branchId":201285990,"sqlUndoLogs":[{
"afterImage":{"rows":[{"fields":[{"keyType":"PrimaryKey","name":"ID","type":4,"value":3},{"keyType":"NULL","name":"COUNT","type":4,"value":98}]}],"tableName":"storage_tbl"},
"beforeImage":{"rows":[{"fields":[{"keyType":"PrimaryKey","name":"ID","type":4,"value":3},{"keyType":"NULL","name":"COUNT","type":4,"value":100}]}],
"tableName":"storage_tbl"},
"sqlType":"UPDATE",
"tableName":"storage_tbl"}],
"xid":"172.19.5.94:8091:201285989"}
log_status: 0
log_created: 2019-01-15 21:55:37
log_modified: 2019-01-15 21:55:37
ext: NULL
最终回滚的sql会生成
update storage_tbl set count=100 where count=98 and id=3;
回到我们的第二个问题:
2.即便回滚的sql能正确生成,如何保证一定能回滚成功?
如果回滚的时候count字段已经变化了,能保证回滚成功吗?
我们来考虑一个场景:对于一个减库存的业务,如果事务回滚,就需要把库存+1来进行回滚。
- 但是由于我们希望能够做到阿里所说的高性能,在phase1提交成功后就可以马上开启第二个事务。当第二个事务成功完成phase2的提交后,第一个事务被通知需要回滚了。这时候必然会失败。
- 如果我们对于整个仓库在整个分布式事务完全提交之前加锁,性能又会大大降低。
由于这个模式是0入侵的,开发人员可能甚至在使用的过程中根本不会思考并发场景会导致的问题,直接理解成按本地事务的方式去实现,很有可能到线上才发现大问题。对于扣减的业务,回滚失败可能问题还不大,对于增加的业务,回滚失败很有可能就意味着公司资产的损失。
五、补充
在测试回滚的过程中发现一个比较诡异的地方,在测试demo的时候起了4个业务进程AccountServiceImpl,OrderServiceImpl,StorageServiceImpl,BusinessServiceImpl。
我在执行回滚日志的地方增加了一条日志。
AbstractUndoExecutor
undoPrepare(undoPST, undoValues, pkValue);
int affectRow = undoPST.executeUpdate();
if(affectRow>0) {
LOGGER.info("执行回滚语句成功...undoSQL:{}",undoSQL);
}else{
LOGGER.error("执行回滚语句失败...undoSQL:{}",undoSQL);
}
结果发现所有的回滚都在全在AccountServiceImpl里面回滚了。难道还要求分布式事务涉及的所有进程的DB权限都必须共享>-<?
六、总结
总体我认为fescar的想法还是很美好,但是在实际落地过程中能否真正地解决问题我认为还是要再三经过考证才行。本人水平有限,可能对fescar理解有误的地方,欢迎大家指正。
七、问题
最后提几个问题,方便我们后面带着问题往下一步步深入了解
- undo log是如何生成的?如何知道修改前后的值?
- tc和rm的通讯方式是怎样,txid如何生成?
- rm强依赖tc,tc如何实现高可用?
- undo log这种实现方式如何解决ABA的问题?
- fescar能够解决哪些业务场景的一致性问题?