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

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

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

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

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

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

项目结构

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

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

bash 复制代码
git clone git@gitee.com:goodluckv/ali-cloud-common.git
git clone git@gitee.com:goodluckv/ali-cloud-gateway.git
git clone git@gitee.com:goodluckv/ali-cloud-user.git
git clone git@gitee.com:goodluckv/ali-cloud-goods.git
git clone git@gitee.com: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搭建分布式项目 网关

希望本文可以帮到你。

相关推荐
神奇小汤圆2 分钟前
Unsafe魔法类深度解析:Java底层操作的终极指南
后端
暮色妖娆丶11 分钟前
Spring 源码分析 BeanFactoryPostProcessor
spring boot·spring·源码
神奇小汤圆35 分钟前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生1 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling1 小时前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅1 小时前
springBoot项目有几个端口
java·spring boot·后端
Luke君607971 小时前
Spring Flux方法总结
后端
define95271 小时前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li1 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶2 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring