同步 vs 异步:为什么需要解耦
同步架构(紧耦合):
用户下单
│
├──▶ 订单服务(~10ms)
│ │
│ ├──▶ 库存服务(~20ms)
│ │ └── 扣减库存
│ ├──▶ 支付服务(~200ms)
│ │ └── 处理支付
│ ├──▶ 通知服务(~50ms)
│ │ └── 发短信
│ └──▶ 积分服务(~30ms)
│ └── 加积分
│
用户等待:10+20+200+50+30 = 310ms 才能收到响应!
任何一个服务超时 → 整个下单请求失败
──────────────────────────────────────────────
异步架构(消息队列解耦):
用户下单
│
├──▶ 订单服务(~10ms)
│ └── 写订单到 DB
│ └── 发消息到 MQ → 立即返回!用户响应 10ms
│
│ 消息队列(Kafka)
│ │
│ ┌─────────┼─────────┐
│ ▼ ▼ ▼
│ 库存服务 支付服务 通知服务
│ (异步处理) (异步处理) (异步处理)
优势:
① 用户响应快(10ms vs 310ms)
② 服务解耦(通知服务宕机不影响下单)
③ 削峰填谷(突发流量由MQ缓冲)
④ 重试容易(消息可以重新消费)
Kafka 核心概念
Apache Kafka 是目前最主流的分布式消息队列,最初由 LinkedIn 开发,每天处理超过 7 万亿条消息。
Kafka 架构全景:
Producer Kafka Cluster Consumer
┌──────────┐ ┌─────────────────────────────┐ ┌──────────────┐
│ Service │ │ Topic: order-events │ │ Inventory │
│ A │───▶│ ┌──────┐ ┌──────┐ ┌──────┐ │───▶│ Service │
│ │ │ │ P-0 │ │ P-1 │ │ P-2 │ │ │ (Group A) │
└──────────┘ │ └──────┘ └──────┘ └──────┘ │ └──────────────┘
┌──────────┐ │ │ ┌──────────────┐
│ Service │ │ Topic: payment-events │ │ Notification │
│ B │───▶│ ┌──────┐ ┌──────┐ │───▶│ Service │
│ │ │ │ P-0 │ │ P-1 │ │ │ (Group A) │
└──────────┘ │ └──────┘ └──────┘ │ └──────────────┘
└─────────────────────────────┘ ┌──────────────┐
│ Analytics │
│ (Group B) │
└──────────────┘
同一 Topic 可以被多个 Consumer Group 独立消费!
核心概念详解
Topic(主题)
消息的分类,类似数据库的表。Producer 向 Topic 写入消息,Consumer 从 Topic 读取消息。
Partition(分区)
Topic 的物理分片。每个 Partition 是一个有序的、不可变的消息序列(追加写入)。Partition 是 Kafka 并行处理的基本单位。
Offset(偏移量)
Partition 中每条消息的唯一序号(从 0 开始递增)。Consumer 通过 Offset 追踪消费进度,可以回溯重放历史消息。
Consumer Group
一组共同消费同一 Topic 的消费者。每个 Partition 只能被同一 Group 中的一个 Consumer 消费(并行消费)。不同 Group 独立消费,互不影响。
Broker(代理)
Kafka 集群中的一个节点,负责存储 Partition 数据和处理读写请求。
Replication Factor
每个 Partition 的副本数。RF=3 表示数据在 3 个 Broker 上各有一份,2 个节点宕机仍不丢数据。
Partition 内部结构(有序 + 可回放):
Partition 0:
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ ← Offset
│order │order │order │order │order │order │
│#1001 │#1002 │#1003 │#1004 │#1005 │#1006 │
└──────┴──────┴──────┴──────┴──────┴──────┘
↑
Consumer A 当前消费到这里(Offset=3)
可以 Seek 回到 Offset=0 重新消费(回溯重放!)
Consumer Group 并行消费(3个Partition,3个Consumer):
Partition 0 ──▶ Consumer 1(并行)
Partition 1 ──▶ Consumer 2(并行)
Partition 2 ──▶ Consumer 3(并行)
吞吐量 = 单 Consumer 吞吐 × Partition 数量
消息幂等性
在分布式系统中,消息投递存在三种语义,每种都有代价:
At-Most-Once
至多一次
至多一次
消息可能丢失,但绝不重复。发出去不确认,出错就丢弃。适合:日志、监控数据(丢一条无所谓)。
At-Least-Once
至少一次
至少一次
消息一定会被投递,但可能重复。失败就重试,直到收到确认。这是 Kafka 的默认行为。
Exactly-Once
恰好一次
恰好一次
消息恰好被处理一次。实现最复杂,需要幂等 Producer + 事务。Kafka 0.11+ 原生支持(同一集群内)。
业务层实现幂等性
场景:订单支付消息可能被重复消费
方案一:幂等 Key + 去重表
┌─────────────────────────────────────────┐
│ 消费消息前,先查去重表 │
│ │
│ SELECT id FROM processed_messages │
│ WHERE message_id = 'msg-uuid-123' │
│ │
│ IF 存在 → 跳过(已处理过) │
│ IF 不存在 → 处理 + INSERT 去重记录 │
└─────────────────────────────────────────┘
方案二:数据库唯一约束
┌─────────────────────────────────────────┐
│ INSERT INTO payment_records │
│ (order_id, amount, status) │
│ VALUES (123, 100.00, 'PAID') │
│ ON CONFLICT (order_id) DO NOTHING │
│ │
│ 重复消费 → INSERT 失败 → 安全忽略 │
└─────────────────────────────────────────┘
方案三:乐观锁版本号
┌─────────────────────────────────────────┐
│ UPDATE orders SET status='PAID', │
│ version=version+1 │
│ WHERE id=123 AND version=5 │
│ │
│ 只有当前版本匹配才更新,重复处理无效 │
└─────────────────────────────────────────┘
事件驱动架构(EDA)
以「电商下单」为例的事件驱动流程:
┌──────────┐
│ Order │──发布──▶ OrderCreated { orderId, userId, items, total }
│ Service │
└──────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌────────────┐ ┌──────────────┐ ┌────────────────┐
│ Inventory │ │ Payment │ │ Notification │
│ Service │ │ Service │ │ Service │
│ │ │ │ │ │
│ 订阅事件 │ │ 订阅事件 │ │ 订阅事件 │
│ 扣减库存 │ │ 发起支付 │ │ 发邮件/短信 │
└────────────┘ └──────────────┘ └────────────────┘
事件驱动的好处:
① Publisher 不需要知道 Subscriber 是谁(解耦)
② 新增服务只需订阅 Topic,不修改原有代码(开闭原则)
③ 审计日志天然完整(所有事件都记录在 Kafka 中)
Saga 模式:分布式事务
微服务场景下,一个业务操作跨越多个服务,每个服务有独立的数据库,无法使用传统的 2PC(两阶段提交)。Saga 是主流的解决方案。
Saga 编排模式(Choreography):
① CreateOrder → 发布 OrderCreated 事件
② InventoryService 订阅 → 扣减库存 → 发布 InventoryReserved
③ PaymentService 订阅 → 扣款 → 发布 PaymentCompleted
④ NotificationService 订阅 → 发短信
失败补偿:
③ 如果 PaymentService 失败 → 发布 PaymentFailed
② InventoryService 订阅 PaymentFailed → 回补库存(补偿事务)
① OrderService 订阅 → 将订单标记为失败
优点:无中心控制器,真正去中心化
缺点:流程难以追踪,Debug 困难
──────────────────────────────────────────────────
Saga 指挥模式(Orchestration):
Saga Orchestrator(指挥者)
│
├──① 调用 InventoryService.reserveItem()
│ ✓ 成功
├──② 调用 PaymentService.charge()
│ ✗ 失败
├──③ 补偿:调用 InventoryService.cancelReservation()
│ ✓ 补偿成功
└──④ 将订单标记为失败,通知用户
优点:流程清晰,易于监控和 Debug
缺点:引入中心化的 Orchestrator,可能成为瓶颈
实际应用:
Uber 使用 Saga 处理行程创建(跨行程、司机、支付三个服务)
Amazon 用 Saga 处理订单履行(跨库存、支付、物流)
▶ 面试要点
- Kafka 为什么这么快?顺序磁盘 I/O(比随机读写快 100 倍)+ 零拷贝(sendfile 系统调用)+ 批量压缩。
- 消息队列的三大价值:解耦(生产者不知消费者)、削峰(缓冲突发流量)、异步(非关键路径不阻塞)。
- Exactly-Once 的代价:性能下降约 20%,且只在 Kafka 内部保证,Consumer 侧仍需业务幂等。