1、准备工作
- 第一步:先创建一个空工程:seata-tcc
- 第二步:将无事务订单项目解压到seata-tcc文件夹下。(可访问 git 仓库 https://gitee.com/benwang6/seata-samples下载无事务订单项目)
- 第三步:在IDEA 按两下 shift 搜索add maven project,选择需要导入的模块的pom.xml文件,将项目都导入工程中。
2、TCC事务入门案例
2.1 Order启动全局事务,添加“保存订单”分支事务
2.1.1 父工程添加seata依赖
- 打开父工程order-parent的pom.xml文件,取消seata依赖的注释。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-seata</artifactId> <version>${spring-cloud-alibaba-seata.version}</version> <exclusions> <exclusion> <artifactId>seata-all</artifactId> <groupId>io.seata</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>${seata.version}</version> </dependency>
2.1.2 三个配置文件
1、 application.yml
- 设置全局事务组的组名:
spring: application: name: order datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost/seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 username: root password: root # 事务组设置 cloud: alibaba: seata: tx-service-group: order_tx_group server: port: 8083 eureka: client: service-url: defaultZone: http://localhost:8761/eureka mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: cn.tedu.entity configuration: map-underscore-to-camel-case: true logging: level: cn.tedu.mapper: debug
2、regirstry.conf
```go
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
# application = "default"
# weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
password = ""
cluster = "default"
timeout = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
```
3、 file.conf
```go
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
# order_tx_group 与 yml 中的 “tx-service-group: order_tx_group” 配置一致
# “seata-server” 与 TC 服务器的注册名一致
# 从eureka获取seata-server的地址,再向seata-server注册自己,设置group
vgroupMapping.order_tx_group = "seata-server"
#only support when registry.type=file, please don't set multiple addresses
order_tx_group.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
```
2.1.3 OrderMapper 添加新的数据库操作
- 根据前面的分析,订单数据操作有以下三项:
- 插入订单、修改订单状态、删除订单;
- 在 OrderMapper 中已经有插入订单的方法,现在需要添加修改订单和删除订单的方法(删除方法从BaseMapper继承):
@Mapper public interface OrderMapper extends BaseMapper<Order> { //创建订单 void create(Order order); //修改订单状态 void updateStatus(Long id,Integer status); //删除订单--使用继承的方法 deleteById() }
2.3.4 修改OrderMapper.xml映射文件
<?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.tedu.mapper.OrderMapper" >
<resultMap id="BaseResultMap" type="cn.tedu.entity.Order" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="product_id" property="productId" jdbcType="BIGINT" />
<result column="count" property="count" jdbcType="INTEGER" />
<result column="money" property="money" jdbcType="DECIMAL" />
<result column="status" property="status" jdbcType="INTEGER" />
</resultMap>
<insert id="create">
INSERT INTO `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
VALUES(#{id}, #{userId}, #{productId}, #{count}, #{money},#{status});
</insert>
<update id="updateStatus">
update `order` set status = #{status} where id = #{id}
</update>
<delete id="deleteById">
delete from `order` where id = #{id}
</delete>
</mapper>
2.2 Seata 实现订单的 TCC 操作方法
- Try - prepareCreateOrder()
- Confirm - commit()
- Cancel - rollback()
2.2.1 添加一个工具类 ResultHolder
- 第二阶段为了处理幂等性问题这里首先添加一个工具类 ResultHolder。
- 幂等性控制,如果重复执行提交或回滚就和执行一次结果是一样的。
- 这个工具也可以在第二阶段 Confirm 或 Cancel 阶段对第一阶段的成功与否进行判断,在第一阶段成功时需要保存一个标识。
package cn.tedu.order.tcc; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ResultHolder { private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>(); public static void setResult(Class<?> actionClass, String xid, String v) { Map<String, String> results = map.get(actionClass); if (results == null) { synchronized (map) { if (results == null) { results = new ConcurrentHashMap<>(); map.put(actionClass, results); } } } results.put(xid, v); } public static String getResult(Class<?> actionClass, String xid) { Map<String, String> results = map.get(actionClass); if (results != null) { return results.get(xid); } return null; } public static void removeResult(Class<?> actionClass, String xid) { Map<String, String> results = map.get(actionClass); if (results != null) { results.remove(xid); } } }
2.2.2 定义TCC接口 OrderTccAction
- Seata 实现 TCC 操作需要定义一个接口,我们在接口中添加以下方法:
/** * 为了避免seata的bug,这里不使用封装的对象,而是一个一个单独的传值。 * BusinessActionContext 业务上下文对象,用来向第二阶段传递数据。 * @BusinessActionContextParameter 用来把参数放入上下文对象 */ @LocalTCC public interface OrderTccAction { @TwoPhaseBusinessAction(name = "OrderTccAction") boolean prepare(BusinessActionContext ctx, //该注解使该值放到上下文对象中 @BusinessActionContextParameter(paramName = "orderId") Long orderId,Long userId, Long productId, Integer count, BigDecimal money); boolean commit(BusinessActionContext ctx); boolean rollback(BusinessActionContext ctx);
2.2.3 定义实现类 OrderTccActionImpl
- 在每个方法上添加 @Transactional //控制本地事务。
@Component public class OrderTccActionImpl implements OrderTccAction{ @Autowired private OrderMapper orderMapper; @Transactional //控制本地事务 @Override public boolean prepare(BusinessActionContext ctx, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) { orderMapper.create(new Order(orderId,userId,productId,count,money,0)); //一阶段成功标记 ResultHolder.setResult(OrderTccAction.class, ctx.getXid(), "p"); return true; } @Transactional @Override public boolean commit(BusinessActionContext ctx) { //二阶段执行之前,判断如果没有标记,则不执行二阶段操作 if(ResultHolder.getResult(OrderTccAction.class, ctx.getXid())==null){ return true; } //从上下文对象取出orderId Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString()); orderMapper.updateStatus(orderId, 1); //第二阶段完成时,删除标记。 ResultHolder.removeResult(OrderTccAction.class, ctx.getXid()); return true; } @Transactional @Override public boolean rollback(BusinessActionContext ctx) { //二阶段执行之前,判断如果没有标记,则不执行二阶段操作 if(ResultHolder.getResult(OrderTccAction.class, ctx.getXid())==null){ return true; } //从上下文对象取出orderId Long orderId = Long.valueOf(ctx.getActionContext("orderId").toString()); orderMapper.deleteById(orderId); //第二阶段完成时,删除标记。 ResultHolder.removeResult(OrderTccAction.class, ctx.getXid()); return true; } }
2.2.4 在业务OrderServiceImpl代码中调用 Try 阶段方法
- 业务代码中不再直接保存订单数据,而是调用 TCC 第一阶段方法prepare(),并添加全局事务注解 @GlobalTransactional;
@Service public class OrderServiceImpl implements OrderService{ @Autowired private OrderMapper orderMapper; @Autowired private EasyIdClient easyIdClient; @Autowired private StorageClient storageClient; @Autowired private AccountClient accountClient; @Autowired private OrderTccAction orderTccAction; @GlobalTransactional //启动全局事务 @Override public void create(Order order) { //远程调用ID发号器,获取订单id Long id = Long.valueOf(easyIdClient.nextId("order_business")); order.setId(id); //不直接完成业务数据操作,而是调用tcc的第一阶段方法 //orderMapper.create(order); //orderTccAction实例,是动态代理对象,用AOP添加了切面代码,创建上下文对象,然后传入原始方法。 orderTccAction.prepare(null, order.getId(), order.getUserId(), order.getProductId(), order.getCount(), order.getMoney()); //远程调用库存,减少库存 storageClient.decrease(order.getProductId(), order.getCount()); //远程调用账户,扣减账户 accountClient.decrease(order.getUserId(), order.getMoney()); } }
2.3 Storage添加“减少库存”分支事务
2.3.1 有三个文件需要配置:
- application.yml
- registry.conf
- file.conf
- 这三个文件的设置与上面 order 项目的配置完全相同,请参考上面订单配置一章进行配置。
2.3.2 StorageMapper 添加新的数据库操作
-
根据前面的分析,库存数据操作有以下三项:
- 冻结库存、冻结库存量修改为已售出量、解冻库存
在 StorageMapper 中添加三个方法:
@Mapper public interface StorageMapper extends BaseMapper<Storage> { void decrease(Long productId,Integer count); //查询商品库存,用来判断是否有足够的库存 Storage findByProductId(Long productId); //可用---->冻结 void updateResidueToFrozen(Long productId,Integer count); //冻结---->已售出 void updateFrozenToUsed(Long productId,Integer count); //冻结---->可用 void updateFrozenToResidue(Long productId,Integer count); }
- 冻结库存、冻结库存量修改为已售出量、解冻库存
2.3.3 修改StorageMapper.xml映射文件
<?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.tedu.mapper.StorageMapper" >
<resultMap id="BaseResultMap" type="cn.tedu.entity.Storage" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="product_id" property="productId" jdbcType="BIGINT" />
<result column="total" property="total" jdbcType="INTEGER" />
<result column="used" property="used" jdbcType="INTEGER" />
<result column="residue" property="residue" jdbcType="INTEGER" />
</resultMap>
<update id="decrease">
UPDATE storage SET used = used + #{count},residue = residue - #{count} WHERE product_id = #{productId}
</update>
<select id="findByProductId" resultMap="BaseResultMap">
select * from storage where product_id = #{productId}
</select>
<update id="updateResidueToFrozen">
update storage set Residue=Residue-#{count},Frozen=Frozen+#{count}
where product_id =#{productId}
</update>
<update id="updateFrozenToUsed">
update storage set Frozen=Frozen-#{count}, used=used+#{used}
where product_id =#{productId}
</update>
<update id="updateFrozenToResidue">
update storage set Frozen=Frozen-#{count},Residue=Residue+#{count}
where product_id =#{productId}
</update>
</mapper>
2.4 Seata 实现库存的 TCC 操作方法
2.4.1 添加一个工具类 ResultHolder
package cn.tedu.storage.tcc;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ResultHolder {
private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public static void setResult(Class<?> actionClass, String xid, String v) {
Map<String, String> results = map.get(actionClass);
if (results == null) {
synchronized (map) {
if (results == null) {
results = new ConcurrentHashMap<>();
map.put(actionClass, results);
}
}
}
results.put(xid, v);
}
public static String getResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
return results.get(xid);
}
return null;
}
public static void removeResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
results.remove(xid);
}
}
}
2.4.2 定义TCC接口 StorageTccAction
-
添加 TCC 接口,在接口中添加以下方法:
Try - prepare()
Confirm - commit()
Cancel - rollback()@LocalTCC public interface StorageTccAction { @TwoPhaseBusinessAction(name = "StorageTccAction") //标识两阶段业务操作 boolean prepare(BusinessActionContext ctx, @BusinessActionContextParameter(paramName = "productId") Long productId,//把数据放入上下文对象中,向第二阶段传递。 @BusinessActionContextParameter(paramName = "count") Integer count); boolean commit(BusinessActionContext ctx); boolean rollback(BusinessActionContext ctx);
2.4.3 定义实现类 StorageTccActionImpl
@Component
public class StorageTccActionImpl implements StorageTccAction {
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long productId, Integer count) {
Storage storage = storageMapper.findByProductId(productId);//查询库存够不够
if (storage.getResidue()<count) {//库存小于冻结数量,无法继续执行冻结操作
throw new RuntimeException("库存不足!");
}
storageMapper.updateResidueToFrozen(productId, count);
//一阶段成功,添加成功的标记:StorageTccAction.class ----> ctx.getXid() -----> "p"
ResultHolder.setResult(StorageTccAction.class, ctx.getXid(), "p");
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext ctx) {
//二阶段执行之前,判断如果没有标记,则不执行二阶段操作
if(ResultHolder.getResult(StorageTccAction.class, ctx.getXid())==null){
return true;
}
Long productId = Long.valueOf(ctx.getActionContext("productId").toString());
Integer count = Integer.valueOf(ctx.getActionContext("count").toString());
storageMapper.updateFrozenToUsed(productId, count);
//第二阶段完成时,删除标记。
ResultHolder.removeResult(StorageTccAction.class, ctx.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext ctx) {
//二阶段执行之前,判断如果没有标记,则不执行二阶段操作
if(ResultHolder.getResult(StorageTccAction.class, ctx.getXid())==null){
return true;
}
Long productId = Long.valueOf(ctx.getActionContext("productId").toString());
Integer count = Integer.valueOf(ctx.getActionContext("count").toString());
storageMapper.updateFrozenToResidue(productId, count);
//第二阶段完成时,删除标记。
ResultHolder.removeResult(StorageTccAction.class, ctx.getXid());
return true;
}
}
2.4.4 在业务 StorageServiceImpl 代码中调用 Try 阶段方法
@Service
@GlobalTransactional //启动全局事务
public class StorageServiceImpl implements StorageService{
@Autowired
private StorageTccAction storageTccAction;
@Override
public void decrease(Long productId, Integer count) {
storageTccAction.prepare(null, productId, count);
}
}
2.5 Account添加“扣减金额”分支事务
2.5.1 有三个文件需要配置:
- application.yml
- registry.conf
- file.conf
- 这三个文件的设置与上面 order 项目的配置完全相同,请参考上面订单配置一章进行配置。
2.5.2 AccountMapper 添加新的数据库操作
- 根据前面的分析,库存数据操作有以下三项:
冻结账户、冻结账户量修改为消费量、解冻账户
在 AccountMapper 中添加三个方法:@Mapper public interface AccountMapper extends BaseMapper<Account> { //扣减账户金额 void decrease(Long userId, BigDecimal money); //查询账户 Account findUserId(Long userId); //可用 ----> 冻结 void updateResidueToFrozen(Long userId, BigDecimal money); //冻结 ----> 已消费 void updateFrozenToUsed(Long userId, BigDecimal money); //冻结 ----> 可用 void updateFrozenToResidue(Long userId, BigDecimal money); }
2.5.3 修改AccountMapper.xml映射文件
<?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.tedu.mapper.AccountMapper" >
<resultMap id="BaseResultMap" type="cn.tedu.entity.Account" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="total" property="total" jdbcType="DECIMAL" />
<result column="used" property="used" jdbcType="DECIMAL" />
<result column="residue" property="residue" jdbcType="DECIMAL"/>
<result column="frozen" property="frozen" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE account SET residue = residue - #{money},used = used + #{money} where user_id = #{userId};
</update>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM account WHERE user_id=#{userId}
</select>
<update id="updateFrozen">
UPDATE account SET residue=#{residue},frozen=#{frozen} WHERE user_id=#{userId}
</update>
<update id="updateFrozenToUsed">
UPDATE account SET frozen=frozen-#{money}, used=used+#{money} WHERE user_id=#{userId}
</update>
<update id="updateFrozenToResidue">
UPDATE account SET frozen=frozen-#{money}, residue=residue+#{money} WHERE user_id=#{userId}
</update>
</mapper>
2.6 Seata 实现账户的 TCC 操作方法
2.6.1 添加一个工具类ResultHolder
package cn.tedu.account.tcc;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ResultHolder {
private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public static void setResult(Class<?> actionClass, String xid, String v) {
Map<String, String> results = map.get(actionClass);
if (results == null) {
synchronized (map) {
if (results == null) {
results = new ConcurrentHashMap<>();
map.put(actionClass, results);
}
}
}
results.put(xid, v);
}
public static String getResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
return results.get(xid);
}
return null;
}
public static void removeResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
results.remove(xid);
}
}
}
2.6.2 定义TCC接口AccountTccAction
@LocalTCC
public interface AccountTccAction {
@TwoPhaseBusinessAction(name = "AccountTccAction",commitMethod = "commit", rollbackMethod = "rollback")
boolean prepare(BusinessActionContext ctx,
//该注解使该值放到上下文对象中
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
boolean commit(BusinessActionContext ctx);
boolean rollback(BusinessActionContext ctx);
}
2.6.3 定义实现类AccountTccActionImpl
@Component
@SLF4j
public class AccountTccActionImpl implements AccountTccAction {
@Autowired
private AccountMapper accountMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long userId, BigDecimal money) {
log.info("减少账户金额,第一阶段锁定金额,userId="+userId+", money="+money);
//BigDecimal是对象不能直接比大小,要用compareTo()方法来进行比值
Account account = accountMapper.findUserId(userId);
if (account.getResidue().compareTo(money) < 0) {
throw new RuntimeException("可用金额不足");
}
accountMapper.updateResidueToFrozen(userId, money);
ResultHolder.setResult(AccountTccAction.class , ctx.getXid(), "p");
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext ctx) {
if(ResultHolder.getResult(AccountTccAction.class, ctx.getXid())==null){
return true;
}
Long userId = Long.valueOf(ctx.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(ctx.getActionContext("money").toString());
accountMapper.updateFrozenToUsed(userId, money);
ResultHolder.removeResult(AccountTccAction.class, ctx.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext ctx) {
if(ResultHolder.getResult(AccountTccAction.class, ctx.getXid())==null){
return true;
}
Long userId = Long.valueOf(ctx.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(ctx.getActionContext("money").toString());
accountMapper.updateFrozenToResidue(userId,money);
ResultHolder.removeResult(AccountTccAction.class, ctx.getXid());
return true;
}
}
2.6.4 在业务AccountServiceImpl代码中调用Try阶段方法
@Service
@GlobalTransactional //启动全局事务
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountTccAction accountTccAction;
@Override
public void decrease(Long userId, BigDecimal money) {
accountTccAction.prepare(null, userId, money);
}
}
2.7 测试
按顺序启动服务:
Eureka
Seata Server
Easy Id Generator
Storage
Account
Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
观察 account 的控制台日志或数据库的数据变化: