【springcloud】快速搭建一套分布式服务springcloudalibaba(三)

第三篇 基于nacos搭建分布式项目 分布式事务(分布式锁+事务)

项目所需 maven + nacos + java8 + idea + git + mysql(下单) + redis(分布式锁)

本文主要讲解客户下单时扣减库存的操作,网关系统/用户系统/商品系统/订单系统

请先准备好环境,可以直接clone下来项目去部署。

基于nacos搭建分布式项目 分布式事务

项目结构

第一篇快速部署一套分布式服务

第二篇 基于nacos搭建分布式项目 网关

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。看门狗机制需要系统更多的资源消耗。

事务机制

首先引入分布式事务的概念性单词

  1. TC(Transaction Coordinator):事务协调器,负责全局事务的提交或回滚(独立部署)。
  2. TM(Transaction Manager):事务管理器(集成在业务服务中),负责开启、提交或回滚全局事务。
  3. RM(Resource Manager):资源管理器(集成在数据库访问层,如JDBC代理),负责分支事务的注册、状态报告和本地事务管理。

2pc 模式

2PC(Two-Phase Commit,两阶段提交)是分布式事务的经典协议,用于确保跨多个节点的事务要么全部提交,要么全部回滚,保证 ACID 中的 原子性(Atomicity)。需要数据库支持XA协议。

2pc 模式 执行流程
  1. 阶段1:准备阶段(Prepare Phase)
    协调者(Coordinator) 向所有 参与者(Participants) 发送 Prepare 请求,询问是否可以提交事务。
    参与者 执行本地事务(锁定资源但不提交),并记录 Undo/Redo 日志(用于回滚或恢复)。
    参与者 返回响应:
    Yes:表示可以提交(本地事务执行成功,并锁定资源)。
    No:表示无法提交(本地事务失败,或资源冲突)。
  2. 阶段2:提交/回滚阶段(Commit/Rollback Phase)
    如果所有参与者都返回 Yes:
    协调者 发送 Commit 命令,参与者提交事务并释放锁。
    参与者 返回 Ack 表示提交成功。
    如果有任一参与者返回 No 或超时:
    协调者 发送 Rollback 命令,参与者回滚事务(使用 Undo 日志恢复数据)。
    参与者 返回 Ack 表示回滚成功。
    实际的执行逻辑
  3. TM调用xa_start()开始事务
  4. 执行各RM上的SQL操作
  5. TM调用xa_end()结束事务分支
  6. TM调用xa_prepare()询问各RM
  7. 根据prepare结果决定xa_commit()或xa_rollback()
  8. 必要时使用xa_recover()处理悬挂事务
2pc 模式 关键点

各个服务控制自身的资源预占,协调者控制是否全都提交还是回滚。

各个服务资源预占后需要等待协调者 提交/回滚指令,提交回滚后协调者还需要继续等待各个服务是否执行成功的响应,需要多次通信确认(Prepare(是否可以修改预占) + Commit/rollback(修改/回滚) + Ack(所有服务确认完成))

协调者如果没有发送指令 不提交或者回滚,会导致参与者一直锁定资源等待命令。 可以考虑AT 模式(自动补偿),不会一直挂起,锁定资源。

AT 模式(自动补偿)

AT(Auto Transaction)模式 基于 2PC(两阶段提交) 优化而来,但不需要数据库支持 XA 协议,适用于大多数业务场景。

AT 模式执行流程

阶段1:执行本地事务并提交

  1. TM 开启全局事务(@GlobalTransactional),向 TC 注册全局事务(生成 XID)。
  2. 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)。
  1. RM 提交本地事务(数据库层面提交),但全局事务未完成(TC 未收到所有分支事务的成功报告)。

阶段2:全局提交或回滚

如果所有分支事务成功:

TC 通知所有 RM 提交(实际只需删除 undo_log 记录,因为数据已提交)。

如果任一分支事务失败:

TC 通知所有 RM 回滚,RM 根据 undo_log 恢复数据(反向补偿)。

AT 模式 关键点

各个事务参与方执行各自的事务,事务协议由单一服务去控制,当前操作参与中的任意节点有失败的,通知所有参与方回滚。(不适用不同用户同时下单同一商品的情况,如果使用该协议 需配合分布式锁同时使用)

TCC 模式(手动补偿)

TCC(Try-Confirm-Cancel)模式 适用于需要更高灵活性的业务,如金融支付、积分兑换等。

TCC 模式执行流程
  1. Try:预留资源(如冻结用户金额)。
  2. Confirm:确认操作(如实际扣款)。
  3. 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才会释放资源,会造成一定的线程阻塞。

本地消息表(最终一致性)

本地消息表是一种基于最终一致性思想的分布式事务解决方案,它通过将分布式事务拆分为多个本地事务,并借助消息表来保证事务的最终一致性。

本地消息表执行流程
  1. 将分布式事务拆分为多个本地事务
  2. 通过本地消息表记录事务状态
  3. 使用定时任务或消息队列确保所有参与方最终完成操作
本地消息表关键点

实现简单,不需要额外组件

基于最终一致性,对性能影响小

适用于长事务与允许事务短暂不一致的场景

各个服务也需要实现回滚操作。

事务模式对比

*[HTML]:

方案 一致性 性能 侵入性 适用场景
AT 最终一致性 ⭐⭐⭐⭐ 普通业务(订单、库存)
TCC 最终一致性 ⭐⭐⭐ 高并发(秒杀、支付)
SAGA 最终一致性 ⭐⭐ 长事务(物流、跨服务流程)
XA 强一致性 金融、银行(强一致性要求)
2PC 强一致性 传统数据库(如 Oracle XA)
本地消息表 最终一致性 ⭐⭐ 异步场景(如订单+消息通知)

具体实现

模式比较多,本文主要手写 TCC 模式(下一篇文章再看需要实现其他模式)。

AT ,2PC,SAGA,TCC都有现成的车轮(Seata)

TCC 具体实现

库存系统 乐观锁实现(version)

下单调用执行流程

  1. 调用预减库存方法preDeductStock 预减,成功则返回true
  2. 调用确认扣减库存confirmDeductStock扣减库存 成功则返回true(这里需要注意即便使用了库存预减也要判断数量和接口幂等,避免因超时重试等扣减多次)
  3. 失败后调用回滚预扣减库存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状态

为什么要加入预备记录表

  1. 库存预减操作,提交扣减操作都有可能因为网络或者其他原因重试 如果没有预减成功就调用取消方法 会导致Cancel在Try之前到达,导致脏数据。
  2. 如果扣减成功但订单系统调用超时重试,会重复扣减库存。
  3. 加入预备记录表唯一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}

结尾

第一篇快速部署一套分布式服务

第二篇 基于nacos搭建分布式项目 网关

希望本文可以帮到你。

相关推荐
兰亭序咖啡7 分钟前
学透Spring Boot — 018. 优雅支持多种响应格式
java·spring boot·后端
审计侠14 分钟前
Go语言-初学者日记(八):构建、部署与 Docker 化
开发语言·后端·golang
AskHarries26 分钟前
如何开通google Free Tier长期免费云服务器(1C/1G)
后端
码界筑梦坊29 分钟前
基于Django的二手交易校园购物系统
大数据·后端·python·信息可视化·django
东方珵蕴32 分钟前
Logo语言的区块链
开发语言·后端·golang
烛阴32 分钟前
从零到RESTful API:Express路由设计速成手册
javascript·后端·express
uhakadotcom39 分钟前
Mars与PyODPS DataFrame:功能、区别和使用场景
后端·面试·github
信徒_1 小时前
Spring 怎么解决循环依赖问题?
java·后端·spring
小杨4042 小时前
springboot框架项目实践应用十五(扩展sentinel区分来源)
spring boot·后端·spring cloud
FirstMrRight3 小时前
自动挡线程池OOM最佳实践
java·后端