事件模型基础:事件的结构与分类

聊事件模型,咱们得先搞清楚一件事:事件到底是什么?

很多人觉得事件就是个消息,发出去就完事了。但我在项目中踩过不少坑,发现如果事件的结构定义不清楚,后面数据治理就是一团乱麻。今天咱们就把事件的结构和分类掰开揉碎了讲清楚。

事件的三层结构

一个完整的事件,我习惯把它拆成三层:事件头、事件体、事件元数据。这三层各司其职,缺一不可。

核心原则:事件头负责路由,事件体负责业务,元数据负责治理。三者分离,互不干扰。

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"
  }
}

你看,这个事件结构清晰,三层分离。事件头负责路由和追踪,事件体只放业务数据,元数据描述事件本身。这样设计,不管是消费、存储还是治理,都方便很多。

事件分类对比

分类 作用范围 消费者 典型场景 数据完整性要求
领域事件 同一限界上下文 同一服务内的其他聚合 订单创建后扣库存 低(可查询补充)
集成事件 跨限界上下文 其他服务或外部系统 订单状态同步到物流 高(需包含完整数据)
系统事件 基础设施层 监控、运维系统 磁盘告警、服务心跳 中(关注时效性)

事件结构关系图

下面这张图展示了事件的三层结构和三种分类之间的关系:

事件结构关系图 事件头 (Header) 事件体 (Body) 事件元数据 (Metadata) 完整事件 领域事件 集成事件 系统事件 说明: 事件头 + 事件体 + 事件元数据 构成一个完整事件。 领域事件、集成事件、系统事件是事件的三种分类,它们都遵循相同的三层结构。

实际项目中的选择策略

说了这么多,那在实际项目中怎么选?我分享几个经验:

  1. 同一个微服务内部:优先用领域事件。事件体可以精简,因为消费者可以直接查数据库。
  2. 不同微服务之间:用集成事件。事件体要完整,因为下游服务可能没有数据源。
  3. 基础设施监控:用系统事件。关注的是时效性和告警级别,数据量通常很大,要做好限流。

避坑指南:我曾经在一个项目里把集成事件当领域事件用,事件体只放了一个 ID。结果下游服务查不到数据,因为数据库分库了。从那以后,我定了一个规矩:跨服务的事件,必须包含消费者需要的所有数据。

嗯,事件的结构和分类就讲到这里。记住三层结构和三种分类,后面讲事件建模和治理的时候,这些基础会反复用到。