在DDD中使用SQL维护业务不变量的方案探讨
关于DDD并发下业务不变量校验的方案分析
针对你提到的「客户订单总数不得超过3单」的并发校验问题,直接拆解各方案的优劣及更优实现:
方案一:带前置校验的原子SQL插入
核心是把校验+插入做成数据库层面的原子操作,用单条SQL实现:
INSERT INTO orders (customer_id, order_no, ...) SELECT ?, ?, ... WHERE (SELECT COUNT(*) FROM orders WHERE customer_id = ? AND status != 'CANCELLED') < 3;
执行后若受影响行数为0,说明校验不通过,直接返回业务错误。
优点
- 完全依赖数据库原子性,无需额外并发控制逻辑,实现简单。
- 从根源避免竞态条件,单条SQL在数据库内部是原子执行的。
缺点
- 业务规则硬编码到SQL中,与领域模型(Customer聚合根)的逻辑脱节,违反DDD「业务逻辑内聚到领域层」的核心原则。
- 后续规则迭代(比如区分订单类型、排除已取消订单)会导致SQL臃肿难维护,且无法通过单元测试覆盖业务逻辑。
方案二:Customer聚合根加版本号(乐观锁)
回归DDD设计思路:让Customer聚合根维护「有效订单数」属性,addOrder方法内校验规则,通过乐观锁解决并发问题:
- 从数据库加载包含「订单数」「版本号」的Customer聚合根。
- 调用
customer.addOrder(),领域层校验订单数是否小于3。 - 在同一事务中,插入新订单,同时以「加载时的版本号」为条件更新Customer的订单数和版本号。
- 若更新失败(版本号不匹配),说明存在并发修改,触发重试逻辑(前端提示用户稍后再试或后台自动重试)。
优点
- 业务规则完全内聚在Customer聚合根中,符合DDD架构理念,便于单元测试和规则迭代。
- 乐观锁性能优于悲观锁,适合大多数高并发场景(只要冲突率不是极高)。
缺点
- 需要实现重试逻辑,增加了代码复杂度。
- 若并发冲突率极高,重试会导致性能下降,需额外做降级处理。
更优实现:聚合根乐观锁+事务原子性
基于方案二做DDD适配优化:
- 把订单创建逻辑封装在Customer聚合根的
addOrder方法中,直接生成Order实体(包含所有必要业务属性)。 - 事务中绑定两个操作:插入Order、更新Customer的订单数和版本号,确保原子性。
- 用数据库乐观锁(版本号)检测并发冲突,冲突时返回明确的业务错误,由上层决定重试策略。
如果数据库支持特殊锁机制(比如PostgreSQL的SELECT ... FOR UPDATE SKIP LOCKED),在冲突率极高的场景下,可考虑用悲观锁,但需严格控制锁的粒度,避免影响其他业务操作。
总结
若业务规则简单、对DDD架构要求不高,方案一可快速落地;但遵循DDD架构的项目中,方案二(乐观锁)是更合理的选择,它能保持领域模型的完整性,同时解决并发问题。追求极致DDD实践的话,推荐采用「聚合根乐观锁+事务原子性」的优化方案。
内容的提问来源于stack exchange,提问作者fernando1979
相关产品推荐
相关产品推荐

