核心问题
我们到底要解决什么根本问题?
第一性原理的思考方式是:
回到事物最基本的条件,将其拆解,再重新组合。
映射到软件架构:
不要从工具开始思考,从问题的物理性质开始思考。
坏问题:
我们要不要上 Kafka?
好问题:
我们需要解决的是解耦、缓冲、削峰、持久化、重放、广播,还是跨系统一致性?
工具是答案的一种形状,不是问题本身。
架构决策的常见错误
很多架构错误不是因为工具差,而是因为问题还没问清楚。
例如:
别人都用 Kafka,所以我们也用 Kafka。
这句话的问题不在 Kafka,而在“别人都用”不是架构理由。
同样的误区还有:
- 大厂都微服务,所以我们也微服务。
- 以后会变复杂,所以先上分布式。
- 为了可扩展性,先做插件系统。
- 为了灵活,所有配置都做成动态化。
- 为了标准化,先搞统一平台。
这些选择可能是对的,但不能因为听起来高级就默认正确。
把工具名拿掉
当我们说“需要 Kafka”时,先把 Kafka 这个词拿掉。
问:
- 是否需要异步?
- 是否需要削峰?
- 是否需要消息持久化?
- 是否需要消费者组?
- 是否需要消息重放?
- 是否需要广播给多个下游?
- 是否能接受最终一致?
- 是否需要严格顺序?
- 数据丢失的代价是什么?
- 运维复杂度是否值得?
这些问题回答完,工具选择才有基础。
也许答案是 Kafka。
也许只是:
Postgres table + background worker
也许是:
Redis List / Stream
也许是:
SQS / Cloud Tasks / BullMQ
也许根本不需要队列:
同步调用 + 重试 + 幂等
示例:课程购买后发放访问权
假设在线课程平台里,用户支付成功后,需要:
- 创建购买记录。
- 开通课程访问权。
- 发送邮件。
- 更新推荐系统。
- 通知企业管理员。
很多人会立刻想到事件总线:
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 可能很合理。
问题不在工具,问题在:
你是在解决问题,还是在购买一种“看起来像成熟系统”的感觉?
小结
- 架构决策要从问题性质开始,而不是从工具名开始。
- “别人都用”不是架构理由。
- 先问需要解耦、缓冲、削峰、持久化、重放、广播,还是一致性。
- 早期系统常常可以用
Postgres table + worker解决异步问题。 - 复杂工具必须由真实复杂性支付。
- 第一性原理不是反复杂,而是反未经证明的复杂。