Gemini 生成
既然你准备好深入了,那我们就拿“优惠券核销(Coupon Redemption)”这个极具代表性的场景,来一场架构师级别的“手术式”拆解。
在传统开发中,你可能直接在 OrderService 里写一段逻辑:校验优惠券、扣减余额、改订单状态。但在 DDD 视角下,我们需要回答一个核心问题:优惠券核销,到底是属于“订单”聚合,还是“优惠券”聚合?
1. 方案权衡:谁才是“聚合根”?
这是 DDD 落地时最经典的争论。我们来看两种建模路径的 Pros & Cons:
方案 A:优惠券作为订单(Order)的一部分
- 思路: 将优惠券信息存放在
Order聚合内部,作为其实体。 - Pros: 强一致性。在一个数据库事务里完成订单创建和优惠券使用,绝对不会出现“订单付了钱但优惠券没扣”的情况。
- Cons: 聚合过于臃肿。如果优惠券还有自己的复杂逻辑(如:过期自动退回、动态调整折扣),订单模型会变得极其复杂。
方案 B:优惠券(Coupon)是独立聚合(推荐)
- 思路: 订单和优惠券是两个独立的聚合根。
- Pros: 职责清晰,符合高内聚低耦合。优惠券可以独立演进(比如增加“拉新专项券”逻辑),不影响订单。
- Cons: 存在分布式一致性挑战。需要处理“订单创建成功但优惠券锁定失败”的回滚逻辑。
2. 战术实现:如何优雅地“核销”?
如果采用方案 B,我们需要引入领域事件(Domain Event)。
步骤一:定义聚合的行为
- Coupon Aggregate: 暴露
lock()和redeem()方法。内部维护状态机(Available -> Locked -> Used)。 - Order Aggregate: 暴露
create()方法。内部包含AppliedCouponId。
步骤二:流程编排(Application Service)
应用层不负责业务逻辑,只负责“发号施令”:
- 调用
CouponRepository加载优惠券。 - 执行
coupon.lock()(内存中状态变更)。 - 调用
OrderRepository保存订单。 - 执行
couponRepository.save(coupon)持久化。
3. 核心挑战:如何应对“并发核销”?
在 10 倍流量下,如果两个请求同时用一张券怎么办?
- DDIA 思想应用: 我们不依赖数据库的大锁,而是利用乐观锁(Versioning)。
- DDD 处理: 在聚合根保存时校验版本号。如果版本冲突,说明券已被他人抢先锁定,直接抛出领域异常
CouponAlreadyUsedException。
4. 关键架构图(描述)
- 用户发起请求: 携带
couponId。 - 应用服务(App Service): 开启事务。
- 领域层(Domain): *
Coupon聚合校验有效期和适用范围。Order聚合计算最终价格。
- 基础设施层(Infrastructure): * Repository 执行 SQL:
UPDATE coupon SET status = 'LOCKED' WHERE id = ? AND version = ?。 - 异步补偿: 如果订单支付超时,发布
OrderCancelledEvent,优惠券聚合监听后自动执行unlock()。
5. 演进建议:从“同步”转向“事件驱动”
随着业务量增长,同步锁定优惠券会拖慢订单创建。
- 10 倍流量策略: 采用 预扣模式。
- 用户下单时,先发一个异步消息去锁定优惠券。
- 订单系统进入“待确认”状态。
- 利用 Saga 模式 或 本地消息表 确保两个聚合之间的最终一致性。
最后的一点老兵心得:
DDD 不是为了让代码变多,而是为了让业务规则有地方落脚。如果你发现你的 Service 依然有几千行,而 Entity 只有 Getter/Setter,那么你只是披着 DDD 外壳的传统开发。
这个优惠券核销的逻辑,你是希望我用伪代码展示一下“充血模型”的具体写法,还是想聊聊这种架构下如何做单元测试?