TCC
1. 简介
- TCC模式即将每个服务业务操作分为两个阶段:
- 第一个阶段: 检查并预留(冻结)相关资源,可视为一种临时操作
- 第二阶段根: 据所有服务业务的Try状态来操作,如果都成功,则进行Confirm操作,如果任意一个Try发生错误,则全部Cancel,特征在于它不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务,不同于AT的是就是需要自行定义各个阶段的逻辑,对业务有侵入。
- TCC使用要求就是业务接口都必须实现三段逻辑:
- 准备操作 Try:完成所有业务检查,预留必须的业务资源。
- 确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务能且只能成功一次。
- 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。
2. 基本原理
TCC 与 Seata AT 事务一样都是两阶段事务,它与 AT 事务的主要区别为:
- TCC 对业务代码侵入严重
每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。 - TCC 效率更高
不必对数据加全局锁,允许多个事务同时操作数据。
2.1 第一阶段 Try
假如用户购买 100 元商品,要扣减 100 元。
TCC 事务首先对这100元的扣减金额进行预留,或者说是先冻结这100元:
2.2 第二阶段 Confirm/Cancel
- Confirm
如果第一阶段能够顺利完成,那么说明“扣减金额”业务(分支事务)最终肯定是可以成功的。当全局事务提交时, TC会控制当前分支事务进行提交,如果提交失败,TC 会反复尝试,直到提交成功为止。当全局事务提交时,就可以使用冻结的金额来最终实现业务数据操作: - Cancel
如果全局事务回滚,就把冻结的金额进行解冻,恢复到以前的状态,TC 会控制当前分支事务回滚,如果回滚失败,TC 会反复尝试,直到回滚完成为止。
多个事务并发
多个TCC全局事务允许并发,它们执行扣减金额时,只需要冻结各自的金额即可:
.
3. seate-TCC事务案例
3.1 项目创建
将之前无事务版本seata项目解压到新项目中即可
3.2 开启seata server-TCC事务协调器
开启之前的seata server
4. order添加TCC事务
4.1 添加依赖
将orer-parent项目中的pom.xml文件中注释的依赖解除注释
4.1 resource配置
复制之前项目配置文件即可
- application.yml - 事务组组名
- registry.conf - eureak地址
- file.conf - 事务组对应的协调器
4.2 添加TCC操作接口和实现类
4.2.1 OrderMapper 添加更新订单状态、删除订单
package cn.tedu.mapper;
import cn.tedu.entity.Order;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface OrderMapper extends BaseMapper<Order> {
void create(Order order);
void updateStatus(Long produtId, Integer status);
}
4.2.2 修改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`=#{orderId};
</update>
<delete id="deleteById">
DELETE FROM `order` WHERE `id`=#{orderId}
</delete>
</mapper>
4.3 添加TCC接口和实现
4.3.1 接口
package cn.tedu.tcc;
import cn.tedu.entity.Order;
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;
import java.math.BigDecimal;
@LocalTCC
public interface OrderTccAction {
/**
* BusinessActionContext ctx
* TCC的上下文对象,通过ctx对象,在两个阶段传递业务数据
*
* @BusinessActionContextParameter
* 把数据放入上下文对象,通过上下文对象可以把数据传递到第二阶段
*
* @return
*/
//try
@TwoPhaseBusinessAction(name = "OrderTCCAction")
boolean prepare(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "id") Long id,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count,
@BusinessActionContextParameter(paramName = "money") BigDecimal money,
@BusinessActionContextParameter(paramName = "status") Integer status);
//Confirm
boolean commit(BusinessActionContext ctx);
//Cancel
boolean rollback(BusinessActionContext ctx);
}
4.3.2 实现类
package cn.tedu.tcc;
import cn.tedu.entity.Order;
import cn.tedu.mapper.OrderMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Component
public class OrderTccActionImpl implements OrderTccAction {
@Autowired
private OrderMapper orderMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long id, Long userId, Long productId, Integer count, BigDecimal money, Integer status) {
//插入冻结订单
Order order = new Order(id, userId, productId, count, money, 0);
orderMapper.create(order);
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext ctx) {
/**
* ctx在向第二阶段传递时,会先转成json,再转回上下文对象
* 其中的整数在转换过程中可能变成Integer,也可能是Long
*/
Long orderId = Long.valueOf(ctx.getActionContext("id").toString());
//修改状态为正常
orderMapper.updateStatus(orderId, 1);
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext ctx) {
//删除订单
Long orderId = Long.valueOf(ctx.getActionContext("id").toString());
orderMapper.deleteById(orderId);
return true;
}
}
4.4 修改OrderServiceImpl
package cn.tedu.service;
import cn.tedu.entity.Order;
import cn.tedu.feign.AccountClient;
import cn.tedu.feign.EasyIdClient;
import cn.tedu.feign.StorageClient;
import cn.tedu.mapper.OrderMapper;
import cn.tedu.tcc.OrderTccAction;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceImpl implements OrderService{
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountClient accountClient;
@Autowired
private StorageClient storageClient;
@Autowired
private EasyIdClient easyIdClient;
@Autowired
private OrderTccAction orderTccAction;
@Override
@GlobalTransactional//启动全局事务
public void create(Order order) {
// TODO: 从全局唯一id发号器获得id,这里暂时随机产生一个 orderId
// Long orderId = Long.valueOf(new Random().nextInt(Integer.MAX_VALUE));
// order.setId(orderId);
//调用全局唯一id发号器,获取一个唯一的id
String s = easyIdClient.nextId("order_business");
Long orderId = Long.valueOf(s);
// orderMapper.create(order);
/*
orderTccAction 是一个动态代理对象,其中AOP添加了拦截器,会拦截调用,在拦截器中创建上下文对象
*/
orderTccAction.prepare(null, order.getId(), order.getUserId(), order.getProductId(), order.getCount(), order.getMoney(), order.getStatus());
// TODO: 调用storage,修改库存
// storageClient.decrease(order.getProductId(), order.getCount());
// TODO: 调用account,修改账户余额
// accountClient.decrease(order.getUserId(), order.getMoney());
}
}
4.5 启动order测试
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
查看控制台:
5. storage添加TCC事务
5.1 复制resource配置以及ResultHolder工具类
5.2 修改StorageMapper接口
package cn.tedu.mapper;
import cn.tedu.entity.Storage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface StorageMapper extends BaseMapper<Storage> {
void decrease(Long productId, Integer count);
//根据id查询库存shuju,zhijie使用继承的方法 selecetById()
//冻结库存
void updateFrozen(Long productId, Integer count);
//把冻结库存变成已售出
void updateFrozenToUsed(Long productId, Integer count);
//把冻结库存恢复成可用库存
void updateFrozenToResidue(Long productId, Integer count);
}
5.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="selectById" resultMap="BaseResultMap">
select * from storage where product_id = #{productId}
</select>
<update id="updateFrozen">
UPDATE storage SET
residue=#{residue},
frozen=#{frozen}
WHERE product_id=#{productId}
</update>
<update id="updateFrozenToUsed">
UPDATE storage SET
frozen=frozen-#{count},
used=used+#{count}
WHERE product_id=#{productId}
</update>
<update id="updateFrozenToResidue">
UPDATE storage SET
frozen=frozen-#{count},
residue=residue+#{count}
WHERE product_id=#{productId}
</update>
</mapper>
5.4 添加TCC接口和实现类
package cn.tedu.tcc;
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;
import java.math.BigDecimal;
@LocalTCC
public interface StorageTccAction {
/**
* BusinessActionContext ctx
* TCC的上下文对象,通过ctx对象,在两个阶段传递业务数据
*
* @BusinessActionContextParameter
* 把数据放入上下文对象,通过上下文对象可以把数据传递到第二阶段
*
* @return
*/
//try
@TwoPhaseBusinessAction(name = "StorageTCCAction")
boolean prepare(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count);
//Confirm
boolean commit(BusinessActionContext ctx);
//Cancel
boolean rollback(BusinessActionContext ctx);
}
package cn.tedu.tcc;
import cn.tedu.entity.Storage;
import cn.tedu.mapper.StorageMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Component
public class StorageTccActionImpl implements StorageTccAction {
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long productId, Integer count) {
Storage storage = storageMapper.selectById(productId);
if (storage.getResidue() < count) {
throw new RuntimeException("库存不足");
}
storageMapper.updateFrozen(productId, count);
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext ctx) {
Long productId = Long.valueOf(ctx.getActionContext("productId").toString());
Integer count = Integer.valueOf(ctx.getActionContext("count").toString());
storageMapper.updateFrozenToUsed(productId, count);
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext ctx) {
Long productId = Long.valueOf(ctx.getActionContext("productId").toString());
Integer count = Integer.valueOf(ctx.getActionContext("count").toString());
storageMapper.updateFrozenToResidue(productId, count);
return true;
}
}
5.5 修改StorageServcieImpl
package cn.tedu.service;
import cn.tedu.mapper.StorageMapper;
import cn.tedu.tcc.StorageTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StorageServiceImpl implements StorageService{
// @Autowired
// private StorageMapper storageMapper;
@Autowired
private StorageTccAction storageTccAction;
@Override
public void decrease(Long productId, Integer count) {
// storageMapper.decrease(productId,count);
storageTccAction.prepare(null, productId, count);
}
}
5.6 测试
- 按顺序启动服务:
Eureka
Seata Server
Easy Id Generator
Storage
Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
查看控制台:
6. account添加TCC事务
6.1 修改resouce配置
6.2 修改AccoutMapper接口
package cn.tedu.mapper;
import cn.tedu.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.math.BigDecimal;
public interface AccountMapper extends BaseMapper<Account> {
void decrease(Long userId, BigDecimal money);
//查询 -- 使用继承方法
//冻结
void updateFrozen(Long userId, BigDecimal money);
//冻结 --> 已使用
void updateFrozenToUsed(Long userId, BigDecimal money);
//冻结 --> 可用
void updateFrozenResidue(Long userId, BigDecimal money);
}
6.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>
6.4 添加TCC接口和实现类
package cn.tedu.tcc;
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;
import java.math.BigDecimal;
@LocalTCC
public interface AccountTccAction {
@TwoPhaseBusinessAction(name = "AccountTccAction")
boolean prepare(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money")BigDecimal money
);
boolean commit(BusinessActionContext ctx);
boolean rollback(BusinessActionContext ctx);
}
package cn.tedu.tcc;
import cn.tedu.entity.Account;
import cn.tedu.mapper.AccountMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.concurrent.CountDownLatch;
public class AccountTccActionImpl implements AccountTccAction {
@Autowired
private AccountMapper accountMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long userId, BigDecimal money) {
Account account = accountMapper.selectById(userId);
//a.compareTo(b) a大返回整数,a小返回负数,相同返回0
if (account.getResidue().compareTo(money) < 0) {
throw new RuntimeException("金额不足");
}
accountMapper.updateFrozen(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.updateFrozenResidue(userId, money);
ResultHolder.removeResult(AccountTccAction.class, ctx.getXid());
return true;
}
}
6.5 修改AccountServiceImpl类
package cn.tedu.servie;
import cn.tedu.mapper.AccountMapper;
import cn.tedu.tcc.AccountTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class AccountServiceImpl implements AccountService {
// @Autowired
// private AccountMapper accountMapper;
@Autowired
private AccountTccAction accountTccAction;
@Override
public void decrease(Long userId, BigDecimal money) {
accountTccAction.prepare(null, userId, money);
}
}
6.6 测试
- 按顺序启动服务:
Eureka
Seata Server
Easy Id Generator
Storage
Account
Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
观察 account 的控制台日志:
观察 storage 的控制台日志:
观察 Order 的控制台日志:
7. 幂等性控制
如果TC协调器,要求重复执行提交或回滚,多次操作,要和一次操作结果一样
- 第一阶段:
- 完成时,先保存一个第一阶段完成标记
- 第二阶段:
- 先检查标记,如果有则提交或回滚;反之则不提交
7.1 ResultHolder工具类
package cn.tedu.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);
}
}
}
7.2 修改StorageTccActionImpl
package cn.tedu.tcc;
import cn.tedu.entity.Storage;
import cn.tedu.mapper.StorageMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Component
public class StorageTccActionImpl implements StorageTccAction {
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long productId, Integer count) {
Storage storage = storageMapper.selectById(productId);
if (storage.getResidue() < count) {
throw new RuntimeException("库存不足");
}
storageMapper.updateFrozen(productId, count);
//第一阶段完成时保存标记
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;
}
}