Add Milky note: [Milky] 为您整理《第08课:DDD领域事件:系统解耦神技》笔记 | BV1PFVj6sEi2

This commit is contained in:
liulu
2026-06-20 03:09:11 +08:00
parent 842022c23b
commit 24a71523e4

View File

@@ -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<OutboxMessage> 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、外部服务
│ │
└─────────────────────────────────────────────────────────────────┘
```
**核心理念**
- 把数据库、用户界面等**技术实现细节**看作应用的"郊区"
- 让**最宝贵的纯粹业务逻辑**住在最核心的"市中心"
- 这样业务逻辑可以独立于技术细节进行测试和演化
---
## 九、观众反馈补充
| 反馈内容 | 备注 |
|----------|------|
| "这种可读性不好吧" | 可能是指代码示例的格式,建议实际项目中使用更规范的代码格式 |
---
> **下节预告**:六边形架构思想的详细揭秘,如何组织应用程序结构最合理。