From 24a71523e455d00e136ff3aecdbd2ca4968082e4 Mon Sep 17 00:00:00 2001 From: liulu Date: Sat, 20 Jun 2026 03:09:11 +0800 Subject: [PATCH] =?UTF-8?q?Add=20Milky=20note:=20[Milky]=20=E4=B8=BA?= =?UTF-8?q?=E6=82=A8=E6=95=B4=E7=90=86=E3=80=8A=E7=AC=AC08=E8=AF=BE?= =?UTF-8?q?=EF=BC=9ADDD=E9=A2=86=E5=9F=9F=E4=BA=8B=E4=BB=B6=EF=BC=9A?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E8=A7=A3=E8=80=A6=E7=A5=9E=E6=8A=80=E3=80=8B?= =?UTF-8?q?=E7=AC=94=E8=AE=B0=20|=20BV1PFVj6sEi2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- InBox/_08__DDD____________BV1PFV_6_E_2___.__ | 542 +++++++++++++++++++ 1 file changed, 542 insertions(+) create mode 100644 InBox/_08__DDD____________BV1PFV_6_E_2___.__ diff --git a/InBox/_08__DDD____________BV1PFV_6_E_2___.__ b/InBox/_08__DDD____________BV1PFV_6_E_2___.__ new file mode 100644 index 0000000..5089a07 --- /dev/null +++ b/InBox/_08__DDD____________BV1PFV_6_E_2___.__ @@ -0,0 +1,542 @@ +# DDD领域事件:系统解耦神技 + +## 一、问题场景:串行业务的致命缺陷 + +### 典型错误写法 + +在日常开发中,很多工程师会写出这样的代码:一个核心业务后面跟着一大串同步操作。 + +```java +// 创建订单流程(错误示范) +public void createOrder() { + // 第一步:创建订单 - 成功 + Order order = orderRepository.save(new Order()); + + // 第二步:扣减库存 - 成功 + inventoryService.decreaseStock(order.getProductId()); + + // 第三步:发送短信 - 失败! + smsService.sendNotification(order.getUserId()); + + // 第四步:增加积分 - 被堵死 + pointsService.addPoints(order.getUserId()); + + // 第五步:通知商家 - 被堵死 + merchantService.notify(order.getMerchantId()); +} +``` + +### 问题分析 + +当第三步发送短信失败时,会导致的后果: + +| 步骤 | 结果 | 影响 | +|------|------|------| +| 创建订单 | ✓ 成功 | 订单已入库 | +| 扣减库存 | ✓ 成功 | 库存已扣 | +| 发送短信 | ✗ 失败 | 事务回滚 | +| 增加积分 | 被堵死 | 无法执行 | +| 通知商家 | 被堵死 | 无法执行 | + +**最终结果**:整个订单被迫回滚,用户看到"下单失败"。就因为短信服务商抖动了一下,一单生意就黄了。 + +## 二、核心思想转变:从铁链到广播 + +### 两种思维模式对比 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 铁链子思维(串行耦合) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ [创建订单] → [扣减库存] → [发短信] → [加积分] → [通知商家] │ +│ ↓ ↓ ↓ ↓ ↓ │ +│ └────────────┴────────────┴────────────┴───────────┘ │ +│ ↓ │ +│ 任一环节失败 = 整条链断裂 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ 广播站思维(发布订阅) │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ [订单已创建] │ +│ ╱ ╲ ╲ │ +│ ╱ ╲ ╲ │ +│ ↓ ↓ ↓ │ +│ [短信服务] [积分服务] [商家服务] │ +│ (收音机) (收音机) (收音机) │ +│ ↓ ↓ ↓ │ +│ 发短信 加积分 通知商家 │ +│ │ +│ 谁也不影响谁,互相独立,可单独扩展 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 核心理念 + +> **核心业务只管把自己最核心的事做完,剩下的让它们自己去听消息,然后干活。** + +## 三、领域事件(Domain Event)定义 + +### 什么是领域事件 + +**定义**:领域事件描述的是一件已经发生的事,它是一个**事实陈述**,告诉系统"发生了什么",而不是"要去做什么"。 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 领域事件 = 已发生的事实 = 不可更改的历史记录 │ +│ │ +│ ✗ 不是命令:"去支付订单" (orderPay - 将来时) │ +│ ✓ 是事实:"订单已支付" (orderPaid - 过去时) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 命名规范:必须用过去时态 + +这个命名习惯背后是整个思维模式的巨大转变: + +| 类型 | 错误写法(命令式) | 正确写法(事实式) | +|------|-------------------|-------------------| +| 订单 | `orderPay` | `orderPaid` | +| 用户 | `userRegister` | `userRegistered` | +| 支付 | `doPayment` | `paymentCompleted` | +| 发货 | `shipOrder` | `orderShipped` | + +### 领域事件的比喻 + +可以把领域事件想象成一封信: + +``` +┌──────────────────────────────────────────────┐ +│ 📧 领域事件信件 │ +├──────────────────────────────────────────────┤ +│ │ +│ 核心业务忙活完了,写了一封信: │ +│ │ +│ "嘿,我这完事儿了,你们看着办吧。" │ +│ │ +│ 然后把信往邮桶里一扔,就啥也不管了。 │ +│ │ +│ 他不关心: │ +│ ✗ 谁会收到这封信 │ +│ ✗ 收到信的人会拿他干嘛 │ +│ │ +│ 他只关心: │ +│ ✓ 把这封信发出去 │ +│ │ +└──────────────────────────────────────────────┘ +``` + +## 四、发布订阅模式实现 + +### 三步走流程 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 发布订阅模式三步走 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 第一步 │ │ 第二步 │ │ 第三步 │ │ +│ │ 发 生 │ → │ 送 信 │ → │ 干 活 │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ 在内存中创建 事务提交成功后 订阅者收到事件 │ +│ event对象 消息总线发出 各自执行任务 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 具体代码实现 + +#### 第一步:在核心业务中发布事件 + +```java +// 订单服务 - 只负责核心逻辑 +public class OrderService { + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private DomainEventPublisher eventPublisher; // 事件发布器 + + @Transactional + public Order createOrder(OrderCreateCommand command) { + // 核心业务:创建订单 + Order order = new Order(); + order.setUserId(command.getUserId()); + order.setProductId(command.getProductId()); + order.setAmount(command.getAmount()); + + // 核心业务:扣减库存(这个也是核心) + inventoryService.decreaseStock(command.getProductId()); + + // 保存订单 + order = orderRepository.save(order); + + // 发布领域事件 - 订单已创建 + OrderCreatedEvent event = new OrderCreatedEvent(order); + eventPublisher.publish(event); + + return order; + } +} +``` + +#### 第二步:事务提交后发送事件 + +```java +// 事件发布器实现 +@Component +public class DomainEventPublisherImpl implements DomainEventPublisher { + + @Autowired + private ApplicationEventPublisher springPublisher; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleAfterCommit(DomainEvent event) { + // 只有事务成功提交后,才真正发送事件 + springPublisher.publishEvent(event); + } +} +``` + +#### 第三步:订阅者各自干活 + +```java +// 短信服务 - 订阅者 +@Component +public class SmsNotificationService { + + @EventListener + public void handleOrderCreated(OrderCreatedEvent event) { + // 收到订单创建事件,发短信 + smsService.send(event.getOrder().getUserId(), "订单创建成功"); + } +} + +// 积分服务 - 订阅者 +@Component +public class PointsService { + + @EventListener + public void handleOrderCreated(OrderCreatedEvent event) { + // 收到订单创建事件,增加积分 + addPoints(event.getOrder().getUserId(), 100); + } +} + +// 商家服务 - 订阅者 +@Component +public class MerchantService { + + @EventListener + public void handleOrderCreated(OrderCreatedEvent event) { + // 收到订单创建事件,通知商家备货 + notifyMerchant(event.getOrder().getMerchantId()); + } +} +``` + +## 五、解耦带来的巨大好处 + +### 订单服务代码变化 + +**重构前**(订单服务知道所有下游服务): + +```java +public void createOrder() { + // 创建订单... + // 扣减库存... + smsService.send(); // 订单服务需要知道短信服务 + pointsService.add(); // 订单服务需要知道积分服务 + merchantService.notify(); // 订单服务需要知道商家服务 +} +``` + +**重构后**(订单服务只关心自己): + +```java +public void createOrder() { + // 创建订单... ✓ + // 扣减库存... ✓ + // 发布事件... ✓ + // 就这么简单! +} +``` + +### 扩展新功能示例 + +**场景**:产品经理要求增加"发送欢迎邮件"功能 + +| 传统方式 | 领域事件方式 | +|----------|--------------| +| 修改订单服务代码 | 不碰订单服务 | +| 添加邮件发送逻辑 | 开发新的邮件服务 | +| 回归测试所有功能 | 单独测试邮件服务 | +| 风险:可能引入bug | 完全解耦,安全 | + +```java +// 新增邮件服务 - 完全独立,不碰任何老代码 +@Component +public class WelcomeEmailService { + + @EventListener + public void handleOrderCreated(OrderCreatedEvent event) { + sendWelcomeEmail(event.getOrder().getUserId()); + } +} +``` + +## 六、消息丢失问题与解决方案 + +### 致命问题 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 消息丢失场景 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. 业务数据(订单)成功写入数据库 │ +│ 2. 事务成功提交 │ +│ 3. 程序准备发送事件到消息队列 │ +│ 4. 💥 服务器断电! │ +│ 5. 事件永远丢失了 │ +│ │ +│ 结果:订单已创建,但没有人知道,短信没发、积分没加... │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 解决方案:发件箱模式(Outbox Pattern) + +#### 核心思想 + +> **不要在业务事务里直接调用外部消息队列,而是把业务数据和事件消息打包放在同一个事务里。** + +#### 表结构设计 + +```sql +-- 订单表 +CREATE TABLE orders ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + product_id BIGINT NOT NULL, + amount DECIMAL(10,2), + status VARCHAR(20), + created_at TIMESTAMP +); + +-- 发件箱表 +CREATE TABLE outbox ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + aggregate_type VARCHAR(50), -- 聚合根类型,如 'Order' + aggregate_id BIGINT, -- 聚合根ID + event_type VARCHAR(100), -- 事件类型,如 'OrderCreatedEvent' + payload JSON, -- 事件内容(JSON格式) + status VARCHAR(20) DEFAULT 'PENDING', -- PENDING / SENT + created_at TIMESTAMP, + sent_at TIMESTAMP +); +``` + +#### 实现代码 + +```java +// 使用发件箱模式的订单服务 +@Service +public class OrderServiceWithOutbox { + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private OutboxRepository outboxRepository; + + @Transactional + public Order createOrder(OrderCreateCommand command) { + // 1. 创建并保存订单 + Order order = new Order(); + order.setUserId(command.getUserId()); + order.setProductId(command.getProductId()); + order = orderRepository.save(order); + + // 2. 扣减库存 + inventoryService.decreaseStock(command.getProductId()); + + // 3. 创建事件消息并写入发件箱表 + // 注意:和订单表在同一个事务里! + OrderCreatedEvent event = new OrderCreatedEvent(order); + OutboxMessage message = new OutboxMessage(); + message.setAggregateType("Order"); + message.setAggregateId(order.getId()); + message.setEventType("OrderCreatedEvent"); + message.setPayload(JSON.toJSONString(event)); + message.setStatus("PENDING"); + outboxRepository.save(message); + + // 4. 事务提交时,订单和消息都会被写入数据库 + return order; + } +} +``` + +#### 后台进程可靠发送 + +```java +// 后台任务:扫描发件箱并发送消息 +@Component +public class OutboxProcessor { + + @Autowired + private OutboxRepository outboxRepository; + + @Autowired + private MessageBroker messageBroker; // Kafka/RabbitMQ等 + + @Scheduled(fixedDelay = 100) // 每100毫秒执行一次 + public void processOutbox() { + // 1. 取出所有待发送的消息 + List messages = outboxRepository + .findByStatus("PENDING") + .stream() + .limit(100) // 批量处理,最多100条 + .collect(Collectors.toList()); + + for (OutboxMessage message : messages) { + try { + // 2. 发送到消息队列 + messageBroker.send(message.getTopic(), message.getPayload()); + + // 3. 更新状态为已发送 + message.setStatus("SENT"); + message.setSentAt(LocalDateTime.now()); + outboxRepository.save(message); + + } catch (Exception e) { + // 发送失败,保持PENDING状态,下次重试 + log.error("发送消息失败: {}", message.getId(), e); + } + } + } +} +``` + +#### 原子性保证 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 原子性保证 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────┐ │ +│ │ 数据库事务(原子操作) │ │ +│ │ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ 订单表 │ │ 发件箱表 │ │ │ +│ │ │ (orders) │ │ (outbox) │ │ │ +│ │ └─────────────┘ └─────────────┘ │ │ +│ │ ✓ 全部成功 或 ✗ 全部回滚 │ │ +│ └─────────────────────────────────────────────┘ │ +│ │ +│ 绝对不会出现:订单入库了,但事件没写入 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 最终一致性 + +发件箱模式带来的取舍: + +| 特性 | 说明 | +|------|------| +| **一致性类型** | 最终一致性(Eventually Consistent) | +| **即时性** | 发短信、加积分不会瞬间完成 | +| **保证** | 系统保证这些操作最终一定会被处理 | +| **延迟** | 通常在毫秒到秒级,取决于后台进程频率 | + +## 七、核心要点总结 + +### 四大黄金法则 + +| 序号 | 要点 | 说明 | +|------|------|------| +| 1 | **事件命名用过去时** | 因为它代表一个既定事实,不是命令 | +| 2 | **发布订阅模式** | 让主业务和次要业务彻底分家 | +| 3 | **真正的解耦** | 扩展功能时,只需增加新的订阅者,不动老代码 | +| 4 | **关注点分离** | 核心代码保持纯粹,只关心业务规则 | + +### 思维模式对比 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 传统方式 vs 领域事件 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 传统方式: │ +│ ┌─────────┐ │ +│ │ 订单服务 │ → 短信服务 → 积分服务 → 商家服务 → ... │ +│ └─────────┘ ↓ ↓ ↓ │ +│ 耦合 耦合 耦合 │ +│ 一改全改,风险极高 │ +│ │ +│ ───────────────────────────────────────────────────────────── │ +│ │ +│ 领域事件: │ +│ ┌─────────┐ 发布 │ +│ │ 订单服务 │ ───────────→ [订单已创建事件] │ +│ └─────────┘ ↙ ↘ ↘ │ +│ 短信 积分 商家 │ +│ 订阅 订阅 订阅 │ +│ 互不干扰,可独立扩展 │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 八、架构思想延伸 + +### 六边形架构(端口与适配器) + +视频最后引出了一个重要的架构思想: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 六边形架构 │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────┐ │ +│ │ 核心业务逻辑 │ ← 市中心 │ +│ │ (Domain) │ 最宝贵的部分 │ +│ └───────────────┘ │ +│ ↙ ↘ ↘ │ +│ ┌────────┐ ┌────────┐ ┌────────┐ │ +│ │ 端口1 │ │ 端口2 │ │ 端口3 │ │ +│ └────────┘ └────────┘ └────────┘ │ +│ │ +│ ═══════════════════════════════════════════════════════════ │ +│ │ +│ 市中心:业务逻辑 │ +│ 郊区:技术实现(数据库、UI、外部服务) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**核心理念**: +- 把数据库、用户界面等**技术实现细节**看作应用的"郊区" +- 让**最宝贵的纯粹业务逻辑**住在最核心的"市中心" +- 这样业务逻辑可以独立于技术细节进行测试和演化 + +--- + +## 九、观众反馈补充 + +| 反馈内容 | 备注 | +|----------|------| +| "这种可读性不好吧" | 可能是指代码示例的格式,建议实际项目中使用更规范的代码格式 | + +--- + +> **下节预告**:六边形架构思想的详细揭秘,如何组织应用程序结构最合理。 \ No newline at end of file