Seata 开源了 TCC 模式,该模式由蚂蚁金服贡献。TCC 模式需要用户根据自己的业务场景实现Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段 执行 Try 方式,在二阶段提交执行Confirm方法,二阶段回滚执行 Cancel 方法。

TCC 三个方法描述:
业务模型分 2 阶段设计:
用户接入 TCC ,最重要的是考虑如何将自己的业务模型拆成两阶段来实现。
以“扣钱”场景为例,在接入 TCC 前,对 A 账户的扣钱,只需一条更新账户余额的 SQL 便能完成;但是在接入 TCC 之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆成两阶段,实现成三个方法,并且保证一阶段 Try 成功的话二阶段 Confirm 一定能成功。

Try 方法作为一阶段准备方法,需要做资源的检查和预留。在扣钱场景下,Try 要做的事情是就是检查账户余额是否充足,预留转账资金,预留的方式就是冻结 A 账户的 转账资金。Try 方法执行之后,账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使用。
二阶段 Confirm 方法执行真正的扣钱操作。Confirm 会使用 Try 阶段冻结的资金,执行账号扣款。Confirm 方法执行之后,账号 A 在一阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。如果二阶段是回滚的话,就需要在 Cancel 方法内释放一阶段 Try 冻结的 30 元,使账号 A 的回到初始状态,100 元全部可用。
用户接入 TCC 模式,最重要的事情就是考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个方法,并且保证 Try 成功 Confirm 一定能成功。相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。
针对RM端,实现起来需要完成try/commit/rollback的实现,所以步骤相对较多但是前三步骤和AT模式一样

(1) 修改数据库表结构,增加预留检查字段,用于提交和回滚
- ALTER TABLE `seata_order`.`t_order` ADD COLUMN `status` INT ( 0 ) NULL COMMENT '订单状态-0不可⽤,事务未提交 , 1-可⽤,事务提交';
-
- ALTER TABLE `seata_points`.`t_points` ADD COLUMN `frozen_points` INT ( 0 ) NULL DEFAULT 0 COMMENT '冻结积分' AFTER `points`;
-
- ALTER TABLE `seata_storage`.`t_storage` ADD COLUMN `frozen_storage` INT ( 0 ) NULL DEFAULT 0 COMMENT '冻结库存' AFTER `goods_id`;
(2)lagou_order工程改造
接口
- package com.lagou.order.service;
-
-
- import com.baomidou.mybatisplus.extension.service.IService;
- import com.lagou.order.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;
-
- /**
- * 接口被Seata管理,根据事务的状态完成提交或回滚操作
- */
- @LocalTCC
- public interface OrderService extends IService
{ - @TwoPhaseBusinessAction(name = "addTCC", commitMethod = "addCommit", rollbackMethod = "addRollback")
- // 该注解中name属性定义的名称必须保持全局唯一, commitMethod默认名称为"commit",rollbackMethod默认名称为"rollback"
- void add(@BusinessActionContextParameter(paramName = "order") Order order); // 该注解是将此方法中的Order参数放到BusinessActionContext上下文对象中,供我们定义的方法使用,paramName默认为 ""
-
- public boolean addCommit(BusinessActionContext context); // 该方法的返回值类型是固定的
-
- public boolean addRollback(BusinessActionContext context); // 该方法的返回值类型是固定的
- }
实体类增加一个字段
- package com.lagou.order.entity;
-
-
- import com.baomidou.mybatisplus.annotation.IdType;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableId;
- import com.baomidou.mybatisplus.annotation.TableName;
- import lombok.Data;
-
- import java.io.Serializable;
-
- /**
- * 订单实体类
- */
- @Data
- @TableName("t_order")
- public class Order implements Serializable {
-
- @TableId
- private Long id;//订单id
- @TableField
- private Integer goodsId;// 商品ID
- @TableField
- private Integer num;//商品数量
- @TableField
- private Double money;//商品总金额
- @TableField
- private java.util.Date createTime;//订单创建时间
- @TableField
- private String username;//用户名称
-
- @TableField
- private Integer status; // 订单状态
- }
-
实现类
- package com.lagou.order.service.impl;
-
- import com.alibaba.fastjson.JSON;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.lagou.order.entity.Order;
- import com.lagou.order.mapper.OrderMapper;
- import com.lagou.order.service.OrderService;
- import io.seata.rm.tcc.api.BusinessActionContext;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.stereotype.Service;
-
- import java.util.Date;
- @Slf4j
- @Service
- public class OrderServiceImpl extends ServiceImpl
implements OrderService { -
- @Override
- public void add(Order order) {
- order.setCreateTime(new Date());//设置订单创建时间
- order.setStatus(0); // try 阶段-预检查
- this.save(order);//保存订单
- }
-
- @Override
- public boolean addCommit(BusinessActionContext context) {
- Object jsonOrder = context.getActionContext("order");
- Order order = JSON.parseObject(jsonOrder.toString(), Order.class);
- order = this.getById(order.getId());
- if (order != null) {
- order.setStatus(1); // 提交操作,1代表订单可用
- this.saveOrUpdate(order);
- }
- log.info("----------->xid"+context.getXid()+" 提交成功!");
- return true; // 注意方法必须返回为true
- }
-
- @Override
- public boolean addRollback(BusinessActionContext context) {
- Object jsonOrder = context.getActionContext("order");
- Order order = JSON.parseObject(jsonOrder.toString(), Order.class);
- order = this.getById(order.getId());
- if (order != null) {
- this.removeById(order.getId()); // 回滚操作-删除订单
- }
- log.info("----------->xid"+context.getXid()+" 回滚成功!");
- return true;
- }
- }
(3)lagou_points工程改造
接口改造
- package com.lagou.points.service;
-
- import com.baomidou.mybatisplus.extension.service.IService;
- import com.lagou.points.entity.Points;
- 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;
-
- @LocalTCC
- public interface PointsService extends IService
{ - @TwoPhaseBusinessAction(name = "increaseTCC", commitMethod = "increaseCommit", rollbackMethod = "increaseRollback")
- public void increase(@BusinessActionContextParameter(paramName = "username") String username,
- @BusinessActionContextParameter(paramName = "points") Integer points);
-
- public boolean increaseCommit(BusinessActionContext context);
-
- public boolean increaseRollback(BusinessActionContext context);
- }
实体类增加一个字段
- package com.lagou.points.entity;
-
- import com.baomidou.mybatisplus.annotation.IdType;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableId;
- import com.baomidou.mybatisplus.annotation.TableName;
- import lombok.Data;
-
- /**
- * 积分实体类
- */
- @Data
- @TableName("t_points")
- public class Points {
- @TableId(value = "ID", type = IdType.AUTO)
- private Integer id;//积分ID
- @TableField
- private String username;//用户名
- @TableField
- private Integer points;//增加的积分
-
- @TableField
- private Integer frozenPoints; // 冻结积分
- }
实现类改造
- package com.lagou.points.service.impl;
-
-
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.lagou.points.mapper.PointsMapper;
- import com.lagou.points.entity.Points;
- import com.lagou.points.service.PointsService;
- import io.seata.rm.tcc.api.BusinessActionContext;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- /**
- * 会员积分服务
- */
- @Slf4j
- @Service
- public class PointsServiceImpl extends ServiceImpl
implements PointsService { - @Autowired
- PointsMapper pointsMapper;
-
- /**
- * 会员增加积分
- *
- * @param username 用户名
- * @param points 增加的积分
- * @return 积分对象
- */
- public void increase(String username, Integer points) {
- QueryWrapper
wrapper = new QueryWrapper(); - wrapper.lambda().eq(Points::getUsername, username);
- Points userPoints = this.getOne(wrapper);
- if (userPoints == null) {
- userPoints = new Points();
- userPoints.setUsername(username);
- // userPoints.setPoints(points);
- userPoints.setFrozenPoints(points); // try-设置冻结积分
- } else {
- // userPoints.setPoints(userPoints.getPoints() + points);
- userPoints.setFrozenPoints(points); // try-设置冻结积分
- }
- this.saveOrUpdate(userPoints);
- }
-
- @Override
- public boolean increaseCommit(BusinessActionContext context) {
- // 查询⽤户积分
- QueryWrapper
wrapper = new QueryWrapper(); - wrapper.lambda().eq(Points::getUsername, context.getActionContext("username"));
- Points userPoints = this.getOne(wrapper);
- if (userPoints != null) {
- // 增加用户积分
- userPoints.setPoints(userPoints.getPoints() + userPoints.getFrozenPoints());
- // 冻结积分清零
- userPoints.setFrozenPoints(0);
- this.saveOrUpdate(userPoints);
- }
- log.info("--------->xid=" + context.getXid() + " 提交成功!");
- return true;
- }
-
-
- @Override
- public boolean increaseRollback(BusinessActionContext context) {
- // 查询用户积分
- QueryWrapper
wrapper = new QueryWrapper(); - wrapper.lambda().eq(Points::getUsername, context.getActionContext("username"));
- Points userPoints = this.getOne(wrapper);
- if (userPoints != null) {
- // 冻结积分清零
- userPoints.setFrozenPoints(0);
- this.saveOrUpdate(userPoints);
- }
- log.info("--------->xid=" + context.getXid() + " 回滚成功!");
- return true;
- }
- }
(4)lagou_stroage工程改造
接口改造
- package com.lagou.storage.service;
-
- import com.baomidou.mybatisplus.extension.service.IService;
- import com.lagou.storage.entity.Storage;
- 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;
-
- /**
- * 仓库服务
- */
- @LocalTCC
- public interface StorageService extends IService
{ - @TwoPhaseBusinessAction(name = "decreaseTCC", commitMethod = "decreaseCommit", rollbackMethod = "decreaseRollback")
- public void decrease(@BusinessActionContextParameter(paramName = "goodsId") Integer goodsId,
- @BusinessActionContextParameter(paramName = "quantity") Integer quantity);
-
- public boolean decreaseCommit(BusinessActionContext context);
-
- public boolean decreaseRollback(BusinessActionContext context);
- }
实体类新增一个字段
- package com.lagou.storage.entity;
-
- import com.baomidou.mybatisplus.annotation.IdType;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableId;
- import com.baomidou.mybatisplus.annotation.TableName;
- import lombok.Data;
-
- @Data
- @TableName("t_storage")
- public class Storage {
- @TableId(value = "id", type = IdType.AUTO)
- private Integer id;// 库存ID
- @TableField
- private String goodsId;// 商品ID
- @TableField
- private Integer storage;// 库存量
-
- @TableField
- private Integer frozenStorage;// 冻结库存
- }
实现类改造
- package com.lagou.storage.service.impl;
-
- import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.lagou.storage.entity.Storage;
- import com.lagou.storage.mapper.StorageMapper;
- import com.lagou.storage.service.StorageService;
- import io.seata.rm.tcc.api.BusinessActionContext;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.stereotype.Service;
-
- /**
- * 仓库服务
- */
- @Service
- @Slf4j
- public class StorageServiceImpl extends ServiceImpl
implements StorageService { -
- /**
- * 减少库存
- *
- * @param goodsId 商品ID
- * @param quantity 减少数量
- * @return 库存对象
- */
- public void decrease(Integer goodsId, Integer quantity) {
- QueryWrapper
wrapper = new QueryWrapper(); - wrapper.lambda().eq(Storage::getGoodsId, goodsId);
- Storage goodsStorage = this.getOne(wrapper);
- if (goodsStorage.getStorage() >= quantity) {
- // goodsStorage.setStorage(goodsStorage.getStorage() - quantity);
- // 设置冻结库存
- goodsStorage.setFrozenStorage(quantity);
- } else {
- throw new RuntimeException(goodsId + "库存不足,目前剩余库存:" + goodsStorage.getStorage());
- }
- this.saveOrUpdate(goodsStorage);
- }
-
- @Override
- public boolean decreaseCommit(BusinessActionContext context) {
- QueryWrapper
wrapper = new QueryWrapper(); - wrapper.lambda().eq(Storage::getGoodsId,
- context.getActionContext("goodsId"));
- Storage goodsStorage = this.getOne(wrapper);
- if (goodsStorage != null) {
- // 扣减库存
- goodsStorage.setStorage(goodsStorage.getStorage() - goodsStorage.getFrozenStorage());
- // 冻结库存清零
- goodsStorage.setFrozenStorage(0);
- this.saveOrUpdate(goodsStorage);
- }
- log.info("--------->xid=" + context.getXid() + " 提交成功!");
- return true;
- }
-
- @Override
- public boolean decreaseRollback(BusinessActionContext context) {
- QueryWrapper
wrapper = new QueryWrapper(); - wrapper.lambda().eq(Storage::getGoodsId, context.getActionContext("goodsId"));
- Storage goodsStorage = this.getOne(wrapper);
- if (goodsStorage != null) {
- // 冻结库存清零
- goodsStorage.setFrozenStorage(0);
- this.saveOrUpdate(goodsStorage);
- }
- log.info("--------->xid=" + context.getXid() + " 回滚成功!");
- return true;
- }
- }
针对我们工程lagou_bussiness是事务的发起者,所以是TM端,其它工程为RM端. 所以我们只需要在lagou_common_db完成即可,因为lagou_bussiness方法里面没有对数据库操作.所以只需要将之前AT模式的代理数据源去掉即可.注意:如果lagou_bussiness也对数据库操作了.也需要完成try/commit/rollback的实现 。

代码实现:
- package com.lagou.bussiness.service.impl;
-
- import com.lagou.bussiness.feign.OrderServiceFeign;
- import com.lagou.bussiness.feign.PointsServiceFeign;
- import com.lagou.bussiness.feign.StorageServiceFeign;
- import com.lagou.bussiness.service.BussinessService;
- import com.lagou.bussiness.utils.IdWorker;
- import io.seata.spring.annotation.GlobalTransactional;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
-
- /**
- * 业务逻辑
- */
- @Service
- public class BussinessServiceImpl implements BussinessService {
-
- @Autowired
- OrderServiceFeign orderServiceFeign;
- @Autowired
- PointsServiceFeign pointsServiceFeign;
-
- @Autowired
- StorageServiceFeign storageServiceFeign;
-
- @Autowired
- IdWorker idWorker;
-
- /**
- * 商品销售
- *
- * @param goodsId 商品id
- * @param num 销售数量
- * @param username 用户名
- * @param money 金额
- */
- // @Transactional
- @GlobalTransactional(name = "sale", timeoutMills = 100000, rollbackFor = Exception.class)
- public void sale(Integer goodsId, Integer num, Double money, String username) {
- //创建订单
- orderServiceFeign.addOrder(idWorker.nextId(), goodsId, num, money, username);
- //增加积分
- pointsServiceFeign.increase(username, (int) (money / 10));
- //扣减库存
- storageServiceFeign.decrease(goodsId, num);
- }
- }
示例代码下载