Seate一站式分布式事务解决方案,真香......

Seata 是什么?

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

只分享常用的两种,本人项目中也是用的这两种(AT模式,TCC模式),哈哈哈

Seate安装配置

1.下载seata

官网下载地址:https://seata.io/zh-cn/blog/download.html (下载二进制文件)

下载完成后,将文件解压到任意地方 (不要忘记解压的地方)

接下来的大部分操作都在下图的根目录展开

 

2.创建seata数据库

新建数据库seata,然后在解压的seata文件找到script->server->db->mysql.sql,执行这个sql脚本。

3.修改config.txt文件

config.txt文件在seata/script/config-center目录下,需要修改的地方如下:

三个mode改为db,模式也支持file、redis

 数据库信息改为自己的

4.修改seata配置文件

进入seata/conf目录下,有两个配置文件,把application.yml 随意修改一个名字,然后把 application.example.yml修改成application.yml 作为主要配置文件。

修改application.yml文件,这里使用的nacos作为注册中心,所以需要修改的地方有: 

将原来application.yml 文件中的console和security拷贝走,复制到主要配置文件(最初的application.example.yml)文件中 

然后开始修改主要配置文件(最初的application.example.yml)中的设置

首先修改seata,我当时使用的是nacos,如下:

seata:
    config:
        type: nacos #类型有多个,本人项目使用的nacos
        nacos:
            server-addr: 127.0.0.1:8848 #nacos地址
            namespace: nacos配置的命名空间
            group: SEATE_GROUP #默认这个
            username: nacos #nacos账号
            password: nacos #nacos密码
            ......

 注意这些部分的配置

修改seata下的registry:

此操作和上方相同,修改nacos配置,注意(group和上面要一致)

修改seata下的store:

此步操作和修改config.txt文件相同,三个mode改为db, 数据库信息改为自己的

5.启动服务

找到seata-server.bat,点击启动,路径为:seata->bin->seata-server.bat,成功后可以在nacos控制台服务列表中看到多了一个服务。

6.整合SpringCloud

添加pom依赖: 

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2.2.2.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

分布式事务一致性,cap定理、base理论 

事务的一致性

  • 强一致性:系统中的某个数据被成功更新后,后续的访问都能看到更新后的值;
  • 弱一致性:系统中的某个数据被更新后,后续的访问可能得到更新后的值,也可能是更改前的值;
  • 最终一致性:系统中的某个数据被更新,经过一段时间后,最终所有的访问都是更新的值;

刚性事务:遵循ACID原则,强一致性;

柔性事务:遵循BASE理论,最终一致性;

事务的四个特性(ACID):

原子性(Atomicity):事务被视为一个不可分割的最小工作单元,事务中的所有操作要么全部提交成功,要么全部失败回滚;
一致性(Consistency):数据库总是从一个一致性的状态转换到另外一个一致性的状态;
隔离性(Isolation):一个事务所做的修改在最终提交以前,对其他事务是不可见的;
持久性(Durability):一旦事务提交,则其所做的修改就会永久保存到数据库中;
柔性事务对事务四个特性(ACID)的支持情况:

原子性:完全支持。
一致性:只提供最终一致性支持。
隔离性:不完全保证,通常为了系统的吞吐和性能,会一定程度上放弃对隔离性的要求。
持久性:完全支持。

CAP 定理

CAP 也就是 Consistency(一致性)Availability(可用性)Partition Tolerance(分区容错性) 这三个单词首字母组合。

CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:

  • 一致性(Consistency) : 所有节点访问同一份最新的数据副本
  • 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
  • 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。

 

 对于分布式项目来说,一定要满足分区容错性,如果要满足一致性,则可用性无法满足,反之也是如此,所以一致性和可用性似乎成了矛盾,导致分布式系统分为了AP、CP

BASE理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)的缩写。

              

在分布式系统中,CAP理论是指导思维,而BASE理论是CAP理论中AP的延伸,是对 CAP 中的一致性和可用性进行一个权衡的结果,核心思想是:即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。

基本可用(Basically Available):指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。
柔性状态(Soft state):指允许系统存在中间状态,并认为该中间状态不会影响系统整体可用性。比如,允许不同节点间副本同步的延时就是柔性状态的体现。
最终一致性(Eventually consistent):指系统中的所有副本经过一定时间后,最终能够达到一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性

AT 模式 

前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。

​ 

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

​ 

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

 

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

工作机制

以一个示例来说明整个 AT 分支的工作过程。

业务表:product

​ 

AT 分支事务的业务逻辑:

update product set name = 'GTS' where name = 'TXC';

一阶段

过程:

        1.解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息。

        2.查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。

select id, name, since from product where name = 'TXC';

 得到前镜像:

        3.执行业务 SQL:更新这条记录的 name 为 'GTS'。

        4.查询后镜像:根据前镜像的结果,通过 主键 定位数据。

select id, name, since from product where id = 1;

得到后镜像:

        5.插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

{
	"branchId": 641789253,
	"undoItems": [{
		"afterImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}

        6.提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。

        7.本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。

        8.将本地事务提交的结果上报给 TC。

二阶段-回滚

        1.收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。

        2.通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。

        3.数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。

        4.根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:

update product set name = 'TXC' where id = 1;

        5.提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

二阶段-提交

        1.收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。

        2.异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

回滚日志表

UNDO_LOG Table:不同数据库在类型上会略有差别。

以 MySQL 为例:

-- 注意此处0.7.0+ 增加字段 context
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

总结 

原理介绍了那么多,很头疼?总的来说,两个注解打天下,入口方法使用@GlobabTransaction,

内部方法,远程方法使用@Transaction。

打个样:

 @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public void integralPay(String orderNo, String token) {
        // 1. 先基于订单编号查询订单对象,判断订单是否存在
        OrderInfo orderInfo = this.findByOrderNo(orderNo);
        if (orderInfo == null) {
            throw new BusinessException(SeckillCodeMsg.REMOTE_DATA_ERROR);
        }

        // 检查是否是当前用户在发起支付
        this.checkOrderUser(token, orderInfo);

        // 2. 判断订单状态是否为未支付
        if (!OrderInfo.STATUS_ARREARAGE.equals(orderInfo.getStatus())) {
            throw new BusinessException(SeckillCodeMsg.ORDER_STATUS_ERROR);
        }
        // 3. 封装请求积分支付 vo 对象
        OperateIntergralVo vo = this.buildIntegralVo(orderInfo);
        // 4. 远程请求积分支付接口
        Result<String> result = integralFeignApi.doPay(vo);
        // 5. 判断远程是否支付成功
        if (result.hasError()) {
            throw new BusinessException(new CodeMsg(result.getCode(), result.getMsg()));
        }
        // 6. 更新订单状态为支付成功
        int row = orderInfoMapper.changePayStatus(orderNo, OrderInfo.STATUS_ACCOUNT_PAID, OrderInfo.PAY_TYPE_INTERGRAL);
        if (row == 0) {
            throw new BusinessException(SeckillCodeMsg.ORDER_STATUS_ERROR);
        }
        // 7. 记录支付日志
        try {
            PayLog log = this.buildPayLog(result.getData(), orderInfo, PayLog.PAY_TYPE_INTERGRAL);
            payLogMapper.insert(log);
        } catch (SQLException e) {
            throw new BusinessException(SeckillCodeMsg.REPEAT_PAY_ERROR);
        }
    }

TCC 模式

回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

​ 

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.

AT 模式基于 支持本地 ACID 事务 的 关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

数据库添加表:t_account_log

​ 

代码

话不多说,上代码。

 Service接口:

@LocalTCC
public interface IUsableIntegralService {
    /**
     * 积分支付接口
     *
     * @param vo 积分支付 vo
     * @return 支付流水号
     */
    String doPay(OperateIntergralVo vo);

    /**
     * 积分退款
     *
     * @param vo 积分退款 vo
     * @return 是否退款成功
     */
    boolean refund(IntegralRefundVo vo);

    /**
     * 一阶段
     * TCC 的 try 方法,执行资源预留操作
     * 先检查余额是否足够,并且冻结积分
     *
     * @param vo      操作积分 vo 对象
     * @param context 事务上下文对象,该对象在 try 阶段传入 null 即可,RM 会自动创建并注入该参数
     * @return 账户流水 id
     */
    @TwoPhaseBusinessAction(name = "tryIncrIntegral", commitMethod = "commitIncrIntegral", rollbackMethod = "rollbackIncrIntegral")
    String tryIncrIntegral(@BusinessActionContextParameter(paramName = "integralVo") OperateIntergralVo vo, BusinessActionContext context);

    /**
     * 二阶段
     * TCC 的 confirm 方法,执行确认资源扣除操作
     * 扣除真实积分 & 扣除冻结积分
     * 增加账户变动流水记录
     *
     * @param context 事务上下文对象
     * @return 账户流水 id
     */
    String commitIncrIntegral(BusinessActionContext context);

    /**
     * 二阶段
     * TCC 的 rollback 方法,执行回滚 try 预留资源
     * 扣除 try 阶段冻结的金额
     *
     * @param context 事务上下文对象
     * @return 账户流水 id
     */
    void rollbackIncrIntegral(BusinessActionContext context);
}

实现类:


    @Override
    public String tryIncrIntegral(OperateIntergralVo vo, BusinessActionContext context) {
        log.info("[积分支付] 执行一阶段 TRY 方法,准备冻结金额:xid={}, branchId={}, params={}", context.getXid(), context.getBranchId(), JSON.toJSONString(vo));
        // 1. 查询金额(totalAmount-freezedAmount),判断是否足够
        // 2. 增加冻结金额
        int row = usableIntegralMapper.freezeIntergral(vo.getUserId(), vo.getValue());
        if (row == 0) {
            // 余额不足
            throw new BusinessException(IntergralCodeMsg.INTERGRAL_NOT_ENOUGH);
        }
        // 3. 保存账户变动日志,设置状态为 TRY
        AccountLog accountLog = this.buildAccountLog(vo.getPk(), vo.getValue(), vo.getInfo(), AccountLog.TYPE_DECR);
        accountLogMapper.insert(accountLog);
        return accountLog.getTradeNo();
    }

    @Override
    public String commitIncrIntegral(BusinessActionContext context) {
        JSONObject json = (JSONObject) context.getActionContext("integralVo");
        log.info("[积分支付] 执行二阶段 CONFIRM 方法,提交积分变动操作:xid={}, branchId={}, params={}", context.getXid(), context.getBranchId(), json);
        // 1. 先查询之前 TRY 阶段执行的记录是否存在
        AccountLog accountLog = accountLogMapper.selectByPkAndStatus(json.getString("pk"), AccountLog.ACCOUNT_LOG_STATUS_TRY);
        if (accountLog == null) {
            log.warn("[积分支付] 执行积分支付的二阶段 COMMIT 操作失败,查询不到前置 TRY 操作日志..");
            return null;
        }
        // 2. 执行扣除总金额、冻结金额
        usableIntegralMapper.commitChange(json.getLong("userId"), json.getLong("value"));

        // 3. 更新账户日志变动的状态为 CONFIRM
        accountLogMapper.changeStatus(accountLog.getTradeNo(), AccountLog.ACCOUNT_LOG_STATUS_CONFIRM);
        return accountLog.getTradeNo();
    }

    @Override
    public void rollbackIncrIntegral(BusinessActionContext context) {
        JSONObject json = (JSONObject) context.getActionContext("integralVo");
        log.info("[积分支付] 执行二阶段 ROLLBACK 方法,提交积分变动操作:xid={}, branchId={}, params={}", context.getXid(), context.getBranchId(), json);
        // 1. 先查询之前 TRY 阶段执行的记录是否存在
        AccountLog accountLog = accountLogMapper.selectByPkAndStatus(json.getString("pk"), AccountLog.ACCOUNT_LOG_STATUS_TRY);
        if (accountLog == null) {
            log.warn("[积分支付] 执行积分支付的二阶段 ROLLBACK 操作失败,查询不到前置 TRY 操作日志..");
            return;
        }
        // 2. 取消冻结
        usableIntegralMapper.unFreezeIntergral(json.getLong("userId"),
                json.getLong("value"));
        // 3. 将操作日志状态更新为回滚
        accountLogMapper.changeStatus(accountLog.getTradeNo(), AccountLog.ACCOUNT_LOG_STATUS_CANCEL);
    }

Mapper:

 /**
     * 插入日志
     *
     * @param accountLog
     */
    void insert(AccountLog accountLog);

    /**
     * 按照 pk 和 type 查询日志对象
     *
     * @param pkValue 订单编号
     * @param type    操作类型
     * @return 日志对象
     */
    AccountLog selectByPkAndType(@Param("pkValue") String pkValue, @Param("type") int type);

    AccountLog selectByPkAndStatus(@Param("pk") String pk, @Param("status") int status);

    void changeStatus(String tradeNo, int status);

 XML:

    <insert id="insert">
        insert into t_account_log (trade_no, pk_value, type, amount, gmt_time, info, status)
        values (#{tradeNo}, #{pkValue}, #{type}, #{amount}, #{gmtTime}, #{info}, #{status})
    </insert>

    <update id="changeStatus">
        update t_account_log
        set status = #{status}
        where trade_no = #{tradeNo}
    </update>

    <select id="selectByPkAndType" resultType="cn.wolfcode.domain.AccountLog">
        select trade_no tradeNo, pk_value pkValue, type, amount, gmt_time gmtTime, info, status
        from t_account_log
        where pk_value = #{pkValue}
          and type = #{type}
    </select>
    <select id="selectByPkAndStatus" resultType="cn.wolfcode.domain.AccountLog">
        select trade_no tradeNo, pk_value pkValue, type, amount, gmt_time gmtTime, info, status
        from t_account_log
        where pk_value = #{pk}
          and status = #{status}
    </select>

优化:TCC异常处理:空回滚、幂等性、防悬挂

实现类:

    @Override
    public String tryIncrIntegral(OperateIntergralVo vo, BusinessActionContext context) {
        log.info("[积分支付] 执行一阶段 TRY 方法,准备冻结金额:xid={}, branchId={}, params={}", context.getXid(), context.getBranchId(), JSON.toJSONString(vo));
        // 1. 先检查是否已经回滚过
        AccountLog accountLog = accountLogMapper.selectByPkAndStatus(vo.getPk(), AccountLog.ACCOUNT_LOG_STATUS_CANCEL);
        if (accountLog != null) {
            // 之前已经回滚过,就不能再继续执行 TRY 操作
            throw new BusinessException(IntergralCodeMsg.ILLEGAL_OPERATION);
        }
        // 2. 查询金额(totalAmount-freezedAmount),判断是否足够
        // 3. 增加冻结金额
        int row = usableIntegralMapper.freezeIntergral(vo.getUserId(), vo.getValue());
        if (row == 0) {
            // 余额不足
            throw new BusinessException(IntergralCodeMsg.INTERGRAL_NOT_ENOUGH);
        }

        // 4. 插入事务记录
        accountLog = insertAccountLog(vo, context, AccountLog.ACCOUNT_LOG_STATUS_TRY);
        return accountLog.getTradeNo();
    }

    private AccountLog insertAccountLog(OperateIntergralVo vo, BusinessActionContext context, Integer status) {
        AccountLog accountLog;
        accountLog = this.buildAccountLog(vo.getPk(), vo.getValue(), vo.getInfo(), AccountLog.TYPE_DECR);
        accountLog.setTxId(context.getXid());
        accountLog.setActionId(context.getBranchId() + "");
        accountLog.setStatus(status);
        accountLogMapper.insert(accountLog);
        return accountLog;
    }

    @Override
    public String commitIncrIntegral(BusinessActionContext context) {
        JSONObject json = (JSONObject) context.getActionContext("integralVo");
        log.info("[积分支付] 执行二阶段 CONFIRM 方法,提交积分变动操作:xid={}, branchId={}, params={}", context.getXid(), context.getBranchId(), json);
        // 1. 先按照事务 id 查询日志记录对象
        AccountLog accountLog = accountLogMapper.selectByTxId(context.getXid());
        if (accountLog == null) {
            log.warn("[积分支付] 操作流程异常,未查询到一阶段 TRY 方法执行记录...");
            throw new BusinessException(IntergralCodeMsg.ILLEGAL_OPERATION);
        }
        // 2. 判断状态是否为以回滚
        if (accountLog.getStatus().equals(AccountLog.ACCOUNT_LOG_STATUS_CANCEL)) {
            log.warn("[积分支付] 操作流程异常,已执行过回滚操作...");
            throw new BusinessException(IntergralCodeMsg.ILLEGAL_OPERATION);
        } else if (accountLog.getStatus().equals(AccountLog.ACCOUNT_LOG_STATUS_CONFIRM)) {
            // 3. 判断状态是否为以提交
            log.warn("[积分支付] 重复执行 COMMIT 方法,执行幂等操作...");
            return accountLog.getTradeNo();
        }

        // 4. 执行扣除总金额、冻结金额
        usableIntegralMapper.commitChange(json.getLong("userId"), json.getLong("value"));

        // 5. 更新账户日志变动的状态为 CONFIRM
        accountLogMapper.changeStatus(accountLog.getTradeNo(), AccountLog.ACCOUNT_LOG_STATUS_CONFIRM);
        return accountLog.getTradeNo();
    }

    @Override
    public void rollbackIncrIntegral(BusinessActionContext context) {
        JSONObject json = (JSONObject) context.getActionContext("integralVo");
        log.info("[积分支付] 执行二阶段 ROLLBACK 方法,提交积分变动操作:xid={}, branchId={}, params={}", context.getXid(), context.getBranchId(), json);
        // 1. 先查询之前 TRY 阶段执行的记录是否存在
        AccountLog accountLog = accountLogMapper.selectByTxId(context.getXid());
        if (accountLog == null) {
            // 说明执行没有执行过 TRY 操作,执行空回滚,插入空回滚记录
            this.insertAccountLog(json.toJavaObject(OperateIntergralVo.class), context, AccountLog.ACCOUNT_LOG_STATUS_CANCEL);
            return;
        }

        if (accountLog.getStatus().equals(AccountLog.ACCOUNT_LOG_STATUS_CONFIRM)) {
            log.warn("[积分支付] 当前事务已经 COMMIT,无法执行 CANCEL,流程异常...");
            // 如果当前状态为已经 COMMIT,那说明流程异常,直接抛出异常
            throw new BusinessException(IntergralCodeMsg.ILLEGAL_OPERATION);
        } else if (accountLog.getStatus().equals(AccountLog.ACCOUNT_LOG_STATUS_CANCEL)) {
            log.warn("[积分支付] 重复执行 CANCEL 方法,执行幂等操作...");
            return;
        }

        // 2. 取消冻结
        usableIntegralMapper.unFreezeIntergral(json.getLong("userId"),
                json.getLong("value"));
        // 3. 将操作日志状态更新为回滚
        accountLogMapper.changeStatus(accountLog.getTradeNo(), AccountLog.ACCOUNT_LOG_STATUS_CANCEL);
    }

 Mapper新增:

    AccountLog selectByTxId(String txId);

 XML新增:

    <select id="selectByTxId" resultType="cn.wolfcode.domain.AccountLog">
        select trade_no tradeNo, pk_value pkValue, type, amount, gmt_time gmtTime, info, tx_id txId, action_id actionId, status
        from t_account_log
        where tx_id = #{txId}
    </select>

 总结

 TCC使用麻烦,所有逻辑全靠自己判断,TCC只帮我们自动调用了方法,不过TCC使用更加灵活,针对一些特殊场景使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值