1、TCC模式介绍
Seata 开源了 TCC 模式,该模式由蚂蚁金服贡献。TCC 模式需要用户根据自己的业务场景实现Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段 执行 Try 方式,在二阶段提交执行Confirm方法,二阶段回滚执行 Cancel 方法。
TCC 三个方法描述:
- Try:资源的检测和预留;
- Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
- Cancel:预留资源释放。
业务模型分 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 模式高很多。
2、TCC模式改造案例
2.1、RM端改造
针对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<Order> {
@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<OrderMapper, Order> 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<Points> {
@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<PointsMapper, Points> implements PointsService {
@Autowired
PointsMapper pointsMapper;
/**
* 会员增加积分
*
* @param username 用户名
* @param points 增加的积分
* @return 积分对象
*/
public void increase(String username, Integer points) {
QueryWrapper<Points> wrapper = new QueryWrapper<Points>();
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<Points> wrapper = new QueryWrapper<Points>();
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<Points> wrapper = new QueryWrapper<Points>();
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<Storage> {
@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<StorageMapper, Storage> implements StorageService {
/**
* 减少库存
*
* @param goodsId 商品ID
* @param quantity 减少数量
* @return 库存对象
*/
public void decrease(Integer goodsId, Integer quantity) {
QueryWrapper<Storage> wrapper = new QueryWrapper<Storage>();
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<Storage> wrapper = new QueryWrapper<Storage>();
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<Storage> wrapper = new QueryWrapper<Storage>();
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;
}
}
2.2、TM端改造
针对我们工程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);
}
}
示例代码下载