前往小程序,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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
linux p2v步骤,P2V操作完整步骤,物理机转换openstack虚拟机
注意:P2V之前系统盘要给足够,迁移会出现根目录空间不足情况。 本次实验根目录有800G,virsh pool池用的是/var/lib/glance的空间1.8T。
全栈程序员站长
2022/09/17
1.6K0
linux p2v步骤,P2V操作完整步骤,物理机转换openstack虚拟机
kali2020 U盘安装 并持久化
下载分区工具 DiskGenius 对u盘进行分区。 新建一个1G的活动分区,目的是为了以后也可以用这个U盘来转存一些小文件,小电影。因为Windows是不能识别linux的文件系统的,如果整个盘都用来装系统,
逍遥子大表哥
2021/12/19
1.7K0
kali2020 U盘安装 并持久化
最新版P2V教程及最新客户端整理
P2V其实就是物理到虚拟,即Physical to virtual ,简称P2V。
IT运维技术圈
2022/06/26
6820
最新版P2V教程及最新客户端整理
P2v, V2v 实践
p2v,就是physical machine to virtual machine,物理机转换成虚拟机,物理机有硬件和
全栈程序员站长
2022/09/17
2.4K0
P2v, V2v 实践
guestfs这么强大你知道吗
libguestfs 是Redhat开源的一组工具集,主要用来访问和修改虚拟机的磁盘。其功能非常强大,我们常用的监控虚拟机磁盘使用率、P2V、V2V、备份克隆虚拟机、格式化重置虚拟机磁盘大小等功能libguestfs都能提供。甚至定制操作系统、操作windows虚拟机注册表这样的功能它也包含其中。
虚拟化云计算
2018/04/08
2.6K0
guestfs这么强大你知道吗
Centos7下通过virt-p2v虚拟化Centos7服务器
之前在 Centos 6下通过virt-p2v进行虚拟化转化(For Linux Server)这篇Blog中写过在Centos6下通过virt-p2v进行虚拟化服务器,但是发现在Centos7中,virt-p2v client的客户端
全栈程序员站长
2022/09/13
1.1K0
Centos7下通过virt-p2v虚拟化Centos7服务器
kvm 安装 windows 虚拟机
将 /etc/sysconfig/selinux 中的 SELinux=enforcing 修改为 SELinux=disabled
用户8824291
2022/02/25
2.4K0
ESXI 迁移至KVM (V2V迁移)
1.1.1 ESXI将虚拟机导出    导出ova模板    将导出的ova模板导入到KVM环境中。 1.1.2 配置KVM环境 详情参考:http://www.cnblogs.com/clsn/p/
惨绿少年
2018/04/19
1.2K0
ESXI 迁移至KVM (V2V迁移)
物理机转虚拟机(P2V)概述、操作方法及生产环境经验总结
P2V即物理机转虚拟机,要求将物理机原封不动的转化成虚拟机,保持文件系统、任何文件都不发生变化。
力哥聊运维与云计算
2019/06/28
14.3K3
物理机转虚拟机(P2V)概述、操作方法及生产环境经验总结
使用 Converter Standalone进行P2V操作指导「建议收藏」
VMware vCenter Converter Standalone 是一款免费程序,可以安装在运行 Windows 的物理计算机上。 vConverter Standalone 会将硬盘驱动器上的数据复制到虚拟磁盘文件 (.vmdk) 中,此文件随后可在其他 VMware 产品中使用。 该过程不会影响您的计算机,在使用 Converter 之后您可以继续使用计算机。VMware vCenter Converter 可以在多种硬件上运行,并支持最常用的 Microsoft Windows 操作系统版本。实现:
全栈程序员站长
2022/09/17
6.9K0
使用 Converter Standalone进行P2V操作指导「建议收藏」
系统安装部署系列教程(四):制作PE系统
版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/u011054333/article/details/89762107
乐百川
2019/07/02
3.2K0
系统安装部署系列教程(四):制作PE系统
扔掉U盘,网启PE,桌面维护的摸鱼利器
注意,本文对读者有一定选择性,如果你并非是企业桌面维护,则本文可以略过,请点击作者头像查看其他精彩文章~
王忘杰
2022/09/22
1.2K0
扔掉U盘,网启PE,桌面维护的摸鱼利器
如何优雅的搞垮服务器,再优雅的救活
新开发的jar包部署在老服务器上,版本是Red Hat Enterprise Linux AS release 4 (Nahant Update 5),提示需要高版本jdk,高版本jdk提示glibc版本太低得升级,是的,就像套娃。 使用编译源码的方式将glibc由2.3升级到2.9,升级完ls命令不好使了。用LD_PRELOAD方法解决了ls命令不好使的问题后还挺有成就感的呢! 轻度强迫症的我当然要重启,然后
震八方紫面昆仑侠
2020/11/05
1.1K0
如何优雅的搞垮服务器,再优雅的救活
重装系统的几种方法:
装机版光盘安装:这种安装最简单,它集成了所有驱动和些常用软件,省去了重装系统后又要重装驱动和某些常用软件的麻烦。
时代疯
2021/07/19
1.2K0
云计算工程师之Linux指令集锦
LINUX简介与安装 一、Linux基础认知知识: 多使用者、多任务、多层次 Linux:开源、免费、安全、稳定 Linux中一切皆文件 Linux严格区分大小写
用户9184480
2024/12/13
980
Kali Linux 秘籍 第一章 安装和启动Kali
Kali Linux,简称Kali,是用于安全攻击的最新Linux发行版。它是BackTrack Linux的后继者。不像多数Linux发行版那样,Kali Linux用于渗透测试。渗透测试是一种通过模拟攻击评估计算机系统或网络安全性的方法。在整本书中,我们将会探索一些Kali Linux所提供的工具。
ApacheCN_飞龙
2022/11/27
2K1
CentOS 6.6下Xen虚拟化实战
因为从RHEL6版本开始,RedHat就不在原生支持Xen,改为投奔KVM的怀抱。尽管KVM号称支援HVM技术的全虚拟化,但KVM毕竟起步晚,远不如Xen技术成熟,运行多个实例时经常发生系统崩溃的现象,当然也不排斥未来KVM会更加高效的可能。所以安装Xen的方法只有两种,一种是自己下载源代码自行编译,但这种方法太耗费精力,所以忽略,另一中就是使用第三方软件源进行安装。互联网有大量的第三方源以及开源组织提供编译好的源码,其中Xen made Easy开源项目为我们提供了Xen4在RHEL6/CentOS6的安
小小科
2018/05/02
1.1K0
CentOS 6.6下Xen虚拟化实战
关于电脑怎么重装系统的方法
关于电脑怎么重装系统的方法相信大家都知道,但是对于一些对电脑操作不够了解或者是女性朋友不懂电脑操作的,重装系统就会搞得很难了,其实重装电脑系统的方法有很多种,大家可以看一下以下重装电脑系统的方法中,有哪个方法是自己会操作的,接下来就为大家介绍关于电脑怎么重装系统的相关内容。
用户8739405
2021/06/25
1.2K0
kvm研究和实践
qemu-kvm 该软件包主要包含KVM内核模块和基于KVM重构后的QEMU模拟器。KVM模块作为整个虚拟化环境的核心工作在系统空间,负责CPU和内存的调度。QEMU作为模拟器工作在用户空间,负责虚拟机I/O模拟。 依赖包qemu-img 主要用来QEMU磁盘镜像的管理,如新建一块磁盘镜像给虚拟机。
初心不改
2020/01/23
1.8K0
KVM镜像管理利器-guestfish使用详解
1. 虚拟机镜像挂载及w2k8虚拟机启动自检慢问题解决办法 2. KVM虚拟化与guestfish套件 3. guestfish安装与注意事项 4. 使用guestfish查看虚拟机信息 5. 使用guestfish查看虚拟机分区及文件系统 6. 去掉磁盘空洞--KVM虚拟镜像的稀疏问题 7. 用guestfish操作虚拟机内部文件 8. guestfish修改镜像格式和大小 9. guestfish挂载、修改、运行救援方式
力哥聊运维与云计算
2019/06/28
3.3K0
相关推荐
linux p2v步骤,P2V操作完整步骤,物理机转换openstack虚拟机
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文