seata tcc使用1-RM工程搭建

前言

本文使用工具版本如下:
seata-1.4.2
dubbo-3.0.5
mysql-8

1.概念

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

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为 两阶段事务模型 TCC 模式,不依赖于底层数据资源的事务支持:
  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。 所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。 那TCC事务开发就需要完成prepare、commit、rollback三个方法逻辑。

2.开发

2.1 概览图

seata tcc事务概览

2.2 工程总体结构

工程总体结构

2.3 订单模块

2.3.1 order-api模块

2.3.1.1 工程结构

order-api工程结构

2.3.1.2 TccOrderService

import com.liuhangs.learning.seata.tcc.order.vo.TccOrderVO;

/**
 * 创建订单 dubbo接口定义
 */
public interface TccOrderService {

    boolean createOrder(TccOrderVO tccOrderVO);
}

2.3.2 order模块

2.3.2.1 工程结构

order工程结构

2.3.2.2 建表语句

CREATE TABLE test1.seata_tcc_order (
	`id` BIGINT(64) auto_increment NOT NULL COMMENT '自增ID' PRIMARY KEY,
	`order_id` varchar(64) NOT NULL COMMENT '订单ID' ,
	`user_id` BIGINT NOT NULL COMMENT '用户ID',
	`amount` INT NOT NULL COMMENT '订单金额',
	`is_delete` INT DEFAULT 0 NOT null COMMENT '是否删除,0-未删除,1-已删除',
	`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  	`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	key(order_id),
	key(user_id)
)
ENGINE=InnoDB
COMMENT='订单表';

CREATE TABLE test1.seata_tcc_order_tx (
	`id` BIGINT(64) auto_increment NOT NULL COMMENT '自增ID' PRIMARY KEY,
	`order_id` varchar(64) NOT NULL COMMENT '订单ID' ,
	`xid` varchar(128) NOT NULL COMMENT '事务ID',
	`status` INT NOT NULL DEFAULT 0 COMMENT '事务状态,0-初始状态,1-已提交,9-已回滚',
	`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  	`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	key(order_id),
	key(xid)
)
ENGINE=InnoDB
COMMENT='订单事务表';

2.3.2.3 pom依赖

<dependencies>
    <dependency>
      <groupId>com.liuhangs.learning</groupId>
      <artifactId>seata-tcc-order-api</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.2.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>3.0.5</version>
    </dependency>
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-registry-nacos</artifactId>
      <version>3.0.5</version>
    </dependency>
    <dependency>
      <groupId>io.seata</groupId>
      <artifactId>seata-spring-boot-starter</artifactId>
      <version>1.4.2</version>
    </dependency>
    <dependency>
      <groupId>com.esotericsoftware.kryo</groupId>
      <artifactId>kryo</artifactId>
      <version>2.24.0</version>
    </dependency>
    <dependency>
      <groupId>com.esotericsoftware</groupId>
      <artifactId>kryo</artifactId>
      <version>4.0.2</version>
    </dependency>
    <dependency>
      <groupId>de.javakaffee</groupId>
      <artifactId>kryo-serializers</artifactId>
      <version>0.42</version>
    </dependency>
  </dependencies>

2.3.2.4 application.yml

server:
  port: 7001
  servlet:
    context-path: /

spring:
  application:
    name: seata-tcc-order
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://127.0.0.1:3306/test1?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root


mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/**/*Mapper.xml

dubbo:
  application:
    name: seata-tcc-order
  registry:
    address: nacos://127.0.0.1:8848
  protocol:
    port: 20883
    name: dubbo

seata:
  enabled: true
  tx-service-group: seata_learning_tx_group #事务分组,需要和seata服务器一致
  #这里很关键,使用XA模式时,需要关掉seata的数据源代理,否则会出现无法回滚或者部分回滚的问题
  #enable-auto-data-source-proxy: false
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: edc58481-b3b7-4596-8cfb-de304633c6d2
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: edc58481-b3b7-4596-8cfb-de304633c6d2
      group: SEATA_GROUP

2.3.2.5 配置类SeataTccOrderConfig

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@EnableDubbo(scanBasePackages = "com.liuhangs.learning.seata.tcc.order.service")
@MapperScan("com.liuhangs.learning.seata.tcc.order.mapper")
@Configuration
public class SeataTccOrderConfig {

}

2.3.2.6 TccOrderMapper

import org.apache.ibatis.annotations.Mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liuhangs.learning.seata.tcc.order.mapper.po.TccOrderPO;

@Mapper
public interface TccOrderMapper extends BaseMapper<TccOrderPO> {

}

2.3.2.7 TccOrderTxMapper

import org.apache.ibatis.annotations.Mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liuhangs.learning.seata.tcc.order.mapper.po.TccOrderPO;
import com.liuhangs.learning.seata.tcc.order.mapper.po.TccOrderTxPO;

@Mapper
public interface TccOrderTxMapper extends BaseMapper<TccOrderTxPO> {

}

2.3.2.8 dubbo接口实现TccOrderServiceImpl

import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.liuhangs.learning.seata.tcc.order.vo.TccOrderVO;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 创建订单 dubbo接口实现类
 */
@DubboService
@Service
@RequiredArgsConstructor
@Slf4j
public class TccOrderServiceImpl implements TccOrderService {

    private final TccOrderTxService tccOrderTxService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean createOrder(TccOrderVO tccOrderVO) {
        return tccOrderTxService.prepare(null, tccOrderVO.getOrderId(), tccOrderVO.getUserId(), tccOrderVO.getAmount());
    }
}

2.3.2.9 TCC分支事务定义TccOrderTxService

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;

/**
 * TCC分支事务定义
 */
@LocalTCC
public interface TccOrderTxService {

    /**
     * OrderTcc为分支事务名称,全局唯一
     * commitMethod为提交方法名称
     * rollbackMethod为回滚方法名称
     * @BusinessActionContextParameter注解表示将参数传给commit和rollback方法,
     * 在commit和rollback方法可以通过actionContext.getActionContext("orderId")方式获取参数
     */
    @TwoPhaseBusinessAction(name = "OrderTcc", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "orderId") String orderId,
        @BusinessActionContextParameter(paramName = "userId") Long userId,
        @BusinessActionContextParameter(paramName = "amount") Long amount);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean rollback(BusinessActionContext actionContext);
}

2.3.2.10 TCC分支事务实现

import java.util.Objects;

import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.liuhangs.learning.seata.tcc.order.mapper.TccOrderMapper;
import com.liuhangs.learning.seata.tcc.order.mapper.TccOrderTxMapper;
import com.liuhangs.learning.seata.tcc.order.mapper.po.TccOrderPO;
import com.liuhangs.learning.seata.tcc.order.mapper.po.TccOrderTxPO;

import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class TccOrderTxServiceImpl implements TccOrderTxService {

    private final TccOrderMapper tccOrderMapper;

    private final TccOrderTxMapper tccOrderTxMapper;

    @Override
    public boolean prepare(BusinessActionContext actionContext, String orderId, Long userId, Long amount) {
        log.info("准备事务开始,xid={}, orderId={}", actionContext.getXid(), orderId);
        // 幂等
        LambdaQueryWrapper<TccOrderTxPO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TccOrderTxPO::getXid, actionContext.getXid());
        TccOrderTxPO alreadyExistsTx = tccOrderTxMapper.selectOne(queryWrapper);
        // 如果已经存在事务,并且状态是已提交或者已回滚,表示事务已经执行完成,直接返回
        // 结合rollback方法的防止空回滚插入的事务数据,可以解决悬挂问题
        if(Objects.nonNull(alreadyExistsTx)) {
            log.info("触发幂等,xid={}, orderId={}", actionContext.getXid(), orderId);
            return true;
        }

        // 插入订单
        TccOrderPO tccOrderPO = new TccOrderPO();
        tccOrderPO.setOrderId(orderId);
        tccOrderPO.setUserId(userId);
        tccOrderPO.setAmount(amount);
        tccOrderMapper.insert(tccOrderPO);

        // 插入订单事务
        TccOrderTxPO tccOrderTxPO = new TccOrderTxPO();
        tccOrderTxPO.setOrderId(orderId);
        tccOrderTxPO.setXid(actionContext.getXid());
        // 初始状态
        tccOrderTxPO.setStatus(0);
        tccOrderTxMapper.insert(tccOrderTxPO);
        log.info("准备事务结束,xid={}, orderId={}", actionContext.getXid(), orderId);
        return true;
    }

    @Override
    public boolean commit(BusinessActionContext actionContext) {
        log.info("提交事务开始,xid={}", actionContext.getXid());
        // 幂等
        LambdaQueryWrapper<TccOrderTxPO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TccOrderTxPO::getXid, actionContext.getXid());
        TccOrderTxPO alreadyExistsTx = tccOrderTxMapper.selectOne(queryWrapper);
        // 如果已经存在事务,并且状态是已提交或者已回滚,表示事务已经执行完成,直接返回
        if(Objects.nonNull(alreadyExistsTx) && !Objects.equals(alreadyExistsTx.getStatus(), 0)) {
            log.info("触发幂等,xid={}", actionContext.getXid());
            return true;
        }

        TccOrderTxPO tccOrderTxPO = new TccOrderTxPO();
        tccOrderTxPO.setXid(actionContext.getXid());
        // 提交状态
        tccOrderTxPO.setStatus(1);

        LambdaUpdateWrapper<TccOrderTxPO> wapper = new LambdaUpdateWrapper<>();
        wapper.eq(TccOrderTxPO::getXid, actionContext.getXid());
        tccOrderTxMapper.update(tccOrderTxPO, wapper);
        log.info("提交事务结束,xid={}", actionContext.getXid());
        return true;
    }

    @Override
    public boolean rollback(BusinessActionContext actionContext) {
        String orderId = (String) actionContext.getActionContext("orderId");
        log.info("回滚事务开始,xid={}, orderId={}", actionContext.getXid(), orderId);
        // 幂等
        LambdaQueryWrapper<TccOrderTxPO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TccOrderTxPO::getXid, actionContext.getXid());
        TccOrderTxPO alreadyExistsTx = tccOrderTxMapper.selectOne(queryWrapper);
        // 如果已经存在事务,并且状态是已提交或者已回滚,表示事务已经执行完成,直接返回
        if(Objects.nonNull(alreadyExistsTx) && !Objects.equals(alreadyExistsTx.getStatus(), 0)) {
            log.info("触发幂等,xid={}, orderId={}", actionContext.getXid(), orderId);
            return true;
        }

        // 空回滚,没有事务初始记录,就开始回滚,就表示空回滚
        if(Objects.isNull(alreadyExistsTx)) {
            log.info("触发空回滚,xid={}, orderId={}", actionContext.getXid(), orderId);
            // 插入订单事务
            TccOrderTxPO tccOrderTxPO = new TccOrderTxPO();
            tccOrderTxPO.setOrderId(orderId);
            tccOrderTxPO.setXid(actionContext.getXid());
            // 回滚状态
            tccOrderTxPO.setStatus(9);
            tccOrderTxMapper.insert(tccOrderTxPO);
            return true;
        }

        // 删除订单数据
        TccOrderPO tccOrderPO = new TccOrderPO();
        tccOrderPO.setIsDelete(1);
        LambdaUpdateWrapper<TccOrderPO> orderWrapper = new LambdaUpdateWrapper<>();
        orderWrapper.eq(TccOrderPO::getOrderId, orderId);
        tccOrderMapper.update(tccOrderPO, orderWrapper);

        // 更新事务表状态
        TccOrderTxPO tccOrderTxPO = new TccOrderTxPO();
        tccOrderTxPO.setXid(actionContext.getXid());
        // 提交状态
        tccOrderTxPO.setStatus(9);

        LambdaUpdateWrapper<TccOrderTxPO> wapper = new LambdaUpdateWrapper<>();
        wapper.eq(TccOrderTxPO::getXid, actionContext.getXid());
        tccOrderTxMapper.update(tccOrderTxPO, wapper);
        log.info("回滚事务结束,xid={}, orderId={}", actionContext.getXid(), orderId);
        return true;
    }
}

2.4 钱包模块

2.4.1 wallet-api模块

2.4.1.1 工程结构

wallet-api工程结构

2.4.1.2 TccWalletService

import com.liuhangs.learning.seata.tcc.wallat.vo.TccUserBalanceVO;

/**
 * 钱包扣款 dubbo接口定义
 */
public interface TccWalletService {

    boolean deductionBalance(TccUserBalanceVO tccUserBalanceVO);
}

2.4.2 wallet模块

2.4.2.1 工程结构

wallet工程结构

2.4.2.2 建表语句init.sql

CREATE TABLE test2.seata_tcc_wallet_balance (
	id BIGINT(64) auto_increment NOT NULL PRIMARY key COMMENT '自增ID',
	user_id BIGINT NOT NULL COMMENT '用户ID',
	user_name varchar(32) NOT NULL COMMENT '用户名称',
	balance BIGINT(64) NOT NULL COMMENT '用户余额',
	freeze_balance BIGINT(64) NOT NULL DEFAULT 0 COMMENT '用户冻结余额',
	`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  	`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	is_delete INT DEFAULT 0 NOT null,
	key(user_id)
)
ENGINE=InnoDB
COMMENT='钱包余额表';

INSERT INTO test2.seata_tcc_wallet_balance (user_id,user_name,balance,create_time,update_time,is_delete) VALUES
	 (1,'tom',3000000,'2022-01-29 10:33:16','2022-02-09 09:27:50',0);

CREATE TABLE test2.seata_tcc_wallet_bill (
	id BIGINT(64) auto_increment NOT NULL PRIMARY key COMMENT '自增ID',
	user_id BIGINT NOT NULL COMMENT '用户ID',
	user_name varchar(32) NOT NULL COMMENT '用户名称',
	order_id varchar(64) NOT NULL COMMENT '订单ID' ,
	amount BIGINT(64) NOT NULL COMMENT '消费金额',
	remark varchar(64) default NULL COMMENT '备注' ,
	`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  	`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	is_delete INT DEFAULT 0 NOT null,
	key(user_id)
)
ENGINE=InnoDB
COMMENT='钱包账单表';

CREATE TABLE test1.seata_tcc_wallet_tx (
	`id` BIGINT(64) auto_increment NOT NULL COMMENT '自增ID' PRIMARY KEY,
	`xid` varchar(128) NOT NULL COMMENT '事务ID',
	`status` INT NOT NULL DEFAULT 0 COMMENT '事务状态,0-初始状态,1-已提交,9-已回滚',
	`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  	`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	key(xid)
)
ENGINE=InnoDB
COMMENT='钱包事务表';

2.4.2.3 pom依赖

<dependencies>
    <dependency>
      <groupId>com.liuhangs.learning</groupId>
      <artifactId>seata-tcc-wallet-api</artifactId>
      <version>1.0.0-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.2.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>3.0.5</version>
    </dependency>
    <dependency>
      <groupId>org.apache.dubbo</groupId>
      <artifactId>dubbo-registry-nacos</artifactId>
      <version>3.0.5</version>
    </dependency>
    <dependency>
      <groupId>io.seata</groupId>
      <artifactId>seata-spring-boot-starter</artifactId>
      <version>1.4.2</version>
    </dependency>
    <dependency>
      <groupId>com.esotericsoftware.kryo</groupId>
      <artifactId>kryo</artifactId>
      <version>2.24.0</version>
    </dependency>
    <dependency>
      <groupId>com.esotericsoftware</groupId>
      <artifactId>kryo</artifactId>
      <version>4.0.2</version>
    </dependency>
    <dependency>
      <groupId>de.javakaffee</groupId>
      <artifactId>kryo-serializers</artifactId>
      <version>0.42</version>
    </dependency>
  </dependencies>

2.4.2.4 application.yml

server:
  port: 7002
  servlet:
    context-path: /

spring:
  application:
    name: seata-tcc-wallet
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://127.0.0.1:3306/test2?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    auto-mapping-behavior: full
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mapper/**/*Mapper.xml

dubbo:
  application:
    name: seata-tcc-wallet
  registry:
    address: nacos://127.0.0.1:8848
  protocol:
    port: 20884
    name: dubbo

seata:
  enabled: true
  tx-service-group: seata_learning_tx_group
  #这里很关键,使用XA模式时,需要关掉seata的数据源代理,否则会出现无法回滚或者部分回滚的问题
  #enable-auto-data-source-proxy: false
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: edc58481-b3b7-4596-8cfb-de304633c6d2
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: edc58481-b3b7-4596-8cfb-de304633c6d2
      group: SEATA_GROUP

2.4.2.5 配置类TccWalletConfig

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@EnableDubbo(scanBasePackages = "com.liuhangs.learning.seata.tcc.wallet.service")
@MapperScan("com.liuhangs.learning.seata.tcc.wallet.mapper")
@Configuration
public class TccWalletConfig {

}

2.4.2.6 TccWalletBalanceMapper

import org.apache.ibatis.annotations.Mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liuhangs.learning.seata.tcc.wallet.mapper.po.TccWalletBalancePO;

/**
 * 钱包余额mapper
 */
@Mapper
public interface TccWalletBalanceMapper extends BaseMapper<TccWalletBalancePO> {

}

2.4.2.7 TccWalletBillMapper

import org.apache.ibatis.annotations.Mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liuhangs.learning.seata.tcc.wallet.mapper.po.TccWalletBillPO;

/**
 * 钱包账单mapper
 */
@Mapper
public interface TccWalletBillMapper extends BaseMapper<TccWalletBillPO> {

}

2.4.2.8 TccWalletTxMapper

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liuhangs.learning.seata.tcc.wallet.mapper.po.TccWalletTxPO;

/**
 * 钱包事务mapper
 */
@Mapper
public interface TccWalletTxMapper extends BaseMapper<TccWalletTxPO> {

    /**
     * 此方法为了解决悬挂问题。以下是问题过程(涉及到mysql mvcc的原理):
     * 当prepare方法调用超时,开始调用rollback回滚方法,
     * rollback方法查询seata_tcc_wallet_tx表没有事务,插入一条已回滚(9)的数据,
     * prepare方法此时也正在调用,查询seata_tcc_wallet_tx表,由于rollback方法没有提交,prepare方法本地事务不可见已回滚(9)的数据,
     * 此时就会再插入一条初始化(0)的数据,
     * 为了解决这个问题,使用for update语句将此事务xid的数据加一个间隙锁,在prepare方法或者rollback方法事务提交以前,数据加上间隙锁,另一个事务会被阻塞,直到任意一个事务提交。
     * 其实这里有三种情况:
     * 1.prepare方法先执行到此sql,获得间隙锁,继续执行完成prepare方法中insert语句,此时才执行rollback方法,执行此sql会阻塞,等待prepare方法提交。
     * 2.rollback方法先执行到此sql,获得间隙锁,继续执行完成rollback方法中insert语句,此时才执行到prepare方法的此sql,prepare方法会阻塞,等待rollback方法提交。
     * 3.rollback方法先执行到此sql,获得间隙锁,此时prepare方法也执行到此sql,同时也获得间隙锁,
     * 然后rollback方法执行到insert语句,获取排他锁X失败,转为得到共享锁S,等待排他锁X,
     * 此时prepare方法也执行到insert语句,获取排他锁X失败,转为得到共享锁S,等待排他锁X,
     * 产生死锁,会报错:Deadlock found when trying to get lock; try restarting transaction
     * 然后自动释放rollback方法事务,prepare方法执行完成,rollback方法再次尝试回滚。
     * 在prepare方法或者rollback方法事务提交后,mvcc生成的readview视图就将已提交的事务当成可见的,另一个事务就可以查询出来了,
     * 这样保证prepare方法和rollback方法并发时,插入seata_tcc_wallet_tx表的数据只有一条。
     * @param xid
     * @return
     */
    @Select("select id, xid, status from seata_tcc_wallet_tx where xid = #{xid} for update")
    TccWalletTxPO selectByXidForUpdate(@Param("xid") String xid);

}

2.4.2.9 dubbo接口实现TccWalletServiceImpl

import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.liuhangs.learning.seata.tcc.wallat.service.TccWalletService;
import com.liuhangs.learning.seata.tcc.wallat.vo.TccUserBalanceVO;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 钱包扣款和记账单dubbo接口
 */
@DubboService  //dubbo服务提供类
@Service
@RequiredArgsConstructor
@Slf4j
public class TccWalletServiceImpl implements TccWalletService {

    private final TccWalletTxService tccWalletTxService;

    @Override
    //@Transactional(rollbackFor = Exception.class) //这里加上事务不能解决悬挂问题,涉及到mvcc的原理
    public boolean deductionBalance(TccUserBalanceVO tccUserBalanceVO) {
        return tccWalletTxService.prepare(null, tccUserBalanceVO.getOrderId(), tccUserBalanceVO.getUserId(),
            tccUserBalanceVO.getUserName(), tccUserBalanceVO.getAmount());
    }
}
2.4.2.10 TCC分支事务定义TccWalletTxService
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;

/**
 * 钱包TCC事务定义
 * @author hang.liu6
 * @version 1.0
 * @date 2022/2/9 14:31
 */
@LocalTCC
public interface TccWalletTxService {

    /**
     * WalletTcc为分支事务名称
     * commitMethod提交方法名称
     * rollbackMethod回滚方法名称
     * @BusinessActionContextParameter注解表示将参数传给commit和rollback方法,
     * 在commit和rollback方法可以通过actionContext.getActionContext("orderId")方式获取参数
     */
    @TwoPhaseBusinessAction(name = "WalletTcc", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "orderId") String orderId,
        @BusinessActionContextParameter(paramName = "userId") Long userId,
        String userName,
        @BusinessActionContextParameter(paramName = "amount") Long amount);

    /**
     * Commit boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * Rollback boolean.
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean rollback(BusinessActionContext actionContext);
}

2.4.2.11 TCC分支事务实现

import java.util.Objects;
import java.util.concurrent.TimeUnit;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.liuhangs.learning.seata.tcc.wallet.mapper.TccWalletBalanceMapper;
import com.liuhangs.learning.seata.tcc.wallet.mapper.TccWalletBillMapper;
import com.liuhangs.learning.seata.tcc.wallet.mapper.TccWalletTxMapper;
import com.liuhangs.learning.seata.tcc.wallet.mapper.po.TccWalletBalancePO;
import com.liuhangs.learning.seata.tcc.wallet.mapper.po.TccWalletBillPO;
import com.liuhangs.learning.seata.tcc.wallet.mapper.po.TccWalletTxPO;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.spring.annotation.GlobalLock;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class TccWalletTxServiceImpl implements TccWalletTxService {

    private final TccWalletBalanceMapper tccWalletBalanceMapper;

    private final TccWalletBillMapper tccWalletBillMapper;

    private final TccWalletTxMapper tccWalletTxMapper;

    private static final Object lock = new Object();

    @Override
    @Transactional(rollbackFor = Exception.class) //这里加上事务不能解决悬挂问题,涉及到mvcc的原理
    public boolean prepare(BusinessActionContext actionContext, String orderId, Long userId, String userName, Long amount) {
        log.info("准备事务开始,xid={}, orderId={}", actionContext.getXid(), orderId);
        // 冻结余额
        LambdaUpdateWrapper<TccWalletBalancePO> updateBalanceWrapper = new LambdaUpdateWrapper<>();
        updateBalanceWrapper.setSql("balance = balance - " + amount);
        updateBalanceWrapper.setSql("freeze_balance = freeze_balance + " + amount);
        updateBalanceWrapper.eq(TccWalletBalancePO::getUserId, userId);
        tccWalletBalanceMapper.update(null, updateBalanceWrapper);

        // 插入账单
        TccWalletBillPO tccWalletBillPO = new TccWalletBillPO();
        tccWalletBillPO.setUserId(userId);
        tccWalletBillPO.setUserName(userName);
        tccWalletBillPO.setOrderId(orderId);
        tccWalletBillPO.setAmount(amount);
        tccWalletBillMapper.insert(tccWalletBillPO);

        // 插入订单事务
        TccWalletTxPO tccWalletTxPO = new TccWalletTxPO();
        tccWalletTxPO.setXid(actionContext.getXid());
        // 初始状态
        tccWalletTxPO.setStatus(0);

        // 幂等
        log.info("准备阶段查询是否存在事务");
        TccWalletTxPO alreadyExistsTx = tccWalletTxMapper.selectByXidForUpdate(actionContext.getXid());
        // 如果已经存在事务,并且状态是已提交或者已回滚,表示事务已经执行完成,直接返回
        // 结合rollback方法的防止空回滚插入的事务数据,可以解决悬挂问题
        if (Objects.nonNull(alreadyExistsTx)) {
            log.info("触发幂等,xid={}, orderId={}", actionContext.getXid(), orderId);
            throw new RuntimeException("事务数据已经存在");
        }
        tccWalletTxMapper.insert(tccWalletTxPO);
        log.info("准备事务结束,xid={}, orderId={}", actionContext.getXid(), orderId);
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean commit(BusinessActionContext actionContext) {
        log.info("提交事务开始,xid={}", actionContext.getXid());
        // 幂等
        LambdaQueryWrapper<TccWalletTxPO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(TccWalletTxPO::getXid, actionContext.getXid());
        TccWalletTxPO alreadyExistsTx = tccWalletTxMapper.selectOne(queryWrapper);
        // 如果已经存在事务,并且状态是已提交或者已回滚,表示事务已经执行完成,直接返回
        if(Objects.nonNull(alreadyExistsTx) && !Objects.equals(alreadyExistsTx.getStatus(), 0)) {
            log.info("触发幂等,xid={}", actionContext.getXid());
            return true;
        }

        Long amount = Long.valueOf(actionContext.getActionContext("amount").toString());
        Long userId = Long.valueOf(actionContext.getActionContext("userId").toString());
        // 冻结金额扣除
        LambdaUpdateWrapper<TccWalletBalancePO> updateBalanceWrapper = new LambdaUpdateWrapper<>();
        updateBalanceWrapper.setSql("freeze_balance = freeze_balance - " + amount);
        updateBalanceWrapper.eq(TccWalletBalancePO::getUserId, userId);
        tccWalletBalanceMapper.update(null, updateBalanceWrapper);

        TccWalletTxPO tccWalletTxPO = new TccWalletTxPO();
        tccWalletTxPO.setXid(actionContext.getXid());
        // 提交状态
        tccWalletTxPO.setStatus(1);

        LambdaUpdateWrapper<TccWalletTxPO> wapper = new LambdaUpdateWrapper<>();
        wapper.eq(TccWalletTxPO::getXid, actionContext.getXid());
        tccWalletTxMapper.update(tccWalletTxPO, wapper);
        log.info("提交事务结束,xid={}", actionContext.getXid());
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class) //这里加上事务不能解决悬挂问题,涉及到mvcc的原理
    public boolean rollback(BusinessActionContext actionContext) {
        String orderId = (String) actionContext.getActionContext("orderId");
        log.info("回滚事务开始,xid={}, orderId={}", actionContext.getXid(), orderId);
        // 幂等
        log.info("回滚阶段查询是否存在事务");
        TccWalletTxPO alreadyExistsTx = tccWalletTxMapper.selectByXidForUpdate(actionContext.getXid());
        // 如果已经存在事务,并且状态是已提交或者已回滚,表示事务已经执行完成,直接返回
        if (Objects.nonNull(alreadyExistsTx) && !Objects.equals(alreadyExistsTx.getStatus(), 0)) {
            log.info("触发幂等,xid={}, orderId={}", actionContext.getXid(), orderId);
            return true;
        }

        // 空回滚,没有事务初始记录,就开始回滚,就表示空回滚
        // 空回滚只是记录下已经回滚,防止空回滚后悬挂的问题
        if (Objects.isNull(alreadyExistsTx)) {
            log.info("触发空回滚,xid={}, orderId={}", actionContext.getXid(), orderId);
            // 插入订单事务
            TccWalletTxPO tccWalletTxPO = new TccWalletTxPO();
            tccWalletTxPO.setXid(actionContext.getXid());
            // 回滚状态
            tccWalletTxPO.setStatus(9);
            tccWalletTxMapper.insert(tccWalletTxPO);
            log.info("空回滚事务结束,xid={}, orderId={}", actionContext.getXid(), orderId);
            return true;
        }

        // 更新事务表状态
        TccWalletTxPO tccWalletTxPO = new TccWalletTxPO();
        tccWalletTxPO.setXid(actionContext.getXid());
        // 提交状态
        tccWalletTxPO.setStatus(9);

        LambdaUpdateWrapper<TccWalletTxPO> wapper = new LambdaUpdateWrapper<>();
        wapper.eq(TccWalletTxPO::getXid, actionContext.getXid());
        tccWalletTxMapper.update(tccWalletTxPO, wapper);

        Long amount = Long.valueOf(actionContext.getActionContext("amount").toString());
        Long userId = Long.valueOf(actionContext.getActionContext("userId").toString());
        // 还原冻结金额
        LambdaUpdateWrapper<TccWalletBalancePO> updateBalanceWrapper = new LambdaUpdateWrapper<>();
        updateBalanceWrapper.setSql("balance = balance + " + amount);
        updateBalanceWrapper.setSql("freeze_balance = freeze_balance - " + amount);
        updateBalanceWrapper.eq(TccWalletBalancePO::getUserId, userId);
        tccWalletBalanceMapper.update(null, updateBalanceWrapper);

        // 删除钱包账单
        LambdaUpdateWrapper<TccWalletBillPO> updateBillWrapper = new LambdaUpdateWrapper<>();
        updateBillWrapper.setSql("is_delete = 1");
        updateBillWrapper.eq(TccWalletBillPO::getOrderId, orderId);
        tccWalletBillMapper.update(null, updateBillWrapper);
        log.info("回滚事务结束,xid={}, orderId={}", actionContext.getXid(), orderId);
        return true;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章