Seata-TCC模式

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);
    }
}

示例代码下载 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悠然予夏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值