Spring Cloud整合Seata实现TCC分布式事务模式案例
创始人
2024-04-05 04:21:49
0

文章目录

  • 一、前言
  • 二、TCC介绍
    • 1、TCC解决方案开源组件
    • 2、seata-tcc
  • 三、Seata实现TCC案例
    • 1、表结构和项目搭建
    • 2、常用注解和类
        • 1)@TwoPhaseBusinessAction
        • 2)@LocalTCC
        • 3)@BusinessActionContextParameter
        • 4)BusinessActionContext
    • 3、具体代码
      • 0)Spring Cloud Alibaba版本
      • 1)tcc-order
        • 1> pom.xml
        • 2> OrderController
        • 3> Order
        • 4> StockFeignClient
        • 5> OrderDAO
        • 6> OrderMapper
        • 7> OrderService
        • 8> OrderServiceImpl
        • 9> OrderApplication
        • 10> application.yml
        • 11> file.conf
      • 2)tcc-stock
        • 1> pom.xml
        • 2> StockController
        • 3> Stock
        • 4> StockDAO
        • 5> StockMapper
        • 6> StockService
        • 7> StockServiceImpl
        • 8> StockApplication
        • 9> application.yml
        • 10> file.conf
    • 4、TCC模式分布式事务效果演示
      • 1)请求正常
      • 2)请求异常
    • 5、JPA和Seata-TCC集成的坑
      • 原因分析
  • 四、TCC的一些问题
    • 1、空回滚
    • 2、幂等
    • 3、悬挂
  • 五、总结

一、前言

更多内容见Seata专栏:https://blog.csdn.net/saintmm/category_11953405.html

至此,seata系列的内容已出:

  1. can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;
  2. Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
  3. Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版
  4. 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)
  5. 【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】
  6. 【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server
  7. 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么
  8. 【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
  9. 【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信
  10. 【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务
  11. 【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?
  12. 【微服务39】分布式事务Seata源码解析七:图解Seata事务执行流程之开启全局事务
  13. 分布式事务Seata源码解析八:本地事务执行流程(AT模式下)
  14. 分布式事务Seata源码解析九:分支事务如何注册到全局事务
  15. 分布式事务Seata源码解析十:AT模式回滚日志undo log详细构建过程
  16. 分布式事务Seata源码解析11:全局事务执行流程之两阶段全局事务提交
  17. 分布式事务Seata源码解析12:全局事务执行流程之全局事务回滚

二、TCC介绍

TCC是Try-Confirm-Cancel的简称。TCC要求每个分支事务包含三个操作:预处理Try、确认 提交Confirm、回滚Cancel;

  • 预处理Try,做一些业务检查、资源预留/冻结;它于随后的confirm 一起构成一个完成的业务逻辑;
  • 确认提交Confirm,所有分支事务都预处理try成功之后,开始执行confirm。Confirm中有一个潜在规则:只要Try成功,Confirm一定要成功;假如Confirm阶段出错了,需要通过异常重试、补偿措施 或 人工处理保障confirm的成功
  • 回滚Cancel,只要有一个分支事务预处理try不成功,就要对所有的分支事务做回滚操作;做预留资源的释放。这里也有一个潜规则:只要Try失败,Cancel一定要成功;假如Cancel阶段出错了,需要通过异常重试、补偿措施 或 人工处理保障Cancel的成功

分布式的全局事务,整体 都是 两阶段提交;TCC自然也是 两阶段提交。但TCC不同于XA协议那种是资源层面的分布式事务,数据强一致性,在整个两阶段提交过程中,需要一直持有资源的锁;其只是业务层面的分布式事务,数据呈现最终一致性,仅会在某一个阶段短暂持有资源的锁,而不会在整个两个阶段过程中一直持有。

1、TCC解决方案开源组件

  • tcc-transaction
  • ByteTCC
  • hmily
  • LCN
  • seata

目前seata社区活跃度最高、版本一直在迭代。

2、seata-tcc

一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

在这里插入图片描述

TCC 模式,不依赖于底层数据资源的事务支持:

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

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

三、Seata实现TCC案例

本文使用当下最新的seata版本1.5.2,该版本Spring Cloud Alibaba并未支持,并且改动后近支持从File读取配置的方式;用Nacos等配置中心会出现版本兼容问题,很多类找不到。

使用其他seata版本都是一样的操作,就TCC使用方式而言没有任何区别。

案例的业务操作纯属为了简单演示,请勿往真实业务场景联想!!!

案例中的demo见资源(资源审核通过后补充连接到此处):
在这里插入图片描述

1、表结构和项目搭建

在这里插入图片描述

TCC模式案例我们做简化处理,仅采用Order和Stock两个模块,其中Order作为全局事务的发起者 / 参与者,Stock仅作为全局事务的参与者(分支事务)。

具体表结构,参考博文:Spring Cloud整合Seata、Nacos实现分布式事务案例,undo_log表示AT模式下的,所以TCC模式并不会使用到。

2、常用注解和类

1)@TwoPhaseBusinessAction

在这里插入图片描述

该注解用在接口的 Try 方法上。其中三个方法最重要:

  • name(),该tcc的bean名称,全局唯一;
  • commitMethod(), 二阶段确认方法(默认方法名commit);
  • rollbackMethod(), 二阶段取消方法(默认方法名rollback);

2)@LocalTCC

@LocalTCC 适用于SpringCloud+Feign模式下的TCC。

该注解需要添加到 Try 方法所在的接口上,表示实现该接口的类被 seata 来管理,seata 根据事务的状态,自动调用我们定义的方法;如果try()没问题则调用 commit() 方法,否则调用 rollback() 方法。

3)@BusinessActionContextParameter

该注解用来修饰 try() 方法的入参,被修饰的入参可以在 commit() 方法和 rollback() 方法中通过 BusinessActionContext 获取。

4)BusinessActionContext

BusinessActionContext 是 seata tcc 的事务上下文,用于存放 tcc 事务的一些数据;

在接口 commit() 方法和 rollback() 方法的实现代码中,可以通过 BusinessActionContext 来获取参数;Seata 会自动注入参数。

3、具体代码

0)Spring Cloud Alibaba版本

tcc-order和tcc-stock模块所属的maven 父Module的pom.xml文件如下:


4.0.0org.springframework.bootspring-boot-starter-parent2.3.12.RELEASE tcc-ordertcc-stockcom.sainttransaction-seata0.0.1-SNAPSHOTtransaction-seatatransaction-seatapom1.82.3.12.RELEASEHoxton.SR122.2.8.RELEASE1.2.88.0.22org.apache.commonscommons-lang33.12.0org.projectlomboklomboktruecom.alibabafastjson2.0.10org.springframework.bootspring-boot-dependencies${spring-boot.version}pomimportorg.springframework.cloudspring-cloud-dependencies${spring-cloud.version}pomimportcom.alibaba.cloudspring-cloud-alibaba-dependencies${spring-cloud-alibaba.version}pomimportcom.alibabadruid-spring-boot-starter${druid.version}mysqlmysql-connector-java${mysql.version}org.springframework.bootspring-boot-maven-plugin${spring-boot.version}org.projectlomboklombok

1)tcc-order

在这里插入图片描述

整体包括:pom.xml、一个Controller、一个entity、一个Dao、一个Mapper、一个Service、一个ServiceImpl、一个Controller、一个启动类;resources目录下两个配置文件:application.yml、file.conf

1> pom.xml


transaction-seatacom.saint0.0.1-SNAPSHOT4.0.0tcc-order88org.mybatis.spring.bootmybatis-spring-boot-starter2.2.2org.springframework.bootspring-boot-starter-data-redisorg.springframework.cloudspring-cloud-loadbalancerorg.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-testtestmysqlmysql-connector-javaruntimecom.alibabadruid-spring-boot-starterorg.springframework.bootspring-boot-starter-data-jpaorg.springframework.cloudspring-cloud-starter-openfeigncom.alibaba.cloudspring-cloud-starter-alibaba-seataio.seataseata-allio.seataseata-all1.5.2

2> OrderController

这里要通过@GlobalTransactional注解开启全局事务,TCC的注解仅用于分支事务。

package com.saint.order.controller;import com.saint.order.service.impl.OrderServiceImpl;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author Saint*/
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderController {private final OrderServiceImpl orderService;/*** 127.0.0.1:9021/create/commit?userId=1001&commodityCode=2001&count=1*/@GetMapping("/create/commit")@GlobalTransactional // 开启全局事务public Boolean create(String userId, String commodityCode, Integer count) {orderService.create(userId, commodityCode, count);return true;}/*** 127.0.0.1:9021/create/rollback?userId=1001&commodityCode=2001&count=1*/@GetMapping("/create/rollback")@GlobalTransactional // 开启全局事务public Boolean createRollback(String userId, String commodityCode, Integer count) {orderService.create(userId, commodityCode, count);// 异常int i = 1 / 0;return true;}}

3> Order

package com.saint.order.entity;import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;import javax.persistence.*;
import java.math.BigDecimal;/*** @author Saint*/
@Entity
@Table(name = "order_tbl")
@DynamicUpdate
@DynamicInsert
@NoArgsConstructor
@Data
public class Order {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "user_id")private String userId;@Column(name = "commodity_code")private String commodityCode;@Column(name = "money")private BigDecimal money;@Column(name = "count")private Integer count;}

4> StockFeignClient

StockFeignClient用于通过OpenFeign调用tcc-stock;

package com.saint.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;/*** @author Saint*/
// 指定url的FeignClient在file作为注册中心时使用
@FeignClient(name = "stock-service", url = "127.0.0.1:9011")
//@FeignClient(name = "stock-service")
public interface StockFeignClient {@GetMapping("/deduct")void deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);}

5> OrderDAO

package com.saint.order.repository;import com.saint.order.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.transaction.annotation.Transactional;/*** @author Saint*/
public interface OrderDAO extends JpaRepository, JpaSpecificationExecutor {@Transactionalvoid deleteByUserIdAndCommodityCode(String userId, String commodityCode);}

6> OrderMapper

package com.saint.order.repository;import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;/*** @author Saint*/
@Mapper
public interface OrderMapper {@Delete("delete from order_tbl where commodity_code = #{commodityCode} and user_id = #{userId}")Integer deleteOrderByCodeAndUserId(@Param("commodityCode") String commodityCode, @Param("userId") Integer userId);}

7> OrderService

package com.saint.order.service;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;/*** 使用SpringCloud的远程调用(Feign)必须要加上@LocalTCC注解** @author Saint*/
@LocalTCC
public interface OrderService {/*** 自定义两阶段提交* 

* name = 该tcc的bean名称,全局唯一* commitMethod 二阶段确认方法(默认方法名commit)* rollbackMethod 二阶段取消方法(默认方法名rollback)** @BusinessActionContextParameter注解 传递参数到二阶段中*/@TwoPhaseBusinessAction(name = "createOrder", commitMethod = "commitCreate", rollbackMethod = "rollbackCreate")void create(@BusinessActionContextParameter(paramName = "userId") String userId,@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,@BusinessActionContextParameter(paramName = "orderCount") Integer orderCount);/*** 自定义二阶段确认方法** @param context TCC上下文* @return*/boolean commitCreate(BusinessActionContext context);/*** 自定义二阶段取消方法** @param context TCC上下文* @return*/boolean rollbackCreate(BusinessActionContext context); }

8> OrderServiceImpl

package com.saint.order.service.impl;import com.alibaba.fastjson.JSONObject;
import com.saint.order.entity.Order;
import com.saint.order.feign.StockFeignClient;
import com.saint.order.repository.OrderDAO;
import com.saint.order.repository.OrderMapper;
import com.saint.order.service.OrderService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;/*** @author Saint*/
@Service
@Slf4j
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderServiceImpl implements OrderService {private final StockFeignClient stockFeignClient;private final OrderDAO orderDAO;private final OrderMapper orderMapper;@Autowiredprivate RedisTemplate redisTemplate;@Transactionalpublic void create(String userId, String commodityCode, Integer orderCount) {log.info("start TCC, xid = " + RootContext.getXID());BigDecimal orderMoney = new BigDecimal(orderCount).multiply(new BigDecimal(5));Order order = new Order();order.setUserId(userId);order.setCommodityCode(commodityCode);order.setCount(orderCount);order.setMoney(orderMoney);Order savedOrder = orderDAO.save(order);stockFeignClient.deduct(commodityCode, orderCount);// 在redis中保存订单详情!redisTemplate.opsForValue().set("order::", JSONObject.toJSONString(savedOrder));}@Override@Transactionalpublic boolean commitCreate(BusinessActionContext context) {log.info("start commit, xid = " + context.getXid());// todo 如果一阶段是资源预留,这里则要提交资源return true;}@Override@Transactionalpublic boolean rollbackCreate(BusinessActionContext context) {log.info("start rollback, xid = {}, context = {}", context.getXid(), JSONObject.toJSONString(context));// todo 如果一阶段是资源预留,这里则要释放资源、做非关系型数据库的回滚操作;//  对于MQ等中间件的回滚 要依赖try执行之前的BusinessActionContext数据(方法的入参),因为无法直接获取到try执行时产生的数据String commodityCode = context.getActionContext("commodityCode").toString();String userId = context.getActionContext("userId").toString();log.info("commodityCode = {}, userId = {}", commodityCode, userId);
//        orderDAO.deleteByUserIdAndCommodityCode(userId, commodityCode);Integer deleteRows = orderMapper.deleteOrderByCodeAndUserId(commodityCode, Integer.valueOf(userId));log.info("delete rows: {}", deleteRows);redisTemplate.delete("order::");return true;}}

9> OrderApplication

package com.saint.order;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;/*** @author Saint*/
@SpringBootApplication
@EnableFeignClients
@EnableJpaRepositories
public class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}}

10> application.yml

server:port: 9021
spring:application:name: order-servicedatasource:druid:url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=trueusername: rootpassword: 123456driverClassName: com.mysql.cj.jdbc.DriverinitialSize: 5minIdle: 5maxActive: 20maxWait: 10000testOnBorrow: truetestOnReturn: falsetimeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000jpa:show-sql: truedatabase-platform: org.hibernate.dialect.MySQL5Dialectredis:host: 127.0.0.1port: 6379# 超时时间 单位:毫秒timeout: 50000
#    password: 123456seata:# 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致tx-service-group: saint-trade-tx-grouplogging:level:com.saint.order.repository: trace

11> file.conf

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true# the client batch send request enableenableClientBatchSendRequest = true#thread factory for nettythreadFactory {bossThreadPrefix = "NettyBoss"workerThreadPrefix = "NettyServerNIOWorker"serverExecutorThread-prefix = "NettyServerBizHandler"shareBossWorker = falseclientSelectorThreadPrefix = "NettyClientSelector"clientSelectorThreadSize = 1clientWorkerThreadPrefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTbossThreadSize = 1#auto default pin or 8workerThreadSize = "default"}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}
service {#transaction service group mappingvgroupMapping.saint-trade-tx-group = "seata-server-sh"#only support when registry.type=file, please don't set multiple addressesseata-server-sh.grouplist = "127.0.0.1:8091"#degrade, current not supportenableDegrade = false#disable seatadisableGlobalTransaction = false
}client {rm {asyncCommitBufferLimit = 10000lock {retryInterval = 10retryTimes = 30retryPolicyBranchRollbackOnConflict = true}reportRetryCount = 5tableMetaCheckEnable = falsereportSuccessEnable = false}tm {commitRetryCount = 5rollbackRetryCount = 5}undo {dataValidation = truelogSerialization = "jackson"logTable = "undo_log"}log {exceptionRate = 100}
}

2)tcc-stock

在这里插入图片描述

整体包括:pom.xml、一个Controller、一个entity、一个Dao、一个Mapper、一个Service、一个ServiceImpl、一个Controller、一个启动类;resources目录下两个配置文件:application.yml、file.conf

1> pom.xml


transaction-seatacom.saint0.0.1-SNAPSHOT4.0.0tcc-stock88org.mybatis.spring.bootmybatis-spring-boot-starter2.2.2org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-testtestmysqlmysql-connector-javaruntimecom.alibabadruid-spring-boot-starterorg.springframework.bootspring-boot-starter-data-jpaorg.springframework.cloudspring-cloud-starter-openfeigncom.alibaba.cloudspring-cloud-starter-alibaba-seataio.seataseata-allio.seataseata-all1.5.2

2> StockController

这里要通过@GlobalTransactional注解开启全局事务,TCC的注解仅用于分支事务。

package com.saint.stock.controller;import com.saint.stock.service.StockService;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;/*** @author Saint*/
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockController {private final StockService stockService;@GetMapping(path = "/deduct")public Boolean deduct(String commodityCode, Integer count) {stockService.deduct(commodityCode, count);return true;}@GetMapping(path = "/tt")public String testRollback() {Map map = new HashMap<>();map.put("commodityCode", "2001");map.put("count", 10000);BusinessActionContext context = new BusinessActionContext("xid_", "123213321", map);stockService.rollbackDeduct(context);return "success";}
}

3> Stock

package com.saint.stock.entity;import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;/*** @author Saint*/
@Entity
@Table(name = "stock_tbl")
@DynamicUpdate
@DynamicInsert
@Data
public class Stock {@Idprivate Long id;private String commodityCode;private Integer count;
}

4> StockDAO

package com.saint.stock.repository;import com.saint.stock.entity.Stock;
import org.springframework.data.jpa.repository.JpaRepository;/*** @author Saint*/
public interface StockDAO extends JpaRepository {Stock findByCommodityCode(String commodityCode);}

5> StockMapper

package com.saint.stock.repository;import com.saint.stock.entity.Stock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;/*** @author Saint*/
@Mapper
public interface StockMapper {@Update("update stock_tbl set count = #{count} where id = #{id}")Integer updateStockCount(Stock stock);@Select("select * from stock_tbl where commodity_code = #{commodityCode}")Stock queryStockByCode(@Param("commodityCode") String commodityCode);
}

6> StockService

package com.saint.stock.service;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;/*** 使用SpringCloud的远程调用(Feign)必须要加上@LocalTCC注解** @author Saint*/
@LocalTCC
public interface StockService {/*** 自定义两阶段提交* 

* name = 该tcc的bean名称,全局唯一* commitMethod 二阶段确认方法(默认方法名commit)* rollbackMethod 二阶段取消方法(默认方法名rollback)** @BusinessActionContextParameter注解 传递参数到二阶段中*/@TwoPhaseBusinessAction(name = "deductStock", commitMethod = "commitDeduct", rollbackMethod = "rollbackDeduct")void deduct(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,@BusinessActionContextParameter(paramName = "count") Integer count);/*** 自定义二阶段确认方法** @param context TCC上下文* @return*/boolean commitDeduct(BusinessActionContext context);/*** 自定义二阶段取消方法** @param context TCC上下文* @return*/boolean rollbackDeduct(BusinessActionContext context); }

7> StockServiceImpl

package com.saint.stock.service.impl;import com.alibaba.fastjson.JSONObject;
import com.saint.stock.entity.Stock;
import com.saint.stock.repository.StockDAO;
import com.saint.stock.repository.StockMapper;
import com.saint.stock.service.StockService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;/*** @author Saint*/
@Service
@Slf4j
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockServiceImpl implements StockService {private final StockDAO stockDAO;private final StockMapper stockMapper;@Override@Transactionalpublic void deduct(String commodityCode, Integer count) {log.info("start TCC, xid = " + RootContext.getXID());Stock stock = stockDAO.findByCommodityCode(commodityCode);stock.setCount(stock.getCount() - count);stockDAO.save(stock);}@Override@Transactionalpublic boolean commitDeduct(BusinessActionContext context) {log.info("start commit, xid = " + context.getXid());// todo 如果一阶段是资源预留,这里则要提交资源return true;}@Override@Transactionalpublic boolean rollbackDeduct(BusinessActionContext context) {log.info("start rollback, xid = {}, context = {}", context.getXid(), JSONObject.toJSONString(context));// todo 如果一阶段是资源预留,这里则要释放资源、做非关系型数据库的回滚操作;//  对于MQ等中间件的回滚 要依赖try执行之前的BusinessActionContext数据(方法的入参),因为无法直接获取到try执行时产生的数据String commodityCode = context.getActionContext("commodityCode").toString();String count = context.getActionContext("count").toString();log.info("commodityCode = {}, count = {}", commodityCode, count);// todo jpa,使用有问题,原因在于seata TCC中的cancel通过反射调用,update/delete操作对应的SQL都没有执行!!。走正常的Spring容器下列方法没有问题
//        Stock stock = stockDAO.findByCommodityCode(commodityCode);
//        stock.setCount(stock.getCount() + Integer.valueOf(count));
//        stockDAO.save(stock);// mybatisStock stock = stockMapper.queryStockByCode(commodityCode);stock.setCount(stock.getCount() + Integer.valueOf(count));stockMapper.updateStockCount(stock);return true;}
}

8> StockApplication

package com.saint.stock;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;/*** @author Saint*/
@SpringBootApplication
@EnableJpaRepositories
public class StockApplication {public static void main(String[] args) {SpringApplication.run(StockApplication.class, args);}
}

9> application.yml

server:port: 9011
spring:application:name: stock-servicedatasource:druid:url: jdbc:mysql://127.0.0.1:3306/seata_stock?useUnicode=trueusername: rootpassword: 123456driverClassName: com.mysql.cj.jdbc.DriverinitialSize: 5minIdle: 5maxActive: 20maxWait: 10000testOnBorrow: truetestOnReturn: falsetimeBetweenEvictionRunsMillis: 60000minEvictableIdleTimeMillis: 300000jpa:show-sql: trueseata:# 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致tx-service-group: saint-trade-tx-grouplogging:level:com.saint.stock.repository: trace

10> file.conf

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true# the client batch send request enableenableClientBatchSendRequest = true#thread factory for nettythreadFactory {bossThreadPrefix = "NettyBoss"workerThreadPrefix = "NettyServerNIOWorker"serverExecutorThread-prefix = "NettyServerBizHandler"shareBossWorker = falseclientSelectorThreadPrefix = "NettyClientSelector"clientSelectorThreadSize = 1clientWorkerThreadPrefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTbossThreadSize = 1#auto default pin or 8workerThreadSize = "default"}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}
service {#transaction service group mappingvgroupMapping.saint-trade-tx-group = "seata-server-sh"#only support when registry.type=file, please don't set multiple addressesseata-server-sh.grouplist = "127.0.0.1:8091"#degrade, current not supportenableDegrade = false#disable seatadisableGlobalTransaction = false
}client {rm {asyncCommitBufferLimit = 10000lock {retryInterval = 10retryTimes = 30retryPolicyBranchRollbackOnConflict = true}reportRetryCount = 5tableMetaCheckEnable = falsereportSuccessEnable = false}tm {commitRetryCount = 5rollbackRetryCount = 5}undo {dataValidation = truelogSerialization = "jackson"logTable = "undo_log"}log {exceptionRate = 100}
}

4、TCC模式分布式事务效果演示

分别启动tcc-order、tcc-stock

1)请求正常

分布式事务成功,模拟正常下单、扣库存

Order控制台输出:
在这里插入图片描述

Stock控制台输出:
在这里插入图片描述

请求访问:http://127.0.0.1:9001/purchase/commit

2)请求异常

分布式事务失败,模拟下单成功、扣库存成功、回到tcc-order继续执行业务逻辑时抛出异常,最终同时回滚。

Order控制台输出:
在这里插入图片描述

Stock控制台输出:
在这里插入图片描述

请求访问:http://127.0.0.1:9001/purchase/rollback;

5、JPA和Seata-TCC集成的坑

TCC回滚的时候是通过反射找到相应类和类方法,在回滚时如果使用JPA对数据做Update/Delete/Insert时的SQL语句都没有执行。

比如,SQL应该为:

Hibernate: select order0_.id as id1_0_, order0_.commodity_code as commodit2_0_, order0_.count as count3_0_, order0_.money as money4_0_, order0_.user_id as user_id5_0_ from order_tbl order0_ where order0_.user_id=? and order0_.commodity_code=?
Hibernate: delete from order_tbl where id=?
Hibernate: select stock0_.id as id1_0_, stock0_.commodity_code as commodit2_0_, stock0_.count as count3_0_ from stock_tbl stock0_ where stock0_.commodity_code=?
Hibernate: update stock_tbl set count=? where id=?

但实际为:
在这里插入图片描述
在这里插入图片描述

原因分析

seata TCC中的cancel()、commit()方法是通过反射调用,update/delete/insert操作对应的SQL都没执行!!走正常的Spring容器执行相应操作则没有问题。

在使用JDBC、Mybatis时也均没有问题。

推测是因为Spring Data JPA对类动态代理的问题。

四、TCC的一些问题

空回滚、幂等、悬挂。seata1.5.1版本已经进行了兼容处理,具体问题成因和解决方案见下一篇文章。

1、空回滚

try()未执行,Cancel()执行了;

2、幂等

多次执行了Cancel()、Confirm()方法;

3、悬挂

Cancel()比try()先执行;

五、总结

1> TCC优点:

  • 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC差;其属于最终一致性。
  • 性能好一些,相比于XA,整个两阶段提交过程不会一直占用数据库资源。

2> TCC缺点:

  • TCC模型对业务的侵入性太强;事务回滚严重依赖于代码编写,代码量很大、很难维护。
  • 一般来说支付、交易等核心业务场景,可能会用TCC来严格保证分布式事务的一致性,所有分支事务要么全部成功,要么全部回滚。
  • 一般的业务场景下,尽量别用TCC作为分布式事务的解决方案;因为自己手写回滚/补偿逻辑,会造成业务代码臃肿且很难维护。

相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...