Skip to content

29 · 缓存、消息队列与事件系统选型

一句话点题:缓存不是数据库,消息队列不是银弹,事件系统也不是「加个 Kafka」就完事。它们解决的是三类压力:读热点、写洪峰、跨边界协作。选之前先问:我是在降低延迟、削平峰值,还是解耦状态推进?


🧰 技术栈选型篇第 3 章 · 本章只练一件事

28 章 讲事实源和读模型。本章讲事实源周围最常见的三类中间层:缓存(Cache,临时加速读)、消息队列(Message Queue,异步排队)、事件系统(Event System,用事件表达事实变化)。它们能救系统,也能把系统变得更难懂。


开场:这三个东西经常被混用

很多架构图里会出现:

   App ──▶ Redis ──▶ MQ ──▶ Kafka ──▶ Worker

然后大家说:有缓存、有队列、有事件驱动,很高级。但真正要问的是:

  • Redis 里放的是可丢的缓存,还是不该丢的业务状态?
  • MQ 是为了削峰,还是为了让两个服务异步协作?
  • Kafka 里的消息是命令(Command,让别人做事),还是事件(Event,告诉别人发生了什么)?
  • 消费失败、重复消费、乱序、积压怎么办?

**架构判断:**中间层的价值不在名字,而在它改变了什么质量属性:延迟、吞吐、可用性、耦合度、一致性、恢复成本。


一、缓存:只加速,不篡位

缓存适合解决读热点:

   用户请求 ──▶ 应用 ──▶ 缓存命中 → 快速返回
                     └─ 缓存未命中 → 查主库 → 写缓存 → 返回

但缓存最容易犯三个错:

错误后果正确姿势
把缓存当事实源缓存丢了数据就丢主库是事实源,缓存可重建
不设计失效策略用户看到旧数据或脏数据TTL(过期时间)、主动失效、版本号
所有请求一起穿透主库被打爆空值缓存、请求合并、限流、预热

缓存选型也要看数据形态:

缓存类型适合注意
本地缓存配置、字典、低频变化数据多实例不一致,更新慢
分布式缓存(如 Redis)热点对象、会话、计数、限流网络开销、容量、淘汰策略
CDN(内容分发网络)图片、视频、静态资源、公开页面失效延迟、边缘缓存一致性

判断句:如果缓存丢了,系统应该变慢,而不是变错。变错,说明你把业务状态偷偷放进缓存了。


二、消息队列:把「现在必须做」改成「可以排队做」

消息队列最常见的价值是削峰填谷:

   洪峰请求 ──▶ 入口限流 ──▶ 队列 ──▶ Worker 按能力消费 ──▶ 主库

它把瞬时压力变成可控排队,常见于:

  • 抢票锁座后的出票通知。
  • 下单成功后的发券、短信、邮件。
  • 文档上传后的解析、切块、索引。
  • 视频上传后的转码。

但队列引入后,同步世界变成异步世界:

新问题你必须回答
重复消息消费者是否幂等?同一条消息处理两次会怎样?
消息丢失生产、存储、消费确认链路怎么保证?
消息乱序是否需要同一业务键内有序?
队列积压用户看到什么?系统如何降级?
死信(Dead Letter,处理失败的消息)失败消息去哪里?谁来修?

所以,队列不是「加了就稳定」,而是把问题从请求延迟转成了异步一致性与恢复


三、事件系统:记录发生了什么,而不是命令别人做什么

事件(Event)和命令(Command)很容易混:

类型含义例子谁负责结果
命令 Command请你做某事CreateOrderSendEmail接收方要执行成功或失败
事件 Event某事已经发生OrderPaidTicketLocked订阅方按需反应

事件系统适合跨边界传播事实:

   订单服务:订单已支付(OrderPaid)

        ├─ 库存服务:确认扣减
        ├─ 通知服务:发短信
        ├─ 数据平台:更新报表
        └─ 风控服务:记录行为

事件的好处是解耦:订单服务不需要知道所有下游。但代价是:

  • 事件 schema(结构定义)一旦发布,下游会依赖,升级要兼容。
  • 下游处理失败时,事实已经发生,不能简单回滚。
  • 事件太细会淹没系统,太粗又表达不清。
  • 事件链太长,排障会变难,必须有 trace(链路追踪)。

四、Kafka、RabbitMQ、Redis Streams、NATS 该怎么理解

别先背产品名,先看通信语义:

类型常见代表更像什么适合
任务队列RabbitMQ、Celery、Sidekiq派活给 worker后台任务、邮件、图片处理
日志型事件流Kafka、Pulsar可回放的事实日志事件总线、数据同步、审计、流处理
轻量消息/流Redis Streams、NATS简单快速的异步通道中小规模异步、低延迟内部消息
云托管队列SQS、Pub/Sub少运维的可靠队列云上业务、团队不想自运维

选型时问四件事:

  1. 需要消息可回放吗?需要就偏事件流。
  2. 需要复杂路由和投递确认吗?任务队列更合适。
  3. 团队能不能运维集群?不能就优先托管。
  4. 消息是不是核心审计事实?是的话,持久化、保留周期、schema 治理都要严肃对待。

五、Outbox:别让数据库事务和消息发送各干各的

最经典的坑:

   1. 写订单成功
   2. 发送 OrderCreated 消息失败
   结果:主库里有订单,下游永远不知道

或反过来:

   1. 消息发出成功
   2. 写订单失败
   结果:下游收到一个不存在的订单

Outbox(发件箱模式)的做法是:

   同一个本地事务:
   写业务表 + 写 outbox 表


   后台投递器扫描 outbox → 发消息 → 标记已投递

它不让「写事实」和「发事件」分裂。代价是多了一张表、一个投递器、幂等和重试逻辑,但换来的是跨服务一致性可控。这正是 11 章 的核心套路。


六、一个选型模板

md
### ADR-029:文档入库使用队列削峰,索引事件使用 Kafka

- 背景:企业知识库上传高峰会同时触发解析、切块、向量化和索引,同步处理导致上传接口超时。
- 选择:上传接口只保存原文和元数据,写入任务队列;解析完成后发布 DocumentIndexed 事件到 Kafka,供搜索、审计和报表订阅。
- 放弃:用户不能立刻搜索到刚上传文档,允许 1-3 分钟索引延迟。
- 换来:上传链路稳定,后台处理可限速、重试、扩容,下游通过事件解耦。
- 风险:队列积压会影响可搜索时间;需要积压告警、死信处理和幂等消费者。

🎯 随堂检验

🤔关于缓存,哪句话最符合本章判断?
  • A缓存越多越好,能放缓存就不要查数据库
  • B缓存丢了系统应该变慢,而不是变错;主库仍应是事实源
  • C缓存可以替代所有数据库事务
🤔一个订单服务写库成功后需要通知库存、通知和报表系统。最应该警惕的问题是?
  • A事件名字不够好听
  • B写数据库和发消息不是一个原子动作,可能出现主库有事实但消息没发出,需要 Outbox 等模式兜底
  • C只要用了 Kafka 就天然 exactly-once

本章小结

  • 缓存解决读热点:它应该可重建,不能偷偷变成事实源。
  • 消息队列解决洪峰和异步任务:它把同步延迟问题换成异步一致性、积压和恢复问题。
  • 事件系统传播事实变化:事件是「发生了什么」,不是「命令别人做什么」。
  • 选产品先选语义:任务队列、事件流、轻量消息、云托管队列解决的问题不同。
  • Outbox 是跨服务一致性的基本功:写业务事实和写待发送事件要在同一个本地事务里完成。

承上启下:缓存、队列、事件解决的是服务背后的压力与协作。下一章 30 · API 与服务通信选型,我们看服务之间正面怎么说话:REST、gRPC、GraphQL、Webhook、事件 API,到底各自适合什么边界。


相关链接

💬 评论