前言

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

对于Seata不太了解的朋友,可以看下我之前写的文章:

TCC模式

TCC模式怎么理解

TCC(Try-Confirm-Cancel)实际上是服务化的两阶段提交协议,业务开发者需要实现这三个服务接口,第一阶段服务由业务代码编排来调用 Try 接口进行资源预留,所有参与者的 Try 接口都成功了,事务管理器会提交事务,并调用每个参与者的 Confirm 接口真正提交业务操作,否则调用每个参与者的Cancel接口回滚事务。

Seata 框架把每组 TCC 接口当做一个 Resource,称为 TCC Resource。这套 TCC 接口可以是 RPC,也以是服务内 JVM 调用。在业务启动时,Seata 框架会自动扫描识别到 TCC 接口的调用方和发布方。如果是 RPC 的话,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等。

扫描到 TCC 接口的调用方和发布方之后。如果是发布方,会在业务启动时向 TC 注册 TCC Resource,与 DataSource Resource 一样,每个资源也会带有一个资源 ID。

如果是调用方,Seata 框架会给调用方加上切面,与 AT 模式一样,在运行时,该切面会拦截所有对 TCC 接口的调用。每调用一次 Try 接口,切面会先向 TC 注册一个分支事务,然后才去执行原来的 RPC 调用。当请求链路调用完成后,TC 通过分支事务的资源 ID 回调到正确的参与者去执行对应 TCC 资源的 Confirm 或 Cancel 方法。

如何设计和异常控制

TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。

TCC 三个方法描述:

  • Try:资源的检测和预留;
  • Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
  • Cancel:预留资源释放;

业务模型分2阶段设计

用户接入 TCC ,最重要的是考虑如何将自己的业务模型拆成两阶段来实现。

以“扣钱”场景为例,在接入 TCC 前,对 A 账户的扣钱,只需一条更新账户余额的 SQL 便能完成;但是在接入 TCC 之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆成两阶段,实现成三个方法,并且保证一阶段 Try 成功的话 二阶段 Confirm 一定能成功。

如上图所示,

Try 方法作为一阶段准备方法,需要做资源的检查和预留。在扣钱场景下,Try 要做的事情是就是检查账户余额是否充足,预留转账资金,预留的方式就是冻结 A 账户的 转账资金。Try 方法执行之后,账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使用。

二阶段 Confirm 方法执行真正的扣钱操作。Confirm 会使用 Try 阶段冻结的资金,执行账号扣款。Confirm 方法执行之后,账号 A 在一阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。

如果二阶段是回滚的话,就需要在 Cancel 方法内释放一阶段 Try 冻结的 30 元,使账号 A 的回到初始状态,100 元全部可用。

用户接入 TCC 模式,最重要的事情就是考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个方法,并且保证 Try 成功 Confirm 一定能成功。相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。

TCC设计 - 允许空回滚

首先是空回滚。什么是空回滚?空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成

什么样的情形会造成空回滚呢?可以看图中的第 2 步,前面讲过,注册分支事务是在调用 RPC 时,Seata 框架的切面会拦截到该次调用请求,先向 TC 注册一个分支事务,然后才去执行 RPC 调用逻辑。如果 RPC 调用逻辑有问题,比如调用方机器宕机、网络异常,都会造成 RPC 调用失败,即未执行 Try 方法。但是分布式事务已经开启了,需要推进到终态,因此,TC 会回调参与者二阶段 Cancel 接口,从而形成空回滚。

那会不会有空提交呢?理论上来说不会的,如果调用方宕机,那分布式事务默认是回滚的。如果是网络异常,那 RPC 调用失败,发起方应该通知 TC 回滚分布式事务,这里可以看出为什么是理论上的,就是说发起方可以在 RPC 调用失败的情况下依然通知 TC 提交,这时就会发生空提交,这种情况要么是编码问题,要么开发同学明确知道需要这样做。

那怎么解决空回滚呢?前面提到,Cancel 要识别出空回滚,直接返回成功。那关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。因此,需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID,第一阶段 Try 方法里会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

TCC设计 - 幂等控制

接下来是幂等。幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接口,因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致资损等严重问题。

什么样的情形会造成重复提交或回滚?从图中可以看到,提交或回滚是一次 TC 到参与者的网络调用。因此,网络故障、参与者宕机等都有可能造成参与者 TCC 资源实际执行了二阶段防范,但是 TC 没有收到返回结果的情况,这时,TC 就会重复调用,直至调用成功,整个分布式事务结束。

怎么解决重复执行的幂等问题呢?一个简单的思路就是记录每个分支事务的执行状态。在执行前状态,如果已执行,那就不再执行;否则,正常执行。前面在讲空回滚的时候,已经有一张事务控制表了,事务控制表的每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,用来记录每个分支事务的执行状态。

如图所示,该状态字段有三个值,分别是初始化、已提交、已回滚。Try 方法插入时,是初始化状态。二阶段 Confirm 和 Cancel 方法执行后修改为已提交或已回滚状态。当重复调用二阶段接口时,先获取该事务控制表对应记录,检查状态,如果已执行,则直接返回成功;否则正常执行。

TCC设计 - 防悬挂

最后是防悬挂。按照惯例,咱们来先讲讲什么是悬挂。悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为允许空回滚的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。但是这之后 Try 方法才真正开始执行,预留业务资源,前面提到事务并发控制的业务加锁,对于一个 Try 方法预留的业务资源,只有该分布式事务才能使用,然而 Seata 框架认为该分布式事务已经结束,也就是说,当出现这种情况时,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。

什么样的情况会造成悬挂呢?按照前面所讲,在 RPC 调用时,先注册分支事务,再执行 RPC 调用,如果此时 RPC 调用的网络发生拥堵,通常 RPC 调用是有超时时间的,RPC 超时以后,发起方就会通知 TC 回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者,真正执行,从而造成悬挂。

怎么实现才能做到防悬挂呢?根据悬挂出现的条件先来分析下,悬挂是指二阶段 Cancel 执行完后,一阶段才执行。也就是说,为了避免悬挂,如果二阶段执行完成,那一阶段就不能再继续执行。因此,当一阶段执行时,需要先检查二阶段是否已经执行完成,如果已经执行,则一阶段不再执行;否则可以正常执行。那怎么检查二阶段是否已经执行呢?大家是否想到了刚才解决空回滚和幂等时用到的事务控制表,可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取该记录,如果记录存在,就认为二阶段已经执行;否则二阶段没执行。

Dubbo + Seata 实战案例

关于环境准备和目录结构,大家详见: 微服务痛点 - 基于Dubbo + Seata的分布式事务(AT)模式

业务模型两阶段改造

Storage商品库存

  1. 数据库添加冻结商品库存数
# 创建商品库存表
create table if not exists storage.tcc_storage
(
id bigint auto_increment
primary key,
commodity_code varchar(50) null comment '商品编码',
name varchar(255) null comment '商品名称',
count int null comment '商品库存数',
frozen_count int default 0 null comment '冻结商品库存数'
);
  1. 将原来的扣减商品库存一步逻辑修改成两阶段逻辑操作:
package cn.mushuwei.storage.api;

import cn.mushuwei.storage.api.dto.CommodityDTO;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction; /**
* @author jamesmsw
* @date 2020/12/1 9:37 上午
*/
@LocalTCC
public interface StorageApi { /**
* 扣减库存准备
*
* @param actionContext 业务动作上下文
* @param commodityDTO 库存信息
* @return 是/否
*/
@TwoPhaseBusinessAction(name = "decreaseStorageTcc", commitMethod = "decreaseStorageCommit", rollbackMethod = "decreaseStorageCancel")
Boolean decreaseStoragePrepare(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "commdityDTO") CommodityDTO commodityDTO); /**
* 扣减库存提交
*
* @param actionContext 业务动作上下文
* @return 是/否
*/
Boolean decreaseStorageCommit(BusinessActionContext actionContext); /**
* 扣减库存回滚
*
* @param actionContext 业务动作上下文
* @return 是/否
*/
Boolean decreaseStorageCancel(BusinessActionContext actionContext);
}
  1. 数据持久化操作逻辑修改:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.mushuwei.storage.dao.StorageDao"> <update id="tccDecreaseStoragePrepare">
update tcc_storage set count = count - #{count},
frozen_count = frozen_count + #{count}
where commodity_code = #{commodityCode}
</update> <update id="tccDecreaseStorageCommit">
update tcc_storage set frozen_count = frozen_count - #{count}
where commodity_code = #{commodityCode}
</update> <update id="tccDecreaseStorageCancel">
update tcc_storage set count = count + #{count},
frozen_count = frozen_count - #{count}
where commodity_code = #{commodityCode}
</update>
</mapper>

Account用户

  1. **数据库添加账号冻结余额: **
# 创建用户账户表
create table if not exists tcc_account
(
id bigint auto_increment
primary key,
user_id varchar(50) null comment '用户编号',
amount double(50,2) null comment '账号余额',
frozen_amount double(50,2) default 0.00 null comment '账号冻结余额'
);
  1. 将原来的扣减用户余额一步逻辑修改成两阶段逻辑操作:
package cn.mushuwei.account.api;

import cn.mushuwei.account.api.dto.AccountDTO;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction; /**
* @author jamesmsw
* @date 2020/12/1 5:20 下午
*/
@LocalTCC
public interface AccountApi { /**
* 从账号扣钱准备
*
* @param accountDTO
* @param actionContext 业务动作上下文
* @return 是/否
*/
@TwoPhaseBusinessAction(name = "decreaseAccountTcc", commitMethod = "decreaseAccountCommit", rollbackMethod = "decreaseAccountCancel")
Boolean decreaseAccountPrepare(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "accountDTO") AccountDTO accountDTO); /**
* 从账号扣钱提交
*
* @param actionContext
* @return 是/否
*/
Boolean decreaseAccountCommit(BusinessActionContext actionContext); /**
* 从账号扣钱取消
*
* @param actionContext
* @return 是/否
*/
Boolean decreaseAccountCancel(BusinessActionContext actionContext);
}
  1. 数据持久化操作逻辑修改:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.mushuwei.account.dao.AccountDao"> <!-- 以下是tcc模式所需的数据库操作 -->
<update id="tccDecreaseAccountPrepare">
update tcc_account set amount = amount - #{amount},
frozen_amount = frozen_amount + #{amount}
where user_id = #{userId}
</update> <update id="tccDecreaseAccountCommit">
update tcc_account set frozen_amount = frozen_amount - #{amount}
where user_id = #{userId}
</update> <update id="tccDecreaseAccountCancel">
update tcc_account set amount = amount + #{amount},
frozen_amount = frozen_amount - #{amount}
where user_id = #{userId}
</update> </mapper>

Order订单

  1. 数据库添加订单创建状态:
create table if not exists `order`.tcc_order
(
id bigint auto_increment
primary key,
order_no varchar(100) null comment '订单号',
user_id varchar(50) null comment '用户编号',
code varchar(100) null comment '商品编码',
count int null comment '商品数量',
amount double(50,2) null comment '消费总金额',
status tinyint null comment '状态,1-预创建;2-创建成功;3-创建失败'
);
  1. 将原来的创建订单一步逻辑修改成两阶段逻辑操作:
package cn.mushuwei.order.api;

import cn.mushuwei.order.api.dto.OrderDTO;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction; /**
* @author jamesmsw
* @date 2020/12/1 5:57 下午
*/
@LocalTCC
public interface OrderApi { /**
* 创建订单准备
*
* @param orderDTO
* @param actionContext 业务动作上下文
* @return
*/
@TwoPhaseBusinessAction(name = "createOrderTcc", commitMethod = "createOrderCommit", rollbackMethod = "createOrderCancel")
Boolean createOrderPrepare(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "orderDTO") OrderDTO orderDTO); /**
* 创建订单提交
*
* @param actionContext 业务动作上下文
* @return
*/
Boolean createOrderCommit(BusinessActionContext actionContext); /**
* 创建订单取消
*
* @param actionContext 业务动作上下文
* @return
*/
Boolean createOrderCancel(BusinessActionContext actionContext);
}
  • TwoPhaseBusinessAction注解标记这是个TCC接口,同时指定commitMethod,rollbackMethod的名称BusinessActionContext是TCC事务中的上下文对象 BusinessActionContextParameter注解标记的参数会在上下文中传播,即能通过BusinessActionContext对象在commit方法及cancle方法中取到该参数值

  • RM 的接口上面必须要有@LocalTCC 注解,且必须在接口上面

  1. 数据持久化操作逻辑修改:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.mushuwei.order.dao.OrderDao"> <insert id="tccCreateOrderPrepare" keyProperty="id" useGeneratedKeys="true"
parameterType="cn.mushuwei.order.entity.OrderDO">
insert into `tcc_order` (order_no,
user_id,
code,
count,
amount,
status)
VALUES (#{order.orderNo},
#{order.userId},
#{order.code},
#{order.count},
#{order.amount},
#{order.status})
</insert> <update id="tccCreateOrderCommitOrCancel">
update `tcc_order` set status = #{status}
where order_no = #{orderNo}
</update> </mapper>

演示

启动Dubbo、Seata、MySQ并初始化数据, 使各服务应用注册到Seata上

  • Dubbo、Seata和MySQL服务
mushuwei@mushuweideMacBook-Pro-2 seata % docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0c9c325a039c mysql:latest "docker-entrypoint.s…" 2 weeks ago Up 7 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp mysql5.7
b8031fa865cd seataio/seata-server "java -Djava.securit…" 2 weeks ago Up 20 seconds 0.0.0.0:8091->8091/tcp seata_seata-server_1
2af927368a15 apache/dubbo-admin "java -XX:+UnlockExp…" 2 weeks ago Up 2 hours 0.0.0.0:8080->8080/tcp dubbo_admin_1
7afec07234c9 zookeeper "/docker-entrypoint.…" 2 weeks ago Up 2 hours 2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp, 8080/tcp dubbo_zookeeper_1
  • 初始化数据
mysql> use storage;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> select * from tcc_storage;
+----+----------------+------+-------+--------------+
| id | commodity_code | name | count | frozen_count |
+----+----------------+------+-------+--------------+
| 1 | cola | ???? | 2000 | 0 |
+----+----------------+------+-------+--------------+
1 row in set (0.00 sec) mysql> use account;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> select * from tcc_account;
+----+---------+---------+---------------+
| id | user_id | amount | frozen_amount |
+----+---------+---------+---------------+
| 1 | user123 | 1250.00 | 0.00 |
+----+---------+---------+---------------+
1 row in set (0.00 sec) mysql> use order;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> select * from tcc_order;
Empty set (0.00 sec)
  • 启动Storage、Account、Order和Business

  • Seata上各应用的注册情况
Starting seata_seata-server_1 ... done
Attaching to seata_seata-server_1
seata-server_1 | [0.001s][warning][gc] -Xloggc is deprecated. Will use -Xlog:gc:/var/log/seata_gc.log instead.
seata-server_1 | [0.015s][info ][gc] Using G1
seata-server_1 | [0.841s][info ][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 14M->4M(32M) 11.654ms
seata-server_1 | SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
seata-server_1 | SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
seata-server_1 | SLF4J: See also http://www.slf4j.org/codes.html#replay
seata-server_1 | 08:16:30.938 INFO --- [ main] io.seata.server.Server : The server is running in container.
seata-server_1 | 08:16:30.972 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is registry
seata-server_1 | 08:16:30.980 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is /seata-server/resources/registry.conf
seata-server_1 | [1.385s][info ][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 15M->6M(32M) 14.280ms
seata-server_1 | 08:16:31.221 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is file.conf
seata-server_1 | 08:16:31.222 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is file.conf
seata-server_1 | WARNING: An illegal reflective access operation has occurred
seata-server_1 | WARNING: Illegal reflective access by net.sf.cglib.core.ReflectUtils$2 (file:/seata-server/libs/cglib-3.1.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
seata-server_1 | WARNING: Please consider reporting this to the maintainers of net.sf.cglib.core.ReflectUtils$2
seata-server_1 | WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
seata-server_1 | WARNING: All illegal access operations will be denied in a future release
seata-server_1 | [1.734s][info ][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 16M->7M(32M) 6.400ms
seata-server_1 | [2.101s][info ][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 18M->7M(32M) 4.828ms
seata-server_1 | 08:16:31.924 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091
seata-server_1 | 08:26:12.007 INFO --- [rverHandlerThread_1_1_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/storage', applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0xae1ea1b1, L:/172.20.0.2:8091 - R:/172.20.0.1:52380],client version:1.3.0
seata-server_1 | 08:26:12.080 INFO --- [rverHandlerThread_1_2_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/storage', applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0xae1ea1b1, L:/172.20.0.2:8091 - R:/172.20.0.1:52380],client version:1.3.0
seata-server_1 | 08:26:33.704 INFO --- [rverHandlerThread_1_3_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/account', applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0xd949a994, L:/172.20.0.2:8091 - R:/172.20.0.1:52396],client version:1.3.0
seata-server_1 | 08:26:33.758 INFO --- [rverHandlerThread_1_4_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/account', applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0xd949a994, L:/172.20.0.2:8091 - R:/172.20.0.1:52396],client version:1.3.0
seata-server_1 | 08:26:57.466 INFO --- [rverHandlerThread_1_5_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/order', applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xfd51f88b, L:/172.20.0.2:8091 - R:/172.20.0.1:52412],client version:1.3.0
seata-server_1 | 08:26:57.518 INFO --- [rverHandlerThread_1_6_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/order', applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xfd51f88b, L:/172.20.0.2:8091 - R:/172.20.0.1:52412],client version:1.3.0
seata-server_1 | 08:27:10.600 INFO --- [ettyServerNIOWorker_1_4_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0x0e0b6c24, L:/172.20.0.2:8091 - R:/172.20.0.1:52424],client version:1.3.0
seata-server_1 | 08:27:32.694 INFO --- [ettyServerNIOWorker_1_5_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0x2fd20474, L:/172.20.0.2:8091 - R:/172.20.0.1:52432],client version:1.3.0
seata-server_1 | 08:27:56.453 INFO --- [ettyServerNIOWorker_1_6_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xc8f6ba94, L:/172.20.0.2:8091 - R:/172.20.0.1:52436],client version:1.3.0
seata-server_1 | 08:28:15.847 INFO --- [rverHandlerThread_1_7_500] i.s.c.r.processor.server.RegRmProcessor : RM register success,message:RegisterRMRequest{resourceIds='null', applicationId='seata-action-business', transactionServiceGroup='service_tx_group'},channel:[id: 0x9ef75d68, L:/172.20.0.2:8091 - R:/172.20.0.1:52444],client version:1.3.0
seata-server_1 | 08:28:15.863 INFO --- [ettyServerNIOWorker_1_7_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='seata-action-business', transactionServiceGroup='service_tx_group'},channel:[id: 0x2b6c19d5, L:/172.20.0.2:8091 - R:/172.20.0.1:52440],client version:1.3.0

检查各服务Service在Dubbo上的情况

正常流程-模拟用户下单,看下各应用的二阶段提交日志

  • 执行business模块test/java目录下的business.http文件,对接口发起请求
POST localhost:8084/business/buy
Content-Type: application/json {
"userId" : "user123",
"commodityCode" : "cola",
"count" : 2,
"amount" : 5.0
}
  • 各数据库数据变化
mysql> use storage;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> select * from storage;
+----+----------------+------+-------+
| id | commodity_code | name | count |
+----+----------------+------+-------+
| 1 | cola | ???? | 1998 |
+----+----------------+------+-------+
1 row in set (0.00 sec) mysql> use account;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> select * from account;
+----+---------+---------+
| id | user_id | amount |
+----+---------+---------+
| 1 | user123 | 1245.00 |
+----+---------+---------+
1 row in set (0.00 sec) mysql> use order;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A Database changed
mysql> select * from order;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order' at line 1
mysql> select * from `order`;
+----+----------------------------------+---------+------+-------+--------+
| id | order_no | user_id | code | count | amount |
+----+----------------------------------+---------+------+-------+--------+
| 5 | dbde6ebfd72b4ad5aeba67d67ade6894 | user123 | cola | 2 | 5.00 |
+----+----------------------------------+---------+------+-------+--------+
1 row in set (0.00 sec)
  • 各应用下二阶段提交情况,下面日志以Storage应用为例
2020-12-28 17:59:40.778  INFO 28287 --- [ctor_RMROLE_1_1] io.seata.rm.AbstractRMHandler            : the rm client received response msg [version=1.5.0-SNAPSHOT,extraData=null,identified=true,resultCode=null,msg=null] from tc server.
2020-12-28 17:59:40.783 DEBUG 28287 --- [:20881-thread-2] c.m.s.d.S.tccDecreaseStoragePrepare : ==> Preparing: update tcc_storage set count = count - ?, frozen_count = frozen_count + ? where commodity_code = ?
2020-12-28 17:59:40.822 DEBUG 28287 --- [:20881-thread-2] c.m.s.d.S.tccDecreaseStoragePrepare : ==> Parameters: 2(Integer), 2(Integer), cola(String)
2020-12-28 17:59:40.830 DEBUG 28287 --- [:20881-thread-2] c.m.s.d.S.tccDecreaseStoragePrepare : <== Updates: 1
2020-12-28 17:59:41.662 INFO 28287 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchCommitProcessor : rm client handle branch commit process:xid=172.20.0.2:8091:86882407747166208,branchId=86882409689128960,branchType=TCC,resourceId=decreaseStorageTcc,applicationData={"actionContext":{"action-start-time":1609149580502,"sys::prepare":"decreaseStoragePrepare","sys::rollback":"decreaseStorageCancel","sys::commit":"decreaseStorageCommit","commdityDTO":{"commodityCode":"cola","count":2},"host-name":"172.17.54.171","actionName":"decreaseStorageTcc"}}
2020-12-28 17:59:41.664 INFO 28287 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch committing: 172.20.0.2:8091:86882407747166208 86882409689128960 decreaseStorageTcc {"actionContext":{"action-start-time":1609149580502,"sys::prepare":"decreaseStoragePrepare","sys::rollback":"decreaseStorageCancel","sys::commit":"decreaseStorageCommit","commdityDTO":{"commodityCode":"cola","count":2},"host-name":"172.17.54.171","actionName":"decreaseStorageTcc"}}
2020-12-28 17:59:41.675 DEBUG 28287 --- [h_RMROLE_1_1_16] c.m.s.d.S.tccDecreaseStorageCommit : ==> Preparing: update tcc_storage set frozen_count = frozen_count - ? where commodity_code = ?
2020-12-28 17:59:41.676 DEBUG 28287 --- [h_RMROLE_1_1_16] c.m.s.d.S.tccDecreaseStorageCommit : ==> Parameters: 2(Integer), cola(String)
2020-12-28 17:59:41.681 DEBUG 28287 --- [h_RMROLE_1_1_16] c.m.s.d.S.tccDecreaseStorageCommit : <== Updates: 1
2020-12-28 17:59:41.704 INFO 28287 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractResourceManager : TCC resource commit result : true, xid: 172.20.0.2:8091:86882407747166208, branchId: 86882409689128960, resourceId: decreaseStorageTcc
2020-12-28 17:59:41.705 INFO 28287 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed

异常流程-模拟用户下单,看下各应用的二阶段提交日志

  • 修改BusinessServiceImpl类,并重启
 private boolean flag;

    @Override
@GlobalTransactional(timeoutMills = 300000, name = "seata-demo-business")
public Boolean handleBusiness(BusinessDTO businessDTO) {
flag = false;
log.info("开始全局事务,XID = " + RootContext.getXID());
CommodityDTO commodityDTO = new CommodityDTO();
commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
commodityDTO.setCount(businessDTO.getCount());
boolean storageResult = storageApi.decreaseStorage(commodityDTO); OrderDTO orderDTO = new OrderDTO();
orderDTO.setUserId(businessDTO.getUserId());
orderDTO.setCommodityCode(businessDTO.getCommodityCode());
orderDTO.setOrderCount(businessDTO.getCount());
orderDTO.setOrderAmount(businessDTO.getAmount());
boolean orderResult = orderApi.createOrder(orderDTO); //打开注释测试事务发生异常后,全局回滚功能
if (!flag) {
throw new RuntimeException("测试抛异常后,分布式事务回滚!");
} if (!storageResult || !orderResult) {
throw new RuntimeException("失败");
}
return true;
}
  • 执行business模块test/java目录下的business.http文件,对接口发起请求
POST http://localhost:8084/business/buy

HTTP/1.1 500
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 21 Dec 2020 08:46:24 GMT
Connection: close {
"timestamp": "2020-12-21T08:46:24.678+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "",
"path": "/business/buy"
}
  • 各数据库数据变化

无任何变化

  • 各应用下二阶段提交情况,下面日志以Storage应用为例
2020-12-28 18:04:12.125  WARN 28287 --- [erverWorker-3-1] o.a.d.remoting.transport.AbstractServer  :  [DUBBO] All clients has disconnected from /172.17.54.171:20881. You can graceful shutdown now., dubbo version: 2.7.8, current host: 172.17.54.171
2020-12-28 18:04:12.126 INFO 28287 --- [erverWorker-3-1] o.a.d.r.t.netty4.NettyServerHandler : [DUBBO] The connection of /172.17.54.171:53500 -> /172.17.54.171:20881 is disconnected., dubbo version: 2.7.8, current host: 172.17.54.171
2020-12-28 18:05:00.344 INFO 28287 --- [erverWorker-3-2] o.a.d.r.t.netty4.NettyServerHandler : [DUBBO] The connection of /172.17.54.171:54267 -> /172.17.54.171:20881 is established., dubbo version: 2.7.8, current host: 172.17.54.171
2020-12-28 18:05:13.544 INFO 28287 --- [:20881-thread-5] c.m.storage.provider.StorageApiImpl : commodityDTO: CommodityDTO(id=null, commodityCode=cola, name=null, count=2), actionContext: [xid:172.20.0.2:8091:86883805524140032,branch_Id:86883805897433088,action_name:decreaseStorageTcc,action_context:{action-start-time=1609149913535, sys::prepare=decreaseStoragePrepare, sys::rollback=decreaseStorageCancel, sys::commit=decreaseStorageCommit, commdityDTO=CommodityDTO(id=null, commodityCode=cola, name=null, count=2), host-name=172.17.54.171, actionName=decreaseStorageTcc}]
2020-12-28 18:05:13.557 DEBUG 28287 --- [:20881-thread-5] c.m.s.d.S.tccDecreaseStoragePrepare : ==> Preparing: update tcc_storage set count = count - ?, frozen_count = frozen_count + ? where commodity_code = ?
2020-12-28 18:05:13.558 DEBUG 28287 --- [:20881-thread-5] c.m.s.d.S.tccDecreaseStoragePrepare : ==> Parameters: 2(Integer), 2(Integer), cola(String)
2020-12-28 18:05:13.562 DEBUG 28287 --- [:20881-thread-5] c.m.s.d.S.tccDecreaseStoragePrepare : <== Updates: 1
2020-12-28 18:05:13.782 INFO 28287 --- [h_RMROLE_1_2_16] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=172.20.0.2:8091:86883805524140032,branchId=86883805897433088,branchType=TCC,resourceId=decreaseStorageTcc,applicationData={"actionContext":{"action-start-time":1609149913535,"sys::prepare":"decreaseStoragePrepare","sys::rollback":"decreaseStorageCancel","sys::commit":"decreaseStorageCommit","commdityDTO":{"commodityCode":"cola","count":2},"host-name":"172.17.54.171","actionName":"decreaseStorageTcc"}}
2020-12-28 18:05:13.784 INFO 28287 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.20.0.2:8091:86883805524140032 86883805897433088 decreaseStorageTcc
2020-12-28 18:05:13.789 DEBUG 28287 --- [h_RMROLE_1_2_16] c.m.s.d.S.tccDecreaseStorageCancel : ==> Preparing: update tcc_storage set count = count + ?, frozen_count = frozen_count - ? where commodity_code = ?
2020-12-28 18:05:13.789 DEBUG 28287 --- [h_RMROLE_1_2_16] c.m.s.d.S.tccDecreaseStorageCancel : ==> Parameters: 2(Integer), 2(Integer), cola(String)
2020-12-28 18:05:13.793 DEBUG 28287 --- [h_RMROLE_1_2_16] c.m.s.d.S.tccDecreaseStorageCancel : <== Updates: 1
2020-12-28 18:05:13.815 INFO 28287 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractResourceManager : TCC resource rollback result : true, xid: 172.20.0.2:8091:86883805524140032, branchId: 86883805897433088, resourceId: decreaseStorageTcc
2020-12-28 18:05:13.815 INFO 28287 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked

以上代码,我已经上传到GitHub中了,大家详见: https://github.com/sanshengshui/seata-dubbo-action,TCC模式在TCC分支上。

到此,基于Dubbo + Seata的分布式事务已经讲解完毕。

参考文章

微服务痛点-基于Dubbo + Seata的分布式事务(TCC模式)的更多相关文章

  1. 微服务痛点-基于Dubbo + Seata的分布式事务(AT)模式

    前言 Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务.Seata 将为用户提供了 AT.TCC.SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案. ...

  2. 通过Dapr实现一个简单的基于.net的微服务电商系统(十九)——分布式事务之Saga模式

    在之前的系列文章中聊过分布式事务的一种实现方案,即通过在集群中暴露actor服务来实现分布式事务的本地原子化.但是actor服务本身有其特殊性,场景上并不通用.所以今天来讲讲分布式事务实现方案之sag ...

  3. .NET Core微服务之基于Ocelot+Butterfly实现分布式追踪

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.什么是Tracing? 微服务的特点决定了功能模块的部署是分布式的,以往在单应用环境下,所有的业务都在同一个服务器上,如果服务器出现错 ...

  4. 微服务、分库分表、分布式事务管理、APM链路跟踪性能分析演示项目

    好多年没发博,最近有时间整理些东西,分享给大家. 所有内容都在github项目liuzhibin-cn/my-demo中,基于SpringBoot,演示Dubbo微服务 + Mycat, Shardi ...

  5. .Net Core with 微服务 - 使用 AgileDT 快速实现基于可靠消息的分布式事务

    前面对于分布式事务也讲了好几篇了(可靠消息最终一致性 分布式事务 - TCC 分布式事务 - 2PC.3PC),但是还没有实战过.那么本篇我们就来演示下如何在 .NET 环境下实现一个基于可靠消息的分 ...

  6. 手把手0基础项目实战(一)——教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)...

    原文:手把手0基础项目实战(一)--教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)... 本文你将学到什么? 本文将以原理+实战的方式,首先对& ...

  7. .NET Core微服务之基于Ocelot实现API网关服务

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.啥是API网关? API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口.这样就可以明显的简化客户端 ...

  8. .NET Core微服务之基于Ocelot实现API网关服务(续)

    Tip: 此篇已加入.NET Core微服务基础系列文章索引 一.负载均衡与请求缓存 1.1 负载均衡 为了验证负载均衡,这里我们配置了两个Consul Client节点,其中ClientServic ...

  9. 庐山真面目之十二微服务架构基于Docker搭建Consul集群、Ocelot网关集群和IdentityServer版本实现

    庐山真面目之十二微服务架构基于Docker搭建Consul集群.Ocelot网关集群和IdentityServer版本实现 一.简介      在第七篇文章<庐山真面目之七微服务架构Consul ...

随机推荐

  1. shardingsphere与分布式事务

    rt https://blog.csdn.net/l1028386804/article/details/79769043 https://blog.csdn.net/qq_20387013/arti ...

  2. Asp.NetCore之AutoMapper基础篇

    应用场景 现在由于前后端技术的分离,后端程序员在使用ORM框架开发后台API接口的时候,往往会将数据库的"数据模型"直接提供给前端.而大多数时候,可能这些数据并不能够满足前端展示的 ...

  3. YoyoGo微服务框架入门系列-快速编写WEB API

    前言 YoyoGo是一个使用Golang编写的一个简单.轻量.快速.基于依赖注入的微服务框架,目前依然在研发阶段,欢迎Star以及一起参与到框架的研发 GitHub地址:https://github. ...

  4. python核心高级学习总结4-------python实现进程通信

    Queue的使用 Queue在数据结构中也接触过,在操作系统里面叫消息队列. 使用示例 # coding=utf-8 from multiprocessing import Queue q = Que ...

  5. celery使用-win10和linux

    win10启动方式 celery -A celery_tasks.main worker -l debug -P eventlet linux启动方式 /usr/local/bin/celery ce ...

  6. PyQt学习随笔:ListView控件的视图和数据模型分离案例

    Qt 中view类控件的目的是实现数据和模型分离,控件展示数据,数据保存在数据存储中,数据存储中的数据改变了,则控件中展示的数据跟随改变.当设计时只指定了一个控件和一个数据存储关联时,这种分离虽然也能 ...

  7. 谈谈传说中的redo log是什么?有啥用?

    目录 一.引出 redo log 的作用 二.思考一个问题: 三.redo log block 四.redo log buffer 五.redo log的刷盘时机 六.推荐参数 七.redo log ...

  8. RocketMQ开发者指南

    1. 概念和特性 概念:介绍RocketMQ的基本概念模型 1 消息模型(Message Model) RocketMQ主要由 Producer.Broker.Consumer 三部分组成,其中Pro ...

  9. mybatis批量修改数据

    xxxMapper.xml: <update id="updateSensorWarnings" parameterType="java.util.List&quo ...

  10. k8s之yaml文件书写格式

    k8s之yaml文件书写格式 1 # yaml格式的pod定义文件完整内容: 2 apiVersion: v1 #必选,版本号,例如v1 3 kind: Pod #必选,Pod 4 metadata: ...