• SpringCloud - Spring Cloud Alibaba 之 Seata分布式事务服务;TCC事务模式机制(二十三)


    阅读本文前可先参考:

    SpringCloud - Spring Cloud Alibaba 之 Seata分布式事务服务详解;部署(十八)_MinggeQingchun的博客-CSDN博客

    SpringCloud - Spring Cloud Alibaba 之 Seata分布式事务服务;AT事务模式(二十)_MinggeQingchun的博客-CSDN博客

    Seata TCC 事务模式

    AT模式基本上能满足我们使用分布式事务大部分需求,但涉及非关系型数据库与中间件的操作、跨公司服务的调用、跨语言的应用调用就需要结合TCC模式

    http://seata.io/zh-cn/docs/dev/mode/tcc-mode.html

    一个分布式的全局事务,整体是两阶段提交Try - [Comfirm/Cancel]的模型

    根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode

    AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库

    • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录
    • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志
    • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚

    TCC 模式,不依赖于底层数据资源的事务支持(需要程序员编写代码实现提交和回滚):

    • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑
    • 二阶段 commit 行为:调用 自定义 的 commit 逻辑
    • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑

    TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中

    通俗来说,Seata的TCC模式就是手工版本的AT模式,它允许你自定义两阶段的处理逻辑而不需要依赖AT模式的undo_log回滚表

    1、创建数据库、表、插入数据等

    (1)accountdb账户库、account账户表

    1. SET NAMES utf8mb4;
    2. SET FOREIGN_KEY_CHECKS = 0;
    3. -- ----------------------------
    4. -- Table structure for account
    5. -- ----------------------------
    6. DROP TABLE IF EXISTS `account`;
    7. CREATE TABLE `account` (
    8. `id` int(20) NOT NULL AUTO_INCREMENT,
    9. `user_id` int(20) NULL DEFAULT NULL,
    10. `balance` decimal(20, 0) NULL DEFAULT NULL,
    11. `update_time` datetime(6) NULL DEFAULT NULL,
    12. PRIMARY KEY (`id`) USING BTREE
    13. ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    14. SET FOREIGN_KEY_CHECKS = 1;

    (2)productdb产品库、product产品表

    1. SET NAMES utf8mb4;
    2. SET FOREIGN_KEY_CHECKS = 0;
    3. -- ----------------------------
    4. -- Table structure for product
    5. -- ----------------------------
    6. DROP TABLE IF EXISTS `product`;
    7. CREATE TABLE `product` (
    8. `id` int(20) NOT NULL AUTO_INCREMENT,
    9. `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    10. `price` decimal(10, 2) NULL DEFAULT NULL,
    11. `stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    12. `add_time` datetime(6) NULL DEFAULT NULL,
    13. `update_time` datetime(6) NULL DEFAULT NULL,
    14. PRIMARY KEY (`id`) USING BTREE
    15. ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    16. SET FOREIGN_KEY_CHECKS = 1;

    (3)orderdb订单库、orders 订单表

    1. SET NAMES utf8mb4;
    2. SET FOREIGN_KEY_CHECKS = 0;
    3. -- ----------------------------
    4. -- Table structure for orders
    5. -- ----------------------------
    6. DROP TABLE IF EXISTS `orders`;
    7. CREATE TABLE `orders` (
    8. `id` int(20) NOT NULL AUTO_INCREMENT,
    9. `user_id` int(20) NULL DEFAULT NULL,
    10. `product_id` int(20) NULL DEFAULT NULL,
    11. `pay_amount` decimal(20, 0) NULL DEFAULT NULL,
    12. `add_time` datetime(6) NULL DEFAULT NULL,
    13. `update_time` datetime(6) NULL DEFAULT NULL,
    14. PRIMARY KEY (`id`) USING BTREE
    15. ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    16. SET FOREIGN_KEY_CHECKS = 1;

    (4)undo_log表  

    Seata AT 模式

    1. -- 注意此处0.7.0+ 增加字段 context
    2. CREATE TABLE `undo_log` (
    3. `id` bigint(20) NOT NULL AUTO_INCREMENT,
    4. `branch_id` bigint(20) NOT NULL,
    5. `xid` varchar(100) NOT NULL,
    6. `context` varchar(128) NOT NULL,
    7. `rollback_info` longblob NOT NULL,
    8. `log_status` int(11) NOT NULL,
    9. `log_created` datetime NOT NULL,
    10. `log_modified` datetime NOT NULL,
    11. PRIMARY KEY (`id`),
    12. UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    13. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

    注:

    每个库必须创建 undo_log 表,是 Seata AT模式必须创建的表,主要用于分支事务的回滚

    Seata AT 模式

     

    一、SpringBoot单体应用多数据源TCC事务

    在Spring Boot单体项目中,使用了多数据源,就要保证多个数据源的数据一致性,即产生了分布式事务的问题,采用Seata的AT事务模式来解决该分布式事务问题

    以下图购物下单为例

    1、创建一个 springboot应用,命名 springcloud-alibaba-3-seata-tcc-transaction 

    2、添加依赖

    1. <groupId>com.companygroupId>
    2. <artifactId>springcloud-alibaba-3-seata-tcc-transactionartifactId>
    3. <version>1.0.0version>
    4. <name>springcloud-alibaba-3-seata-tcc-transactionname>
    5. <description>Demo project for Spring Bootdescription>
    6. <properties>
    7. <java.version>1.8java.version>
    8. <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    9. <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
    10. <spring-boot.version>2.3.12.RELEASEspring-boot.version>
    11. <spring-cloud-alibaba.version>2.2.7.RELEASEspring-cloud-alibaba.version>
    12. properties>
    13. <dependencies>
    14. <dependency>
    15. <groupId>org.springframework.bootgroupId>
    16. <artifactId>spring-boot-starter-webartifactId>
    17. dependency>
    18. <dependency>
    19. <groupId>org.springframework.bootgroupId>
    20. <artifactId>spring-boot-starter-actuatorartifactId>
    21. dependency>
    22. <dependency>
    23. <groupId>org.springframework.bootgroupId>
    24. <artifactId>spring-boot-starter-testartifactId>
    25. <scope>testscope>
    26. <exclusions>
    27. <exclusion>
    28. <groupId>org.junit.vintagegroupId>
    29. <artifactId>junit-vintage-engineartifactId>
    30. exclusion>
    31. exclusions>
    32. dependency>
    33. <dependency>
    34. <groupId>org.projectlombokgroupId>
    35. <artifactId>lombokartifactId>
    36. dependency>
    37. <dependency>
    38. <groupId>mysqlgroupId>
    39. <artifactId>mysql-connector-javaartifactId>
    40. dependency>
    41. <dependency>
    42. <groupId>org.mybatis.spring.bootgroupId>
    43. <artifactId>mybatis-spring-boot-starterartifactId>
    44. <version>2.1.3version>
    45. dependency>
    46. <dependency>
    47. <groupId>io.seatagroupId>
    48. <artifactId>seata-spring-boot-starterartifactId>
    49. <version>1.4.2version>
    50. dependency>
    51. <dependency>
    52. <groupId>com.baomidougroupId>
    53. <artifactId>dynamic-datasource-spring-boot-starterartifactId>
    54. <version>3.2.0version>
    55. dependency>
    56. <dependency>
    57. <groupId>com.alibaba.nacosgroupId>
    58. <artifactId>nacos-clientartifactId>
    59. <version>2.1.0version>
    60. dependency>
    61. dependencies>
    62. <dependencyManagement>
    63. <dependencies>
    64. <dependency>
    65. <groupId>com.alibaba.cloudgroupId>
    66. <artifactId>spring-cloud-alibaba-dependenciesartifactId>
    67. <version>${spring-cloud-alibaba.version}version>
    68. <type>pomtype>
    69. <scope>importscope>
    70. dependency>
    71. <dependency>
    72. <groupId>org.springframework.cloudgroupId>
    73. <artifactId>spring-cloud-dependenciesartifactId>
    74. <version>Hoxton.SR12version>
    75. <type>pomtype>
    76. <scope>importscope>
    77. dependency>
    78. <dependency>
    79. <groupId>org.springframework.bootgroupId>
    80. <artifactId>spring-boot-dependenciesartifactId>
    81. <version>${spring-boot.version}version>
    82. <type>pomtype>
    83. <scope>importscope>
    84. dependency>
    85. dependencies>
    86. dependencyManagement>
    87. <build>
    88. <plugins>
    89. <plugin>
    90. <groupId>org.apache.maven.pluginsgroupId>
    91. <artifactId>maven-compiler-pluginartifactId>
    92. <configuration>
    93. <source>1.8source>
    94. <target>1.8target>
    95. <encoding>UTF-8encoding>
    96. configuration>
    97. plugin>
    98. <plugin>
    99. <groupId>org.springframework.bootgroupId>
    100. <artifactId>spring-boot-maven-pluginartifactId>
    101. plugin>
    102. <plugin>
    103. <groupId>org.mybatis.generatorgroupId>
    104. <artifactId>mybatis-generator-maven-pluginartifactId>
    105. <version>1.4.0version>
    106. <configuration>
    107. <configurationFile>src/main/resources/generatorConfig.xmlconfigurationFile>
    108. <verbose>trueverbose>
    109. <overwrite>trueoverwrite>
    110. configuration>
    111. plugin>
    112. plugins>
    113. <resources>
    114. <resource>
    115. <directory>src/main/javadirectory>
    116. <includes>
    117. <include>**/*.xmlinclude>
    118. includes>
    119. resource>
    120. <resource>
    121. <directory>src/main/resourcesdirectory>
    122. <includes>
    123. <include>**/*.*include>
    124. includes>
    125. resource>
    126. <resource>
    127. <directory>src/main/webappdirectory>
    128. <targetPath>META-INF/servicestargetPath>
    129. <includes>
    130. <include>**/*.*include>
    131. includes>
    132. resource>
    133. resources>
    134. build>

    3、 application.properties配置文件

    1. #内嵌服务器端口
    2. server.port=8081
    3. #应用服务名称
    4. spring.application.name=springcloud-alibaba-3-seata-tcc-transaction
    5. # 设置默认的数据源或者数据源组,默认值即为master
    6. spring.datasource.dynamic.primary=order-ds
    7. # 订单order数据源配置
    8. spring.datasource.dynamic.datasource.order-ds.url=jdbc:mysql://192.168.133.129:3306/orderdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
    9. spring.datasource.dynamic.datasource.order-ds.driver-class-name=com.mysql.cj.jdbc.Driver
    10. spring.datasource.dynamic.datasource.order-ds.username=root
    11. spring.datasource.dynamic.datasource.order-ds.password=123456
    12. # 商品product数据源配置
    13. spring.datasource.dynamic.datasource.product-ds.url=jdbc:mysql://192.168.133.129:3306/productdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
    14. spring.datasource.dynamic.datasource.product-ds.driver-class-name=com.mysql.cj.jdbc.Driver
    15. spring.datasource.dynamic.datasource.product-ds.username=root
    16. spring.datasource.dynamic.datasource.product-ds.password=123456
    17. # 账户account数据源配置
    18. spring.datasource.dynamic.datasource.account-ds.url=jdbc:mysql://192.168.133.129:3306/accountdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
    19. spring.datasource.dynamic.datasource.account-ds.driver-class-name=com.mysql.cj.jdbc.Driver
    20. spring.datasource.dynamic.datasource.account-ds.username=root
    21. spring.datasource.dynamic.datasource.account-ds.password=123456
    22. # 是否启动对Seata的集成
    23. spring.datasource.dynamic.seata=true
    24. #-----------------------------------------------------------
    25. #单机版 tc server 配置
    26. # Seata应用编号,默认为 ${spring.application.name}
    27. seata.application-id=springboot-tcc-seata
    28. # Seata事务组编号,用于TC集群名,一般格式为:${spring.application.name}-group
    29. seata.tx-service-group=springboot-tcc-seata-group
    30. # 虚拟组和分组的映射 seata.service.vgroup-mapping.${seata.tx-service-group}=default
    31. seata.service.vgroup-mapping.springboot-tcc-seata-group=default
    32. # 分组和Seata服务的映射,此处default指上面 seata.service.vgroup-mapping.springboot-seata-group 的值 default
    33. seata.service.grouplist.default=192.168.133.129:8091
    34. # 存储模式 默认 file模式
    35. seata.config.type=file
    36. # 默认为 file
    37. seata.registry.type=file
    38. #------------------------------------------------------------
    39. #------------------------------------------------------------
    40. ##集群版 tc server 配置
    41. ## Seata应用编号,默认为 ${spring.application.name}
    42. #seata.application-id=springboot-tcc-seata
    43. ## Seata事务组编号,用于TC集群名,一般格式为:${spring.application.name}-group
    44. #seata.tx-service-group=springboot-tcc-seata-group
    45. ## 虚拟组和分组的映射 seata.service.vgroup-mapping.${seata.tx-service-group}=default
    46. #seata.service.vgroup-mapping.springboot-tcc-seata-group=default
    47. #------------------------------------------------------------
    48. #设置使用注册中心
    49. #seata.registry.type=nacos
    50. #seata.registry.nacos.cluster=default
    51. #seata.registry.nacos.application=seata-server
    52. #seata.registry.nacos.group=SEATA_GROUP
    53. #seata.registry.nacos.server-addr=192.168.133.129:8848
    54. #
    55. #seata.enable-auto-data-source-proxy=false
    56. #seata.client.rm.lock.retry-policy-branch-rollback-on-conflict=false
    57. #配置 undo_log 的解析方式
    58. seata.client.undo.log-serialization=myJackson

    4、编写相应的 controller、model、mapper、service类,这里只给出调用顺序相关的类

     controller测试类

    1. @Slf4j //lombok
    2. @RestController
    3. public class OrderController {
    4. @Autowired
    5. private OrderService orderService;
    6. @RequestMapping("/order")
    7. public Integer createOrder(@RequestParam("userId") Integer userId,
    8. @RequestParam("productId") Integer productId) throws Exception {
    9. log.info("请求下单, 用户:{}, 商品:{}", userId, productId);
    10. return orderService.createOrder(userId, productId);
    11. }
    12. }
    13. @Slf4j
    14. @Service
    15. public class OrderServiceImpl implements OrderService {
    16. @Autowired
    17. private OrdersMapper ordersMapper;
    18. @Autowired
    19. private AccountService accountService;
    20. @Autowired
    21. private ProductService productService;
    22. @Override
    23. @DS(value = "order-ds")
    24. @GlobalTransactional //seata全局事务注解, TM 事务发起方
    25. public Integer createOrder(Integer userId, Integer productId) throws Exception {
    26. Integer amount = 1; // 购买数量暂时设置为 1
    27. log.info("当前 XID: {}", RootContext.getXID());
    28. // 减库存
    29. Product product = productService.reduceStock(productId, amount);
    30. // 减余额
    31. accountService.reduceBalance(userId, product.getPrice());
    32. // 下订单
    33. Orders order = new Orders();
    34. order.setUserId(userId);
    35. order.setProductId(productId);
    36. order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount)));
    37. ordersMapper.insertSelective(order);
    38. log.info("下订单: {}", order.getId());
    39. int a = 10 / 0;
    40. // 返回订单编号
    41. return order.getId();
    42. }
    43. }

    AccountService

    1. @LocalTCC
    2. public interface AccountService {
    3. /**
    4. * 扣除余额
    5. * 定义两阶段提交
    6. * name = reduceStock为一阶段try方法
    7. * commitMethod = commitTcc 为二阶段确认方法
    8. * rollbackMethod = cancel 为二阶段取消方法
    9. * BusinessActionContextParameter注解 可传递参数到二阶段方法
    10. *
    11. * @param userId 用户ID
    12. * @param money 扣减金额
    13. * @throws Exception 失败时抛出异常
    14. */
    15. @TwoPhaseBusinessAction(name = "reduceBalance", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    16. void reduceBalance(@BusinessActionContextParameter(paramName = "userId") Integer userId,
    17. @BusinessActionContextParameter(paramName = "money") BigDecimal money);
    18. /**
    19. * 确认方法、可以另命名,但要保证与commitMethod一致
    20. * context可以传递try方法的参数
    21. *
    22. * @param context 上下文
    23. * @return boolean
    24. */
    25. boolean commitTcc(BusinessActionContext context);
    26. /**
    27. * 二阶段取消方法
    28. *
    29. * @param context 上下文
    30. * @return boolean
    31. */
    32. boolean cancelTcc(BusinessActionContext context);
    33. }
    34. @Slf4j
    35. @Service
    36. public class AccountServiceImpl implements AccountService {
    37. @Autowired
    38. private AccountMapper accountMapper;
    39. @DS(value = "account-ds")
    40. @Override
    41. public void reduceBalance(Integer userId, BigDecimal money) {
    42. log.info("当前 XID: {}", RootContext.getXID());
    43. // 检查余额
    44. Account account = accountMapper.selectAccountByUserId(userId);
    45. if (account.getBalance().doubleValue() < money.doubleValue()) {
    46. throw new RuntimeException("余额不足");
    47. }
    48. // 扣除余额
    49. int updateCount = accountMapper.reduceBalance(userId, money);
    50. // 扣除成功
    51. if (updateCount == 0) {
    52. throw new RuntimeException("余额不足");
    53. }
    54. log.info("扣除用户 {} 余额成功", userId);
    55. //int a = 10 / 0;
    56. }
    57. /**
    58. * tcc服务(confirm)方法
    59. * 可以空确认
    60. *
    61. * @param context 上下文
    62. * @return boolean
    63. */
    64. @DS(value = "account-ds")
    65. @Override
    66. public boolean commitTcc(BusinessActionContext context) {
    67. log.info("Confirm阶段,AccountServiceImpl, commitTcc --> xid = {}", context.getXid() + ", commitTcc提交成功");
    68. return true;
    69. }
    70. /**
    71. * tcc服务(cancel)方法
    72. * 实现中间件、非关系型数据库的回滚操作
    73. * @param context 上下文
    74. * @return boolean
    75. */
    76. @DS(value = "account-ds")
    77. @Override
    78. public boolean cancelTcc(BusinessActionContext context) {
    79. log.info("Cancel阶段,AccountServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");
    80. //可以实现中间件、非关系型数据库的回滚操作
    81. log.info("Cancel阶段,AccountServiceImpl, cancelTcc this data: userId= {}, money = {}", context.getActionContext("userId"), context.getActionContext("money"));
    82. //进行数据库回滚处理
    83. Integer userId = (Integer)context.getActionContext("userId");
    84. BigDecimal money = (BigDecimal)context.getActionContext("money");
    85. //把余额再加回去
    86. accountMapper.increaseBalance(userId, money);
    87. return true;
    88. }
    89. }

    ProductService 

    1. @LocalTCC
    2. public interface ProductService {
    3. /**
    4. * 减库存
    5. *
    6. * 定义两阶段提交
    7. * name = reduceStock为一阶段try方法
    8. * commitMethod = commitTcc 为二阶段确认方法
    9. * rollbackMethod = cancel 为二阶段取消方法
    10. * BusinessActionContextParameter注解 可传递参数到二阶段方法
    11. *
    12. * @param productId 商品ID
    13. * @param amount 扣减数量
    14. * @throws Exception 扣减失败时抛出异常
    15. */
    16. @TwoPhaseBusinessAction(name = "reduceStock", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    17. Product reduceStock(@BusinessActionContextParameter(paramName = "productId") Integer productId,
    18. @BusinessActionContextParameter(paramName = "amount") Integer amount);
    19. /**
    20. * 二阶段提交方法
    21. *
    22. * 确认方法、可以另命名,但要保证与commitMethod一致
    23. * context可以传递try方法的参数
    24. *
    25. * @param context 上下文
    26. * @return boolean
    27. */
    28. boolean commitTcc(BusinessActionContext context);
    29. /**
    30. * 二阶段回滚方法
    31. *
    32. * @param context 上下文
    33. * @return boolean
    34. */
    35. boolean cancelTcc(BusinessActionContext context);
    36. }
    37. @Slf4j
    38. @Service
    39. public class ProductServiceImpl implements ProductService {
    40. @Autowired
    41. private AccountService accountService;
    42. @Autowired
    43. private ProductMapper productMapper;
    44. @DS(value = "product-ds")
    45. @Override
    46. public Product reduceStock(Integer productId, Integer amount) {
    47. log.info("当前 XID: {}", RootContext.getXID());
    48. // 检查库存
    49. Product product = productMapper.selectByPrimaryKey(productId);
    50. if (product.getStock() < amount) {
    51. throw new RuntimeException("库存不足");
    52. }
    53. // 扣减库存
    54. int updateCount = productMapper.reduceStock(productId, amount);
    55. // 扣除成功
    56. if (updateCount == 0) {
    57. throw new RuntimeException("库存不足");
    58. }
    59. // 扣除成功
    60. log.info("扣除 {} 库存成功", productId);
    61. return product;
    62. }
    63. /**
    64. * tcc服务(confirm)方法
    65. * 可以空确认
    66. *
    67. * @param context 上下文
    68. * @return boolean
    69. */
    70. @DS(value = "product-ds")
    71. @Override
    72. public boolean commitTcc(BusinessActionContext context) {
    73. log.info("Confirm阶段,ProductServiceImpl, commitTcc --> xid = " + context.getXid() + ", commitTcc提交成功");
    74. return true;
    75. }
    76. /**
    77. * tcc服务(cancel)方法
    78. *
    79. * @param context 上下文
    80. * @return boolean
    81. */
    82. @DS(value = "product-ds")
    83. @Override
    84. public boolean cancelTcc(BusinessActionContext context) {
    85. log.info("Cancel阶段,ProductServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");
    86. //实现中间件、非关系型数据库的回滚操作
    87. log.info("Cancel阶段,ProductServiceImpl, cancelTcc this data: {}, {}", context.getActionContext("productId"), context.getActionContext("amount"));
    88. //进行数据库回滚处理
    89. Integer productId = (Integer)context.getActionContext("productId");
    90. Integer amount = (Integer)context.getActionContext("amount");
    91. //把库存再加回去 (避免数据出问题,加个锁,分布式环境下就需要分布式锁)
    92. productMapper.increaseStock(productId, amount);
    93. return true;
    94. }
    95. }

    @LocalTCC注解

    标识此TCC为本地模式,即该事务是本地调用,非RPC调用

    @LocalTCC一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法即可

    @TwoPhaseBusinessAction注解

    标识为TCC模式,注解try方法,其中name为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod指提交方法,rollbackMethod指事务回滚方法,指定好三个方法之后,Seata会根据事务的成功或失败,通过动态代理去帮我们自动调用提交或者回滚

    @BusinessActionContextParameter 注解

    将参数传递到二阶段(commitMethod/rollbackMethod)的方法

    BusinessActionContext

    指TCC事务上下文,携带了业务方法的参数

    二、Spring Cloud Alibaba的TCC分布式事务

    测试应用

    1、创建 4 个SpringBoot 模块

    (1)springcloud-alibaba-3-seata-tcc-commons

    1. <groupId>com.companygroupId>
    2. <artifactId>springcloud-alibaba-3-seata-tcc-commonsartifactId>
    3. <version>1.0.0version>
    4. <dependencies>
    5. <dependency>
    6. <groupId>org.projectlombokgroupId>
    7. <artifactId>lombokartifactId>
    8. <version>1.18.16version>
    9. dependency>
    10. <dependency>
    11. <groupId>org.springframework.cloudgroupId>
    12. <artifactId>spring-cloud-starter-openfeignartifactId>
    13. <version>3.0.0version>
    14. dependency>
    15. dependencies>
    1. @FeignClient(name = "springcloud-alibaba-3-seata-tcc-account")
    2. public interface FeignAccountService {
    3. /**
    4. * 扣除余额
    5. *
    6. * @param userId 用户ID
    7. * @param money 扣减金额
    8. * @throws Exception 失败时抛出异常
    9. */
    10. @PostMapping("/account/reduceBalance")
    11. void reduceBalance(@RequestParam("userId") Integer userId, @RequestParam("money") BigDecimal money);
    12. }
    13. @FeignClient(name = "springcloud-alibaba-3-seata-tcc-order")
    14. public interface FeignOrderService {
    15. /**
    16. * 创建订单
    17. *
    18. * @param userId 用户ID
    19. * @param productId 产品ID
    20. * @return 订单编号
    21. * @throws Exception 创建订单失败,抛出异常
    22. */
    23. Integer createOrder(Integer userId, Integer productId) throws Exception;
    24. }
    25. @FeignClient(name = "springcloud-alibaba-3-seata-tcc-product")
    26. public interface FeignProductService {
    27. /**
    28. * 减库存
    29. *
    30. * @param productId 商品ID
    31. * @param amount 扣减数量
    32. * @throws Exception 扣减失败时抛出异常
    33. */
    34. @PostMapping("/product/reduceStock")
    35. Product reduceStock(@RequestParam("productId") Integer productId, @RequestParam("amount") Integer amount);
    36. }

    (2)springcloud-alibaba-3-seata-tcc-order

    1. <groupId>com.companygroupId>
    2. <artifactId>springcloud-alibaba-3-seata-tcc-orderartifactId>
    3. <version>1.0.0version>
    4. <name>springcloud-alibaba-3-seata-tcc-ordername>
    5. <description>Demo project for Spring Bootdescription>
    6. <properties>
    7. <java.version>1.8java.version>
    8. <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    9. <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
    10. <spring-boot.version>2.3.12.RELEASEspring-boot.version>
    11. <spring-cloud-alibaba.version>2.2.7.RELEASEspring-cloud-alibaba.version>
    12. properties>
    13. <dependencies>
    14. <dependency>
    15. <groupId>org.springframework.bootgroupId>
    16. <artifactId>spring-boot-starter-webartifactId>
    17. dependency>
    18. <dependency>
    19. <groupId>com.alibaba.cloudgroupId>
    20. <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    21. dependency>
    22. <dependency>
    23. <groupId>org.springframework.bootgroupId>
    24. <artifactId>spring-boot-starter-testartifactId>
    25. <scope>testscope>
    26. <exclusions>
    27. <exclusion>
    28. <groupId>org.junit.vintagegroupId>
    29. <artifactId>junit-vintage-engineartifactId>
    30. exclusion>
    31. exclusions>
    32. dependency>
    33. <dependency>
    34. <groupId>org.projectlombokgroupId>
    35. <artifactId>lombokartifactId>
    36. dependency>
    37. <dependency>
    38. <groupId>mysqlgroupId>
    39. <artifactId>mysql-connector-javaartifactId>
    40. dependency>
    41. <dependency>
    42. <groupId>org.mybatis.spring.bootgroupId>
    43. <artifactId>mybatis-spring-boot-starterartifactId>
    44. <version>2.1.3version>
    45. dependency>
    46. <dependency>
    47. <groupId>com.alibaba.cloudgroupId>
    48. <artifactId>spring-cloud-starter-alibaba-seataartifactId>
    49. <exclusions>
    50. <exclusion>
    51. <groupId>io.seatagroupId>
    52. <artifactId>seata-spring-boot-starterartifactId>
    53. exclusion>
    54. exclusions>
    55. dependency>
    56. <dependency>
    57. <groupId>io.seatagroupId>
    58. <artifactId>seata-spring-boot-starterartifactId>
    59. <version>1.4.2version>
    60. dependency>
    61. <dependency>
    62. <groupId>com.companygroupId>
    63. <artifactId>springcloud-alibaba-3-seata-tcc-commonsartifactId>
    64. <version>1.0.0version>
    65. dependency>
    66. dependencies>
    67. <dependencyManagement>
    68. <dependencies>
    69. <dependency>
    70. <groupId>com.alibaba.cloudgroupId>
    71. <artifactId>spring-cloud-alibaba-dependenciesartifactId>
    72. <version>${spring-cloud-alibaba.version}version>
    73. <type>pomtype>
    74. <scope>importscope>
    75. dependency>
    76. <dependency>
    77. <groupId>org.springframework.cloudgroupId>
    78. <artifactId>spring-cloud-dependenciesartifactId>
    79. <version>Hoxton.SR12version>
    80. <type>pomtype>
    81. <scope>importscope>
    82. dependency>
    83. <dependency>
    84. <groupId>org.springframework.bootgroupId>
    85. <artifactId>spring-boot-dependenciesartifactId>
    86. <version>${spring-boot.version}version>
    87. <type>pomtype>
    88. <scope>importscope>
    89. dependency>
    90. dependencies>
    91. dependencyManagement>
    92. <build>
    93. <plugins>
    94. <plugin>
    95. <groupId>org.apache.maven.pluginsgroupId>
    96. <artifactId>maven-compiler-pluginartifactId>
    97. <version>3.8.1version>
    98. <configuration>
    99. <source>1.8source>
    100. <target>1.8target>
    101. <encoding>UTF-8encoding>
    102. configuration>
    103. plugin>
    104. <plugin>
    105. <groupId>org.springframework.bootgroupId>
    106. <artifactId>spring-boot-maven-pluginartifactId>
    107. plugin>
    108. plugins>
    109. <resources>
    110. <resource>
    111. <directory>src/main/javadirectory>
    112. <includes>
    113. <include>**/*.xmlinclude>
    114. includes>
    115. resource>
    116. <resource>
    117. <directory>src/main/resourcesdirectory>
    118. <includes>
    119. <include>**/*.*include>
    120. includes>
    121. resource>
    122. resources>
    123. build>
    1. @Slf4j
    2. @RestController
    3. public class OrderController {
    4. @Autowired
    5. private OrderService orderService;
    6. /**
    7. * 下单操作,发起分布式事务
    8. *
    9. * @param userId
    10. * @param productId
    11. * @return
    12. * @throws Exception
    13. */
    14. @RequestMapping("/order")
    15. public Integer createOrder(@RequestParam("userId") Integer userId,
    16. @RequestParam("productId") Integer productId) throws Exception {
    17. log.info("[createOrder] 请求下单, 用户:{}, 商品:{}", userId, productId);
    18. return orderService.createOrder(userId, productId);
    19. }
    20. }
    21. @Slf4j
    22. @Service
    23. public class OrderServiceImpl implements OrderService {
    24. @Autowired
    25. private OrdersMapper ordersMapper;
    26. @Autowired
    27. private FeignProductService feignProductService;
    28. @Autowired
    29. private FeignAccountService feignAccountService;
    30. @Override
    31. @GlobalTransactional //seata全局事务注解, TM 分布式全局事务发起者
    32. public Integer createOrder(Integer userId, Integer productId) {
    33. Integer amount = 1; // 购买数量,暂时设为 1
    34. log.info("[createOrder] 当前 XID: {}", RootContext.getXID());
    35. // 减库存 (feign的调用) http远程调用
    36. Product product = feignProductService.reduceStock(productId, amount);
    37. // 减余额
    38. feignAccountService.reduceBalance(userId, product.getPrice());
    39. // 下订单
    40. Orders order = new Orders();
    41. order.setUserId(userId);
    42. order.setProductId(productId);
    43. order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount)));
    44. ordersMapper.insertSelective(order);
    45. log.info("[createOrder] 下订单: {}", order.getId());
    46. int a = 10 / 0;
    47. // 返回订单编号
    48. return order.getId();
    49. }
    50. }

    (3)springcloud-alibaba-3-seata-tcc-product

    1. @LocalTCC
    2. public interface ProductService {
    3. /**
    4. * 减库存
    5. *
    6. * 定义两阶段提交
    7. * name = reduceStock为一阶段try方法
    8. * commitMethod = commitTcc 为二阶段确认方法
    9. * rollbackMethod = cancel 为二阶段取消方法
    10. * BusinessActionContextParameter注解 可传递参数到二阶段方法
    11. *
    12. * @param productId 商品ID
    13. * @param amount 扣减数量
    14. * @throws Exception 扣减失败时抛出异常
    15. */
    16. @TwoPhaseBusinessAction(name = "reduceStock", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    17. Product reduceStock(@BusinessActionContextParameter(paramName = "productId") Integer productId,
    18. @BusinessActionContextParameter(paramName = "amount") Integer amount);
    19. /**
    20. * 确认方法、可以另命名,但要保证与commitMethod一致
    21. * context可以传递try方法的参数
    22. *
    23. * @param context 上下文
    24. * @return boolean
    25. */
    26. boolean commitTcc(BusinessActionContext context);
    27. /**
    28. * 二阶段取消方法
    29. *
    30. * @param context 上下文
    31. * @return boolean
    32. */
    33. boolean cancelTcc(BusinessActionContext context);
    34. }
    35. @Slf4j
    36. @Service
    37. public class ProductServiceImpl implements ProductService {
    38. @Autowired
    39. private ProductMapper productMapper;
    40. /**
    41. * tcc服务(try)方法
    42. * 也是实际业务方法
    43. *
    44. * @param productId 商品ID
    45. * @param amount 扣减数量
    46. * @return
    47. */
    48. @Override
    49. public Product reduceStock(Integer productId, Integer amount) {
    50. log.info("[reduceStock] 当前 XID: {}", RootContext.getXID());
    51. // 检查库存
    52. Product product = productMapper.selectByPrimaryKey(productId);
    53. if (product.getStock() < amount) {
    54. throw new RuntimeException("库存不足");
    55. }
    56. // 减库存
    57. int updateCount = productMapper.reduceStock(productId, amount);
    58. // 减库存失败
    59. if (updateCount == 0) {
    60. throw new RuntimeException("库存不足");
    61. }
    62. // 减库存成功
    63. log.info("减库存 {} 库存成功", productId);
    64. return product;
    65. }
    66. /**
    67. * tcc服务(confirm)方法
    68. * 可以空确认
    69. *
    70. * @param context 上下文
    71. * @return boolean
    72. */
    73. @Override
    74. public boolean commitTcc(BusinessActionContext context) {
    75. log.info("Confirm阶段,ProductServiceImpl, commitTcc --> xid = " + context.getXid() + ", commitTcc提交成功");
    76. return true;
    77. }
    78. /**
    79. * tcc服务(cancel)方法
    80. *
    81. * @param context 上下文
    82. * @return boolean
    83. */
    84. @Override
    85. public boolean cancelTcc(BusinessActionContext context) {
    86. log.info("Cancel阶段,ProductServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");
    87. //TODO 这里可以实现中间件、非关系型数据库的回滚操作
    88. log.info("Cancel阶段,ProductServiceImpl, cancelTcc this data: {}, {}", context.getActionContext("productId"), context.getActionContext("amount"));
    89. //进行数据库回滚处理
    90. Integer productId = (Integer)context.getActionContext("productId");
    91. Integer amount = (Integer)context.getActionContext("amount");
    92. //把库存再加回去
    93. productMapper.increaseStock(productId, amount);
    94. return true;
    95. }
    96. }

    (4)springcloud-alibaba-3-seata-tcc-account

    1. @LocalTCC
    2. public interface AccountService {
    3. /**
    4. * 扣除余额
    5. * 定义两阶段提交
    6. * name = reduceStock为一阶段try方法
    7. * commitMethod = commitTcc 为二阶段确认方法
    8. * rollbackMethod = cancel 为二阶段取消方法
    9. * BusinessActionContextParameter注解 可传递参数到二阶段方法
    10. *
    11. * @param userId 用户ID
    12. * @param money 扣减金额
    13. * @throws Exception 失败时抛出异常
    14. */
    15. @TwoPhaseBusinessAction(name = "reduceBalance", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
    16. void reduceBalance(@BusinessActionContextParameter(paramName = "userId") Integer userId,
    17. @BusinessActionContextParameter(paramName = "money") BigDecimal money);
    18. /**
    19. * 确认方法、可以另命名,但要保证与commitMethod一致
    20. * context可以传递try方法的参数
    21. *
    22. * @param context 上下文
    23. * @return boolean
    24. */
    25. boolean commitTcc(BusinessActionContext context);
    26. /**
    27. * 二阶段取消方法
    28. *
    29. * @param context 上下文
    30. * @return boolean
    31. */
    32. boolean cancelTcc(BusinessActionContext context);
    33. }
    34. @Slf4j
    35. @Service
    36. public class AccountServiceImpl implements AccountService {
    37. @Autowired
    38. private AccountMapper accountMapper;
    39. @Override
    40. public void reduceBalance(Integer userId, BigDecimal money) {
    41. log.info("[reduceBalance] 当前 XID: {}", RootContext.getXID());
    42. // 检查余额
    43. Account account = accountMapper.selectAccountByUserId(userId);
    44. if (account.getBalance().doubleValue() < money.doubleValue()) {
    45. throw new RuntimeException("余额不足");
    46. }
    47. // 扣除余额
    48. int updateCount = accountMapper.reduceBalance(userId, money);
    49. // 扣除成功
    50. if (updateCount == 0) {
    51. throw new RuntimeException("余额不足");
    52. }
    53. log.info("[reduceBalance] 扣除用户 {} 余额成功", userId);
    54. }
    55. /**
    56. * tcc服务(confirm)方法
    57. * 可以空确认
    58. *
    59. * @param context 上下文
    60. * @return boolean
    61. */
    62. @Override
    63. public boolean commitTcc(BusinessActionContext context) {
    64. log.info("Confirm阶段,AccountServiceImpl, commitTcc --> xid = {}", context.getXid() + ", commitTcc提交成功");
    65. return true;
    66. }
    67. /**
    68. * tcc服务(cancel)方法
    69. *
    70. * @param context 上下文
    71. * @return boolean
    72. */
    73. @Override
    74. public boolean cancelTcc(BusinessActionContext context) {
    75. log.info("Cancel阶段,AccountServiceImpl, cancelTcc --> xid = " + context.getXid() + ", cancelTcc提交失败");
    76. //TODO 这里可以实现中间件、非关系型数据库的回滚操作
    77. log.info("Cancel阶段,AccountServiceImpl, cancelTcc this data: userId= {}, money = {}", context.getActionContext("userId"), context.getActionContext("money"));
    78. //进行数据库回滚处理
    79. Integer userId = (Integer)context.getActionContext("userId");
    80. BigDecimal money = (BigDecimal)context.getActionContext("money");
    81. //幂等性问题
    82. //把余额再加回去
    83. accountMapper.increaseBalance(userId, money);
    84. return true;
    85. }
    86. }
  • 相关阅读:
    PY32F002A系列单片机:高性价比、低功耗,满足多样化应用需求
    【Linux-Windows】简述IPv4子网掩码网关和DNS
    Super easy to understand decision trees (part one)
    FILE类与IO流
    SpringBoot笔记:SpringBoot集成MyBatisPlus、Sqlite实战
    CSP-S初赛基础知识整理
    uni-app:js实现数组中的相关处理
    【Hadoop】学习笔记(四)
    代码随想录算法训练营第46天| 单词拆分,背包问题总结
    【若依(ruoyi)】ztree初始化
  • 原文地址:https://blog.csdn.net/MinggeQingchun/article/details/126240085