核心问题

我们到底要解决什么根本问题?

第一性原理的思考方式是:

回到事物最基本的条件,将其拆解,再重新组合。

映射到软件架构:

不要从工具开始思考,从问题的物理性质开始思考。

坏问题:

我们要不要上 Kafka?

好问题:

我们需要解决的是解耦、缓冲、削峰、持久化、重放、广播,还是跨系统一致性?

工具是答案的一种形状,不是问题本身。

架构决策的常见错误

很多架构错误不是因为工具差,而是因为问题还没问清楚。

例如:

别人都用 Kafka,所以我们也用 Kafka。

这句话的问题不在 Kafka,而在“别人都用”不是架构理由。

同样的误区还有:

  • 大厂都微服务,所以我们也微服务。
  • 以后会变复杂,所以先上分布式。
  • 为了可扩展性,先做插件系统。
  • 为了灵活,所有配置都做成动态化。
  • 为了标准化,先搞统一平台。

这些选择可能是对的,但不能因为听起来高级就默认正确。

把工具名拿掉

当我们说“需要 Kafka”时,先把 Kafka 这个词拿掉。

问:

  1. 是否需要异步?
  2. 是否需要削峰?
  3. 是否需要消息持久化?
  4. 是否需要消费者组?
  5. 是否需要消息重放?
  6. 是否需要广播给多个下游?
  7. 是否能接受最终一致?
  8. 是否需要严格顺序?
  9. 数据丢失的代价是什么?
  10. 运维复杂度是否值得?

这些问题回答完,工具选择才有基础。

也许答案是 Kafka。

也许只是:

Postgres table + background worker

也许是:

Redis List / Stream

也许是:

SQS / Cloud Tasks / BullMQ

也许根本不需要队列:

同步调用 + 重试 + 幂等

示例:课程购买后发放访问权

假设在线课程平台里,用户支付成功后,需要:

  1. 创建购买记录。
  2. 开通课程访问权。
  3. 发送邮件。
  4. 更新推荐系统。
  5. 通知企业管理员。

很多人会立刻想到事件总线:

PaymentSucceeded -> Kafka -> consumers

但先不要急。

用第一性原理拆解:

1. 哪些事情必须立即成功?

购买记录和访问权通常必须成功。

否则用户付了钱却不能看课,这是核心体验事故。

2. 哪些事情可以稍后完成?

邮件、推荐系统、通知企业管理员可以异步。

3. 哪些事情可以失败后重试?

邮件可以重试。

推荐系统更新可以重试。

企业通知可以重试。

4. 哪些事情需要强一致?

支付记录和访问权之间至少需要非常强的业务一致性。

5. 哪些事情需要重放?

如果推荐系统坏了一天,是否需要把昨天所有购买事件重新放给它?

如果需要,就要有事件日志或可查询事实表。

6. 系统规模是多少?

每天 100 单和每秒 1000 单,不是同一个问题。

一个朴素但强的方案

早期系统可能这样就够了:

async function handlePaymentSucceeded(paymentId: string) {
  await db.transaction(async (tx) => {
    const payment = await tx.payments.markSucceeded(paymentId)

    await tx.purchases.create({
      accountId: payment.accountId,
      courseId: payment.courseId,
      paymentId: payment.id,
    })

    await tx.courseAccessGrants.create({
      accountId: payment.accountId,
      courseId: payment.courseId,
      source: "purchase",
    })

    await tx.outboxEvents.create({
      type: "course.purchase.completed",
      payload: {
        accountId: payment.accountId,
        courseId: payment.courseId,
        purchaseId: payment.id,
      },
    })
  })
}

然后后台 worker 处理 outbox:

async function processOutboxEvent(event: OutboxEvent) {
  if (event.type === "course.purchase.completed") {
    await sendPurchaseEmail(event.payload)
    await updateRecommendationProfile(event.payload)
    await notifyOrganizationManager(event.payload)
  }
}

这个方案没有 Kafka,但解决了很多核心问题:

  • 核心数据在同一个事务里完成。
  • 异步任务不会阻塞用户访问课程。
  • outbox event 可以失败重试。
  • 事件存在数据库里,可以追踪。
  • 系统复杂度可控。

等到规模和需求真的长出来,再考虑 Kafka 或更完整的事件平台。

架构选择的第一性原理清单

做技术选型前,可以先问这些问题。

1. 负载性质

  • 请求量是多少?
  • 峰值和平均值差多少?
  • 流量是否可预测?
  • 是读多写少,还是写多读少?

2. 一致性要求

  • 哪些数据必须强一致?
  • 哪些数据可以最终一致?
  • 用户能否接受延迟?
  • 数据短暂不一致的代价是什么?

3. 失败模型

  • 哪些失败可以重试?
  • 哪些失败必须人工介入?
  • 重复执行是否安全?
  • 是否需要幂等键?

4. 顺序要求

  • 事件是否必须按顺序处理?
  • 是全局顺序,还是单个用户、订单、课程内有序?
  • 顺序错乱会造成什么后果?

5. 可恢复性

  • 是否需要重放历史事件?
  • 是否需要审计日志?
  • 是否能从数据库事实重新生成派生状态?

6. 运维成本

  • 团队是否有能力维护这套基础设施?
  • 故障时谁能排查?
  • 本地开发和测试是否会明显变复杂?
  • 这个复杂度是否配得上当前问题?

不同工具解决的不是同一个问题

Postgres table + worker

适合:

  • 早中期业务系统
  • 任务量可控
  • 需要事务一致性
  • 需要简单可靠的异步处理

优势:

  • 简单
  • 可查询
  • 易调试
  • 和核心数据同库事务

代价:

  • 高吞吐和复杂消费者模型有限
  • 需要自己处理锁、重试、并发

Redis List / Stream

适合:

  • 轻量队列
  • 实时性较高
  • 数据可短期保留
  • 已经有 Redis 基础设施

优势:

  • 使用简单

代价:

  • 持久化和审计语义不如数据库或 Kafka 直观
  • 要小心丢消息和消费确认语义

Kafka

适合:

  • 高吞吐事件流
  • 多消费者订阅
  • 事件重放
  • 数据管道
  • 大规模系统解耦

优势:

  • 吞吐高
  • 消费者模型成熟
  • 重放能力强
  • 适合事件驱动架构

代价:

  • 运维复杂
  • 本地开发复杂
  • 概念成本高
  • 对小系统可能过重

第一性原理不是拒绝复杂工具

第一性原理不是说永远不要用 Kafka、微服务、Kubernetes 或复杂架构。

它说的是:

复杂工具必须由真实复杂性支付。

如果你有真实的高吞吐、多消费者、重放、流式处理需求,Kafka 很合理。

如果你有独立团队、独立发布周期、清晰服务边界,微服务很合理。

如果你有复杂部署、弹性伸缩、多租户隔离,Kubernetes 可能很合理。

问题不在工具,问题在:

你是在解决问题,还是在购买一种“看起来像成熟系统”的感觉?

小结

  1. 架构决策要从问题性质开始,而不是从工具名开始。
  2. “别人都用”不是架构理由。
  3. 先问需要解耦、缓冲、削峰、持久化、重放、广播,还是一致性。
  4. 早期系统常常可以用 Postgres table + worker 解决异步问题。
  5. 复杂工具必须由真实复杂性支付。
  6. 第一性原理不是反复杂,而是反未经证明的复杂。