什么是 TCC,TCC 是 Try,Confirm,Cancel 三个词语的缩写, 最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate's Opinion》的论文提出.
TCC 组成
TCC 分为 3 个阶段
Try 阶段: 尝试执行, 完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
Confirm 阶段: 如果所有分支的 Try 都成功了, 则走到 Confirm 阶段. Confirm 真正执行业务, 不作任何业务检查, 只使用 Try 阶段预留的业务资源
Cancel 阶段: 如果所有分支的 Try 有一个失败了, 则走到 Cancel 阶段. Cancel 释放 Try 阶段预留的业务资源.
TCC 分布式事务里, 有 3 个角色, 与经典的 XA 分布式事务一样:
AP / 应用程序, 发起全局事务, 定义全局事务包含哪些事务分支
RM / 资源管理器, 负责分支事务各项资源的管理
TM / 事务管理器, 负责协调全局事务的正确执行, 包括 Confirm,Cancel 的执行, 并处理网络异常
如果我们要进行一个类似于银行跨行转账的业务, 转出 (TransOut) 和转入 (TransIn) 分别在不同的微服务里, 一个成功完成的 TCC 事务典型的时序图如下:
TCC 网络异常
TCC 在整个全局事务的过程中, 可能发生各类网络异常情况, 典型的是空回滚, 幂等, 悬挂. 这里有一篇文章分布式事务的这些常见用法都有坑, 来看看正确姿势, 进行了详细的讲解
TCC 实践
对于前面的跨行转账操作, 最简单的做法是, 在 Try 阶段调整余额, 在 Cancel 阶段反向调整余额, Confirm 阶段则空操作. 这么做带来的问题是, 如果 A 扣款成功, 金额转入 B 失败, 最后回滚, 把 A 的余额调整为初始值. 在这个过程中如果 A 发现自己的余额被扣减了, 但是收款方 B 迟迟没有收到余额, 那么会对 A 造成困扰.
更好的做法是, Try 阶段冻结 A 转账的金额, Confirm 进行实际的扣款, Cancel 进行资金解冻, 这样用户在任何一个阶段, 看到的数据都是清晰明了的.
下面我们进行一个 TCC 事务的具体开发
我们的例子采用 Java 语言, 使用的分布式事务框架为 https://github.com/yedf/dtm, 它对分布式事务的支持非常优雅. 下面来详细讲解 TCC 的组成
我们首先创建用户余额表, 建表语句如下:
- create table if not exists dtm_busi.user_account(
- id int(11) PRIMARY KEY AUTO_INCREMENT,
- user_id int(11) UNIQUE,
- balance DECIMAL(10, 2) not null default '0',
- trading_balance DECIMAL(10, 2) not null default '0',
- create_time datetime DEFAULT now(),
- update_time datetime DEFAULT now(),
- key(create_time),
- key(update_time)
- );
表中, trading_balance 记录正在交易的金额.
我们先编写核心代码, 冻结 / 解冻资金操作, 会检查约束 balance+trading_balance>= 0, 如果约束不成立, 执行失败
- public void adjustTrading(Connection connection, TransReq transReq) throws Exception {
- String sql = "update dtm_busi.user_account set trading_balance=trading_balance+?"
- + "where user_id=? and trading_balance + ? + balance>= 0";
- PreparedStatement preparedStatement = null;
- try {
- preparedStatement = connection.prepareStatement(sql);
- preparedStatement.setInt(1, transReq.getAmount());
- preparedStatement.setInt(2, transReq.getUserId());
- preparedStatement.setInt(3, transReq.getAmount());
- if (preparedStatement.executeUpdate()> 0) {
- System.out.println("交易金额更新成功");
- } else {
- throw new FailureException("交易失败");
- }
- } finally {
- if (null != preparedStatement) {
- preparedStatement.close();
- }
- }
- }
然后是调整余额
- public void adjustBalance(Connection connection, TransReq transReq) throws SQLException {
- PreparedStatement preparedStatement = null;
- try {
- String sql = "update dtm_busi.user_account set trading_balance=trading_balance-?,balance=balance+? where user_id=?";
- preparedStatement = connection.prepareStatement(sql);
- preparedStatement.setInt(1, transReq.getAmount());
- preparedStatement.setInt(2, transReq.getAmount());
- preparedStatement.setInt(3, transReq.getUserId());
- if (preparedStatement.executeUpdate()> 0) {
- System.out.println("余额更新成功");
- }
- } finally {
- if (null != preparedStatement) {
- preparedStatement.close();
- }
- }
- }
下面我们来编写具体的 Try/Confirm/Cancel 的处理函数
- @RequestMapping("barrierTransOutTry")
- public Object TransOutTry(HttpServletRequest request) throws Exception {
- BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap());
- logger.info("barrierTransOutTry branchBarrier:{}", branchBarrier);
- TransReq transReq = extracted(request);
- Connection connection = dataSourceUtil.getConnecion();
- branchBarrier.call(connection, (barrier) -> {
- System.out.println("用户: +" + transReq.getUserId() + ", 转出" + Math.abs(transReq.getAmount()) + "元准备");
- this.adjustTrading(connection, transReq);
- });
- connection.close();
- return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT);
- }
- @RequestMapping("barrierTransOutConfirm")
- public Object TransOutConfirm(HttpServletRequest request) throws Exception {
- BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap());
- logger.info("barrierTransOutConfirm branchBarrier:{}", branchBarrier);
- Connection connection = dataSourceUtil.getConnecion();
- TransReq transReq = extracted(request);
- branchBarrier.call(connection, (barrier) -> {
- System.out.println("用户: +" + transReq.getUserId() + ", 转出" + Math.abs(transReq.getAmount()) + "元提交");
- adjustBalance(connection, transReq);
- });
- connection.close();
- return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT);
- }
- @RequestMapping("barrierTransOutCancel")
- public Object TransOutCancel(HttpServletRequest request) throws Exception {
- BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap());
- logger.info("barrierTransOutCancel branchBarrier:{}", branchBarrier);
- TransReq transReq = extracted(request);
- Connection connection = dataSourceUtil.getConnecion();
- branchBarrier.call(connection, (barrier) -> {
- System.out.println("用户: +" + transReq.getUserId() + ", 转出" + Math.abs(transReq.getAmount()) + "元回滚");
- this.adjustTrading(connection, transReq);
- });
- connection.close();
- return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT);
- }
- // TransIn 相关函数与 TransOut 类似, 这里省略
到此各个子事务的处理函数已经 OK 了, 然后是开启 TCC 事务, 进行分支调用
- @RequestMapping("tccBarrier")
- public String tccBarrier() {
- // 创建 dmt client
- DtmClient dtmClient = new DtmClient(ipPort);
- // 创建 tcc 事务
- try {
- dtmClient.tccGlobalTransaction(dtmClient.genGid(), TccTestController::tccBarrierTrans);
- } catch (Exception e) {
- log.error("tccGlobalTransaction error", e);
- return "fail";
- }
- return "success";
- }
- public static void tccBarrierTrans(Tcc tcc) throws Exception {
- // 用户 1 转出 30 元
- Response outResponse = tcc
- .callBranch(new TransReq(1, -30), svc + "/barrierTransOutTry", svc + "/barrierTransOutConfirm",
- svc + "/barrierTransOutCancel");
- log.info("outResponse:{}", outResponse);
- // 用户 2 转入 30 元
- Response inResponse = tcc
- .callBranch(new TransReq(2, 30), svc + "/barrierTransInTry", svc + "/barrierTransInConfirm",
- svc + "/barrierTransInCancel");
- log.info("inResponse:{}", inResponse);
- }
至此, 一个完整的 TCC 分布式事务编写完成.
如果您想要完整运行一个成功的示例, 那么按照 dtmcli-java-sample 项目的说明搭建好环境启动之后, 运行下面命令运行 tcc 的例子即可
curl http://localhost:8081/tccBarrier
TCC 的回滚
假如银行将金额准备转出用户 2 时, 发现用户 2 的账户异常, 返回失败, 会怎么样? 我们的例子中, 可用户余额为 10000, 发起一笔 100000 的转账会触发异常而失败:
curl http://localhost:8081/tccBarrier
这是事务失败交互的时序图
这个跟成功的 TCC 差别就在于, 当某个子事务返回失败后, 后续就回滚全局事务, 调用各个子事务的 Cancel 操作, 保证全局事务全部回滚.
小结
在这篇文章里, 我们介绍了 TCC 的理论知识, 也通过一个例子, 完整给出了编写一个 TCC 事务的过程, 涵盖了正常成功完成, 以及成功回滚的情况. 相信读者通过这边文章, 对 TCC 已经有了深入的理解.
关于分布式事务中需要处理的幂等, 悬挂, 空补偿, 请参考另一篇文章: 分布式事务的这些常见用法都有坑, 来看看正确姿势
关于分布式事务更多更全面的知识, 请参考分布式事务最经典的七种解决方案
文中使用的例子选自 yedf/dtmcli-java-sample. 使用的分布式事务管理器为 https://github.com/yedf/dtm, 支持多种事务模式: TCC,SAGA,XA, 事务消息 跨语言支持, 已支持 golang,python,PHP,Node.JS 等语言的客户端. 提供子事务屏障功能, 优雅解决幂等, 悬挂, 空补偿等问题.
阅读完此篇干货, 欢迎大家访问 https://github.com/yedf/dtm 项目, 给颗星星支持!
来源: https://segmentfault.com/a/1190000041030430