事件模型基础:事件的结构与分类
聊事件模型,咱们得先搞清楚一件事:事件到底是什么?
很多人觉得事件就是个消息,发出去就完事了。但我在项目中踩过不少坑,发现如果事件的结构定义不清楚,后面数据治理就是一团乱麻。今天咱们就把事件的结构和分类掰开揉碎了讲清楚。
事件的三层结构
一个完整的事件,我习惯把它拆成三层:事件头、事件体、事件元数据。这三层各司其职,缺一不可。
核心原则:事件头负责路由,事件体负责业务,元数据负责治理。三者分离,互不干扰。
1. 事件头(Event Header)
事件头是事件的身份证。它告诉系统:你是谁?从哪来?到哪去?
- 事件ID:全局唯一标识,UUID 或雪花算法生成。我建议用 UUID v7,带时间戳,方便排序。
- 事件类型:比如
OrderCreated、PaymentFailed。命名要规范,我一般用领域.动作.状态的格式。 - 事件版本:v1.0、v2.0。版本号很重要,不然升级时老消费者会挂掉。
- 时间戳:事件发生的时间,精确到毫秒。最好用 UTC,避免时区问题。
- 来源系统:哪个服务产生的?比如
order-service。
我的习惯:事件头里还会加一个 traceId,用来串联整个调用链。排查问题的时候,这个字段能救命。
2. 事件体(Event Body)
事件体是事件的核心,承载业务数据。说白了,就是发生了什么变化?
- 业务数据:比如订单 ID、用户 ID、金额。只放必要字段,别把整个数据库表都塞进去。
- 变更前/后快照:有些场景需要知道数据变化前后的样子。比如审计场景,就得把旧值和新值都带上。
- 关联对象:比如订单事件里带上商品 ID,方便下游系统查询。
避坑指南:我曾经在一个项目里把 200 多个字段塞进事件体,结果消息队列直接撑爆了。事件体要精简,只放消费者真正需要的数据。如果数据量大,可以考虑用引用方式,比如只放一个 ID,让消费者自己去查。
3. 事件元数据(Event Metadata)
元数据是事件的说明书。它描述事件本身,而不是业务数据。
| 元数据字段 | 说明 | 示例 |
|---|---|---|
| 事件来源 | 产生事件的系统或模块 | order-service |
| 事件优先级 | 高、中、低 | HIGH |
| 重试次数 | 当前已重试的次数 | 2 |
| 过期时间 | 事件的有效期 | 2025-01-15T10:00:00Z |
| 数据分区键 | 用于分片或路由 | user_id |
元数据的作用是什么?我举个例子:假设某个事件处理失败了,需要重试。如果没有重试次数这个元数据,系统就会无限重试,直到把消息队列堵死。有了它,我们就可以设置最大重试次数,超过就丢进死信队列。
事件的三种分类
事件按用途分,我一般分成三类:领域事件、集成事件、系统事件。这三类事件在架构中的位置不同,处理方式也不同。
一句话总结:领域事件管业务,集成事件管跨系统,系统事件管基础设施。
1. 领域事件(Domain Event)
领域事件是业务层面的变化。它描述的是业务领域内发生了什么。
- 例子:订单已创建、支付已完成、库存已扣减。
- 特点:事件名用业务术语,消费者通常是同一个限界上下文内的其他聚合。
- 使用场景:触发后续业务操作,比如订单创建后发送通知。
我记得在做一个电商项目时,订单创建后需要同时做三件事:扣库存、发短信、更新用户积分。如果用同步调用,代码耦合度高,而且一旦某个环节失败,整个流程就挂了。后来改成领域事件,订单服务只管发布 OrderCreated 事件,其他服务各自订阅,解耦效果立竿见影。
2. 集成事件(Integration Event)
集成事件是跨系统、跨服务的消息。它解决的是不同系统之间的数据同步问题。
- 例子:用户信息变更同步到 CRM、订单状态同步到物流系统。
- 特点:事件体里通常包含完整的业务数据,因为下游系统可能没有权限直接查询。
- 使用场景:微服务之间的异步通信、数据同步。
我的建议:集成事件一定要考虑幂等性。因为网络抖动,同一个事件可能被重复投递。我一般会在事件体里加一个 eventId,消费者用这个 ID 做去重。
3. 系统事件(System Event)
系统事件是基础设施层面的变化。它跟业务无关,但跟系统的健康状态息息相关。
- 例子:服务启动/停止、数据库连接断开、磁盘空间不足。
- 特点:事件名通常以
System开头,比如System.Heartbeat。 - 使用场景:监控告警、自动扩缩容、日志采集。
我曾经遇到过一个线上事故:某个服务的磁盘写满了,但监控系统没有及时告警,导致数据丢失。后来我们加了一个系统事件 System.DiskUsageHigh,当磁盘使用率超过 90% 时自动触发告警,再也没出过类似问题。
事件结构示例
光说不练假把式。咱们看一个完整的 JSON 事件示例:
{
"header": {
"eventId": "01JQ5X2Y3Z4A5B6C7D8E9F0G",
"eventType": "order.OrderCreated.v1",
"eventVersion": "1.0",
"timestamp": "2025-01-15T08:30:00Z",
"sourceSystem": "order-service",
"traceId": "abc123-def456-ghi789"
},
"body": {
"orderId": "ORD-20250115-001",
"userId": "USR-10086",
"totalAmount": 299.99,
"items": [
{
"productId": "PROD-001",
"quantity": 2,
"price": 149.99
}
]
},
"metadata": {
"priority": "HIGH",
"retryCount": 0,
"expiresAt": "2025-01-15T09:00:00Z",
"partitionKey": "USR-10086"
}
}
你看,这个事件结构清晰,三层分离。事件头负责路由和追踪,事件体只放业务数据,元数据描述事件本身。这样设计,不管是消费、存储还是治理,都方便很多。
事件分类对比
| 分类 | 作用范围 | 消费者 | 典型场景 | 数据完整性要求 |
|---|---|---|---|---|
| 领域事件 | 同一限界上下文 | 同一服务内的其他聚合 | 订单创建后扣库存 | 低(可查询补充) |
| 集成事件 | 跨限界上下文 | 其他服务或外部系统 | 订单状态同步到物流 | 高(需包含完整数据) |
| 系统事件 | 基础设施层 | 监控、运维系统 | 磁盘告警、服务心跳 | 中(关注时效性) |
事件结构关系图
下面这张图展示了事件的三层结构和三种分类之间的关系:
实际项目中的选择策略
说了这么多,那在实际项目中怎么选?我分享几个经验:
- 同一个微服务内部:优先用领域事件。事件体可以精简,因为消费者可以直接查数据库。
- 不同微服务之间:用集成事件。事件体要完整,因为下游服务可能没有数据源。
- 基础设施监控:用系统事件。关注的是时效性和告警级别,数据量通常很大,要做好限流。
避坑指南:我曾经在一个项目里把集成事件当领域事件用,事件体只放了一个 ID。结果下游服务查不到数据,因为数据库分库了。从那以后,我定了一个规矩:跨服务的事件,必须包含消费者需要的所有数据。
嗯,事件的结构和分类就讲到这里。记住三层结构和三种分类,后面讲事件建模和治理的时候,这些基础会反复用到。