前言
本文使用工具版本如下:
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;
}
}