本站前面把 Agent 写成循环、工具和策略。墨坊回答的是另一问:没有模型的时候,一笔订单怎么在多个进程里失败,还不超卖、不把钱扣错。

代码在 GitHub1002/ink-workshop。卖的是松烟墨、油烟墨、宣纸和朱砂印泥,金额用整数分。pom 里写到阶段 10:配置进 Nacos,付款失败后退回库存,并准备了尚未在集群里跑过的 Kubernetes 清单。

一句话定位:

浏览器和 curl 都只打网关。店铺先收下订单,库存后扣;扣得动才能付款,余额不够就把留下的库存加回去。

它不是又一个「注册登录下单」的单体演示。成功标准是:超卖进不了库、同一把幂等钥匙得到同一张单、店铺挂了网关会熔断,而不是页面更好看。

一、它到底在解决什么

同步扣库存的写法是:下单接口里 HTTP 调库存,成功再写订单。库存慢或店铺进程在提交前崩溃,用户刷新就会重下一单,或者钱扣了货没留。

墨坊把这两步拆开:

  1. 先收下。 POST /orders 校验商品、写 PENDING、把扣库存请求放进店铺自己的发件箱,接口返回 202。库存还没动。
  2. 再让库存说话。 库存进程消费 RabbitMQ,按行 update … where available >= quantity。成功回 RESERVED,不够回 REJECTED。
  3. 付款是第三步。 只有 RESERVED 能付。余额够则 PAID;不够则 RELEASING,再发一条释放消息,库存加回去之后变成 CANCELLED。

商品库、库存、订单各用各的 Postgres。店铺用 HTTP 问商品价,用消息问库存——定价可以同步快照,扣库存必须能重试、能幂等。

二、分层:对外只有 8080

墨坊进程与数据边界

进程主类端口库
网关ink.gateway.GatewayApplication8080无
库存ink.inventory.InventoryApplication8081ink_inventory
商品库ink.catalog.CatalogApplication8082ink_catalog
店铺ink.workshop.InkWorkshopApplication8083ink_order
店铺第二份同一个主类,--server.port=80848084同一份 ink_order
店面web/ Vite5173无

两份店铺注册到 Nacos,服务名都是 ink-workshop。网关用 lb://ink-workshop 轮转。商品库和库存不注册,店铺用配置里的固定地址调它们。店面不访问 8081–8084。

响应头 X-Instance-Port 是实际处理请求的店铺端口,用来看见负载打到了 8083 还是 8084。

几个角色后面三篇都会撞上:

角色文件干什么
网关gateway.yml · ShopRateLimitFilter限流、熔断、负载均衡;自己不注册
店铺订单order/OrderService.java收下、付款、写发件箱
发件箱order/OutboxPublisher.java · OutboxClaimsFOR UPDATE SKIP LOCKED,两份店铺抢同一张表
库存inventory/DeductionService · ReleaseService幂等扣减与回补,结果再进库存自己的发件箱
拓扑messaging/StockTopology.javaink.stock 交换机 + 死信

钥匙:订单状态写在店铺库里,库存数量写在库存库里。两边只通过消息对齐,不共享一张表。

三、技术选型(以及故意不选什么)

层选型为什么
运行时JDK 21 · Spring Boot 4.1 · Spring Cloud 2025.1一个仓库五个主类,用 PropertiesLauncher 选入口
库Postgres 16 · Flyway · JPA ddl-auto: validate三套 migration 目录,各管各的 schema
消息RabbitMQ · 发件箱先提交本地事务,再投递;不是「接口里直接 convertAndSend」
发现 / 配置Nacos 3只有店铺注册;限流数字可热更新
弹性Resilience4j 限流 + 熔断网关挡洪水和店铺连续失败
观测X-Request-Id · Zipkin一条请求跨进程仍能对上
前端Vite,只打 http://localhost:8080店面不直连内部端口
测试Testcontainers Postgres + RabbitMQ不连本机 Nacos

几个取舍要提前说死:

  • 金额用分。 没有 BigDecimal 进订单总额;multiplyExact / addExact 溢出直接炸。
  • 密码走 BCrypt,拒绝超过 72 字节。 这是阶段里单独修过的脚枪,不是装饰。
  • Kubernetes 清单在 k8s/,README 写明还没在集群里部署过。 本系列不假装它已经生产就绪。
  • 练习口令全是 ink / nacos。 不要拿到正式环境。

四、怎么跑起来

先起基础设施,再起五个 Java 进程和店面。Windows 上多个 mvn spring-boot:run 会锁 target/classes,README 用 classpath 启动:

docker compose up -d postgres rabbitmq nacos zipkin
# Nacos 3 首次需要初始化管理员密码,见仓库 README

对外只访问 http://localhost:8080 。店面 http://localhost:5173 。新用户余额 50000 分;四件商品各 10 件。一笔 51200 分的单(四块油烟墨)会超过余额,正好走退库存。

mvn test

测试自己拉容器,不连本机 Nacos。

五、本系列怎么读(四篇,按论点切)

不另写 00 路线图。墨坊的核心也不大,按「逐文件九篇」拆会出水分。四篇各自只回答一个问题:

篇只回答核心代码
01(本篇)它不是演示店面README、进程表、三库
02一张订单怎么从 202 活到 PAID / CANCELLEDOrderService · 发件箱 · 状态机
03库存进程为什么能重试DeductionService · ReleaseService · 条件更新
04为什么浏览器只能看见网关限流、熔断、Nacos、Vite

建议 01 → 04 按序读。02 是脊柱,03 是「为什么拆进程」,04 是「失败怎么停在门口」。

这不是 Agent 系列。对照阅读(可选):本站讲 Agent 时,策略门在工具调用前拦住副作用;墨坊的门在网关和库存 SQL 上——副作用同样不准「调用方说了算」。

六、小结

  • 墨坊的产品论题是边界清晰的失败,不是更花的店面。
  • 店铺收下订单,库存后扣;付款失败再回补。
  • 三库 + 发件箱 + 两份店铺抢同一张 outbox。
  • 本系列四篇按论点切,不按目录清单切。

下一篇 02 · 一张订单怎么活 会沿着 POST /orders → PENDING → 发件箱 → 库存结果 → 付款走一遍。