
第三篇 基于nacos搭建分布式项目 分布式事务(分布式锁+事务)
项目所需 maven + nacos + java8 + idea + git + mysql(下单) + redis(分布式锁)
本文主要讲解客户下单时扣减库存的操作,网关系统/用户系统/商品系统/订单系统
请先准备好环境,可以直接clone下来项目去部署。
基于nacos搭建分布式项目 分布式事务
- 项目结构
- 分布式事务
-
- 分布式锁
-
- 何时需要分布式锁?
- 何时不需要分布式锁?
- [1. 基于数据库的实现](#1. 基于数据库的实现)
- [2. 基于redis+Lua脚本实现](#2. 基于redis+Lua脚本实现)
- 事务机制
-
- [2pc 模式](#2pc 模式)
-
- [2pc 模式 执行流程](#2pc 模式 执行流程)
- [2pc 模式 关键点](#2pc 模式 关键点)
- [AT 模式(自动补偿)](#AT 模式(自动补偿))
-
- [AT 模式执行流程](#AT 模式执行流程)
- [AT 模式 关键点](#AT 模式 关键点)
- [TCC 模式(手动补偿)](#TCC 模式(手动补偿))
-
- [TCC 模式执行流程](#TCC 模式执行流程)
- [TCC 模式关键点](#TCC 模式关键点)
- [SAGA 模式(长事务补偿)](#SAGA 模式(长事务补偿))
-
- [SAGA 模式执行流程](#SAGA 模式执行流程)
- [SAGA 模式关键点](#SAGA 模式关键点)
- [XA 模式(强一致性)](#XA 模式(强一致性))
-
- [XA 模式执行流程](#XA 模式执行流程)
- [XA 模式关键点](#XA 模式关键点)
- 本地消息表(最终一致性)
- 事务模式对比
- 具体实现
- 结尾
项目结构
bash
git clone [email protected]:goodluckv/ali-cloud-common.git
git clone [email protected]:goodluckv/ali-cloud-gateway.git
git clone [email protected]:goodluckv/ali-cloud-user.git
git clone [email protected]:goodluckv/ali-cloud-goods.git
git clone [email protected]:goodluckv/ali-cloud-order.git
分布式事务
在分布式系统中,业务逻辑往往涉及多个独立的数据源或服务(例如:订单服务、商品库存服务、支付服务)。如果没有分布式事务:就有可能出现 订单提交成功 但库存扣减失败,导致数据不一致 用户拿不到商品。
分布式事务的核心目标:确保所有参与方的操作要么全部成功(提交),要么全部回滚(撤销),即满足 ACID 中的原子性(Atomicity)和一致性(Consistency)。
单体架构的应用 在加了锁之后 可以直接保证下单减库存操作。如果有一处修改抛异常直接回滚事务即可,下单操作在同一系统函数操作中,所有操作都成功才提交修改,保证了操作的原子性和一致性。
分布式应用是多个系统间的事务操作,在加了分布式锁保证数据一致的情况下 还需要有事务保证机制来保证当前请求中的事务全都成功提交。事务保证机制就是有其中任意节点异常 所有操作都回滚。所有节点都成功的情况下才全部提交。
本文模拟场景 用户下单某一商品 -> 扣减库存 -> 创建订单操作。
分布式锁
何时需要分布式锁?
场景:防止并发冲突
问题:多个事务同时操作同一资源(如库存扣减),即使每个事务本身是原子的,仍可能因并发导致最终状态错误。
示例:
事务A和事务B同时读取库存为100,分别扣减10和20,最终库存可能是80(而非预期的70)。
解决方案:
在事务开始时,先获取分布式锁锁定资源(如商品ID),提交或回滚后释放锁。
何时不需要分布式锁?
若业务允许短暂不一致(如扣库存后异步更新订单状态),可通过消息队列顺序消费或去重替代锁。
1. 基于数据库的实现
唯一索引/主键约束
sql
CREATE TABLE distributed_lock (
lock_name VARCHAR(64) PRIMARY KEY,
owner VARCHAR(255),
expire_time TIMESTAMP
);
获取锁:
sql
INSERT INTO distributed_lock(lock_name, owner, expire_time)
VALUES ('order_lock_${productId}', 'node1', ${time});
释放锁:
sql
DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND owner = 'node1';
在业务中我们进入下单函数 先通过商品id生成指定的lock_name,查询是否有在有效期内的锁,如果有循环等待锁的获取。没有的情况下insert一条数据 ,insert成功则代表拿到当前商品下单锁。执行完成后删除即可。
但这种实现方式有问题 比如 在执行下单操作时如果超出有效期30秒,下一线程还是可以直接拿到锁去下单,但实际上线程1还在操作,这个时候同一商品加减库存还是有可能出现脏数据的情况。
这个时候可以自动续期(看门狗机制),比如
java
int time = 30;
int retryTime = 0;
// 获取锁
int locked = dao.lock(lockName, owner, time);
if (locked == 1) {
// 启动看门狗线程定期续期
Thread renewalThread = new Thread(() -> {
int maxRetries = 5; // 设置最大续期次数
int retryCount = 0;
while(!Thread.currentThread().isInterrupted() &&
retryCount++ < maxRetries) {
try {
Thread.sleep(20000L); // 每20秒续期一次
dao.expire(lockName, 20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
renewalThread.start();
try {
// 执行业务逻辑
doBusiness();
} finally {
// 停止续期线程
renewalThread.interrupt();
// 释放锁
dao.releaseLock(lockName, owner);
}
}
延伸:
但这样还是有问题,比如线程锁续期失败,或者超出最大值,主线程还需要加入与异步线程共享变量(AtomicInteger超时时间),通过Future控制主线程执行最大时长来保证事务。
2. 基于redis+Lua脚本实现
NX 是 "Not eXists" 的缩写,它用于确保只有在键不存在时才执行成功
bash
SET lock_key unique_value NX PX 30000
接着就是释放锁 释放锁为什么使用lua脚本 因为我们需要确保当前key还是当前client的unique_value(线程1 存储lock_key 为 value_1 30秒内没有执行完逻辑,线程2 存储lock_key 为 value_2 这个时候线程1执行完直接可以通过lock_key 删除线程2的锁)。先获取lock_key的值与value_1做比较,如果是则删除,不是则不删除。lua脚本可以保证这一步操作为原子性操作。
bash
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
当然也可以加入自动续期(看门狗机制)。
java
String lockKey = "order_lock";
String clientId = UUID.randomUUID().toString(); // 唯一标识
int expireTime = 30000; // 30秒
// 尝试获取锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, clientId, expireTime, TimeUnit.MILLISECONDS);
if (locked) {
// 启动看门狗线程定期续期
Thread renewalThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(5000); // 每5秒续期一次
redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
renewalThread.start();
try {
// 执行业务逻辑
doBusiness();
} finally {
// 停止续期线程
renewalThread.interrupt();
// 确保最终释放锁(使用Lua脚本)
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
clientId
);
}
} else {
// 获取锁失败处理
handleLockFailed();
}
延伸:
要做好重试次数 与 最长执行时间的处理,避免死锁。与示例1一致。
或者 合理设置超时时间,预估业务执行的最长时间 T,设置锁超时时间为 2T 或 3T。看门狗机制需要系统更多的资源消耗。
事务机制
首先引入分布式事务的概念性单词
- TC(Transaction Coordinator):事务协调器,负责全局事务的提交或回滚(独立部署)。
- TM(Transaction Manager):事务管理器(集成在业务服务中),负责开启、提交或回滚全局事务。
- RM(Resource Manager):资源管理器(集成在数据库访问层,如JDBC代理),负责分支事务的注册、状态报告和本地事务管理。
2pc 模式
2PC(Two-Phase Commit,两阶段提交)是分布式事务的经典协议,用于确保跨多个节点的事务要么全部提交,要么全部回滚,保证 ACID 中的 原子性(Atomicity)。需要数据库支持XA协议。
2pc 模式 执行流程
- 阶段1:准备阶段(Prepare Phase)
协调者(Coordinator) 向所有 参与者(Participants) 发送 Prepare 请求,询问是否可以提交事务。
参与者 执行本地事务(锁定资源但不提交),并记录 Undo/Redo 日志(用于回滚或恢复)。
参与者 返回响应:
Yes:表示可以提交(本地事务执行成功,并锁定资源)。
No:表示无法提交(本地事务失败,或资源冲突)。 - 阶段2:提交/回滚阶段(Commit/Rollback Phase)
如果所有参与者都返回 Yes:
协调者 发送 Commit 命令,参与者提交事务并释放锁。
参与者 返回 Ack 表示提交成功。
如果有任一参与者返回 No 或超时:
协调者 发送 Rollback 命令,参与者回滚事务(使用 Undo 日志恢复数据)。
参与者 返回 Ack 表示回滚成功。
实际的执行逻辑 - TM调用xa_start()开始事务
- 执行各RM上的SQL操作
- TM调用xa_end()结束事务分支
- TM调用xa_prepare()询问各RM
- 根据prepare结果决定xa_commit()或xa_rollback()
- 必要时使用xa_recover()处理悬挂事务
2pc 模式 关键点
各个服务控制自身的资源预占,协调者控制是否全都提交还是回滚。
各个服务资源预占后需要等待协调者 提交/回滚指令,提交回滚后协调者还需要继续等待各个服务是否执行成功的响应,需要多次通信确认(Prepare(是否可以修改预占) + Commit/rollback(修改/回滚) + Ack(所有服务确认完成))
协调者如果没有发送指令 不提交或者回滚,会导致参与者一直锁定资源等待命令。 可以考虑AT 模式(自动补偿),不会一直挂起,锁定资源。
AT 模式(自动补偿)
AT(Auto Transaction)模式 基于 2PC(两阶段提交) 优化而来,但不需要数据库支持 XA 协议,适用于大多数业务场景。
AT 模式执行流程
阶段1:执行本地事务并提交
- TM 开启全局事务(@GlobalTransactional),向 TC 注册全局事务(生成 XID)。
- RM 执行 SQL,Seata 会拦截 SQL,解析并生成 前置快照(before image) 和 后置快照(after image),存入 undo_log 表(用于回滚)。、
sql
-- 例如:UPDATE account SET balance = balance - 100 WHERE user_id = 1;
-- Seata 会记录修改前的数据(before image)和修改后的数据(after image)。
- RM 提交本地事务(数据库层面提交),但全局事务未完成(TC 未收到所有分支事务的成功报告)。
阶段2:全局提交或回滚
如果所有分支事务成功:
TC 通知所有 RM 提交(实际只需删除 undo_log 记录,因为数据已提交)。
如果任一分支事务失败:
TC 通知所有 RM 回滚,RM 根据 undo_log 恢复数据(反向补偿)。
AT 模式 关键点
各个事务参与方执行各自的事务,事务协议由单一服务去控制,当前操作参与中的任意节点有失败的,通知所有参与方回滚。(不适用不同用户同时下单同一商品的情况,如果使用该协议 需配合分布式锁同时使用)
TCC 模式(手动补偿)
TCC(Try-Confirm-Cancel)模式 适用于需要更高灵活性的业务,如金融支付、积分兑换等。
TCC 模式执行流程
- Try:预留资源(如冻结用户金额)。
- Confirm:确认操作(如实际扣款)。
- Cancel:取消操作(如解冻金额)
TCC 模式关键点
各个服务需要手动编写 Try/Confirm/Cancel 逻辑。
相比 AT 模式,TCC 可以更好地控制资源竞争(如库存冻结)。
网络重试可能导致重复调用,TCC 接口必须支持幂等。
在并发下TCC每一步操作都需要保证线程安全,否则也会导致脏数据。比如try阶段线程1下单商品1 冻结10个,线程2接着下单商品1 冻结20个,如果同时下单,会导致实际预占只有20个或者10个,在确认阶段会出现冻结库存不够的情况。
SAGA 模式(长事务补偿)
SAGA 模式 适用于 长时间运行的分布式事务(如跨多个微服务的订单流程),采用 最终一致性。
SAGA 模式执行流程
正向流程:依次执行各个服务的事务(如创建订单 → 扣库存 → 支付)。
逆向补偿:如果某一步失败,则按相反顺序调用补偿接口(如取消支付 → 回滚库存 → 取消订单)。
SAGA 模式关键点
适合长事务:如电商下单、物流配送等耗时较长的流程。
业务补偿需手动实现:各个服务需提供正向和逆向接口。
会出现用户下单成功但库存预占失败,自动取消订单的情况
XA 模式(强一致性)
XA 模式 基于数据库的 XA 协议(如 MySQL XA),适用于需要 强一致性 的场景(如银行转账)。XA模式与2PC中使用到的XA协议 是一回事,都需要数据库层支持。
XA 模式执行流程
TM 开启全局事务,所有 RM 注册到 TC。
RM 执行 SQL 但不提交(XA Prepare)。
TC 检查所有 RM 是否就绪,然后统一提交或回滚(XA Commit/Rollback)。
XA 模式关键点
由单一事务服务管理事务。强一致性:所有分支事务要么全部提交,要么全部回滚。
性能较低:由于需要等待所有 RM 准备就绪,吞吐量较低。
各个事务参与者都必须等待Commit/Rollback才会释放资源,会造成一定的线程阻塞。
本地消息表(最终一致性)
本地消息表是一种基于最终一致性思想的分布式事务解决方案,它通过将分布式事务拆分为多个本地事务,并借助消息表来保证事务的最终一致性。
本地消息表执行流程
- 将分布式事务拆分为多个本地事务
- 通过本地消息表记录事务状态
- 使用定时任务或消息队列确保所有参与方最终完成操作
本地消息表关键点
实现简单,不需要额外组件
基于最终一致性,对性能影响小
适用于长事务与允许事务短暂不一致的场景
各个服务也需要实现回滚操作。
事务模式对比
*[HTML]:
方案 | 一致性 | 性能 | 侵入性 | 适用场景 |
---|---|---|---|---|
AT |
最终一致性 | ⭐⭐⭐⭐ |
低 | 普通业务(订单、库存) |
TCC |
最终一致性 | ⭐⭐⭐ |
高 | 高并发(秒杀、支付) |
SAGA |
最终一致性 | ⭐⭐ |
中 | 长事务(物流、跨服务流程) |
XA |
强一致性 | ⭐ |
低 | 金融、银行(强一致性要求) |
2PC |
强一致性 | ⭐ |
高 | 传统数据库(如 Oracle XA) |
本地消息表 |
最终一致性 | ⭐⭐ |
中 | 异步场景(如订单+消息通知) |
具体实现
模式比较多,本文主要手写 TCC 模式(下一篇文章再看需要实现其他模式)。
AT ,2PC,SAGA,TCC都有现成的车轮(Seata)
TCC 具体实现
库存系统 乐观锁实现(version)
下单调用执行流程
- 调用预减库存方法preDeductStock 预减,成功则返回true
- 调用确认扣减库存confirmDeductStock扣减库存 成功则返回true(这里需要注意即便使用了库存预减也要判断数量和接口幂等,避免因超时重试等扣减多次)
- 失败后调用回滚预扣减库存rollbackPreDeductStock成功则返回true(这里需要注意判断数量和接口幂等,避免因超时重试等回滚多次,还需要注意空回滚比如try未成功执行,直接调用了回滚接口)
解决多次调用或者悬挂(Cancel在Try之前到达)问题在下一个目录。
java
@Service
@Slf4j
public class ProductStockVersionService {
@Autowired
private ProductStockMapper productStockMapper;
public ProductStock getProductStock(Long productId) {
return productStockMapper.selectByProductId(productId);
}
/**
* 预减(version乐观锁)
* @param productStockDTO
* @return
*/
@Transactional
public boolean preDeductStock(ProductStockDTO productStockDTO) {
Long productId = productStockDTO.getProductId();
Integer num = productStockDTO.getNum();
// 检查参数
if (productId == null || num == null || num <= 0) {
return false;
}
// 查询当前库存信息
ProductStock productStock = getProductStock(productId);
if (productStock == null) {
throw new RuntimeException("商品不存在");
}
// 尝试预扣减库存
int affectedRows = productStockMapper.preDeductStock(productId, num, productStock.getVersion());
return affectedRows > 0;
}
/**
* 确认扣减库存(version乐观锁)
* @param productStockDTO
* @return
*/
@Transactional
public boolean confirmDeductStock(ProductStockDTO productStockDTO) {
Long productId = productStockDTO.getProductId();
Integer num = productStockDTO.getNum();
// 检查参数
if (productId == null || num == null || num <= 0) {
return false;
}
// 查询当前库存信息
ProductStock productStock = getProductStock(productId);
if (productStock == null) {
throw new RuntimeException("商品不存在");
}
// 确认扣减库存
int affectedRows = productStockMapper.confirmDeductStock(productId, num, productStock.getVersion());
return affectedRows > 0;
}
/**
* 回滚预扣减库存(version乐观锁)
* @param productStockDTO
* @return
*/
@Transactional
public boolean rollbackPreDeductStock(ProductStockDTO productStockDTO) {
Long productId = productStockDTO.getProductId();
Integer num = productStockDTO.getNum();
// 检查参数
if (productId == null || num == null || num <= 0) {
return false;
}
// 查询当前库存信息
ProductStock productStock = getProductStock(productId);
if (productStock == null) {
throw new RuntimeException("商品不存在");
}
// 回滚预扣减库存
int affectedRows = productStockMapper.rollbackPreDeductStock(productId, num, productStock.getVersion());
return affectedRows > 0;
}
<?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="com.alicloud.goods.alicloudgoods.business.dao.ProductStockMapper">
<resultMap id="BaseResultMap" type="com.goodsapi.demos.dto.ProductStock">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="stock" property="stock" jdbcType="INTEGER"/>
<result column="pre_stock" property="preStock" jdbcType="INTEGER"/>
<result column="version" property="version" jdbcType="INTEGER"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<select id="selectByProductId" resultMap="BaseResultMap">
SELECT * FROM product_stock WHERE product_id = #{productId}
</select>
<!-- 预扣减库存:实际库存减少,预占库存增加 -->
<update id="preDeductStock">
UPDATE product_stock
SET stock = stock - #{num},
pre_stock = pre_stock + #{num},
version = version + 1
WHERE product_id = #{productId}
AND stock >= #{num}
AND version = #{version}
</update>
<!-- 确认扣减库存:预占库存减少 -->
<update id="confirmDeductStock">
UPDATE product_stock
SET pre_stock = pre_stock - #{num},
version = version + 1
WHERE product_id = #{productId}
AND pre_stock >= #{num}
AND version = #{version}
</update>
<!-- 回滚预扣减库存:实际库存增加,预占库存减少 -->
<update id="rollbackPreDeductStock">
UPDATE product_stock
SET stock = stock + #{num},
pre_stock = pre_stock - #{num},
version = version + 1
WHERE product_id = #{productId}
AND pre_stock >= #{num}
AND version = #{version}
</update>
</mapper>
库存系统 加入预备记录表记录TCC状态
为什么要加入预备记录表
- 库存预减操作,提交扣减操作都有可能因为网络或者其他原因重试 如果没有预减成功就调用取消方法 会导致Cancel在Try之前到达,导致脏数据。
- 如果扣减成功但订单系统调用超时重试,会重复扣减库存。
- 加入预备记录表唯一id,xid预备记录表可以避免这些问题
java
@Service
@Slf4j
public class ProductStockTccService {
@Autowired
private ProductStockMapper productStockMapper;
@Autowired
private FrozenInventoryMapper frozenInventoryMapper;
public ProductStock getProductStock(Long productId) {
return productStockMapper.selectByProductId(productId);
}
/**
* 预减(version乐观锁)
*
* @param productStockDTO
* @return
*/
@Transactional
public Res<Boolean> preDeductStock(ProductStockDTO productStockDTO) {
Long productId = productStockDTO.getProductId();
Integer num = productStockDTO.getNum();
String xid = productStockDTO.getXid();
// 检查参数
if (productId == null || num == null || num <= 0 || xid == null) {
return Res.fail("参数有误");
}
// 1. 检查是否已存在冻结记录(防重复处理)
FrozenInventory existRecord = frozenInventoryMapper.selectByXidAndProduct(xid, productId);
if (existRecord != null) {
log.warn("重复冻结请求,xid={}, productCode={}", xid, productId);
return Res.success(true);
}
// 查询当前库存信息
ProductStock productStock = getProductStock(productId);
if (productStock == null) {
return Res.fail("商品不存在");
}
// 2. 尝试预扣减库存
int affectedRows = productStockMapper.preDeductStock(productId, num, productStock.getVersion());
if (affectedRows == 0) {
log.error("库存不足冻结失败,productCode={}, qty={}", productId, num);
return Res.fail("库存不足");
}
// 3. 记录冻结记录
FrozenInventory record = new FrozenInventory();
record.setXid(xid);
record.setProductId(productId);
record.setFreezeQty(num);
record.setStatus(0); // TRY状态
frozenInventoryMapper.insertFreezeRecord(record);
return Res.success(true);
}
/**
* 确认扣减库存(version乐观锁)
*
* @param productStockDTO
* @return
*/
@Transactional
public Res<Boolean> confirmDeductStock(ProductStockDTO productStockDTO) {
Long productId = productStockDTO.getProductId();
Integer num = productStockDTO.getNum();
String xid = productStockDTO.getXid();
// 检查参数
if (productId == null || num == null || num <= 0 || xid == null) {
return Res.fail("参数有误");
}
// 1. 查询冻结记录
FrozenInventory record = frozenInventoryMapper.selectByXidAndProduct(xid, productId);
if (record == null) {
log.error("确认扣减时未找到冻结记录,xid={}, productCode={}", xid, productId);
return Res.fail("冻结记录不存在");
}
// 2. 已处理过的直接返回成功(幂等控制)
if (record.getStatus() == 1) {
return Res.success(true);
}
// 查询当前库存信息
ProductStock productStock = getProductStock(productId);
if (productStock == null) {
return Res.fail("商品不存在");
}
// 3. 确认扣减库存
int affectedRows = productStockMapper.confirmDeductStock(productId, num, productStock.getVersion());
if (affectedRows == 0) {
log.error("确认扣减库存失败,productCode={}, qty={}", productId, num);
return Res.fail("确认扣减库存失败");
}
// 4. 更新冻结记录状态
frozenInventoryMapper.updateStatus(xid, productId, 1); // CONFIRM状态
return Res.success(true);
}
/**
* 回滚预扣减库存(version乐观锁)
*
* @param productStockDTO
* @return
*/
@Transactional
public Res<Boolean> rollbackPreDeductStock(ProductStockDTO productStockDTO) {
Long productId = productStockDTO.getProductId();
Integer num = productStockDTO.getNum();
String xid = productStockDTO.getXid();
// 检查参数
if (productId == null || num == null || num <= 0 || xid == null) {
return Res.fail("参数有误");
}
// 1. 查询冻结记录
FrozenInventory record = frozenInventoryMapper.selectByXidAndProduct(xid, productId);
// 空回滚处理:没有TRY记录直接返回成功
if (record == null) {
log.info("空回滚,xid={}, productCode={}", xid, productId);
return Res.success(true);
}
// 2. 已处理过的直接返回成功(幂等控制)
if (record.getStatus() == 2) {
return Res.success(true);
}
// 查询当前库存信息
ProductStock productStock = getProductStock(productId);
if (productStock == null) {
return Res.fail("商品不存在");
}
// 3. 回滚预扣减库存
int affectedRows = productStockMapper.rollbackPreDeductStock(productId, num, productStock.getVersion());
if (affectedRows == 0) {
log.error("取消冻结库存失败,productCode={}, qty={}", productId, num);
return Res.fail("取消冻结库存失败");
}
// 4. 更新冻结记录状态
frozenInventoryMapper.updateStatus(xid, productId, 2); // CANCEL状态
return Res.success(true);
}
}
@RestController
@Slf4j
@RequestMapping("/goods/version")
public class GoodsVerisonController {
@Autowired
private ProductStockTccService productStockVersionService;
@GetMapping("/queryOne")
public Res<ProductStock> queryOne(Long productId) {
ProductStock productStock = productStockVersionService.getProductStock(productId);
return Res.success(productStock);
}
/**
* 预减库存
*
* @param productStockDTO 商品ID
* @return 操作结果
*/
@PostMapping(value = "/pre-deduct", produces = "application/json")
public Res<Boolean> preDeductStock(@RequestBody ProductStockDTO productStockDTO) {
if (ObjectUtils.isEmpty(productStockDTO.getProductId())) {
return Res.fail("商品id不能为空");
}
if (ObjectUtils.isEmpty(productStockDTO.getNum())) {
return Res.fail("操作数量不能为空");
}
Res<Boolean> res = productStockVersionService.preDeductStock(productStockDTO);
log.info("{} 预减库存 {}", productStockDTO.getProductId(), JSON.toJSONString(res));
return res;
}
/**
* 确认扣减库存
*
* @param productStockDTO 商品ID
* @return 操作结果
*/
@PostMapping(value = "/confirm-deduct", produces = "application/json")
public Res<Boolean> confirmDeductStock(@RequestBody ProductStockDTO productStockDTO) {
if (ObjectUtils.isEmpty(productStockDTO.getProductId())) {
return Res.fail("商品id不能为空");
}
if (ObjectUtils.isEmpty(productStockDTO.getNum())) {
return Res.fail("操作数量不能为空");
}
Res<Boolean> res = productStockVersionService.confirmDeductStock(productStockDTO);
log.info("{} 确认扣减库存 {}", productStockDTO.getProductId(), JSON.toJSONString(res));
return res;
}
/**
* 回滚预扣减库存
*
* @param productStockDTO 商品ID
* @return 操作结果
*/
@PostMapping(value = "/rollback-pre-deduct", produces = "application/json")
public Res<Boolean> rollbackPreDeductStock(@RequestBody ProductStockDTO productStockDTO) {
if (ObjectUtils.isEmpty(productStockDTO.getProductId())) {
return Res.fail("商品id不能为空");
}
if (ObjectUtils.isEmpty(productStockDTO.getNum())) {
return Res.fail("操作数量不能为空");
}
Res<Boolean> res = productStockVersionService.rollbackPreDeductStock(productStockDTO);
log.info("{} 回滚预扣减库存 {}", productStockDTO.getProductId(), JSON.toJSONString(res));
return res;
}
订单系统调用下单
java
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private GoodsVersionRpcService goodsVersionRpcService;
@Transactional
public Res onlineOrder(OrderDTO orderDTO) {
// 用于控制幂等
String xid = UUID.randomUUID().toString().replace("-", "");
ProductStockDTO productStockDTO = new ProductStockDTO();
productStockDTO.setXid(xid);
productStockDTO.setProductId(orderDTO.getProductId());
productStockDTO.setNum(orderDTO.getQuantity());
Res<Boolean> preDeductRes = Res.fail();
Res<Boolean> confirmRes = Res.fail();
try {
// 1. 进行商品预占
preDeductRes = goodsVersionRpcService.preDeduct(productStockDTO);
if (!preDeductRes.isSuccess() || !preDeductRes.getData()) {
return Res.fail("库存不足,扣减失败");
}
// 2. 创建订单
orderDTO.setOrderNo(createOrderNo());
orderDTO.setOrderStatus(1);
orderMapper.insert(orderDTO);
// 3. 确认扣减库存
confirmRes = goodsVersionRpcService.confirmDeductStock(productStockDTO);
if (!confirmRes.isSuccess() || !confirmRes.getData()) {
throw new RuntimeException("确认扣减库存失败,回滚处理");
}
return Res.success(orderDTO);
} finally {
// 如果预扣减库存成功,但确认扣减库存失败,则进行回滚处理
if (preDeductRes.isSuccess() && preDeductRes.getData() && !confirmRes.isSuccess()) {
// 4. 扣减库存失败,回滚库存
Res<Boolean> booleanRes = goodsVersionRpcService.rollbackPreDeductStock(productStockDTO);
if (!booleanRes.isSuccess() || !booleanRes.getData()) {
log.error("回滚库存失败,{}", JSON.toJSONString(productStockDTO));
}
throw new RuntimeException("下单库存扣减失败");
}
}
}
public static String createOrderNo() {
String s = DateUtil.dateToString(DateUtil.DEFAULT_DATE_TIME_FORMAT_DETAIL);
return "ORD" + s;
}
}
@FeignClient("goods-service")
public interface GoodsVersionRpcService {
@GetMapping("/goods/version/queryOne")
Res<ProductStock> queryOne(@RequestParam("productId") Long productId);
/**
* 商品预占
* @param productStockDTO
* @return
*/
@PostMapping(value = "/goods/version/pre-deduct", produces = "application/json")
Res<Boolean> preDeduct(@RequestBody ProductStockDTO productStockDTO);
/**
* 确认扣减库存
* @param productStockDTO
* @return
*/
@PostMapping(value = "/goods/version/confirm-deduct", produces = "application/json")
Res<Boolean> confirmDeductStock(@RequestBody ProductStockDTO productStockDTO);
/**
* 回滚预扣减库存
* @param productStockDTO
* @return
*/
@PostMapping(value = "/goods/version/rollback-pre-deduct", produces = "application/json")
Res<Boolean> rollbackPreDeductStock(@RequestBody ProductStockDTO productStockDTO);
}
启动测试阶段
127.0.0.1:8010 为网关服务
8070 为订单服务
8080 为用户服务
8090 为库存服务
启动各个服务

登录之后 调用网关系统请求order服务下单
bash
curl --location --request POST 'http://127.0.0.1:8010/order/online/push' \
--header 'Authorization: eyJhbGciOiJIUzUxMiJ9.eyJyZWFsTmFtZSI6Iuezu-e7n-euoeeQhuWRmCIsImlkIjoxLCJ1c2VyTmFtZSI6ImFkbWluIiwiZXhwIjoxNzQ2NTEzNDg2LCJpc0RlbCI6MCwidmVyc2lvbiI6MCwiaWF0IjoxNzQzOTIxNDg2LCJwaG9uZU5vIjoiMTk5OTk5OTk5OTkiLCJyZW1hcmtzIjoi6L-Z5piv5LiA5qyh5rOo5YaMIn0.1FdJX0pKJ3M4tDrFeZzxcl25sBlXTgvOjMuBH6CbCh-hd2f2GlIQNr9VxUNtTl9sDgWHiRt4I18gmzWs0r7nkQ' \
--header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Host: 127.0.0.1:8010' \
--header 'Connection: keep-alive' \
--data-raw '{
"productId": 1,
"productName": "测试商品下单",
"quantity": 1,
"totalAmount": 100,
"unitPrice": 100,
"userId": 1
}'
{"code":200,"msg":"success","data":{"id":8,"orderNo":"ORD20250406151212823","userId":1,"productId":1,"productName":"测试商品下单","quantity":1,"unitPrice":100,"totalAmount":100,"orderStatus":1,"payTime":null,"createTime":null,"updateTime":null},"success":true}
控制台打印日志

base
# gateway网关服务
2025-04-06 15:28:13.617 INFO 76727 --- [ctor-http-nio-6] c.g.a.config.GatewayJwtTokenFilter : rawPath: /order/online/push
2025-04-06 15:28:13.619 INFO 76727 --- [ctor-http-nio-6] c.g.a.config.RateLimiterConfig : compositeKey 限流key:127.0.0.1_/order/online/push
# orderservice
2025-04-06 15:28:13.678 INFO 92553 --- [nio-8070-exec-5] c.a.o.a.b.conttroller.OrderController : 系统管理员 请求下单接口 {"productId":1,"productName":"测试商品下单","quantity":1,"totalAmount":100,"unitPrice":100,"userId":1}
2025-04-06 15:28:14.227 INFO 92553 --- [nio-8070-exec-5] c.a.o.a.b.conttroller.OrderController : 系统管理员 请求下单接口 {"code":200,"data":{"id":10,"orderNo":"ORD20250406152814001","orderStatus":1,"productId":1,"productName":"测试商品下单","quantity":1,"totalAmount":100,"unitPrice":100,"userId":1},"msg":"success","success":true}
# 库存服务
2025-04-06 15:26:25.584 INFO 96956 --- [nio-8090-exec-1] c.a.g.a.b.c.GoodsVerisonController : 1 预减库存 {"code":200,"data":true,"msg":"success","success":true}
2025-04-06 15:26:25.809 INFO 96956 --- [nio-8090-exec-2] c.a.g.a.b.c.GoodsVerisonController : 1 确认扣减库存 {"code":200,"data":true,"msg":"success","success":true}
结尾
希望本文可以帮到你。