前言
本文使用工具版本如下:
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 概覽圖
2.2 工程總體結構
2.3 訂單模塊
2.3.1 order-api模塊
2.3.1.1 工程結構
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 工程結構
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 工程結構
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 工程結構
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;
}
}