springcloud+eureka整合seata-tcc模式

时间:2022-07-24
本文章向大家介绍springcloud+eureka整合seata-tcc模式,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

分布式事务中的tcc模式理论介绍的文章非常多,但是网上找到一个代码实现的demo很难,包括阿里的seata官方示例都没有TCC模式的具体实现。今天我们来看一下微服务环境下使用seata TCC模式解决分布式事务的场景,同时提供一个详细的实现。

本文使用的实验环境跟上篇《springcloud+eureka整合分布式事务中间件seata》类似,都是订单、库存和账户3个微服务,全局事务从订单发起:

springboot:2.1.6.RELEASE

orm框架:jdbc

数据库:mysql

数据库连接池:HikariCP

seata server:1.3.0

springcloud:Greenwich.SR2

注:因为微服务采用跟上篇介绍的一样,所以环境搭建就不再重复写了,大家实验过程中有问题的可以参考上篇文章,或者号内留言。

理论回顾

前面我讲了2篇关于seata的文章,都是使用了seata的AT模式,seata AT模式依赖的还是单个服务或单个数据源自己的事务控制(分支事务),采用的是wal的思想,提交事务的时候同时记录undolog,如果全局事务成功,则删除undolog,如果失败,则使用undolog的数据回滚分支事务,最后删除undolog。

TCC模式的特点是不再依赖于undolog,采用2阶段提交的方式,第一阶段使用prepare尝试事务提交,第二阶段使用commit或者rollback让事务提交或者回滚。官方的示例图如下:

从示例图可以看到,TM对全局事务进行管理,RM对分支事务进行管理,而TC管理着全局事务和分支事务的状态,RM需要注册到TC。TM发起全局事务后,调用TM(每个分支事务)的prepare进行try操作,成功后TC会调用RM的commit方法,失败后TC会调用分支事务的rollback方法。

使用spring的事务管理进行尝试

我试图使用spring的编程式事务来实现2阶段提交,我们先看一下prepare方法,代码如下:

public boolean decrease(String xid, Long userId, BigDecimal payAmount) {
    LOGGER.info("------->尝试扣减账户开始account");
    //尝试扣减账户金额,事务不提交

    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    TransactionStatus status = transactionManager.getTransaction(def);
    try {
        accountDao.decrease(userId,payAmount);
        //此处不提交事务
        transactionStatusMap.put(xid, status);
    } catch (Exception e) {
        LOGGER.error("decrease parepare failure:", e);
        return false;
    }
    LOGGER.info("------->尝试扣减账户结束account");
    return true;
}

这样我在这个方法中不提交事务,等到请求调用commit方法时,再提交事务,commit方法代码如下:

public boolean commit(String xid){
    LOGGER.info("commit, xid:{}", xid);
    if (null == transactionStatusMap.get(xid)){
        return true;
    }
    transactionManager.commit(transactionStatusMap.get(xid));
    transactionStatusMap.remove(xid);
    return true;
}

但是spring是不允许这么做的,第二次http请求到来时,线程跟第一次请求的线程不一样了,所以抛出下面异常:

java.lang.IllegalStateException: No value for key [HikariDataSource (HikariPool-1)] bound to thread [http-nio-8181-exec-2]
 at org.springframework.transaction.support.TransactionSynchronizationManager.unbindResource(TransactionSynchronizationManager.java:213) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
 at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCleanupAfterCompletion(DataSourceTransactionManager.java:367) ~[spring-jdbc-5.1.8.RELEASE.jar:5.1.8.RELEASE]
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.cleanupAfterCompletion(AbstractPlatformTransactionManager.java:1007) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:793) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
 at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
 at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:534) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]
 at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:305) ~[spring-tx-5.1.8.RELEASE.jar:5.1.8.RELEASE]

这个异常源码如下:

//TransactionSynchronizationManager类
public static Object unbindResource(Object key) throws IllegalStateException {
  Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
  Object value = doUnbindResource(actualKey);
  if (value == null) {
    throw new IllegalStateException(
        "No value for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
  }
  return value;
}

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
private static Object doUnbindResource(Object actualKey) {
  Map<Object, Object> map = resources.get();//resources是ThreadLocal变量,所以第二个线程不可能取到第一个线程绑定的值
  if (map == null) {
    return null;//此处直接返回null
  }
  Object value = map.remove(actualKey);
  // Remove entire ThreadLocal if empty...
  if (map.isEmpty()) {
    resources.remove();
  }
  // Transparently suppress a ResourceHolder that was marked as void...
  if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
    value = null;
  }
  if (value != null && logger.isTraceEnabled()) {
    logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" +
        Thread.currentThread().getName() + "]");
  }
  return value;
}

使用jdbc进行尝试

整个项目的sql语句跟上篇文章中基本一样,只是少了undo_log表:

#########################seata_order库
use database seata_order;
CREATE TABLE `orders` (
  `id` mediumint(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `product_id` int(11) DEFAULT NULL,
  `COUNT` int(11) DEFAULT NULL COMMENT '数量',
  `pay_amount` decimal(10,2) DEFAULT NULL,
  `status` varchar(100) DEFAULT NULL,
  `add_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `last_update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

#########################seata_pay库
use database seata_pay;
DROP TABLE account;
CREATE TABLE `account` (
  `id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
  `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
  `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
  `balance` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度',
  `last_update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_pay`.`account` (`id`, `user_id`, `total`, `used`, `balance`) VALUES ('1', '1', '1000', '0', '100');

#########################seata_storage库
use database seata_storage;
CREATE TABLE `storage` (
  `id` BIGINT(11) NOT NULL AUTO_INCREMENT,
  `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
  `total` INT(11) DEFAULT NULL COMMENT '总库存',
  `used` INT(11) DEFAULT NULL COMMENT '已用库存',
  `residue` INT(11) DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');

回顾一下实验环境的架构图:

可以看到,order-server既是一个RM,也是一个TM,因为全局事务从这里发起。

这里重点有几个地方说明一下:

1.全局事务从订单服务发起,OrderServiceImpl类create方法,代码如下:

@GlobalTransactional
public boolean create(Order order) {
    String xid = RootContext.getXID();
    LOGGER.info("------->交易开始");
    BusinessActionContext actionContext = new BusinessActionContext();
    actionContext.setXid(xid);
    boolean result = orderSaveImpl.saveOrder(actionContext, order);//订单服务prepare
    if(!result){
        throw new RuntimeException("保存订单失败");
    }
    //远程方法 扣减库存
    LOGGER.info("------->扣减库存开始storage中");
    result = storageApi.decrease(actionContext, order.getProductId(), order.getCount());//库存服务prepare
    if(!result){
        throw new RuntimeException("扣减库存失败");
    }
    LOGGER.info("------->扣减库存结束storage中");
    //远程方法 扣减账户余额
    LOGGER.info("------->扣减账户开始account中");
    result = accountApi.prepare(actionContext, order.getUserId(),order.getPayAmount());//账户服务prepare
    LOGGER.info("------->扣减账户结束account中" + result);
    LOGGER.info("------->交易结束");
    return true;
}

可以看到,全局事务发起的地方需要加@GlobalTransactional注解,这个事务首先获取了全局事务id,也就是xid,然后分别调了3个服务的prepare方法,只要有一个服务prepare返回失败,则抛出异常。

2.两阶段提交,我们以账户服务为例,接口定义如下:

@FeignClient(value = "account-server")
@LocalTCC
public interface AccountApi {
    /**
     * 扣减账户余额
     * @param actionContext save xid
     * @param userId 用户id
     * @param money 金额
     * @return
     */
    @TwoPhaseBusinessAction(name = "accountApi", commitMethod = "commit", rollbackMethod = "rollback")
    @RequestMapping("/account/decrease")
    boolean prepare(@RequestBody BusinessActionContext actionContext, @RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);

    /**
     * Commit boolean.
     *
     * @param actionContext save xid
     * @return the boolean
     */
    @RequestMapping("/account/commit")
    boolean commit(@RequestBody BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext save xid
     * @return the boolean
     */
    @RequestMapping("/account/rollback")
    boolean rollback(@RequestBody BusinessActionContext actionContext);
}

整个接口的注解有2个,一个是FeignClient,因为服务间采用Feign进行通信,不多说明。第二个就是@LocalTCC,这个就标注了这是一个TCC的分支事务接口类,里面定义了TCC要求的3个方法。尤其强调的是,prepare方法上面要加注解@TwoPhaseBusinessAction,注解里面要指定提交和回滚的方法。

这个接口作为feign客户端,请求发送到了账户服务,账户服务接到对应的请求后分别进行处理。

3.账户服务中controller接到请求后,调用对应的service进行处理,代码如下:

@Service("accountServiceImpl")
public class AccountServiceImpl implements AccountService{

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);

    private Map<String, Statement> statementMap = new ConcurrentHashMap<>(100);
    private Map<String, Connection> connectionMap = new ConcurrentHashMap<>(100);

    @Resource
    private DataSource hikariDataSource;

    @Override
    public boolean decrease(String xid, Long userId, BigDecimal payAmount) {
        LOGGER.info("commit, xid:{}", xid);
        LOGGER.info("------->尝试扣减账户开始account");
        //模拟超时异常,全局事务回滚
        /*try {
            Thread.sleep(30*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/

        try {
            //尝试扣减账户金额,事务不提交
            Connection connection = hikariDataSource.getConnection();
            connection.setAutoCommit(false);
            String sql = "UPDATE account SET balance = balance - ?,used = used + ? where user_id = ?";
            PreparedStatement stmt = connection.prepareStatement(sql);
            stmt.setBigDecimal(1, payAmount);
            stmt.setBigDecimal(2, payAmount);
            stmt.setLong(3, userId);
            stmt.executeUpdate();
            statementMap.put(xid, stmt);
            connectionMap.put(xid, connection);
        } catch (Exception e) {
            LOGGER.error("decrease parepare failure:", e);
            return false;
        }

        LOGGER.info("------->尝试扣减账户结束account");

        return true;

    }

    public boolean commit(String xid){
        LOGGER.info("扣减账户金额, commit, xid:{}", xid);
        PreparedStatement statement = (PreparedStatement) statementMap.get(xid);
        Connection connection = connectionMap.get(xid);
        try {
            if (null != connection){
                connection.commit();
            }
        } catch (SQLException e) {
            LOGGER.error("扣减账户失败:", e);
            return false;
        }finally {
            try {
                statementMap.remove(xid);
                connectionMap.remove(xid);
                if (null != statement){
                    statement.close();
                }
                if (null != connection){
                    connection.close();
                }
            } catch (SQLException e) {
                LOGGER.error("扣减账户提交事务后关闭连接池失败:", e);
            }
        }
        return true;
    }

    public boolean rollback(String xid){
        LOGGER.info("扣减账户金额, rollback, xid:{}", xid);
        PreparedStatement statement = (PreparedStatement) statementMap.get(xid);
        Connection connection = connectionMap.get(xid);
        try {
            connection.rollback();
        } catch (SQLException e) {
            return false;
        }finally {
            try {
                if (null != statement){
                    statement.close();
                }
                if (null != connection){
                    connection.close();
                }
                statementMap.remove(xid);
                connectionMap.remove(xid);
            } catch (SQLException e) {
                LOGGER.error("扣减账户回滚事务后关闭连接池失败:", e);
            }
        }
        return true;
    }
}

从上面的代码中我们可以看到,我们使用了jdbc进行了事务管理,prepare方法缓存了Statement和Connection,commit和rollback方法进行事务的提交和回滚,然后释放连接。

实验结果

启动eureka server,seata server,然后启动上面3个服务。

运行之前,我们先看一下数据库的数据,seata_order库中orders表没有数据,seata_pay库中account表和seata_storage库中storage表数据如下图:

1.模拟commit事务

服务启动后,我们用postman发一个post请求,请求url:http://localhost:8180/order/create,请求参数:

{
  "userId":1,
  "productId":1,
  "count":1,
  "money":1,
  "payAmount":50
}

请求成功后,我们查看上面3张表的数据,如下图:

可以看到,数据已经提交成功了。这时我们查看一下order-server日志:

2020-08-23 10:13:00.187  INFO 638408 --- [nio-8180-exec-1] io.seata.tm.TransactionManagerHolder     : TransactionManager Singleton io.seata.tm.DefaultTransactionManager@2501a5fc
2020-08-23 10:13:00.207  INFO 638408 --- [nio-8180-exec-1] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [192.168.59.132:8091:41466478607220736]
2020-08-23 10:13:00.212  INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl  : ------->交易开始
2020-08-23 10:13:00.232  INFO 638408 --- [nio-8180-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-08-23 10:13:00.431  INFO 638408 --- [nio-8180-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-08-23 10:13:00.445  INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl  : ------->扣减库存开始storage中
2020-08-23 10:13:00.645  INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: storage-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:00.899  INFO 638408 --- [nio-8180-exec-1] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-storage-server
2020-08-23 10:13:00.900  INFO 638408 --- [nio-8180-exec-1] c.netflix.loadbalancer.BaseLoadBalancer  : Client: storage-server instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=storage-server,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2020-08-23 10:13:00.906  INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2020-08-23 10:13:00.938  INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: storage-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:00.941  INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client storage-server initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=storage-server,current list of Servers=[10.192.86.60:8182],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone;  Instance count:1;  Active connections count: 0;  Circuit breaker tripped count: 0;  Active connections per server: 0.0;]
},Server stats: [[Server:10.192.86.60:8182;  Zone:defaultZone;  Total Requests:0;  Successive connection failure:0;  Total blackout seconds:0;  Last connection made:Thu Jan 01 08:00:00 CST 1970;  First connection made: Thu Jan 01 08:00:00 CST 1970;  Active Connections:0;  total failure count in last (1000) msecs:0;  average resp time:0.0;  90 percentile resp time:0.0;  95 percentile resp time:0.0;  min resp time:0.0;  max resp time:0.0;  stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@50d8d3a7
2020-08-23 10:13:01.165  INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl  : ------->扣减库存结束storage中
2020-08-23 10:13:01.165  INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl  : ------->扣减账户开始account中
2020-08-23 10:13:01.211  INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: account-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:01.212  INFO 638408 --- [nio-8180-exec-1] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-account-server
2020-08-23 10:13:01.212  INFO 638408 --- [nio-8180-exec-1] c.netflix.loadbalancer.BaseLoadBalancer  : Client: account-server instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=account-server,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2020-08-23 10:13:01.213  INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2020-08-23 10:13:01.214  INFO 638408 --- [nio-8180-exec-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: account-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:01.215  INFO 638408 --- [nio-8180-exec-1] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client account-server initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=account-server,current list of Servers=[10.192.86.60:8181],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone;  Instance count:1;  Active connections count: 0;  Circuit breaker tripped count: 0;  Active connections per server: 0.0;]
},Server stats: [[Server:10.192.86.60:8181;  Zone:defaultZone;  Total Requests:0;  Successive connection failure:0;  Total blackout seconds:0;  Last connection made:Thu Jan 01 08:00:00 CST 1970;  First connection made: Thu Jan 01 08:00:00 CST 1970;  Active Connections:0;  total failure count in last (1000) msecs:0;  average resp time:0.0;  90 percentile resp time:0.0;  95 percentile resp time:0.0;  min resp time:0.0;  max resp time:0.0;  stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@7145f1b9
2020-08-23 10:13:01.640  INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl  : ------->扣减账户结束account中true
2020-08-23 10:13:01.641  INFO 638408 --- [nio-8180-exec-1] i.seata.sample.service.OrderServiceImpl  : ------->交易结束
2020-08-23 10:13:01.648  INFO 638408 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=192.168.59.132:8091:41466478607220736,branchId=41466478707884032,branchType=TCC,resourceId=orderApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580213,"host-name":"10.192.254.57","sys::prepare":"saveOrder","actionName":"orderApi"}}
2020-08-23 10:13:01.651  INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.59.132:8091:41466478607220736 41466478707884032 orderApi {"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580213,"host-name":"10.192.254.57","sys::prepare":"saveOrder","actionName":"orderApi"}}
2020-08-23 10:13:01.652  INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.sample.service.OrderSaveImpl    : 保存订单, commit, xid:192.168.59.132:8091:41466478607220736
2020-08-23 10:13:01.657  INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractResourceManager      : TCC resource commit result : true, xid: 192.168.59.132:8091:41466478607220736, branchId: 41466478707884032, resourceId: orderApi
2020-08-23 10:13:01.659  INFO 638408 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2020-08-23 10:13:01.664  INFO 638408 --- [ch_RMROLE_1_2_8] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=192.168.59.132:8091:41466478607220736,branchId=41466479647408128,branchType=TCC,resourceId=storageApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580446,"host-name":"10.192.254.57","sys::prepare":"decrease","actionName":"storageApi"}}
2020-08-23 10:13:01.664  INFO 638408 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.59.132:8091:41466478607220736 41466479647408128 storageApi {"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321580446,"host-name":"10.192.254.57","sys::prepare":"decrease","actionName":"storageApi"}}
2020-08-23 10:13:01.677  INFO 638408 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractResourceManager      : TCC resource commit result : true, xid: 192.168.59.132:8091:41466478607220736, branchId: 41466479647408128, resourceId: storageApi
2020-08-23 10:13:01.677  INFO 638408 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2020-08-23 10:13:01.680  INFO 638408 --- [ch_RMROLE_1_3_8] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=192.168.59.132:8091:41466478607220736,branchId=41466482671501312,branchType=TCC,resourceId=accountApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321581166,"host-name":"10.192.254.57","sys::prepare":"prepare","actionName":"accountApi"}}
2020-08-23 10:13:01.680  INFO 638408 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler            : Branch committing: 192.168.59.132:8091:41466478607220736 41466482671501312 accountApi {"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598321581166,"host-name":"10.192.254.57","sys::prepare":"prepare","actionName":"accountApi"}}
2020-08-23 10:13:01.695  INFO 638408 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractResourceManager      : TCC resource commit result : true, xid: 192.168.59.132:8091:41466478607220736, branchId: 41466482671501312, resourceId: accountApi
2020-08-23 10:13:01.695  INFO 638408 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2020-08-23 10:13:01.702  INFO 638408 --- [nio-8180-exec-1] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.59.132:8091:41466478607220736] commit status: Committed
2020-08-23 10:13:01.911  INFO 638408 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty  : Flipping property: storage-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-08-23 10:13:02.215  INFO 638408 --- [erListUpdater-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: account-server.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647

仔细查看这段日志,我们发现有3个2阶段提交成功(PhaseTwo_Committed),resourceId分别是orderApi、storageApi和accountApi,而在库存服务和账户服务也分别有相关日志的打印,如下:

2020-08-23 10:13:01.123  INFO 645052 --- [nio-8182-exec-3] i.s.sample.service.StorageServiceImpl    : ------->扣减库存prepare开始
2020-08-23 10:13:01.135  INFO 645052 --- [nio-8182-exec-3] i.s.sample.service.StorageServiceImpl    : ------->扣减库存prepare结束
2020-08-23 10:13:01.150  WARN 645052 --- [nio-8182-exec-3] c.a.c.seata.web.SeataHandlerInterceptor  : xid in change during RPC from 192.168.59.132:8091:41466478607220736 to null
2020-08-23 10:13:01.672  INFO 645052 --- [nio-8182-exec-4] i.s.sample.service.StorageServiceImpl    : 扣减库存金额, commit, xid:192.168.59.132:8091:41466478607220736
2020-08-23 10:13:01.358  INFO 643544 --- [nio-8181-exec-1] i.s.sample.service.AccountServiceImpl    : ------->尝试扣减账户开始account
2020-08-23 10:13:01.358  INFO 643544 --- [nio-8181-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-08-23 10:13:01.606  INFO 643544 --- [nio-8181-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-08-23 10:13:01.623  INFO 643544 --- [nio-8181-exec-1] i.s.sample.service.AccountServiceImpl    : ------->尝试扣减账户结束account
2020-08-23 10:13:01.638  WARN 643544 --- [nio-8181-exec-1] c.a.c.seata.web.SeataHandlerInterceptor  : xid in change during RPC from 192.168.59.132:8091:41466478607220736 to null
2020-08-23 10:13:01.686  INFO 643544 --- [nio-8181-exec-2] i.s.sample.service.AccountServiceImpl    : 扣减账户金额, commit, xid:192.168.59.132:8091:41466478607220736

2.模拟rollback事务

修改OrderServiceImpl中create方法,最后一行代码改为如下:

throw new RuntimeException("调用2阶段提交的rollback方法");
//return true

重启服务后,我们再次模拟发送上面的post请求,参数不变,这时查看order-server的日志如下:

前面的日志都一样,我们看一下回滚日志:

2020-08-23 10:54:54.209  INFO 652376 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=192.168.59.132:8091:41477018662486016,branchId=41477022726766592,branchType=TCC,resourceId=accountApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598324094118,"host-name":"10.192.254.57","sys::prepare":"prepare","actionName":"accountApi"}}
2020-08-23 10:54:54.212  INFO 652376 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.59.132:8091:41477018662486016 41477022726766592 accountApi
2020-08-23 10:54:54.247  INFO 652376 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractResourceManager      : TCC resource rollback result : true, xid: 192.168.59.132:8091:41477018662486016, branchId: 41477022726766592, resourceId: accountApi
2020-08-23 10:54:54.249  INFO 652376 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-08-23 10:54:54.258  INFO 652376 --- [ch_RMROLE_1_2_8] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=192.168.59.132:8091:41477018662486016,branchId=41477020038217728,branchType=TCC,resourceId=storageApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598324093476,"host-name":"10.192.254.57","sys::prepare":"decrease","actionName":"storageApi"}}
2020-08-23 10:54:54.258  INFO 652376 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.59.132:8091:41477018662486016 41477020038217728 storageApi
2020-08-23 10:54:54.301  INFO 652376 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractResourceManager      : TCC resource rollback result : true, xid: 192.168.59.132:8091:41477018662486016, branchId: 41477020038217728, resourceId: storageApi
2020-08-23 10:54:54.301  INFO 652376 --- [ch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-08-23 10:54:54.306  INFO 652376 --- [ch_RMROLE_1_3_8] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=192.168.59.132:8091:41477018662486016,branchId=41477018775732224,branchType=TCC,resourceId=orderApi,applicationData={"actionContext":{"sys::rollback":"rollback","sys::commit":"commit","action-start-time":1598324093164,"host-name":"10.192.254.57","sys::prepare":"saveOrder","actionName":"orderApi"}}
2020-08-23 10:54:54.306  INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.59.132:8091:41477018662486016 41477018775732224 orderApi
2020-08-23 10:54:54.307  INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.sample.service.OrderSaveImpl    : 保存订单金额, rollback, xid:192.168.59.132:8091:41477018662486016
2020-08-23 10:54:54.316  INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractResourceManager      : TCC resource rollback result : true, xid: 192.168.59.132:8091:41477018662486016, branchId: 41477018775732224, resourceId: orderApi
2020-08-23 10:54:54.316  INFO 652376 --- [ch_RMROLE_1_3_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-08-23 10:54:54.336  INFO 652376 --- [nio-8180-exec-1] i.seata.tm.api.DefaultGlobalTransaction  : [192.168.59.132:8091:41477018662486016] rollback status: Rollbacked
2020-08-23 10:54:54.353 ERROR 652376 --- [nio-8180-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 调用2阶段提交的rollback方法] with root cause
java.lang.RuntimeException: 调用2阶段提交的rollback方法
  at io.seata.sample.service.OrderServiceImpl.create(OrderServiceImpl.java:65) ~[classes/:na]

可以看出,上面有3个2阶段的事务回滚(PhaseTwo_Rollbacked),resourceId分别是orderApi、storageApi和accountApi,这时我们再看库存服务和账户服务的日志,如下:

2020-08-23 10:54:54.101  INFO 645052 --- [nio-8182-exec-2] i.s.sample.service.StorageServiceImpl    : ------->扣减库存prepare开始
2020-08-23 10:54:54.103  INFO 645052 --- [nio-8182-exec-2] i.s.sample.service.StorageServiceImpl    : ------->扣减库存prepare结束
2020-08-23 10:54:54.105  WARN 645052 --- [nio-8182-exec-2] c.a.c.seata.web.SeataHandlerInterceptor  : xid in change during RPC from 192.168.59.132:8091:41477018662486016 to null
2020-08-23 10:54:54.189  INFO 643544 --- [nio-8181-exec-4] i.s.sample.service.AccountServiceImpl    : ------->尝试扣减账户开始account
2020-08-23 10:54:54.191  INFO 643544 --- [nio-8181-exec-4] i.s.sample.service.AccountServiceImpl    : ------->尝试扣减账户结束account
2020-08-23 10:54:54.192  WARN 643544 --- [nio-8181-exec-4] c.a.c.seata.web.SeataHandlerInterceptor  : xid in change during RPC from 192.168.59.132:8091:41477018662486016 to null
2020-08-23 10:54:54.223  INFO 643544 --- [nio-8181-exec-5] i.s.sample.service.AccountServiceImpl    : 扣减账户金额, rollback, xid:192.168.59.132:8091:41477018662486016

这时我们再查看数据库,3张表都没有变。可见事务确实做了回滚。

总结

分布式事务的TCC模式和AT模式的本质区别是一个是2阶段提交,一个是交易补偿。seata框架对AT模式的支持是非常方便的,但是对TCC模式的支持,最大的就是自动触发commit和prepare方法,真正的实现还是需要开发人员自己做。

大家有更好的实现2阶段事务提交的方法,欢迎指点。

源码地址:

https://github.com/jinjunzhu/springcloud-eureka-seata-tcc.git