Skip to content

feat(example): add the inventory context to ddk-mall - #76

Merged
poppycoderr merged 2 commits into
mainfrom
feat/mall-inventory
Oct 5, 2026
Merged

poppycoderr merged 2 commits into
mainfrom
feat/mall-inventory

Conversation

@poppycoderr

Copy link
Copy Markdown
Owner

Background

Second step of the ddk-mall reference application: the inventory context. Stock is the hot spot of the order flow. Many orders compete for the same SKU, one order touches several SKUs, and the operations will soon be driven by messages that can arrive more than once.

Changes

  • The inventory context under com.example.mall.inventory, with its own four layers.
    • Stock: on-hand and reserved quantities per SKU. Reserving keeps the goods on hand but not available; they leave the warehouse only when the order is paid.
    • StockReservation: one record per order and SKU. It makes release and deduction repeatable: a second release finds the record already released and changes nothing.
  • InventoryService: reserve for all lines of an order or for none, release, confirm, restock.
  • StockLock port with one implementation: DDK's AggregateLocks.executeAll when Redis is available, in-process locks otherwise.
  • REST endpoints for operations and manual checks; from step 3 on, order events drive reserve, release and confirm.
  • docker-compose.yml gains Redis, and the mysql profile is renamed compose. The default profile still starts without external components.
reserve(orderId, lines)
  lock every SKU of the order, in a fixed order            # StockLock → AggregateLocks.executeAll
    begin transaction                                      # inside the lock, so the lock outlives the commit
      for each line not yet reserved for this order
        stock.reserve(quantity)                            # INSUFFICIENT_STOCK rolls back every line
        save stock (version check), create reservation
    commit
  unlock

Design decisions:

  • A lock and a version check. The lock makes concurrent reservations of one SKU queue instead of failing at commit; the optimistic lock stays as the backstop if the Redis lock is lost.
  • The inventory context knows an order only as a number. It imports nothing from the order context, and the slices rule in ArchitectureTest now has two contexts to check.
  • The default profile falls back to in-process locks and logs a warning that it must not run as more than one instance. This keeps mvn spring-boot:run free of external dependencies.

StockConcurrencyIntegrationTest runs against real MySQL and Redis: twenty orders reserve one unit each of a SKU with three in stock. Exactly three succeed, seventeen get INSUFFICIENT_STOCK, and the stored reserved quantity is three.


背景

ddk-mall 参考应用的第二步:库存上下文。库存是下单流程里的热点:很多订单抢同一个 SKU,一个订单又涉及多个 SKU,而且这些操作很快要由可能重复投递的消息来驱动。

改动

  • 库存上下文 com.example.mall.inventory,内部同样是四层。
    • Stock:每个 SKU 的在库数量和已预占数量。预占的货还在库里,只是不再可售;订单支付后才真正出库。
    • StockReservation:每个订单每个 SKU 一条预占记录。有了它,释放和扣减可以重复执行:第二次释放时记录已经是已释放状态,什么都不会改。
  • InventoryService:为订单的所有行预占(要么全占,要么都不占)、释放、扣减、补货。
  • StockLock 端口及其实现:有 Redis 时用 DDK 的 AggregateLocks.executeAll,否则用进程内的锁。
  • 面向运营操作和手工验证的 REST 接口;第 3 步之后,预占、释放、扣减由订单事件驱动。
  • docker-compose.yml 增加 Redis,mysql profile 改名为 compose。默认 profile 仍然不依赖外部组件。

预占流程见上文伪代码。设计取舍:

  • 既加锁又校验版本。 锁让同一个 SKU 上的并发预占排队,而不是到提交时才冲突;乐观锁保留下来,在 Redis 锁失效时兜底。
  • 库存上下文只把订单当作一个编号。 它不引用订单上下文的任何类型,ArchitectureTest 里的切片规则现在有两个上下文可以检查了。
  • 默认 profile 退回进程内的锁,并在启动时打一条警告,说明不能多实例运行。这样 mvn spring-boot:run 不需要任何外部依赖。

StockConcurrencyIntegrationTest 连接真实的 MySQL 和 Redis:20 个订单各预占 1 件只有 3 件库存的 SKU,恰好 3 个成功,17 个得到 INSUFFICIENT_STOCK,库里记录的预占数量是 3。

@poppycoderr
poppycoderr merged commit b515038 into main Oct 5, 2026
4 checks passed
@poppycoderr
poppycoderr deleted the feat/mall-inventory branch October 5, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant