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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章