更多内容见Seata专栏:https://blog.csdn.net/saintmm/category_11953405.html
至此,seata系列的内容已出:
- can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决;
- Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
- Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版
- 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)
- 【微服务 32】Spring Cloud整合Seata、Nacos实现分布式事务案例(巨细排坑版)【云原生】
- 【微服务33】分布式事务Seata源码解析一:在IDEA中启动Seata Server
- 【微服务34】分布式事务Seata源码解析二:Seata Server启动时都做了什么
- 【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
- 【微服务36】分布式事务Seata源码解析四:图解Seata Client 如何与Seata Server建立连接、通信
- 【微服务37】分布式事务Seata源码解析五:@GlobalTransactional如何开启全局事务
- 【微服务38】分布式事务Seata源码解析六:全局/分支事务分布式ID如何生成?序列号超了怎么办?时钟回拨问题如何处理?
- 【微服务39】分布式事务Seata源码解析七:图解Seata事务执行流程之开启全局事务
- 分布式事务Seata源码解析八:本地事务执行流程(AT模式下)
- 分布式事务Seata源码解析九:分支事务如何注册到全局事务
- 分布式事务Seata源码解析十:AT模式回滚日志undo log详细构建过程
- 分布式事务Seata源码解析11:全局事务执行流程之两阶段全局事务提交
- 分布式事务Seata源码解析12:全局事务执行流程之全局事务回滚
TCC是Try-Confirm-Cancel
的简称。TCC要求每个分支事务包含三个操作:预处理Try、确认 提交Confirm、回滚Cancel;
分布式的全局事务,整体 都是 两阶段提交;TCC自然也是 两阶段提交。但TCC不同于XA协议那种是资源层面的分布式事务,数据强一致性,在整个两阶段提交过程中,需要一直持有资源的锁;其只是业务层面的分布式事务,数据呈现最终一致性,仅会在某一个阶段短暂持有资源的锁,而不会在整个两个阶段过程中一直持有。
目前seata社区活跃度最高、版本一直在迭代。
一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
TCC 模式,不依赖于底层数据资源的事务支持:
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
本文使用当下最新的seata版本1.5.2,该版本Spring Cloud Alibaba并未支持,并且改动后近支持从File读取配置的方式;用Nacos等配置中心会出现版本兼容问题,很多类找不到。
使用其他seata版本都是一样的操作,就TCC使用方式而言没有任何区别。
案例的业务操作纯属为了简单演示,请勿往真实业务场景联想!!!
案例中的demo见资源(资源审核通过后补充连接到此处):
TCC模式案例我们做简化处理,仅采用Order和Stock两个模块,其中Order作为全局事务的发起者 / 参与者,Stock仅作为全局事务的参与者(分支事务)。
具体表结构,参考博文:Spring Cloud整合Seata、Nacos实现分布式事务案例,undo_log表示AT模式下的,所以TCC模式并不会使用到。
该注解用在接口的 Try 方法上。其中三个方法最重要:
@LocalTCC 适用于SpringCloud+Feign模式下的TCC。
该注解需要添加到 Try 方法所在的接口上,表示实现该接口的类被 seata 来管理,seata 根据事务的状态,自动调用我们定义的方法;如果try()没问题则调用 commit() 方法,否则调用 rollback() 方法。
该注解用来修饰 try() 方法的入参,被修饰的入参可以在 commit() 方法和 rollback() 方法中通过 BusinessActionContext 获取。
BusinessActionContext 是 seata tcc 的事务上下文,用于存放 tcc 事务的一些数据;
在接口 commit() 方法和 rollback() 方法的实现代码中,可以通过 BusinessActionContext 来获取参数;Seata 会自动注入参数。
tcc-order和tcc-stock模块所属的maven 父Module的pom.xml文件如下:
4.0.0 org.springframework.boot spring-boot-starter-parent 2.3.12.RELEASE tcc-order tcc-stock com.saint transaction-seata 0.0.1-SNAPSHOT transaction-seata transaction-seata pom 1.8 2.3.12.RELEASE Hoxton.SR12 2.2.8.RELEASE 1.2.8 8.0.22 org.apache.commons commons-lang3 3.12.0 org.projectlombok lombok true com.alibaba fastjson 2.0.10 org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring-cloud-alibaba.version} pom import com.alibaba druid-spring-boot-starter ${druid.version} mysql mysql-connector-java ${mysql.version} org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} org.projectlombok lombok
整体包括:pom.xml、一个Controller、一个entity、一个Dao、一个Mapper、一个Service、一个ServiceImpl、一个Controller、一个启动类;resources目录下两个配置文件:application.yml、file.conf
transaction-seata com.saint 0.0.1-SNAPSHOT 4.0.0 tcc-order 8 8 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2 org.springframework.boot spring-boot-starter-data-redis org.springframework.cloud spring-cloud-loadbalancer org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java runtime com.alibaba druid-spring-boot-starter org.springframework.boot spring-boot-starter-data-jpa org.springframework.cloud spring-cloud-starter-openfeign com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-all io.seata seata-all 1.5.2
这里要通过@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;}}
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;}
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);}
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);}
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);}
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);
}
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;}}
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);}}
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
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}
}
整体包括:pom.xml、一个Controller、一个entity、一个Dao、一个Mapper、一个Service、一个ServiceImpl、一个Controller、一个启动类;resources目录下两个配置文件:application.yml、file.conf
transaction-seata com.saint 0.0.1-SNAPSHOT 4.0.0 tcc-stock 8 8 org.mybatis.spring.boot mybatis-spring-boot-starter 2.2.2 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java runtime com.alibaba druid-spring-boot-starter org.springframework.boot spring-boot-starter-data-jpa org.springframework.cloud spring-cloud-starter-openfeign com.alibaba.cloud spring-cloud-starter-alibaba-seata io.seata seata-all io.seata seata-all 1.5.2
这里要通过@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";}
}
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;
}
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);}
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);
}
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);
}
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;}
}
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);}
}
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
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}
}
分别启动tcc-order、tcc-stock
分布式事务成功,模拟正常下单、扣库存
Order控制台输出:
Stock控制台输出:
请求访问:http://127.0.0.1:9001/purchase/commit
分布式事务失败,模拟下单成功、扣库存成功、回到tcc-order继续执行业务逻辑时抛出异常,最终同时回滚。
Order控制台输出:
Stock控制台输出:
请求访问:http://127.0.0.1:9001/purchase/rollback;
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对类动态代理的问题。
空回滚、幂等、悬挂。seata1.5.1版本已经进行了兼容处理,具体问题成因和解决方案见下一篇文章。
try()未执行,Cancel()执行了;
多次执行了Cancel()、Confirm()方法;
Cancel()比try()先执行;
1> TCC优点:
2> TCC缺点: