一、前言
严格遵守ACID的分布式事务我们称为刚性事务,而遵循BASE理论(基本可用:在故障出现时保证核心功能可用,软状态:允许中间状态出现,最终一致性:不要求分布式事务打成中时间点数据都是一致性的,但是保证达到某个时间点后,数据就处于了一致性了)的事务我们称为柔性事务,其中TCC编程模式就属于柔性事务,本文我们来阐述其理论。
二、TCC编程模式
TCC编程模式本质上也是一种二阶段协议,不同在于TCC编程模式需要与具体业务耦合,下面首先看下TCC编程模式步骤:
- 所有事务参与方都需要实现try,confirm,cancle接口。
- 事务发起方向事务协调器发起事务请求,事务协调器调用所有事务参与者的try方法完成资源的预留,这时候并没有真正执行业务,而是为后面具体要执行的业务预留资源,这里完成了一阶段。(状态机加入)
-
如果事务协调器发现有参与者的try方法预留资源时候发现资源不够,则调用参与方的cancle方法回滚预留的资源,需要注意cancle方法需要实现业务幂等,因为有可能调用失败(比如网络原因参与者接受到了请求,但是由于网络原因事务协调器没有接受到回执)会重试。(补偿机制)
-
如果事务协调器发现所有参与者的try方法返回都OK,则事务协调器调用所有参与者的confirm方法,不做资源检查,直接进行具体的业务操作。
- 如果协调器发现所有参与者的confirm方法都OK了,则分布式事务结束。
- 如果协调器发现有些参与者的confirm方法失败了,或者由于网络原因没有收到回执,则协调器会进行重试。这里如果重试一定次数后还是失败,会怎么样那?常见的是做事务补偿。
蚂蚁金服基于TCC实现了XTS(云上叫DTS),目前在蚂蚁金服云上有对外输出,这里我们来结合其提供的一个例子来具体理解TCC的含义,以下引入蚂蚁金服云实例:
“首先我们假想这样一种场景:转账服务,从银行 A 某个账户转 100 元钱到银行 B 的某个账户,银行 A 和银行 B 可以认为是两个单独的系统,也就是两套单独的数据库。
我们将账户系统简化成只有账户和余额 2 个字段,并且为了适应 DTS 的两阶段设计要求,业务上又增加了一个冻结金额(冻结金额是指在一笔转账期间,在一阶段的时候使用该字段临时存储转账金额,该转账额度不能被使用,只有等这笔分布式事务全部提交成功时,才会真正的计入可用余额)。按这样的设计,用户的可用余额等于账户余额减去冻结金额。这点是理解参与者设计的关键,也是 DTS 保证最终一致的业务约束。”
在try阶段并没有对银行A和B数据库中的余额字段做操作,而是对冻结金额做的操作,对应A银行预留资源操作是对冻结金额加上100元,这时候A银行账号上可用钱为余额字段-冻结金额;对应B银行的操作是对冻结金额上减去100,这时候B银行账号上可用的钱为余额字段-冻结金额。
如果事务协调器调用银行A和银行B的try方法有一个失败了(比如银行A的账户余额不够了),则调用cancle进行回滚操作(具体是对冻结金额做反向操作)。如果调用try方法都OK了,则进入confirm阶段,confirm阶段则不做资源检查,直接做业务操作,对应银行A要在账户余额减去100,然后冻金额减去100;对应银行B要对账户余额字段加上100,然后冻结金额加上100。
最关心的,如果confirm阶段如果有一个参与者失败了,该如何处理,其实上面操作都是xts-client做的,还有一个xts-server专门做事务补偿的。
三、总结
TCC是对二阶段的一个改进,try阶段通过预留资源的方式避免了同步阻塞资源的情况,但是TCC编程需要业务自己实现try,confirm,cancle方法,对业务入侵太大,实现起来也比较复杂。
四,实战代码
最后我写了一个TCC变种分布式事务模板=> TCCJ可供参考: 我这里,我这里的judge模块是对try模块产生结果的审核, 审核不通过执行回滚操作.
/**
* Description: 分布式事务执行模板
* <p>
* 1. 先执行各个服务块业务
* 2. 执行结束通过confirm判定执行结果, 如果失败则进行取消(回滚操作)
* 3. 如果执行服务模块发生异常,则判定后进行
* </p>
* User: zhouzhou
* Date: 2018-08-27
* Time: 10:27
*/
public class TccTemplate {
private static final Logger logger = LoggerFactory.getLogger(TccCallBack.class);
/**
* 分布式事务模板
* @param tccCallBack 分布式事务执行回调
* @param method 当前方法名(封装参数, 可方便捞取数据)
*/
public static <T> TccResult process(TccCallBack tccCallBack, String method, T t) {
// 返回一个消息用于
TccResult tccResult = new TccResult();
String msg = "";
try {
// 执行主业务
tccCallBack.tryExecute();
// 进行确认执行结果,如果结果是false,则执行回滚操作
boolean judge = tccCallBack.judge();
if (judge) {
tccResult.setStatus(true);
msg = String.format("分布式事务{%s}执行成功", method);
logger.info(msg);
// 执行确认操作
tccCallBack.confirm();
} else {
tccResult.setStatus(false);
msg = String.format("分布式事务{%s}执行失败,进行回滚操作", method);
logger.warn(msg);
tccCallBack.cancel();
}
} catch (Exception e) {
// 主流程发生异常, 则直接执行回滚操作
tccResult.setStatus(false);
msg = String.format("分布式事务{%s}执行发生异常{%s},进行回滚操作", method,e.getMessage());
logger.warn(String.format("分布式事务{%s}执行发生异常,进行回滚操作", method), e);
tccCallBack.cancel();
}finally {
// 返回结果Result
tccResult.setMsg(msg);
return tccResult;
}
}
}
当然你需要编写回调接口:
/**
* Description: 基于TCC变成模式的分布式事务回调
* User: zhouzhou
* Date: 2018-08-27
* Time: 10:20
*/
public interface TccCallBack {
/**
* 执行主要分布式业务操作
*/
void tryExecute();
/**
* 确认分布式业务操作最终结果,
* 如果返回true,则不执行cancel,返回false则执行cancel
*/
boolean judge();
/**
* 取消操作
*/
void cancel();
/**
* 确认操作
*/
void confirm();
}