一、工作原理
TCC 与 Seata AT 事务一样都是两阶段事务,需要TC 事务协调器,TM 事务管理器,RM 资源管理器三大组件来支持全局事务的控制。
TCC 与 AT 事务的主要区别为
- TCC 对业务代码侵入严重,每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。
- TCC 效率更高,不必对数据加全局锁,允许多个事务同时操作数据。
1 第一阶段 Try
以账户服务为例,当下订单时要扣减用户账户金额:
假如用户购买 100 元商品,要扣减 100 元,TCC 事务首先对这100元的扣减金额进行预留,或者说是先冻结这100元
2 第二阶段 Confirm
如果第一阶段能够顺利完成,那么说明“扣减金额”业务(分支事务)最终肯定是可以成功的。当全局事务提交时, TC会控制当前分支事务进行提交,如果提交失败,TC 会反复尝试,直到提交成功为止。
当全局事务提交时,就可以使用冻结的金额来最终实现业务数据操作
3 第二阶段 Cancel
如果全局事务回滚,就把冻结的金额进行解冻,恢复到以前的状态,TC 会控制当前分支事务回滚,如果回滚失败,TC 会反复尝试,直到回滚完成为止。
4 多个事务并发的情况
多个TCC全局事务允许并发,它们执行扣减金额时,只需要冻结各自的金额即可
二、订单系统添加TCC事务
1 准备工作
第一步 :idea 新建empty工程,导入 分布式事务案例demo准备工作 创建的无事务版本项目
第二步:父项目seata依赖注释打开
第三步:订单模块order,账户模块account,库存模块storange下的application.yml统一添加事务组组名
spring:
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
第四步:订单模块order,账户模块account,库存模块storange 三个项目的resources目录下的配置文件 file.conf,registry.conf 与 02-分布式事务框架Seata-AT模式 中完全相同 ,可直接拷贝过来
第五步: 幂等性控制
如果重复执行提交或回滚,就和执行一次结果一样
在tcc包中创建工具类 ResultHolder
这个工具也可以在第二阶段 Confirm 或 Cancel 阶段对第一阶段的成功与否进行判断,在第一阶段成功时需要保存一个标识。
ResultHolder可以为每一个全局事务保存一个标识:
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 order启动全局事务,添加“保存订单”分支事务
在订单项目中执行添加订单
TCC 事务操作的代码:
Try - 第一阶段,冻结数据阶段,向订单表直接插入订单,订单状态设置为0(冻结状态)。
Confirm - 第二阶段,提交事务,将订单状态修改成1(正常状态)
Cancel - 第二阶段,回滚事务,删除订单
OrderMapper接口及OrderMapper.xml修改
根据前面的分析,订单数据操作有以下三项:创建订单,修改订单状态,删除订单
public interface OrderMapper extends BaseMapper<Order> {
void create(Order order);//创建订单
//修改状态
void updateStatus(Long id,Integer status);
//删除订单,使用继承的deleteById()
}
<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>
Seata 实现 TCC 操作需要定义一个接口,在 cn.tedu.order.tcc包下创建OrderTccAcction操作接口
@LocalTCC
public interface OrderTccAcction {
/**
* TwoPhaseBusinessAction 第一阶段方法里面已定义 默认第二阶段方法commit,rollback
* 第二阶段方法名是默认值无需配置,不是默认可通过注解指定第二阶段的方法
* 为了避开seata的bug,这里不使用封装的Order对象,而是一个一个的单独传递每一个值
*
* BusinessActionContext上下文对象,用来向第二阶段传递数据
* @BusinessActionContextParameter用来把参数放入上下文对象
*/
@TwoPhaseBusinessAction(name = "OrderTccAcction")
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);
}
添加OrderTccAcction的实现类OrderTccAcctionImpl
@Component
public class OrderTccAcctionImpl implements OrderTccAcction{
@Autowired
private OrderMapper orderMapper;
@Transactional//控制本地事物
@Override
public boolean prepare(BusinessActionContext ctx, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
System.out.println("创建 order 第一阶段,预留资源 - "+businessActionContext.getXid());
orderMapper.create(new Order(orderId, userId, productId, count, money, 0));
//事务成功,保存一个标识,供第二阶段进行判断
ResultHolder.setResult(OrderTccAcction.class,ctx.getXid(),"p");
return true;
}
@Transactional//控制本地事物
@Override
public boolean commit(BusinessActionContext ctx) {
// 防止幂等性,如果commit阶段重复执行则直接返回
if(ResultHolder.getResult(OrderTccAcction.class,ctx.getXid())==null)return true;
//ctx对象先变成json,在向第二阶段传递,会丢失类型信息,取出的orderId可能是Integer或Long
orderMapper.updateStatus((long)ctx.getActionContext("orderId"), 1);
//提交成功是删除标识
ResultHolder.removeResult(OrderTccAcction.class,ctx.getXid());
return true;
}
@Transactional//控制本地事物
@Override
public boolean rollback(BusinessActionContext ctx) {
//第一阶段没有完成的情况下,不必执行回滚,因为第一阶段有本地事务,事务失败时已经进行了回滚。
//如果这里第一阶段成功,而其他全局事务参与者失败,这里会执行回滚
//幂等性控制:如果重复执行回滚则直接返回
if(ResultHolder.getResult(OrderTccAcction.class,ctx.getXid())==null)return true;
orderMapper.deleteById((long)ctx.getActionContext("orderId"));
ResultHolder.removeResult(OrderTccAcction.class,ctx.getXid());
return true;
}
}
在业务代码中调用 Try 阶段方法
业务代码中不再直接保存订单数据,而是调用 TCC 第一阶段方法prepare(),并添加全局事务注解 @GlobalTransactional
OrderServiceImpl类修改
@Autowired
private OrderTccAcction orderTccAcction;
@GlobalTransactional//启动全局事务,OrderTccAcctionImpl中已开启了本地事物
@Override
public void create(Order order) {
order.setId(Long.valueOf(easyIdClient.nextId("order_business")));
//不直接完成业务数据操作orderMapper.create(order),而是调用tcc的第一阶段方法
/**
* orderTccAcction实例,是动态代理对象,用AOP添加了切面代码,创建上下文对象,然后传入原始方法
*/
orderTccAcction.prepare(null, order.getId(), order.getUserId(), order.getProductId(), order.getCount(), order.getMoney());
//后面两个模块没有完成,无法启动,先注释掉
// TODO: 远程调用库存storage,减少库存
//storageClient.decrease(order.getProductId(), order.getCount());
// TODO: 远程调用账户account,扣减账户
//accountClient.decrease(order.getUserId(), order.getMoney());
}
启动服务 访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试,观察控制台日志
3 storage添加减少库存分支事务
在库存项目中执行减少库存
我们要添加以下 TCC 事务操作的代码
Try - 第一阶,冻结数据阶段,将要减少的库存量先冻结
Confirm - 第二阶段,提交事务,使用冻结的库存完成业务数据处理
Cancel - 第二阶段,回滚事务,冻结的库存解冻,恢复以前的库存量
根据前面的分析,库存数据操作有以下三项:冻结库存,冻结库存量修改为已售出量,解冻库存
StorageMapper添加以下方法
//查询商品库存,用来判断是否有足够的库存
Storage findByProductId(Long productId);
//可用---》冻结
void updateResidueToFrozen(Long productId, Integer count);
//提交时,冻结--》已售出
void updateFrozenToUsed(Long productId, Integer count);
//回滚时,冻结--->可用
void updateFrozenToResidue(Long productId, Integer count);
StorageMapper.xml文件修改
<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 used = used + #{count} ,frozen=frozen - #{count} WHERE product_id = #{productId}
</update>
<update id="updateFrozenToResidue">
update storage set residue = residue + #{count} ,frozen=frozen - #{count} WHERE product_id = #{productId}
</update>
在cn.tedu.storage.tcc 添加 TCC 接口StorageTccAction
@LocalTCC
public interface StorageTccAction {
@TwoPhaseBusinessAction(name = "StorageTccActopn")
boolean prepare(BusinessActionContext cxt, @BusinessActionContextParameter(paramName = "productId") Long productId, @BusinessActionContextParameter(paramName = "count") Integer count);
boolean commit(BusinessActionContext cxt);
boolean rollback(BusinessActionContext cxt);
}
实现类
@Component
@Slf4j
public class StorageTccActionImpl implements StorageTccAction{
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext cxt, Long productId, Integer count) {
log.info("减少商品库存,第一阶段,锁定减少的库存量,productId="+productId+", count="+count);
Storage storage = storageMapper.findByProductId(productId);
if(storage.getResidue()<count){
throw new RuntimeException("库存不足");
}
storageMapper.updateResidueToFrozen(productId, count);
//一阶段成功,添加成功标记:StorageTccAction.class-->事务id--->"p"
ResultHolder.setResult(StorageTccAction.class,cxt.getXid(),"p");
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext cxt) {
//二阶段执行之前,判断如果没有标记,则不执行二阶段操作,避免二阶段操作重复执行
if(ResultHolder.getResult(StorageTccAction.class,cxt.getXid())==null) return true; storageMapper.updateFrozenToUsed(Long.getLong(cxt.getActionContext("productId").toString()),Integer.parseInt(cxt.getActionContext("productId").toString()));
//第二阶段完成时,删除标记
ResultHolder.removeResult(StorageTccAction.class,cxt.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext cxt) {
//二阶段执行之前,判断如果没有标记,则不执行二阶段操作,避免二阶段操作重复执行
if(ResultHolder.getResult(StorageTccAction.class,cxt.getXid())==null) return true; storageMapper.updateFrozenToResidue(Long.getLong(cxt.getActionContext("productId").toString()),Integer.parseInt(cxt.getActionContext("productId").toString()));
//第二阶段完成时,删除标记
ResultHolder.removeResult(StorageTccAction.class,cxt.getXid());
return true;
}
}
同理:订单模块也需要相应加幂等性代码,这里不重复演示啦
在业务代码中调用 Try 阶段方法 StorageServiceImpl类修改
@Service
public class StorageServiceImpl implements StorageService{
/*@Autowired
private StorageMapper storageMapper;*/
@Autowired
private StorageTccAction storageTccAction;
@Override
public void decrease(Long productId, Integer count){
storageTccAction.prepare(null, productId, count);
}
}
打开订单服务的远程调用仓库服务的方法的注释
启动服务,调用保存订单地址: http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试
4 account添加扣减金额分支事务
扣减金额 TCC 事务分析本文开头说TCC工作原理就介绍了流程,详情见目录一
根据前面的分析,库存数据操作有三项:1.冻结库存 2.冻结库存量修改为已售出量 3.解冻库存
在 AccountMapper 中添加方法
//查询账户
Account findByUserId(Long userId);
//可用--->冻结
void updateResidueToFrozen(Long userId,BigDecimal money);
//冻结-->已消费
void updateFrozenToUsed(Long userId,BigDecimal money);
//冻结--->可用
void updateFrozenToResidue(Long userId,BigDecimal money);
AccountMapper .xml的sql添加
<select id="findByUserId" resultMap="BaseResultMap">
select * from account where user_id = #{userId};
</select>
<update id="updateResidueToFrozen">
update account set residue=residue-#{money},frozen=frozen+#{money} where user_id = #{userId};
</update>
<update id="updateFrozenToUsed">
update account set used=used+#{money},frozen=frozen-#{money} where user_id = #{userId};
</update>
<update id="updateFrozenToResidue">
update account set residue=residue+#{money},frozen=frozen-#{money} where user_id = #{userId};
</update>
在cn.tedu.account.tcc下建AccountTccAction接口
@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);
}
实现类
@Component
public class AccountTccActionImpl implements AccountTccAction {
@Autowired
private AccountMapper accountMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long userId, BigDecimal money) {
Account accout = accountMapper.findByUserId(userId);
if (accout.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(getClass(), ctx.getXid()) == null) return true;
accountMapper.updateFrozenToUsed(Long.valueOf(ctx.getActionContext("userId").toString()), new BigDecimal(ctx.getActionContext("money").toString()));
//删除标识
ResultHolder.removeResult(AccountTccAction.class, ctx.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext ctx) {
//防止重复回滚
if (ResultHolder.getResult(getClass(), ctx.getXid()) == null) return true;
accountMapper.updateFrozenToResidue(Long.valueOf(ctx.getActionContext("userId").toString()), new BigDecimal(ctx.getActionContext("money").toString()));
//删除标识
ResultHolder.removeResult(AccountTccAction.class, ctx.getXid());
return true;
}
}
在业务代码中调用 Try 阶段方法
AccountServiceImpl 修改
@Autowired
private AccountTccAction accountTccAction;
@Override
public void decrease(Long userId, BigDecimal money) {
accountTccAction.prepare(null, userId, money);
//accountMapper.decrease(userId, money);
}
订单模块远程调用账户的注释打开,启动服务, 访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100 测试