如何在DDD中处理复杂聚合根不变量?图书借阅系统场景
自定义时长借阅图书管理系统的可用性校验扩展方案
核心需求
- 支持自定义借阅时长,严格禁止用户预订指定时间段内任意30分钟时隙已被完全占满的图书
- 可配置订单前后的缓冲期(比如设1天缓冲期,用户要订2022-01-17开始的订单,不能接在2022-01-16结束的订单后面)
- 库存实时补充后,要立刻允许符合条件的预订
- 用
30min slots拆分时间,通过遍历时隙计算库存可用性
场景示例
现有库存:
Fellowship of the ring (x10) The Shining (x1)
用户A预订两本书各1本,时间2022-01-01至2022-01-20;用户B尝试在2022-01-15至2022-01-30预订同款书各1本时,必须报错——因为《The Shining》在两个订单重叠的时隙(2022-01-15至2022-01-20)里库存已经耗尽。
当前设计的瓶颈
现有BookOrder聚合根草图:
BookOrder Members: DateTime From DateTime To Book[] BooksOrdered Methods: CanOrder(from, to, books, stocksForTimeSpanWithMargin) Order(from, to, books, stocksForTimeSpanWithMargin)
扩展性问题
- 全量订单传入聚合根:面对1万+图书、日均1万+订单、覆盖未来2年的场景,数据量爆炸,完全扛不住
- 仅查指定图书的时隙订单:虽然数据量小了,但校验逻辑被迫移到数据库查询层,聚合根没法确认拿到的是完整数据,只能“盲目”基于传入的数据判断,存在校验漏判的风险
可行解决方案
1. 拆分聚合根,搞图书时隙库存专属聚合
把原来的BookOrder拆成两个核心聚合:
- BookSlotInventory:以「单本图书+单个30分钟时隙」为聚合根,专门维护这个图书在此时隙里的已预订数量、可用库存数
- BookOrder:只负责订单的创建、关联图书和时隙,校验逻辑完全依赖
BookSlotInventory的结果
执行逻辑:
- 用户发起预订时,先把目标图书涉及的所有时隙对应的
BookSlotInventory实例查出来 - 逐个时隙校验:
已预订数 + 本次预订数 ≤ 当前总库存(含刚补的新书) - 同时检查缓冲期:确保预订的起始时间和已有订单的结束时间间隔≥配置值
- 所有时隙都过审后,批量更新
BookSlotInventory的已预订数,再创建BookOrder
2. 数据库层预校验+分布式锁防超卖
如果暂时不想动聚合根结构,就用这个快速方案:
- 把校验逻辑封装到数据库查询里:写专门的SQL直接查目标图书在指定时隙范围内的最大已预订量,结合当前库存判断能不能订
-- 示例:查某图书在指定时隙区间内的最大已预订数 SELECT MAX(bo.quantity) FROM book_order bo JOIN book_order_slot bos ON bo.id = bos.order_id WHERE bo.book_id = ? AND bos.slot_time BETWEEN ? AND ? - 加分布式锁:针对要预订的「图书ID+时隙范围」加锁,避免并发下单导致的超卖
- 聚合根做二次确认:把数据库查出来的校验结果传给聚合根,聚合根只需要再检查缓冲期这类业务规则,不用处理全量订单
3. 缓存层优化查询性能
- 对高频查询的图书时隙库存做缓存,缓存键用
bookId_slotTime,值存已预订数 - 预订成功、库存补充时立刻更新对应缓存
- 缓存失效时从数据库拉最新数据,保证数据一致
总结
优先推荐拆分聚合根的方案,符合DDD的设计思路,把时隙库存的校验逻辑闭环在专属聚合里,既保证业务逻辑严谨,又能支撑大规模场景的性能。如果赶进度没法重构,就先上数据库预校验+分布式锁的方案,后续再慢慢调整。
内容的提问来源于stack exchange,提问作者Rasmond
相关产品推荐
相关产品推荐

