SpringCloud分布式事务TCC模式案例三
1新建项目
新建seata-tcc工程,新建Empty-Project工程
关闭idea再打开就可以看到项目
2导入无事务项目
选择各个项目的pom文件,选择add as Maven Project,即可作为maven项目
order启动全局事务,添加“保存订单”分支事务
在订单项目中执行添加订单:
我们要添加以下 TCC 事务操作的代码:
-
T
ry - 第一阶,冻结数据阶段,向订单表直接插入订单,订单状态设置为0(冻结状态)。 -
confirm - 第二阶段,提交事务,将订单状态修改成1(正常状态)。
- cancel - 第二阶段,回滚事务,删除订单。
3order-parent 添加 seata 依赖
打开 order-parent 中注释掉的 seata 依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>order-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order-parent</name>
<description>Demo project for Spring Boot</description>
<properties>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
<druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version>
<seata.version>1.3.0</seata.version>
<spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>${spring-cloud-alibaba-seata.version}</version>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>${seata.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4order项目配置
在order项目的application.yml配置全局事务组的组名
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
在order模块添加连接seat的配置文件registry.conf和file.conf
与AT事务中的配置完全相同
file.conf
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
# order_tx_group 与 yml 中的 “tx-service-group: order_tx_group” 配置一致
# “seata-server” 与 TC 服务器的注册名一致
# 从eureka获取seata-server的地址,再向seata-server注册自己,设置group
# 事务组对应使用哪个协调器
# 事务组组名=协调器服务名
vgroupMapping.order_tx_group = "seata-server"
#only support when registry.type=file, please don't set multiple addresses
order_tx_group.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
# application = "default"
# weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
password = ""
cluster = "default"
timeout = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
orderMapper添加更新订单状态,删除订单
根据前面的分析,订单数据操作有以下三项:
- 插入订单
- 修改订单状态
- 删除订单
在 OrderMapper 中已经有插入订单的方法了,现在需要添加修改订单和删除订单的方法(删除方法从BaseMapper继承):
package cn.tedu.order.mapper;
import cn.tedu.order.entity.Order;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface OrderMapper extends BaseMapper<Order> {
//冻结的订单,status=0
void create(Order order);
//订单从冻结状态改为正常状态,status=1
void updateStatus(Long orderId,int status);
//使用继承的 deleteById() 方法删除订单
}
在resources中的mapper中添加OrderMapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.tedu.order.mapper.OrderMapper" >
<resultMap id="BaseResultMap" type="cn.tedu.order.entity.Order" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="product_id" property="productId" jdbcType="BIGINT" />
<result column="count" property="count" jdbcType="INTEGER" />
<result column="money" property="money" jdbcType="DECIMAL" />
<result column="status" property="status" jdbcType="INTEGER" />
</resultMap>
<insert id="create">
INSERT INTO `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`)
VALUES(#{id}, #{userId}, #{productId}, #{count}, #{money}, #{status});
</insert>
<update id="updateStatus">
UPDATE `order`
SET status=#{status}
WHERE id=#{orderId}
</update>
<delete id="deleteById">
DELETE FROM `order`
WHERE id=#{orderId}
</delete>
</mapper>
5Seata 实现订单的 TCC 操作方法
第一阶段 Try
第二阶段
Confirm
Cancel
第二阶段为了处理幂等性问题这里首先添加一个工具类 ResultHolder。
这个工具也可以在第二阶段 Confirm 或 Cancel 阶段对第一阶段的成功与否进行判断,在第一阶段成功时需要保存一个标识。
ResultHolder可以为每一个全局事务保存一个标识:
package cn.tedu.order.tcc;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ResultHolder {
private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public static void setResult(Class<?> actionClass, String xid, String v) {
Map<String, String> results = map.get(actionClass);
if (results == null) {
synchronized (map) {
if (results == null) {
results = new ConcurrentHashMap<>();
map.put(actionClass, results);
}
}
}
results.put(xid, v);
}
public static String getResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
return results.get(xid);
}
return null;
}
public static void removeResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
results.remove(xid);
}
}
}
Seata 实现 TCC 操作需要定义一个接口,我们在接口中添加以下方法:
- Try -
prepareCreateOrder()
- Confirm -
commit()
- Cancel -
rollback()
package cn.tedu.order.tcc;
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;
import java.math.BigDecimal;
@LocalTCC
public interface OrderTccAction {
//try
//第二阶段方法名,如果不是默认名,需要添加属性配置
/**
* name="orderTccAction",
* commitMethod = "commit",
* rollbackMethod = "rollback"
* @return
*/
//为了避开seata的一个bug,这里不传递封装的order对象
//而是一个一个的传递订单参数
@TwoPhaseBusinessAction(name="orderTccAction")
boolean prepare(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "orderId") Long orderId,
Long userId,
Long productId,
Integer count,
BigDecimal money);
//confirm
boolean commit(BusinessActionContext ctx);
//Cancel
boolean rollback(BusinessActionContext ctx);
}
实现类:
package cn.tedu.order.tcc;
import cn.tedu.order.entity.Order;
import cn.tedu.order.mapper.OrderMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Component
public class OrderTccActionImpl implements OrderTccAction {
@Autowired
private OrderMapper orderMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
//插入状态是0的订单,表示是一个冻结的订单
orderMapper.create(new Order(orderId,userId,productId,count,money,0));
//第一阶段成功,保存一个一阶段的成功标记
ResultHolder.setResult(OrderTccAction.class,ctx.getXid(),"p");
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext ctx) {
//如果没有标记,二阶段不执行
//--第一阶段没有成功
//--第二阶段已经执行过,标记已删除,不再重复
if(ResultHolder.getResult(OrderTccAction.class,ctx.getXid())==null){
return true;
}
//从上下问对象取出orderid
Long orderId =Long.valueOf(ctx.getActionContext("orderId").toString());
//订单的状态为1,表示正常
orderMapper.updateStatus(orderId,1);
//第二阶段成功,删除标记
ResultHolder.removeResult(OrderTccAction.class,ctx.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext ctx) {
if(ResultHolder.getResult(OrderTccAction.class,ctx.getXid())==null){
return true;
}
Long orderId =Long.valueOf(ctx.getActionContext("orderId").toString());
orderMapper.deleteById(orderId);
ResultHolder.removeResult(OrderTccAction.class,ctx.getXid());
return true;
}
}
6在业务代码中调用 Try 阶段方法
业务代码中不再直接保存订单数据,而是调用 TCC 第一阶段方法prepareCreateOrder()
,并添加全局事务注解 @GlobalTransactional
:注入OrderTccAction
package cn.tedu.order.servie;
import cn.tedu.order.entity.Order;
import cn.tedu.order.feign.AccountClient;
import cn.tedu.order.feign.EasyIdClient;
import cn.tedu.order.feign.StorageClient;
import cn.tedu.order.mapper.OrderMapper;
import cn.tedu.order.tcc.OrderTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
public class OrderServiceImpl implements OrderService{
@Autowired
private OrderMapper orderMapper;
@Autowired
private EasyIdClient easyIdClient;
@Autowired
private StorageClient storageClient;
@Autowired
private AccountClient accountClient;
@Autowired
private OrderTccAction orderTccAction;
@Override
public void create(Order order) {
//TODO: 远程调用发号器 生成订单id 生成订单id
String s = easyIdClient.nextId("order_business");
Long orderId = Long.valueOf(s);
order.setId(orderId);
//不在直击执行业务操作存储订单,
//而实改成调用TCC第一阶段方法,冻结订单,预留订单
//orderMapper.create(order);
/**
orderTccAction 是一个动态代理对象,用AOP添加了通知代码,
在通知代码中,创建了上下文对象,并传递到被代理的目标方法中
*/
orderTccAction.prepare(
null,
order.getId(),
order.getUserId(),
order.getProductId(),
order.getCount(),
order.getMoney());
//TODO: 调用库存减少商品库存
//storageClient.decrease(order.getProductId(),order.getCount());
//ToDO: 远程调用账户扣减账户金额
//accountClient.decrease(order.getUserId(),order.getMoney());
}
}
7启动测试order
按顺序启动服务:
Eureka
Seata Server
Easy Id Generator
Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
观察控制台日志:
8异常测试
在业务类处理结束处抛出异常
throw new Exception("测试异常");
再次访问
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
9添加storage模块
在库存项目中执行减少库存:
我们要添加以下 TCC 事务操作的代码:
- try - 第一阶,冻结数据阶段,将要减少的库存量先冻结:
- confirm - 第二阶段,提交事务,使用冻结的库存完成业务数据处理:
- cancel - 第二阶段,回滚事务,冻结的库存解冻,恢复以前的库存量:
10storage项目配置
在application.yml上添加事务组配置
# 事务组设置
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
将order模块中的resources下的file.conf和registry.conf文件复制到storage模块下resources文件夹下
11StorageMapper 添加冻结库存相关方法
根据前面的分析,库存数据操作有以下三项:
- 冻结库存
- 冻结库存量修改为已售出量
- 解冻库存
在 StorageMapper 中添加三个方法:
package cn.tedu.storage.mapper;
import cn.tedu.storage.entity.Storage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface StorageMapper extends BaseMapper<Storage> {
void decrease(Long productId,Integer count);
//查询库存,用来判断是否有足够库存
//使用继承的 selectById()方法
//可用-->冻结
void updateResidueToFrozen(Long productId,Integer count);
//冻结-->已售出
void updateFrozenToUsed(Long productId,Integer count);
//冻结-->可用
void updateFrozenToResidue(Long productId,Integer count);
}
在resources的mapper文件夹下StorageMapper.xml添加实现方法
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.tedu.storage.mapper.StorageMapper" >
<resultMap id="BaseResultMap" type="cn.tedu.storage.entity.Storage" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="product_id" property="productId" jdbcType="BIGINT" />
<result column="total" property="total" jdbcType="INTEGER" />
<result column="used" property="used" jdbcType="INTEGER" />
<result column="residue" property="residue" jdbcType="INTEGER" />
</resultMap>
<update id="decrease">
UPDATE storage
SET used = used + #{count},residue = residue - #{count}
WHERE product_id = #{productId}
</update>
<select id="selectById" resultMap="BaseResultMap">
SELECT *
FROM storage
WHERE product_id = #{productId}
</select>
<update id="updateResidueToFrozen">
UPDATE storage
SET residue = residue - #{count},
frozen = frozen + #{count}
WHERE product_id = #{productId}
</update>
<update id="updateFrozenToUsed">
UPDATE storage
SET frozen = frozen - #{count},
used = used + #{count}
WHERE product_id = #{productId}
</update>
<update id="updateFrozenToResidue">
UPDATE storage
SET residue = residue + #{count},
frozen = frozen - #{count}
WHERE product_id = #{productId}
</update>
</mapper>
Seata 实现库存的 TCC 操作方法
工具类 ResultHolder
:
package cn.tedu.storage.tcc;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ResultHolder {
private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public static void setResult(Class<?> actionClass, String xid, String v) {
Map<String, String> results = map.get(actionClass);
if (results == null) {
synchronized (map) {
if (results == null) {
results = new ConcurrentHashMap<>();
map.put(actionClass, results);
}
}
}
results.put(xid, v);
}
public static String getResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
return results.get(xid);
}
return null;
}
public static void removeResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
results.remove(xid);
}
}
}
添加 TCC 接口,在接口中添加以下方法:
- Try -
prepareDecreaseStorage()
- Confirm -
commit()
- Cancel -
rollback()
package cn.tedu.storage.tcc;
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;
@LocalTCC
public interface StorageTccAction {
@TwoPhaseBusinessAction(name="StorageTccAction")
boolean prepare(BusinessActionContext ctx,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count);
boolean commit(BusinessActionContext ctx);
boolean rollback(BusinessActionContext ctx);
}
实现类:
package cn.tedu.storage.tcc;
import cn.tedu.storage.entity.Storage;
import cn.tedu.storage.mapper.StorageMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class StorageTccActionImpl implements StorageTccAction{
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public boolean prepare(BusinessActionContext ctx, Long productId, Integer count) {
//查询现有库存
Storage storage=storageMapper.selectById(productId);
if(storage.getResidue()<count){
//当第一阶段操作失败时,二阶段回滚操作不能主席那个
throw new RuntimeException("库存不足");
}
//可用-->冻结
storageMapper.updateResidueToFrozen(productId,count);
//保存标识
ResultHolder.setResult(getClass(),ctx.getXid(), "p");
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext ctx) {
Long productId = Long.valueOf(
ctx.getActionContext("productId").toString());
Integer count = Integer.valueOf(
ctx.getActionContext("count").toString());
//防止重复提交
if (ResultHolder.getResult(getClass(), ctx.getXid()) == null) {
return true;
}
// 冻结 --> 已使用
storageMapper.updateFrozenToUsed(productId, count);
//删除标识
ResultHolder.removeResult(getClass(), ctx.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext ctx) {
Long productId = Long.valueOf(
ctx.getActionContext("productId").toString());
Integer count = Integer.valueOf(
ctx.getActionContext("count").toString());
//防止重复回滚
if (ResultHolder.getResult(getClass(), ctx.getXid()) == null) {
return true;
}
// 冻结 --> 可用
storageMapper.updateFrozenToResidue(productId, count);
//删除标识
ResultHolder.removeResult(getClass(), ctx.getXid());
return true;
}
}
12在业务代码中调用 Try 阶段方法
业务代码中调用 TCC 第一阶段方法prepareDecreaseStorage()
,并添加全局事务注解 @GlobalTransactional
:
package cn.tedu.storage.service;
import cn.tedu.storage.mapper.StorageMapper;
import cn.tedu.storage.tcc.StorageTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class StorageServiceImpl implements StorageService{
@Autowired
private StorageMapper storageMapper;
@Autowired
private StorageTccAction storageTccAction;
@Override
public void decrease(Long productId, Integer count) {
//storageMapper.decrease(productId,count);
storageTccAction.prepare(null, productId, count);
}
}
13启动 storage 进行测试
按顺序启动服务:
Eureka
Seata Server
Easy Id Generator
Storage
Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
观察 storage 的控制台日志:
Order日志
14异常测试
在order订单实现类,在调用库存减少后抛出异常
throw new RuntimeException("模拟异常");
重启order服务
再次访问
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
说明storage在系统发生异常时会回滚
15添加account模块
配置
在account模块的application.yml添加事务组
cloud:
alibaba:
seata:
tx-service-group: order_tx_group
将order模块中的resources下的file.conf和registry.conf文件复制到account模块下resources文件夹下
AccountMapper 添加冻结库存相关方法
根据前面的分析,库存数据操作有以下三项:
- 冻结库存
- 冻结库存量修改为已售出量
- 解冻库存
在 AccountMapper 中添加三个方法:
package cn.tedu.account.mapper;
import cn.tedu.account.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
public interface AccountMapper extends BaseMapper<Account> {
void decrease(Long userId, BigDecimal money);
void updateFrozen(@Param("userId") Long userId, @Param("residue") BigDecimal residue, @Param("frozen") BigDecimal frozen);
void updateFrozenToUsed(@Param("userId") Long userId, @Param("money") BigDecimal money);
void updateFrozenToResidue(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
那么对应的 AccountMapper.xml
中添加 sql:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.tedu.account.mapper.AccountMapper" >
<resultMap id="BaseResultMap" type="cn.tedu.account.entity.Account" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="user_id" property="userId" jdbcType="BIGINT" />
<result column="total" property="total" jdbcType="DECIMAL" />
<result column="used" property="used" jdbcType="DECIMAL" />
<result column="residue" property="residue" jdbcType="DECIMAL"/>
<result column="frozen" property="frozen" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE account
SET residue = residue - #{money},used = used + #{money}
where user_id = #{userId};
</update>
<select id="selectById" resultMap="BaseResultMap">
SELECT * FROM account WHERE `user_id`=#{userId}
</select>
<update id="updateFrozen">
UPDATE account SET `residue`=#{residue},`frozen`=#{frozen} WHERE `user_id`=#{userId}
</update>
<update id="updateFrozenToUsed">
UPDATE account SET `frozen`=`frozen`-#{money}, `used`=`used`+#{money} WHERE `user_id`=#{userId}
</update>
<update id="updateFrozenToResidue">
UPDATE account SET `frozen`=`frozen`-#{money}, `residue`=`residue`+#{money} WHERE `user_id`=#{userId}
</update>
</mapper>
16Seata 实现库存的 TCC 操作方法
工具类 ResultHolder
:
package cn.tedu.account.tcc;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ResultHolder {
private static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<Class<?>, Map<String, String>>();
public static void setResult(Class<?> actionClass, String xid, String v) {
Map<String, String> results = map.get(actionClass);
if (results == null) {
synchronized (map) {
if (results == null) {
results = new ConcurrentHashMap<>();
map.put(actionClass, results);
}
}
}
results.put(xid, v);
}
public static String getResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
return results.get(xid);
}
return null;
}
public static void removeResult(Class<?> actionClass, String xid) {
Map<String, String> results = map.get(actionClass);
if (results != null) {
results.remove(xid);
}
}
}
添加 TCC 接口,在接口中添加以下方法:
- Try -
prepareDecreaseAccount()
- Confirm -
commit()
- Cancel -
rollback()
package cn.tedu.account.tcc;
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;
import java.math.BigDecimal;
@LocalTCC
public interface AccountTccAction {
@TwoPhaseBusinessAction(name = "accountTccAction", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepareDecreaseAccount(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
boolean commit(BusinessActionContext businessActionContext);
boolean rollback(BusinessActionContext businessActionContext);
}
实现类:
package cn.tedu.account.tcc;
import cn.tedu.account.entity.Account;
import cn.tedu.account.mapper.AccountMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Component
@Slf4j
public class AccountTccActionImpl implements AccountTccAction{
@Autowired
private AccountMapper accountMapper;
@Transactional
@Override
public boolean prepareDecreaseAccount(BusinessActionContext businessActionContext, Long userId, BigDecimal money) {
//log.info("减少账户金额,第一阶段锁定金额,userId="+userId+", money="+money);
Account account = accountMapper.selectById(userId);
if (account.getResidue().compareTo(money) < 0) {
throw new RuntimeException("账户金额不足");
}
/*
余额-money
冻结+money
*/
accountMapper.updateFrozen(userId, account.getResidue().subtract(money), account.getFrozen().add(money));
//保存标识
ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
return true;
}
@Transactional
@Override
public boolean commit(BusinessActionContext businessActionContext) {
long userId = Long.parseLong(businessActionContext.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(businessActionContext.getActionContext("money").toString());
//log.info("减少账户金额,第二阶段,提交,userId="+userId+", money="+money);
//防止重复提交
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}
accountMapper.updateFrozenToUsed(userId, money);
//删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}
@Transactional
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
long userId = Long.parseLong(businessActionContext.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(businessActionContext.getActionContext("money").toString());
//防止重复回滚
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}
//log.info("减少账户金额,第二阶段,回滚,userId="+userId+", money="+money);
accountMapper.updateFrozenToResidue(userId, money);
//删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}
}
17在业务代码中调用 Try 阶段方法
业务代码中调用 TCC 第一阶段方法prepareDecreaseAccount()
,并添加全局事务注解 @GlobalTransactional
:
package cn.tedu.account.service;
import cn.tedu.account.mapper.AccountMapper;
import cn.tedu.account.tcc.AccountTccAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class AccoutServiceImpl implements AccoutService{
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountTccAction accountTccAction;
@Override
public void decrease(Long userId, BigDecimal money) {
//accountMapper.decrease(userId,money);
accountTccAction.prepareDecreaseAccount(null, userId, money);
}
}
18启动account进行测试
在order服务,开启调用account服务
accountClient.decrease(order.getUserId(),order.getMoney());
按顺序启动服务:
- Eureka
- Seata Server
- Easy Id Generator
- Storage
- Account
- Order
调用保存订单,地址:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
19异常测试
在order调用库存和账户后抛出异常
throw new RuntimeException("模拟异常");
重启order服务
再次访问
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
end