Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >springcloud+eureka整合seata-tcc模式

springcloud+eureka整合seata-tcc模式

作者头像
jinjunzhu
发布于 2020-08-28 07:32:18
发布于 2020-08-28 07:32:18
1.8K00
代码可运行
举报
文章被收录于专栏:个人开发个人开发
运行总次数:0
代码可运行

分布式事务中的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方法,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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方法代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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请求到来时,线程跟第一次请求的线程不一样了,所以抛出下面异常:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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]

这个异常源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//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表:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#########################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方法,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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.两阶段提交,我们以账户服务为例,接口定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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进行处理,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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,请求参数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
  "userId":1,
  "productId":1,
  "count":1,
  "money":1,
  "payAmount":50
}

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,而在库存服务和账户服务也分别有相关日志的打印,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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方法,最后一行代码改为如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
throw new RuntimeException("调用2阶段提交的rollback方法");
//return true

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,这时我们再看库存服务和账户服务的日志,如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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阶段事务提交的方法,欢迎指点。

源码地址:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
https://github.com/jinjunzhu/springcloud-eureka-seata-tcc.git
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 jinjunzhu 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
微服务痛点-基于Dubbo + Seata的分布式事务(TCC模式)
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
sanshengshui
2020/12/31
1.1K0
springcloud+eureka整合阿里seata-xa模式
XA协议是X/Open组织管理的一种分布式协议规范,它采用2阶段提交来管理分布式事务,目前主流的数据库都支持xa协议。
jinjunzhu
2020/11/04
4410
springcloud+eureka整合阿里seata-xa模式
详解分布式事务之 Seata-Client 原理及流程
在分布式系统中,分布式事务是一个必须要解决的问题,目前使用较多的是最终一致性方案。自年初阿里开源了Fescar(四月初更名为Seata)后,该项目受到了极大的关注,目前已接近 8000 Star。Seata 以高性能和零侵入的特性为目标解决微服务领域的分布式事务难题,目前正处于快速迭代中,近期小目标是生产可用的 Mysql 版本。
程序猿DD
2019/05/10
2.2K0
详解分布式事务之 Seata-Client 原理及流程
微服务痛点-基于Dubbo + Seata的分布式事务(AT)模式
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。对于Seata不太了解的朋友,可以看下我之前写的文章: 微服务开发的最大痛点-分布式事务SEATA入门简介。
sanshengshui
2020/12/23
6940
微服务痛点-基于Dubbo + Seata的分布式事务(AT)模式
业务无侵入框架Seata, 解决分布式事务问题
事务的原子性和持久性可以确保在一个事务内,更新多条数据,要么都成功,要么都失败。在一个系统内部,我们可以使用数据库事务来保证数据一致性。那如果一笔交易,涉及到跨多个系统、多个数据库的时候,用单一的数据库事务就没办法解决了。
微观技术
2021/04/19
1K0
业务无侵入框架Seata, 解决分布式事务问题
golang源码分析:seata-go (2)tcc模式
在介绍了seata-go如何部署mac 上学习k8s系列(53)seata-go和如何使用at模式golang源码分析:seata-go (1)at模式后,本文开始介绍如何使用tcc模式。
golangLeetcode
2022/12/17
4290
深度剖析 Seata TCC 模式【图解 + 源码分析】
Seata 目前支持 AT 模式、XA 模式、TCC 模式和 SAGA 模式,之前文章更多谈及的是非侵入式的 AT 模式,今天带大家认识一下同样是二阶段提交的 TCC 模式。
张乘辉
2022/01/24
2.5K0
深度剖析 Seata TCC 模式【图解 + 源码分析】
使用Seata彻底解决Spring Cloud中的分布式事务问题!
随着业务需求的变化,单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
macrozheng
2019/11/23
2.2K0
听说 TCC 不支持 OpenFeign?这个坑松哥必须给大家填了!
友情提示:本文略长略复杂,但是有配套的视频教程,对视频感兴趣的小伙伴戳这里:TienChin 项目配套视频来啦。 在前面的文章中,松哥和大家聊了分布式事务框架 seata 的 at 模式,然后有小伙伴评论说 seata 的 tcc 模式不支持 Spring Boot: 这。。。必然是支持的呀! 我一直觉得网上讲分布式事务的理论很多,案例代码很少,所以咱们今天就整一个例子,一起来把这个捋一捋。 1. 什么是 TCC 模式 相比于上篇文章所聊的 AT 模式,TCC(Try-Confirm-Cancel) 模式
江南一点雨
2022/06/06
3990
听说 TCC 不支持 OpenFeign?这个坑松哥必须给大家填了!
Seata之TCC模式解读
关于TCC的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
一个风轻云淡
2023/11/12
1.3K0
SpringCloud与Seata分布式事务初体验
在本篇文章中我们在SpringCloud环境下通过使用Seata来模拟用户购买商品时由于用户余额不足导致本次订单提交失败,来验证下在MySQL数据库内事务是否会回滚。
恒宇少年
2019/10/14
1K0
Spring Cloud 中的分布式事务,附源码《一》
在开发我的开源项目 prex 时,加入工作流,解决工作流用户与当前系统用户同步问题时,涉及到远程调用操作两个数据库所产生的事务问题,比如系统用户在增加用户同步工作流用户时,系统用户添加成功,工作流用户没有添加成功,则造成数据不一致问题,本地事务无法回滚,那么则使用分布式事务解决方案。
搜云库技术团队
2019/12/02
1.1K0
Spring Cloud同步场景分布式事务怎样做?试试Seata
在微服务架构下,虽然我们会尽量避免分布式事务,但是只要业务复杂的情况下这是一个绕不开的问题,如何保证业务数据一致性呢?本文主要介绍同步场景下使用Seata的AT模式来解决一致性问题。
陶陶技术笔记
2020/06/02
7280
Spring Cloud同步场景分布式事务怎样做?试试Seata
Seata 分布式事务
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三三 个服务来完成。此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。
用户9615083
2022/12/25
8280
Seata 分布式事务
看了 5 种分布式事务方案,我司最终选择了 Seata,真香!
好长时间没发文了,最近着实是有点忙,当爹的第 43 天,身心疲惫。这又赶上年底,公司冲 KPI 强制技术部加班到十点,晚上孩子隔两三个小时一醒,基本没睡囫囵觉的机会,天天处于迷糊的状态,孩子还时不时起一些奇奇怪怪的疹子,总让人担惊受怕的。
程序员小富
2020/11/27
5750
看了 5 种分布式事务方案,我司最终选择了 Seata,真香!
Seata实战-分布式事务简介及demo上手
Seata简介 Seata(Simple Extensible Autonomous Transaction Architecture) 是 阿里巴巴开源的分布式事务中间件,以高效并且对业务 0 侵入的方式,解决微服务场景下面临的分布式事务问题。
爱撸猫的杰
2021/03/04
1.4K0
处理分布式事务(SpringCloud Alibaba Seata)
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
鱼找水需要时间
2023/02/16
2620
处理分布式事务(SpringCloud Alibaba Seata)
分布式事务:Seata框架AT模式及TCC模式执行流程剖析
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚,在分支事务执行的客户端。
朝雨忆轻尘
2020/11/24
2K0
分布式事务:Seata框架AT模式及TCC模式执行流程剖析
分布式事务Seata之AT模式,TCC模式,安装教程,原理等
前面我们说了三篇分布式事务的解决方案,分别是强一致性分布式事务2P,3PC,最终一致性事务TCC,不过它们只是提供了原理,今天我们来说一下具体的 分布式事务框架,对于分布式事务,有很多现成框架,今天我们主要来说一下阿里的Seata,想必Java开发者没有不知道Seata的吧,Seata提供了很多分布式 事务解决方案,比如AT模式,TCC模式,XA模式,Saga模式。
小四的技术之旅
2022/07/26
1.7K0
分布式事务Seata之AT模式,TCC模式,安装教程,原理等
Seata分布式事务落地解决方案
上一篇文章介绍了分布式事务理论和相关解决方案的具体思路,我们下面快速复习一下相关知识点:
大忽悠爱学习
2023/02/13
1K0
Seata分布式事务落地解决方案
推荐阅读
相关推荐
微服务痛点-基于Dubbo + Seata的分布式事务(TCC模式)
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文