一、前言
前面对fescar的源码做了一个分析,核心的流程基本已经有一个比较清晰的认知。这篇文章主要是对fescar的一些设计原理和目前看到一些问题做一个解答。鉴于本人没有跟开发者沟通过,有部分结论可能不一定准确。
二、Q&A
Q1.fescar解决了什么问题?
引用fescar的wiki上的说明:
Fescar 是 阿里巴巴 开源的 分布式事务中间件,以 高效 并且对业务 0 侵入 的方式,解决 微服务 场景下面临的分布式事务问题。
- 解决分布式事务问题
- 对业务0入侵
Q2.fescar如何实现?核心组件有哪些?
业界上分布式一致的解决方案主要有两种。
- 两段提交(2PC)
-
paxos协议(包括raft协议等变体)
fescar 使用的是2PC来实现。
- Transaction Coordinator(TC): 全局和分支事务的状态的保持,驱动这个全局的提交和回滚.
- Transaction Manager(TM): 明确全局事务的范围:开始一个全局事务,提交或者回滚一个全局事务.
- Resource Manager(RM): 管理分支事务工作资源,告诉TC,注册这个分支事务和上报分支事务的状态,驱动分支事务的的提交和回滚。
Q3.如何实现对业务0入侵
两段提交最核心的问题是如何实业务的自动回滚,feacar的解决方案是通过在RM上扩展原有的mysql连接代理,实现监听业务的sql。从而生成对应的回滚sql。
注意:由于需要在db更新前后增加一个select for update来获取变更前后的行值,因此业务要自行考虑这种方式对自己的业务是否允许。
Q4.xid如何生成?xid又是如何传输的?
TM向TC声明的一个事务开始的时候就会生成xid,所以xid是由TC生成的,TC如何保证xid的唯一?详细参考:
xid分析
事实上fescar的官方的xid方案是不能支持HA的,需要自己做扩展。
上面已经介绍了TM和TC的xid的生成过程。那么xid是如何在TM和RM,以及RM和RM之间传递的呢?
其实很简单,在dubbo基础上增加一个TransactionPropagationFilter来实现。
public Result invoke(Invoker> invoker, Invocation invocation) throws RpcException {
String xid = RootContext.getXID();
String rpcXid = RpcContext.getContext().getAttachment(RootContext.KEY_XID);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("xid in RootContext[" + xid + "] xid in RpcContext[" + rpcXid + "]");
}
boolean bind = false;
if (xid != null) {
RpcContext.getContext().setAttachment(RootContext.KEY_XID, xid);
} else {
if (rpcXid != null) {
RootContext.bind(rpcXid);
bind = true;
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("bind[" + rpcXid + "] to RootContext");
}
}
}
RootContext获取xid成功,则会在请求协议的时候带上TX_XID这个属性。否则会读取请求体中的TX_XID写入RootContext。最终会让xid这个属性在请求的过程中一层层往下传递。
Q5.TC的global session和branch session如何存储?
源码详细分析:https://www.jianshu.com/p/051ff5640e1b
TC负责对整个事务的生命周期(状态)进行维护,因此global session和branch session是保存在TC上的。而fecar的官方例子session是保存在内存的,线上环境使用的时候需要自己扩展。
Q6.回滚操作如何保证数据的一致性
第一阶段提交到第二阶段提交中间数据被修改了怎么办?(ABA问题?)
这里的答案是用锁来解决。
fescar官方文档提到fescar在第一阶段提交的时候已经把锁释放掉了。其实feacar会在分支事务开始的时候会加锁,这个锁的状态是保存在TC。事实上官方说的释放锁只是把DB行锁释放,分支事务锁还是会持续保存。
public interface LockManager {
boolean acquireLock(BranchSession branchSession) throws TransactionException;
boolean isLockable(long transactionId, String resourceId, String lockKey) throws TransactionException;
}
这里存在3个问题。
- 这里缺少释放锁的操作,难道是做定时自动释放?难道不应该在第二段提交的时候把锁释放
- 官方提供的实现同样是基于内存的方案。需要做HA的自行进行改造
- 如果有一个分支事务的资源(db)有部分功能不接入fescar,会不会有问题?
对于问题3,我理解是fescar的使用场景的讨论,如果某个业务有一部分接入fescar,一部分不接入,则锁无法限制在第一次提交和回滚之间数据不会被修改的场景。因此我个人认为需要使用fescar必须将某个业务下的所有操作都要求统一接入到fescar,不管其中某个操作是否涉及到分布式事务。
Q8.TC的日志存储和查询
fescar使用自己定义的二进制格式进行日志保存,TransactionWriteStore,负责对事务session写入记录日志。目前只有保存到本地磁盘,线上使用需要一个统一的地方进行保存。而且查询日志的功能也需要自行开发。
Q9.TM如何决定何时需要回滚?
public Object execute(TransactionalExecutor business) throws TransactionalExecutor.ExecutionException {
// 1. get or create a transaction
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// 2. begin transaction
try {
tx.begin(business.timeout(), business.name());
} catch (TransactionException txe) {
throw new TransactionalExecutor.ExecutionException(tx, txe,
TransactionalExecutor.Code.BeginFailure);
}
Object rs = null;
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3. any business exception, rollback.
try {
tx.rollback();
// 3.1 Successfully rolled back
throw new TransactionalExecutor.ExecutionException(tx, TransactionalExecutor.Code.RollbackDone, ex);
} catch (TransactionException txe) {
// 3.2 Failed to rollback
throw new TransactionalExecutor.ExecutionException(tx, txe,
TransactionalExecutor.Code.RollbackFailure, ex);
}
}
// 4. everything is fine, commit.
try {
tx.commit();
} catch (TransactionException txe) {
// 4.1 Failed to commit
throw new TransactionalExecutor.ExecutionException(tx, txe,
TransactionalExecutor.Code.CommitFailure);
}
return rs;
}
当TM执行业务逻辑抛出异常就会触发回滚操作。这意味着如果TM把分支事务的异常catch了,或者分支事务返回一个失败的返回码,但是TM没有对其进行正常的处理,就不能正常地触发回滚行为。
详见:https://www.jianshu.com/p/6547aa46452b
Q10.支持的数据库
目前仅支持mysql,当然你也可以加入到其中对其他数据库进行支持。