当前位置: 首页 > news >正文

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

封面

第三篇 基于nacos搭建分布式项目 分布式事务(分布式锁+事务)
项目所需 maven + nacos + java8 + idea + git + mysql(下单) + redis(分布式锁)
本文主要讲解客户下单时扣减库存的操作,网关系统/用户系统/商品系统/订单系统
请先准备好环境,可以直接clone下来项目去部署。

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

  • 项目结构
  • 分布式事务
    • 分布式锁
      • 何时需要分布式锁?
      • 何时不需要分布式锁?
      • 1. 基于数据库的实现
      • 2. 基于redis+Lua脚本实现
    • 事务机制
      • 2pc 模式
        • 2pc 模式 执行流程
        • 2pc 模式 关键点
      • AT 模式(自动补偿)
        • AT 模式执行流程
        • AT 模式 关键点
      • TCC 模式(手动补偿)
        • TCC 模式执行流程
        • TCC 模式关键点
      • SAGA 模式(长事务补偿)
        • SAGA 模式执行流程
        • SAGA 模式关键点
      • XA 模式(强一致性)
        • XA 模式执行流程
        • XA 模式关键点
      • 本地消息表(最终一致性)
        • 本地消息表执行流程
        • 本地消息表关键点
      • 事务模式对比
    • 具体实现
      • TCC 具体实现
        • 库存系统 乐观锁实现(version)
        • 库存系统 加入预备记录表记录TCC状态
        • 订单系统调用下单
        • 启动测试阶段
          • 启动各个服务
          • 登录之后 调用网关系统请求order服务下单
          • 控制台打印日志
  • 结尾

项目结构

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

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


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. 基于数据库的实现

唯一索引/主键约束

CREATE TABLE distributed_lock (lock_name VARCHAR(64) PRIMARY KEY,owner VARCHAR(255),expire_time TIMESTAMP
);

获取锁:

INSERT INTO distributed_lock(lock_name, owner, expire_time) 
VALUES ('order_lock_${productId}', 'node1', ${time});

释放锁:

DELETE FROM distributed_lock WHERE lock_name = 'order_lock' AND owner = 'node1';

在业务中我们进入下单函数 先通过商品id生成指定的lock_name,查询是否有在有效期内的锁,如果有循环等待锁的获取。没有的情况下insert一条数据 ,insert成功则代表拿到当前商品下单锁。执行完成后删除即可。
但这种实现方式有问题 比如 在执行下单操作时如果超出有效期30秒,下一线程还是可以直接拿到锁去下单,但实际上线程1还在操作,这个时候同一商品加减库存还是有可能出现脏数据的情况。
这个时候可以自动续期(看门狗机制),比如

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” 的缩写,它用于确保只有在键不存在时才执行成功

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脚本可以保证这一步操作为原子性操作。

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

当然也可以加入自动续期(看门狗机制)。

        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 表(用于回滚)。、
-- 例如: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之前到达)问题在下一个目录。

@Service
@Slf4j
public class ProductStockVersionService {@Autowiredprivate ProductStockMapper productStockMapper;public ProductStock getProductStock(Long productId) {return productStockMapper.selectByProductId(productId);}/*** 预减(version乐观锁)* @param productStockDTO* @return*/@Transactionalpublic 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*/@Transactionalpublic 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*/@Transactionalpublic 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_stockSET stock = stock - #{num},pre_stock = pre_stock + #{num},version = version + 1WHERE product_id = #{productId}AND stock >= #{num}AND version = #{version}</update><!-- 确认扣减库存:预占库存减少 --><update id="confirmDeductStock">UPDATE product_stockSET pre_stock = pre_stock - #{num},version = version + 1WHERE product_id = #{productId}AND pre_stock >= #{num}AND version = #{version}</update><!-- 回滚预扣减库存:实际库存增加,预占库存减少 --><update id="rollbackPreDeductStock">UPDATE product_stockSET stock = stock + #{num},pre_stock = pre_stock - #{num},version = version + 1WHERE product_id = #{productId}AND pre_stock >= #{num}AND version = #{version}</update>
</mapper>
库存系统 加入预备记录表记录TCC状态

为什么要加入预备记录表

  1. 库存预减操作,提交扣减操作都有可能因为网络或者其他原因重试 如果没有预减成功就调用取消方法 会导致Cancel在Try之前到达,导致脏数据。
  2. 如果扣减成功但订单系统调用超时重试,会重复扣减库存。
  3. 加入预备记录表唯一id,xid预备记录表可以避免这些问题
@Service
@Slf4j
public class ProductStockTccService {@Autowiredprivate ProductStockMapper productStockMapper;@Autowiredprivate FrozenInventoryMapper frozenInventoryMapper;public ProductStock getProductStock(Long productId) {return productStockMapper.selectByProductId(productId);}/*** 预减(version乐观锁)** @param productStockDTO* @return*/@Transactionalpublic 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*/@Transactionalpublic 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*/@Transactionalpublic 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 {@Autowiredprivate 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;}
订单系统调用下单

@Service
@Slf4j
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate GoodsVersionRpcService goodsVersionRpcService;@Transactionalpublic 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服务下单

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}
控制台打印日志

在这里插入图片描述


# 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搭建分布式项目 网关

希望本文可以帮到你。

相关文章:

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

第三篇 基于nacos搭建分布式项目 分布式事务&#xff08;分布式锁事务&#xff09; 项目所需 maven nacos java8 idea git mysql(下单) redis(分布式锁) 本文主要讲解客户下单时扣减库存的操作&#xff0c;网关系统/用户系统/商品系统/订单系统 请先准备好环境&#xff0…...

Nginx-keepalived-高可用

Nginx 高可用 通常 借助 Keepalived 实现&#xff0c; Keepalived 能通过 VRRP &#xff08;虚拟路由冗余协议&#xff09;让多个 Nginx 服务器 组成一个 热备集群&#xff0c;当主服务器故障时自动切换到备用服务器&#xff0c;保障服务不间断。 一、环境准备 角色IP 地址主…...

Linux系统管理(十九)——欧拉系统硬盘挂载、网络配置以及Docker环境安装

挂载硬盘 如果数据盘在安装操作系统的时候没有挂载&#xff0c;需要自己做一下硬盘的挂载 查看需要挂载硬盘的路径 fdisk -l这里的可挂载的硬盘路径为&#xff1a;/dev/sdb MBR分区方式转换成GPT MBR分区能挂载的硬盘空间有限&#xff0c;无法挂载全部硬盘空间&#xff0…...

vue记忆卡牌游戏

说明&#xff1a; 我希望用vue做一款记忆卡牌游戏 游戏规则如下&#xff1a; 游戏设置&#xff1a;使用3x4的网格&#xff0c;包含3对字母&#xff08;A,B,C,D,E,F&#xff09;。 ​随机洗牌&#xff1a;初始字母对被打乱顺序&#xff0c;生成随机布局。 ​游戏流程&#xff1a…...

LearnOpenGL-笔记-其九

今天让我们完结高级OpenGL的部分&#xff1a; Instancing 很多时候&#xff0c;在场景中包含有大量实例的时候&#xff0c;光是调用GPU的绘制函数这个过程都会带来非常大的开销&#xff0c;因此我们需要想办法在每一次调用GPU的绘制函数时尽可能多地绘制&#xff0c;这个过程就…...

开源软件与自由软件:一场理念与实践的交锋

在科技的世界里&#xff0c;“开源软件”和“自由软件”这两个词几乎无人不知。很多人或许都听说过&#xff0c;它们的代码是公开的&#xff0c;可以供所有人查看、修改和使用。然而&#xff0c;若要细究它们之间的区别&#xff0c;恐怕不少朋友会觉得云里雾里。今天&#xff0…...

关于使用HAL_ADC_Start函数时为什么要放在while里的解释

HAL_ADC_Start() 是一个用于启动 ADC&#xff08;模数转换器&#xff09;转换的函数&#xff0c;那为什么有时候我们会看到它被放在 while 循环里呢&#xff1f;其实取决于你使用的是哪种ADC采样方式&#xff0c;我们来细说&#x1f447;&#xff1a; &#x1f9e0; 一、先搞清…...

Qt 入门 2 之窗口部件 QWidget

Qt 入门2之窗口部件 QWidget Qt Creator 提供的默认基类只有QMainWindow、QWidget和QDialog 这3种&#xff0c;这3种窗体也是以后用得最多的&#xff0c;QMainWindow是带有菜单栏和工具栏的主窗口类,QDialog是各种对话框的基类,而它们全部继承自QWidget。不仅如此,其实所有的窗…...

在 Windows 上安装 WSL Ubuntu 的完整避坑指南:从报错到成功运行

问题背景​​ 最近在尝试通过 ​​Windows Subsystem for Linux (WSL)​​ 安装 Ubuntu 时&#xff0c;遇到了一系列报错。最初的步骤是直接使用 wsl --install 命令&#xff0c;但安装完成后发现系统中并未自动安装默认的 Ubuntu 发行版。随后尝试通过命令行手动选择发行版&a…...

STM32看门狗原理与应用详解:独立看门狗 vs 窗口看门狗(上) | 零基础入门STM32第九十四步

主题内容教学目的/扩展视频看门狗什么是看门狗&#xff0c;原理分析&#xff0c;启动喂狗方法&#xff0c;读标志位。熟悉在程序里用看门狗。 师从洋桃电子&#xff0c;杜洋老师 &#x1f4d1;文章目录 一、看门狗核心原理1.1 工作原理图解1.2 经典水桶比喻 二、STM32看门狗双雄…...

Hyperlane 框架路由功能详解:静态与动态路由全掌握

Hyperlane 框架路由功能详解&#xff1a;静态与动态路由全掌握 Hyperlane 框架提供了强大而灵活的路由功能&#xff0c;支持静态路由和动态路由两种模式&#xff0c;让开发者能够轻松构建各种复杂的 Web 应用。本文将详细介绍这两种路由的使用方法。 静态路由&#xff1a;简单…...

webpack js 逆向 --- 个人记录

网站 aHR0cDovL2FlcmZheWluZy5jb20v加密参数 参数加密位置 方法&#xff1a; 1. 构造自执行函数 !function(e) {// 加载器 }(// 模块1&#xff1b;// 模块2 )2. 找到js的加载器 3. 把上述代码放入第一步构造的自执行函数(完整扣取一整个加载器里的代码)&#xff0c;并用一…...

代码随想录回溯算法03

93.复原IP地址 本期本来是很有难度的&#xff0c;不过 大家做完 分割回文串 之后&#xff0c;本题就容易很多了 题目链接/文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;回溯算法如何分割字符串并判断是合法IP&#xff1f;| LeetCode&#xff1a;93.复原IP地址_哔哩哔…...

SOMEIP通信矩阵解读

目录 1 摘要2 SOME/IP通信矩阵详细属性定义与示例2.1 服务基础属性2.2 数据类型定义2.3 服务实例与网络配置参数2.4 SOME/IP-SD Multicast 配置&#xff08;SOME/IP服务发现组播配置&#xff09;2.5 SOME/IP-SD Unicast 配置2.6 SOME/IP-SD ECU 配置参数详解 3 总结 1 摘要 本…...

keys简单通用命令

目录 一、通配符匹配key 二、key的常用命令 1&#xff09;exists 判断某个key是否存在 2&#xff09;删除key 3&#xff09;expire设置key的过期时间、ttl查询key的过期时间 4&#xff09;type可以获取key对应的value的类型 一、通配符匹配key redis作为键值对类型的数据…...

Linux : 页表

目录 一 前言 二 深入理解页表 三 页表的实际组成 四 总结 一 前言 页表是我们之前在讲到Linux : 进程地址空间-CSDN博客的时候说到的&#xff0c;它是物理内存到进程程序地址空间的一个桥梁&#xff0c;通过它&#xff0c;物理内存的数据和代码才能映射到进程的程序地址空间…...

多智能体优秀开发框架

原文链接:https://i68.ltd/notes/posts/20250402-multi-agent/ motia-面向软件工程师的智能体框架 项目仓库:https://github.com/MotiaDev/motia 1.3k官方网站:https://motia.devMotia 允许开发人员在几分钟内创建&#xff0c;测试和部署生产就绪的 AI 代理&#xff0c;在一个…...

JavaScript创建对象与构造函数

目录 创建对象 一、创建对象的 5 种核心方式 1. 对象字面量&#xff08;直接量&#xff09; 2. 使用 Object.create() 3. 工厂模式 4. 构造函数模式 5. ES6 class 语法&#xff08;语法糖&#xff09; 二、构造函数与 new 关键字 1. 构造函数的作用 2. 构造函数的特征…...

【langchain4j系列教程-02】Langchain4j调用DeepSeek

文章目录 依赖引入代码示例api key如何获取模型名称及价格为什么调用DeepSeek API用的是OpenAIChatModel 这篇文章主要介绍了如何在Java项目中引入DeepSeek的依赖&#xff0c;并提供了调用DeepSeek API的代码示例。 依赖引入 DeepSeek是一个与OpenAI接口标准兼容的人工智能平台…...

c++STL入门

目录 什么是STL&#xff1f; vector容器 构造函数 赋值操作 vector容量和大小 vector存放内置数据类型 vector存放自定义数据类型 存放指针 vector容器嵌套容器 string容器 构造函数 赋值操作 字符串拼接 查找和替换 string字符串比较 string字符存取 string插…...

公有云子账号认证的原理和步骤

1 为什么使用子账号 1.1 子账号认证的背景 主账号权限过大:公有云账号(主账号)对账号中的资源具有完全管理权限,且无法调整其权限大小,多人共用时无法在审计日志中区分出具体使用人,一旦泄露风险极大且难以追溯。 安全需求:为了提高安全性,避免因主账号信息泄露而导致…...

基于Flask的微博舆情数据分析系统

【Flask】基于Flask的微博舆情数据分析系统&#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统能够高效收集、处理微博上的海量数据&#xff0c;实时反映公众对某些事件或话题的舆论倾向&am…...

Mac OS 禁用 SIP 系统完整性保护

关闭并重新启动 Mac&#xff0c;CommandR在启动时按住以进入Recovery Mode.进入Recovery模式后打开终端 -bash-3.2# csrutil disable Turning off System Integrity Protection reguires modifying system security.Allow booting unsigned operating systems and any ker…...

第十四届蓝桥杯省赛真题解析(含C++详细源码)

第十四届蓝桥杯省赛 整数删除满分思路及代码solution1 &#xff08;40% 双指针暴力枚举&#xff09;solution 2&#xff08;优先队列模拟链表 AC&#xff09; 冶炼金属满分代码及思路 子串简写满分思路及代码solution 1&#xff08;60% 双指针&#xff09;solution 2&#xff0…...

整车CAN网络和CANoe

车载网络中主要包含有Can网络,Lin网络,FlexRay,Most,以太网。 500kbps:500波特率,表示的数据传输的速度。表示的是最大的网速传输速度。也就是每秒 500kb BodyCan车身Can InfoCan娱乐信息Can 车身CAN主要连接的是ESB电动安全带 ADB自适应远光灯等 PTCan动力Can 底盘Can...

从扩展黎曼泽塔函数构造物质和时空的结构-16

都是一样的泽塔函数&#xff0c;却呈现出不同的性质&#xff0c;而不同的性质无关于自然数还是质数&#xff0c;完全是由s来决定的。可以猜想&#xff0c; 就是光子&#xff0c;而如果&#xff0c; 就是物质粒子。其中不同的k指出不同的周期或者对应于不同的质数p&#xff0c;虽…...

【C++】list模拟实现

&#x1f4dd;前言&#xff1a; 这篇文章我们来讲讲STL中list的模拟实现&#xff1a; &#x1f3ac;个人简介&#xff1a;努力学习ing &#x1f4cb;个人专栏&#xff1a;C学习笔记 &#x1f380;CSDN主页 愚润求学 &#x1f304;其他专栏&#xff1a;C语言入门基础&#xff0c…...

人脸专注度检测系统(课堂专注度检测、人脸检测、注意力检测系统)

人脸专注度检测系统 项目介绍 本项目是基于Flask、MobileNetV2、Mediapipe的人脸专注度检测系统。 项目采用tensorflow.keras库内置的MobileNetV2预训练模型&#xff0c;对自主采集的少量人脸图片数据迁移训练而得到最终的人脸专注度检测模型。 项目采用前后端分离的技术框架…...

文件操作和IO ——Java

初识文件 首先文件分为&#xff1a; 1.狭义的文件 – 保存在硬盘上的文件。 2.广义的文件 – 操作系统进行资源管理的一种机制。很多的软件/硬件资源&#xff0c;抽象成“文件”来进行表示。 &#xff08;println > 控制台&#xff0c;scanner > 控制台的标准输入&#…...

dmsetup 清理ceph osd残留磁盘分区

在物理机上接入磁盘并准备格式化时&#xff0c;发现磁盘中存在之前残留的 Ceph OSD 分区。尝试运用 fdisk 重新分区、重新格式化&#xff0c;以及使用 sgdisk 格式化&#xff0c;甚至重写磁盘头&#xff0c;都未能成功清理掉这些 OSD 残留分区。最终&#xff0c;借助 dmsetup 直…...

每日一题(小白)字符串娱乐篇16

分析题意可以了解到本题要求在一串字符串中找到所有组合起来排序递增的字符串。我们可以默认所有字符在字符串中的上升序列是1&#xff0c;从第一个字符开始找&#xff0c;如果后面的字符大于前面的字符就说明这是一个上序列那么后面字符所在的数组加一&#xff0c;如果连接不上…...

【Mac 从 0 到 1 保姆级配置教程 11】- Mac 基础配置 Finder、触控板、常用快捷键等

文章目录 前言配置 Finder1. 把我们的家目录请出来2. 显示文件扩展名3. 展示隐藏文件4. 显示路径栏和状态栏5. 固定文件夹到工具栏 基础快捷键1. Finder 导航快捷键2. 文件操作快捷键3. 视图和显示快捷键4. 搜索和选择快捷键5. 实用技巧6. 关于文件创建 配置触控板1. 右键设置2…...

Redis 渐进式rehash怎么判定rehash完成了?

Redis 渐进式 Rehash 的完成判断机制 在 Redis 的字典(dict)结构扩容或缩容时,会使用 渐进式 Rehash 来避免一次性迁移所有键值对导致的阻塞。以下是判断旧哈希表(ht[0])是否全部迁移完毕的核心逻辑: 1. 渐进式 Rehash 的核心流程 Redis 的字典结构包含两个哈希表: t…...

Redis的常用数据结构

三. Redis 的常用数据结构 (redis提供的查询功能, 不像mysql这么强大) 1. 认识数据类型和编码方式 常见数据结构 (数据类型) : string (字符串), list (列表), hash (哈希), set (集合), zset (有序集合). Redis 底层在实现上述数据结构的时候, 会在源码层面进行优化, 来达到…...

c++中的虚函数

在C中&#xff0c;虚函数&#xff08;Virtual Function&#xff09;是一种实现多态的重要机制&#xff0c;它允许在派生类中重写基类的函数&#xff0c;从而在运行时根据对象的实际类型调用相应的函数版本。 1. 虚函数的定义 虚函数是在基类中使用关键字virtual声明的函数。例…...

unreal engine5开发仿鬼泣5的游戏,把敌人击飞到空中4连击

UE5系列文章目录 文章目录 UE5系列文章目录前言一、实现思路二、具体蓝图 前言 unreal engine5开发仿鬼泣5的游戏&#xff0c;把敌人击飞到空中4连击&#xff0c;先看下效果 一、实现思路 unreal engine5开发仿鬼泣5的游戏&#xff0c;把敌人击飞到空中4连击 在Unreal Engi…...

蓝桥杯嵌入式第十四届模拟二(PWM、USART)

一.LED 先配置LED的八个引脚为GPIO_OutPut,锁存器PD2也是,然后都设置为起始高电平,生成代码时还要去解决引脚冲突问题 二.按键 按键配置,由原理图按键所对引脚要GPIO_Input 生成代码,在文件夹中添加code文件夹,code中添加fun.c、fun.h、headfile.h文件,去资源包中把lc…...

深挖 TypeScript 基础数据类型:应用与陷阱

在 TypeScript 的学习与实践过程中&#xff0c;对基础数据类型的深入理解和正确运用&#xff0c;是写出高质量代码的关键。本篇文章会通过探讨数据类型在实际场景中的应用&#xff0c;分析常见错误&#xff0c;帮助大家提升运用 TypeScript 基础数据类型的能力。​ 一、函数参…...

B站视频教材: Yocto项目实战教程 第一章 PPT讲解

B站视频链接,请多多关注本人B站&#xff1a; &#x1f4cc; Yocto项目实战教程&#xff1a;第一章 视频讲解 在当今嵌入式系统快速发展的背景下&#xff0c;越来越多的设备和行业依赖于高效、可定制、可维护的操作系统解决方案。嵌入式Linux系统&#xff0c;凭借其开源灵活性和…...

嵌入式软件开发调试方法

文章目录 1. 利于函数返回值,retrurn 定位错误位置2. 合理使用逻辑分析仪&#xff08;正点原子 厉害&#xff01;&#xff01;&#xff09; 1. 利于函数返回值,retrurn 定位错误位置 如下图所示&#xff0c;设置不同的返回值&#xff0c;0是ok的&#xff0c;其他值均为失败&…...

Sentinel核心源码分析(上)

文章目录 前言一、客户端与Spring Boot整合二、SphU.entry2.1、构建责任链2.2、调用责任链2.2.1、NodeSelectorSlot2.2.2、ClusterBuilderSlot2.2.3、LogSlot2.2.4、StatisticSlot2.2.5、AuthoritySlot2.2.6、SystemSlot2.2.7、FlowSlot2.2.7.1、selectNodeByRequesterAndStrat…...

TCPIP详解 卷1协议 一 概述

相关概念 协议族&#xff1a;一系列相关协议的集合称为一个协议族体系结构&#xff1a;指定一个协议族中的各种协议之间的相互关系并划分需要完成的任务的设计&#xff0c;称为协议族的体系结构。分组交换&#xff1a;数据被分割为固定或可变长度的分组 &#xff0c;每个分组包…...

条件生成对抗网络(Conditional GAN, CGAN)原理及实现(pytorch版)

CGAN 原理及实现 一、CGAN 原理1.1 基本概念1.2 与传统GAN的区别1.3 目标函数1.4 损失函数1.5 条件信息的融合方式1.6 与其他GAN变体的对比1.7 CGAN的应用1.8 改进与变体二、CGAN 实现2.1 导包2.2 数据加载和处理2.3 构建生成器2.4 构建判别器2.5 训练和保存模型2.6 绘制训练损…...

类与对象(上)

【本节目标】 1.面向过程和面向对象初步认识 2.类的引入 3.类的定义 4.类的访问限定符及封装 5.类的作用域 6.类的实例化 7.类的对象大小的计算 8.类成员函数的this指针 1.面向过程和面向对象初步认识 C语言是面向过程的&#xff0c;关注的是过程&#xff0c;…...

MySQL基础 [三] - 数据类型

目录 数据类型分类 ​编辑 数值类型 tinyint bit 浮点类型 float decimal 字符串类型 char varchar varchar和char的比较和选择 日期和时间类型 enum和set enum类型 set类型 enum和set的类型查找 数据类型分类 数值类型 tinyint TINYINT[(M)] [UNSIGNED]是 …...

1.1 测试计划阶段:如何制定高效的测试策略

测试计划阶段&#xff1a;如何制定高效的测试策略 摘要 本文详细介绍了软件测试流程中的测试计划阶段&#xff0c;包括测试策略制定、资源规划、进度安排和风险管理等内容。通过本文&#xff0c;读者可以系统性地了解如何制定有效的测试计划&#xff0c;确保测试工作有序进行…...

Spring 概念

Spring 是一个功能强大、灵活且广泛使用的 Java 企业级开发框架&#xff0c;它诞生于 2003 年&#xff0c;由 Rod Johnson 创建&#xff0c;初衷是简化 Java EE 的开发过程。 一、Spring 是什么&#xff1f; 简单来说&#xff1a; Spring 是一个轻量级的 Java 开发框架&#…...

animals_classification动物分类

数据获取 深度学习训练中第一个是获取数据集&#xff0c;数据集的质量很重要&#xff0c;我们这里做的是动物分类&#xff0c;大致会选择几个动物&#xff0c;来做一个简单的多分类问题&#xff0c;数据获取的方法&#xff0c;鼠鼠我这里选择使用爬虫的方式来对数据进行爬取&a…...

15.QT窗口:主窗口、浮动窗口、对话框

0. 概述 Qt窗口是通过 QMainWindow类 来实现的。 QMainWindow 是一个为用户提供主窗口程序的类&#xff0c;继承自QWidget类&#xff0c;并且提供了一个预定义的布局。QMainWindow包含一个菜单栏&#xff08;menu bar&#xff09;、多个工具栏&#xff08;tool bars&#xff…...

nginx中地理位置访问控制模块geo

1.安装 GeoIP2 模块 Ubuntu/Debian 系统&#xff1a; sudo apt-get update sudo apt-get install nginx-module-geoip2 sudo apt-get install libnginx-mod-http-geoip2CentOS/RHEL 系统&#xff1a; sudo yum install nginx-module-geoip22.下载 GeoIP2 数据库 下载 GeoIP2 …...