一、事件驱动架构概述
事件驱动架构(Event-Driven Architecture,EDA)是一种围绕「事件」构建软件系统的架构风格。在 EDA 中,系统的各个组件通过异步事件消息进行通信,而非直接调用彼此的 API。这种解耦带来了弹性、可扩展性和可观测性的显著提升,但也引入了最终一致性、事件顺序和幂等性等新的挑战。
1.1 请求驱动 vs 事件驱动
传统的请求驱动(Request-Driven)架构中,服务 A 调用服务 B 的 REST API,等待响应后继续处理。这种方式简单直观,但产生了紧耦合:服务 A 必须知道服务 B 的地址、接口和可用性,服务 B 的延迟或故障直接影响服务 A。
事件驱动架构中,服务 A 完成业务操作后发布一个事件(如 OrderCreated 事件)到消息队列,然后立即返回。服务 B 作为事件的消费者,在方便的时候处理这个事件。服务 A 和服务 B 不再直接依赖彼此的存在和响应速度,而是通过事件总线间接协作。
请求驱动模式:
用户 → 订单服务 →(HTTP)→ 库存服务 →(HTTP)→ 支付服务
(每个调用都是阻塞的同步请求)
事件驱动模式:
用户 → 订单服务 ──→ 发布 OrderCreated 事件 ──→ Kafka
↓ ↓
库存服务(订阅) 支付服务(订阅)
──检查库存←──────────────→扣款──
↓ ↓
发布 StockReserved 发布 PaymentCompleted
事件驱动的核心优势:
- 解耦:发布者不关心谁消费事件,消费者不关心事件来源
- 弹性:消费者故障不影响发布者,消息在队列中持久化等待恢复
- 可扩展性:增加消费者实例即可线性提升处理能力
- 可观测性:每个事件都是系统行为的审计日志
1.2 同步 vs 异步的边界
并非所有场景都适合事件驱动。同步调用适合需要立即确认结果、事务性强、低延迟要求的场景(如用户登录验证)。异步事件适合最终一致性可接受、需要解耦、流量削峰的场景(如订单处理、邮件发送)。
实践中,现代微服务通常采用混合模式:核心链路使用同步 API 保证即时反馈,副作用处理使用异步事件。例如用户下单时,订单服务同步返回订单创建结果,同时异步触发库存扣减、邮件通知、积分计算等操作。
二、Event Sourcing 事件溯源
事件溯源是一种将应用状态存储为事件序列的持久化模式。与传统 CRUD(直接更新数据库当前状态)不同,事件溯源只追加不可变事件,通过重演事件序列重建任意时刻的状态。
2.1 传统 CRUD vs 事件溯源
| 维度 | CRUD | Event Sourcing |
|---|---|---|
| 存储内容 | 当前状态 | 状态变更事件序列 |
| 更新操作 | UPDATE/DELETE | 仅 APPEND |
| 历史追溯 | 无(或需审计表) | 完整历史天然存在 |
| 并发冲突 | 乐观锁/悲观锁 | 基于版本号的并发控制 |
| 查询复杂度 | 简单 | 需要投影(Projection) |
| 存储空间 | 小 | 大(累积所有历史) |
| 调试难度 | 中等 | 低(可重演任意时刻) |
2.2 Event Store 设计
Event Store 是事件溯源系统的核心持久化组件,通常基于专门的事件数据库(如 EventStoreDB)或 Kafka 等日志系统实现。每个聚合(Aggregate)实例有一条独立的事件流:
// 订单聚合的事件流示例
type OrderEvent interface {
EventType() string
EventVersion() int
OccurredAt() time.Time
}
type OrderCreated struct {
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
Amount float64 `json:"amount"`
Items []Item `json:"items"`
Timestamp time.Time `json:"timestamp"`
}
type OrderPaid struct {
OrderID string `json:"order_id"`
PaymentID string `json:"payment_id"`
PaidAt time.Time `json:"paid_at"`
}
type OrderShipped struct {
OrderID string `json:"order_id"`
Tracking string `json:"tracking_number"`
ShippedAt time.Time `json:"shipped_at"`
}
Event Store 的存储格式:
{
"stream_id": "order-1001",
"event_id": "evt-uuid-001",
"event_type": "OrderCreated",
"event_version": 1,
"data": {
"order_id": "1001",
"user_id": "42",
"amount": 299.99,
"items": [...]
},
"metadata": {
"correlation_id": "req-uuid",
"causation_id": null,
"timestamp": "2026-08-17T10:00:00Z"
},
"position": 1
}
2.3 投影重建(Projection)
由于事件存储不适合直接查询(需要遍历全量事件),系统通过**投影(Projection)**将事件流转换为查询优化的视图(Read Model)。投影是事件处理器,监听 Event Store 的变更并更新物化视图:
// 订单汇总投影处理器
type OrderSummaryProjection struct {
readDB *sql.DB
}
func (p *OrderSummaryProjection) HandleEvent(event OrderEvent) error {
switch e := event.(type) {
case *OrderCreated:
_, err := p.readDB.Exec(
"INSERT INTO order_summary (id, user_id, amount, status) VALUES (?, ?, ?, ?)",
e.OrderID, e.UserID, e.Amount, "pending",
)
return err
case *OrderPaid:
_, err := p.readDB.Exec(
"UPDATE order_summary SET status = ?, paid_at = ? WHERE id = ?",
"paid", e.PaidAt, e.OrderID,
)
return err
case *OrderShipped:
_, err := p.readDB.Exec(
"UPDATE order_summary SET status = ?, tracking = ? WHERE id = ?",
"shipped", e.Tracking, e.OrderID,
)
return err
}
return nil
}
投影可以构建多个视图服务不同的查询场景:订单汇总视图、用户订单列表视图、运营统计视图,每个视图独立演进。
2.4 快照优化
对于事件数量巨大的聚合(如运行多年的账户),每次都从第一个事件重演会导致性能问题。可以通过**快照(Snapshot)**机制定期保存聚合状态:
// 每 100 个事件生成一个快照
if len(events)%100 == 0 {
snapshot := aggregate.State()
snapshotStore.Save(streamID, len(events), snapshot)
}
// 加载时:读取最新快照 + 快照后事件
snapshot, lastVersion := snapshotStore.Load(streamID)
aggregate.Restore(snapshot)
events := eventStore.LoadAfter(streamID, lastVersion)
for _, event := range events {
aggregate.Apply(event)
}
三、CQRS 命令查询职责分离
CQRS(Command Query Responsibility Segregation)将系统的读写模型分离为独立的两个部分:
- 命令模型(Command Model):处理写操作,执行业务规则,生成领域事件
- 查询模型(Query Model):处理读操作,优化查询性能,数据源是投影视图
3.1 为什么需要 CQRS
传统 CRUD 系统中,同一张数据表既要支持复杂的事务写入(涉及多表关联、外键约束),又要支持各种查询模式(分页、全文搜索、统计聚合)。这些需求在 Schema 设计上是冲突的:
- 写入优化需要范式化(减少冗余、保证一致性)
- 查询优化需要反范式化(减少 JOIN、预计算聚合)
CQRS 通过分离读写模型,让命令侧专注于事务完整性,让查询侧专注于查询效率。
3.2 CQRS 与事件溯源的关系
CQRS 可以与事件溯源结合,也可以独立使用。在实践中,两者通常一起出现:
- 命令侧:接收命令 → 验证 → 修改聚合状态 → 生成事件 → 存入 Event Store
- 查询侧:读取投影视图 → 返回查询结果
客户端 ──→ API Gateway
│
┌────────┴────────┐
↓ ↓
Command Side Query Side
(写模型) (读模型)
│ │
Event Store ←── Projection ──→ Read DB
│ │
└── 领域事件 ──→ Kafka ───────┘
3.3 最终一致性
CQRS 系统的读写模型一致性不是立即的,而是最终一致的(Eventual Consistency)。命令处理成功后,查询模型可能需要几毫秒到几秒才能反映最新状态。这对用户体验设计提出了新要求:
- 乐观更新:前端在发送命令后立即更新本地 UI,不等待查询模型同步
- 读写分离 UI:写操作返回操作结果摘要,读操作从查询模型获取完整详情
- 版本检测:查询结果包含版本号,若版本落后于预期则刷新或提示
3.4 命令设计最佳实践
// 命令结构
type CreateOrderCommand struct {
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
Items []Item `json:"items"`
}
// 命令处理器
type CreateOrderHandler struct {
eventStore EventStore
}
func (h *CreateOrderHandler) Handle(cmd CreateOrderCommand) error {
// 1. 加载聚合(或创建新聚合)
order, err := h.eventStore.Load(cmd.OrderID)
if err != nil {
return err
}
// 2. 执行业务规则
if err := order.Create(cmd.UserID, cmd.Items); err != nil {
return err // 业务规则验证失败
}
// 3. 保存未提交事件到 Event Store
return h.eventStore.Save(order.UncommittedEvents())
}
命令处理器的黄金法则:
- 一个命令只修改一个聚合(保证单聚合事务边界)
- 命令处理器不直接调用外部服务(通过事件委托)
- 命令执行结果通过事件异步传播
四、Outbox 模式
事件驱动系统中的经典难题是事务边界问题:在数据库事务中更新业务数据后,如何确保对应的事件消息可靠发布到消息队列?如果采用"先写数据库再发消息"的顺序,消息可能因网络故障丢失;如果"先发消息再写数据库",消息可能成为幽灵消息(数据库回滚但消息已发出)。
Outbox 模式通过在同一个数据库事务中同时写入业务数据和事件记录,解决了这个难题。
4.1 Outbox 表设计
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_type VARCHAR(100) NOT NULL,
aggregate_id VARCHAR(100) NOT NULL,
event_type VARCHAR(100) NOT NULL,
event_payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
published_at TIMESTAMPTZ,
published BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_outbox_unpublished ON outbox(published, id);
4.2 事务性写入
func (s *OrderService) CreateOrder(ctx context.Context, cmd CreateOrderCommand) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// 1. 写入业务数据
_, err = tx.ExecContext(ctx,
"INSERT INTO orders (id, user_id, amount, status) VALUES ($1, $2, $3, $4)",
cmd.OrderID, cmd.UserID, cmd.TotalAmount, "pending",
)
if err != nil {
return err
}
// 2. 在同一事务中写入 Outbox 记录
eventPayload, _ := json.Marshal(OrderCreatedEvent{
OrderID: cmd.OrderID,
UserID: cmd.UserID,
Amount: cmd.TotalAmount,
})
_, err = tx.ExecContext(ctx,
"INSERT INTO outbox (aggregate_type, aggregate_id, event_type, event_payload) VALUES ($1, $2, $3, $4)",
"Order", cmd.OrderID, "OrderCreated", eventPayload,
)
if err != nil {
return err
}
// 3. 提交事务:业务数据和 Outbox 记录原子性保证
return tx.Commit()
}
4.3 Relay 发布器
单独的 Relay 进程通过 CDC(如 Debezium)或轮询 Outbox 表,将未发布事件转发到 Kafka:
// Outbox Relay(轮询模式)
type OutboxRelay struct {
db *sql.DB
kafka *kafka.Writer
pollInterval time.Duration
}
func (r *OutboxRelay) Run(ctx context.Context) {
ticker := time.NewTicker(r.pollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.pollAndPublish(ctx)
}
}
}
func (r *OutboxRelay) pollAndPublish(ctx context.Context) error {
rows, err := r.db.QueryContext(ctx,
"SELECT id, aggregate_type, aggregate_id, event_type, event_payload FROM outbox WHERE published = FALSE ORDER BY id LIMIT 100",
)
if err != nil {
return err
}
defer rows.Close()
var ids []int64
for rows.Next() {
var id int64
var aggType, aggID, eventType string
var payload []byte
rows.Scan(&id, &aggType, &aggID, &eventType, &payload)
// 发布到 Kafka
err := r.kafka.WriteMessages(ctx, kafka.Message{
Key: []byte(aggID),
Value: payload,
Headers: []kafka.Header{
{Key: "event_type", Value: []byte(eventType)},
{Key: "aggregate_type", Value: []byte(aggType)},
},
})
if err != nil {
return err
}
ids = append(ids, id)
}
// 批量标记为已发布
if len(ids) > 0 {
_, err = r.db.ExecContext(ctx,
"UPDATE outbox SET published = TRUE, published_at = NOW() WHERE id = ANY($1)",
pq.Array(ids),
)
}
return err
}
4.4 Outbox vs 原子性发布
| 方案 | 一致性保证 | 复杂度 | 性能影响 | 推荐场景 |
|---|---|---|---|---|
| Outbox + CDC | 强(事务内写入) | 中 | 低 | 生产环境首选 |
| Outbox + 轮询 | 强 | 低 | 中等 | 无 CDC 支持的数据库 |
| 原子性发布(Kafka TX) | 最终 | 高 | 高 | Kafka 事务 Producer |
| 纯内存缓冲 | 弱 | 低 | 低 | 可接受丢消息的场景 |
五、Saga 分布式事务模式
微服务架构中,业务操作往往涉及多个服务的协同。Saga 模式通过将长事务拆分为一系列本地事务,并通过补偿操作处理失败,解决了分布式事务的难题。
5.1 Saga 的核心概念
一个 Saga 是由多个步骤组成的业务流程,每个步骤对应一个服务的本地事务。如果某个步骤失败,Saga 会执行之前步骤的补偿操作(Compensating Transaction),将系统回滚到一致状态。
Saga 的特点:
- 无全局锁:每个服务只锁定自己的资源,避免分布式死锁
- 最终一致性:Saga 期间系统处于中间状态,完成后达到最终一致
- 补偿必须成功:补偿操作本身必须幂等且不会失败(或通过人工介入)
5.2 编排 Saga(Choreography)
编排 Saga 中,每个服务完成本地事务后发布事件,由事件驱动流程自动推进:
订单服务 库存服务 支付服务 物流服务
│ │ │ │
├──创建订单───────────┼────────────────────┼────────────────────┤
├──发布 OrderCreated ─→│ │ │
│ ├──检查库存 │ │
│ ├──发布 StockReserved ─→│ │
│ │ ├──扣款 │
│ │ ├──发布 PaymentDone ───→│
│ │ │ ├──创建运单
│ │ │ ├──发布 OrderShipped
编排 Saga 的优点是完全去中心化,新增服务自动融入流程。缺点是流程逻辑分散在各个服务中,难以全局理解和调试,且可能出现循环依赖。
5.3 协调 Saga(Orchestration)
协调 Saga 引入一个中央协调器(Orchestrator),由它统一控制流程推进:
// Saga Orchestrator
type OrderSagaOrchestrator struct {
orderClient OrderClient
inventoryClient InventoryClient
paymentClient PaymentClient
logisticsClient LogisticsClient
}
func (o *OrderSagaOrchestrator) Execute(ctx context.Context, order Order) error {
saga := NewSaga(order.ID)
// 步骤 1:创建订单
if err := saga.Step(ctx, func() error {
return o.orderClient.CreateOrder(ctx, order)
}, func() error {
return o.orderClient.CancelOrder(ctx, order.ID)
}); err != nil {
return saga.Compensate(ctx)
}
// 步骤 2:预留库存
if err := saga.Step(ctx, func() error {
return o.inventoryClient.ReserveStock(ctx, order.Items)
}, func() error {
return o.inventoryClient.ReleaseStock(ctx, order.Items)
}); err != nil {
return saga.Compensate(ctx)
}
// 步骤 3:执行支付
if err := saga.Step(ctx, func() error {
return o.paymentClient.Charge(ctx, order.Payment)
}, func() error {
return o.paymentClient.Refund(ctx, order.Payment)
}); err != nil {
return saga.Compensate(ctx)
}
// 步骤 4:创建运单
if err := saga.Step(ctx, func() error {
return o.logisticsClient.CreateShipment(ctx, order)
}, nil); err != nil {
return saga.Compensate(ctx)
}
return nil
}
协调 Saga 的优点是流程逻辑集中、易于理解和测试、支持复杂流程控制(分支、超时、重试)。缺点是引入了单点(协调器),需要保证协调器自身的高可用。
5.4 编排 vs 协调对比
| 维度 | 编排 Saga | 协调 Saga |
|---|---|---|
| 流程定义 | 分散在各服务 | 集中在协调器 |
| 耦合度 | 服务间通过事件间接耦合 | 服务间无直接耦合 |
| 可测试性 | 困难(需要完整环境) | 容易(Mock 服务调用) |
| 可观测性 | 需要分布式追踪 | 天然集中日志 |
| 循环依赖风险 | 高 | 低 |
| 新增步骤 | 修改事件消费者即可 | 修改协调器逻辑 |
| 适用场景 | 简单线性流程 | 复杂分支流程 |
六、Kafka 在事件驱动架构中的角色
Kafka 在事件驱动架构中扮演三重角色:
6.1 Event Store(事件存储)
Kafka 的日志模型天然适合作为 Event Store:
- 不可变日志:消息一旦写入不可修改,与事件溯源的不可变事件一致
- 顺序保证:分区内的消息严格有序,保证同一聚合的事件顺序
- 持久化存储:通过 retention 配置可永久保留事件(或保留数年)
- 时间旅行:Consumer 可从任意 offset 开始消费,重演历史事件
使用 Kafka 作为 Event Store 时,每个聚合对应一个 Kafka Topic Partition(或 Key 分区):
# 订单事件 Topic,按 order_id 分区保证顺序
orders-events (6 partitions)
Key: order_id (hash)
Value: Avro/Protobuf 编码的领域事件
6.2 事件总线(Event Bus)
Kafka 作为系统间异步通信的消息总线:
- 发布-订阅:多个服务订阅同一 Topic,实现一对多事件传播
- 持久化缓冲:即使消费者短暂宕机,消息也不会丢失
- 背压处理:Consumer 按需消费,避免生产者压垮消费者
- 事件广播:不同业务域通过各自的 Topic 隔离,同时保持松耦合
6.3 Saga 协调器
Kafka 也可作为 Saga 协调器的底层通信机制:
- 协调器将 Saga 状态机持久化到 Kafka(避免协调器故障导致 Saga 丢失)
- 协调器通过 Kafka 向各服务发送命令/补偿指令
- 各服务通过 Kafka 回复执行结果
七、实战:电商订单 Saga
本节实现一个完整的订单处理 Saga(协调模式),使用 Kafka 作为通信层。
7.1 领域事件定义
// events.proto
syntax = "proto3";
message OrderCreated {
string order_id = 1;
string user_id = 2;
repeated OrderItem items = 3;
double total_amount = 4;
}
message StockReserved {
string order_id = 1;
bool success = 2;
string failure_reason = 3;
}
message PaymentCompleted {
string order_id = 1;
string payment_id = 2;
bool success = 3;
}
message ShipmentCreated {
string order_id = 1;
string tracking_number = 2;
}
// Saga 协调命令
message SagaCommand {
string saga_id = 1;
string step = 2;
oneof payload {
CreateOrderCmd create_order = 3;
ReserveStockCmd reserve_stock = 4;
ProcessPaymentCmd process_payment = 5;
CreateShipmentCmd create_shipment = 6;
}
}
7.2 协调器实现
package saga
type OrderSaga struct {
sagaID string
orderID string
state SagaState
steps []SagaStep
}
type SagaState int
const (
SagaPending SagaState = iota
SagaOrderCreated
SagaStockReserved
SagaPaymentCompleted
SagaShipmentCreated
SagaCompleted
SagaCompensating
SagaFailed
)
type SagaOrchestrator struct {
kafkaWriter *kafka.Writer
kafkaReader *kafka.Reader
stateStore StateStore
}
func (o *SagaOrchestrator) StartOrderSaga(ctx context.Context, order Order) (*OrderSaga, error) {
saga := &OrderSaga{
sagaID: uuid.New().String(),
orderID: order.ID,
state: SagaPending,
steps: []SagaStep{
{Name: "create_order", Action: o.createOrder},
{Name: "reserve_stock", Action: o.reserveStock, Compensate: o.releaseStock},
{Name: "process_payment", Action: o.processPayment, Compensate: o.refundPayment},
{Name: "create_shipment", Action: o.createShipment},
},
}
// 持久化 Saga 状态到 Kafka
if err := o.persistSagaState(ctx, saga); err != nil {
return nil, err
}
// 发送第一步命令
if err := o.sendCommand(ctx, saga.sagaID, "create_order", order); err != nil {
return nil, err
}
return saga, nil
}
func (o *SagaOrchestrator) HandleEvent(ctx context.Context, event SagaEvent) error {
saga, err := o.stateStore.Load(ctx, event.SagaID)
if err != nil {
return err
}
switch event.Type {
case "OrderCreated":
saga.state = SagaOrderCreated
return o.sendCommand(ctx, saga.sagaID, "reserve_stock", saga.orderID)
case "StockReserved":
if !event.Success {
return o.compensate(ctx, saga)
}
saga.state = SagaStockReserved
return o.sendCommand(ctx, saga.sagaID, "process_payment", saga.orderID)
case "PaymentCompleted":
if !event.Success {
return o.compensate(ctx, saga)
}
saga.state = SagaPaymentCompleted
return o.sendCommand(ctx, saga.sagaID, "create_shipment", saga.orderID)
case "ShipmentCreated":
saga.state = SagaCompleted
return o.persistSagaState(ctx, saga)
}
return nil
}
func (o *SagaOrchestrator) compensate(ctx context.Context, saga *OrderSaga) error {
saga.state = SagaCompensating
// 逆序执行补偿操作
for i := len(saga.steps) - 1; i >= 0; i-- {
step := saga.steps[i]
if step.Compensate != nil {
if err := step.Compensate(ctx, saga); err != nil {
// 补偿失败需要人工介入或重试队列
log.Printf("Saga %s compensation failed at step %s: %v", saga.sagaID, step.Name, err)
saga.state = SagaFailed
return o.persistSagaState(ctx, saga)
}
}
}
saga.state = SagaFailed
return o.persistSagaState(ctx, saga)
}
7.3 幂等性与去重
Saga 中的每个操作必须是幂等的,因为消息可能重复投递:
func (s *OrderService) CreateOrder(ctx context.Context, cmd CreateOrderCmd) error {
// 幂等性检查:如果订单已存在则直接返回成功
existing, err := s.repo.GetOrder(ctx, cmd.OrderID)
if err == nil && existing != nil {
return nil // 已处理过,幂等返回
}
// 创建订单...
order := NewOrder(cmd.OrderID, cmd.UserID, cmd.Items)
if err := s.repo.SaveOrder(ctx, order); err != nil {
return err
}
// 发布 OrderCreated 事件
return s.eventBus.Publish(ctx, "orders-events", OrderCreatedEvent{
OrderID: cmd.OrderID,
UserID: cmd.UserID,
Items: cmd.Items,
})
}
八、事件驱动架构的反模式
8.1 分布式单体
将单体应用简单拆分为多个服务,但服务之间通过同步 HTTP 调用紧密耦合。这不是真正的事件驱动,只是将函数调用变成了网络请求。
解决方案:识别真正的业务边界,使用事件进行异步通信。
8.2 事件爆炸
过度细粒度的事件(如 UserFieldUpdated)导致系统难以理解和维护。
解决方案:事件应当反映业务事实(OrderCancelled),而非字段变更。
8.3 循环依赖
服务 A 监听服务 B 的事件,服务 B 又监听服务 A 的事件,形成循环。
解决方案:事件流向应当是单向的(从核心域到支撑域),使用协调 Saga 避免循环。
8.4 缺少 schema 控制
事件结构随意变更,导致消费者崩溃。
解决方案:使用 Schema Registry 管理事件 Schema,执行兼容性检查。
九、总结
事件驱动架构不是银弹,而是特定场景下的最优解。它的核心价值在于解耦服务边界、提升系统弹性、建立可审计的事件溯源。实施 EDA 时需要配套以下能力:
- Schema 治理:Schema Registry 保证事件契约稳定
- 幂等性设计:所有消费者和操作必须支持重复处理
- 可观测性:分布式追踪(OpenTelemetry)贯穿事件流全链路
- 容错机制:死信队列、超时重试、人工介入工具
- CQRS 投影:为查询侧构建优化的 Read Model
Kafka 是事件驱动架构的理想基础设施:它既是可靠的事件总线,也可作为永久的事件存储(Event Store),还能支撑 Saga 协调器的通信需求。结合 Debezium CDC 实现 Outbox 模式,可以构建出兼具事务完整性和最终一致性的现代分布式系统。
下一步学习:深入掌握 Kafka Streams 的流处理 DSL,将事件驱动从「数据搬运」升级到「实时计算」层面。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。