核心问题
当多个地方都声称自己知道答案时,谁才可信?
软件系统里经常会有多个数据形态:
- 原始事实
- 当前状态
- 缓存
- 搜索索引
- 分析报表
- 前端本地状态
- 第三方系统状态
- 后台管理页展示
它们可能都在描述同一个业务对象,但不一定同样可信。
例如课程访问权:
Purchase
Subscription
CourseAssignment
CourseAccessGrant
Redis cache
Frontend state
Admin dashboard
Analytics table
当它们不一致时,系统必须知道:
哪一个是 source of truth?
Source of Truth 不是“数据库”
很多人会说:
数据库就是唯一真相。
这句话太粗。
数据库里也可能有:
- 事实表
- 状态表
- 派生表
- 缓存表
- 审计表
- 临时迁移表
不是所有表都同样“真”。
例如:
payments
purchases
course_access_grants
course_progress_summary
analytics_daily_course_revenue
其中:
payments可能是支付事实的本地记录。purchases是购买事实。course_access_grants是访问权当前事实或效果。course_progress_summary是投影。analytics_daily_course_revenue是分析汇总。
如果 course_progress_summary 和 LessonProgress 不一致,应该相信 LessonProgress,然后重建 summary。
所以更准确的说法是:
Source of truth 是某个问题的权威数据来源,不一定是某个数据库整体。
对每个问题指定真相来源
不要抽象地问:
哪个系统是真相?
要具体问:
对这个问题,哪个数据是权威来源?
例如:
| 问题 | Source of Truth |
|---|---|
| 用户是否支付成功 | Payment provider + local payment record |
| 用户是否购买某课程 | Purchase |
| 用户现在能否访问课程 | CourseAccessGrant + access policy |
| 用户学习进度 | LessonProgress |
| 课程完成率报表 | 从学习事实生成的 analytics projection |
| 课程是否可购买 | CoursePublicationStatus + sales policy |
一个成熟系统会清楚区分:
谁负责写
谁负责读
谁可以缓存
谁可以重建
谁不能被当成真相
缓存不是事实
缓存最容易被误用成真相。
例如:
const canAccess = await redis.get(`course-access:${accountId}:${courseId}`)
如果缓存返回 true,用户是否一定能看课?
不一定。
可能:
- 用户刚刚退款
- 账号刚被封禁
- 课程刚被下架
- 企业 membership 刚被移除
- 缓存没有及时失效
缓存只能加速判断,不能替代权威判断。
原则:
缓存是性能工具,不是真相来源。
如果缓存被当成真相,缓存失效就会变成业务错误。
派生表不是事实
为了查询性能,我们经常会存派生表:
type CourseProgressSummary = {
accountId: string
courseId: string
completedLessons: number
totalLessons: number
progressPercent: number
updatedAt: Date
}
这没问题,但要明确:
它是 projection,不是 source of truth。
真正事实可能是:
type LessonProgress = {
accountId: string
courseId: string
lessonId: string
status: "not_started" | "in_progress" | "completed"
completedAt?: Date
}
如果 summary 错了,应该能从 lesson progress 重建。
判断一个派生表是否健康,问:
- 它从哪些事实生成?
- 什么时候更新?
- 更新失败怎么办?
- 能不能全量重建?
- 读它的人是否知道它可能延迟?
第三方系统的真相边界
支付、邮件、身份认证等外部服务会让 source of truth 更复杂。
例如支付状态:
Stripe says payment succeeded
local database says payment pending
谁可信?
对于“钱是否真的扣了”,支付平台通常更权威。
对于“我们系统是否已经发放课程访问权”,本地系统更权威。
所以要拆问题:
| 问题 | 真相来源 |
|---|---|
| 钱是否扣成功 | 支付平台 |
| webhook 是否处理 | 本地 payment processing record |
| 是否创建购买记录 | 本地 Purchase |
| 是否发放访问权 | 本地 CourseAccessGrant |
不要用一个字段 paymentStatus 试图代表所有真相。
真相同步需要对账
一旦有多个系统,就需要 reconciliation。
例如定时对账任务:
从支付平台拉取 succeeded payments
-> 找本地 payment record
-> 检查 purchase 是否存在
-> 检查 access grant 是否存在
-> 缺失则补偿或报警
对账不是补丁,而是分布式系统的正常组成部分。
因为网络、webhook、worker、数据库事务都可能失败。
原则:
只要真相跨系统,就需要对账机制。
Read Model 和 Write Model
很多系统会把写模型和读模型分开。
写模型负责保持事实和规则正确:
Purchase
Subscription
CourseAssignment
CourseAccessGrant
Enrollment
读模型负责快速展示:
CourseAccessView
LearnerDashboard
AdminRevenueReport
SearchIndex
读模型可以延迟,可以冗余,可以为查询优化。
但必须知道:
读模型服务体验,写模型维护真相。
如果读模型错了,应该能从写模型重建。
前端状态不是真相
前端状态经常是最短命、最不权威的数据。
例如按钮显示:
Start Learning
不代表用户一定有访问权。
真正进入课程前,后端仍要判断:
canAccessCourse(accountId, courseId)
前端可以乐观更新,但后端必须拥有最终授权判断。
原则:
前端状态可以提升体验,但不能成为安全和权限的真相来源。
Source of Truth 检查清单
设计一个数据流时,问:
- 对每个关键问题,权威数据在哪里?
- 哪些数据只是缓存?
- 哪些数据只是投影?
- 哪些数据来自第三方?
- 当两个来源不一致时,谁赢?
- 是否有对账机制?
- 派生数据能否重建?
- 缓存失效失败会造成性能问题还是业务错误?
- 调用方是否知道读到的数据可能延迟?
- 是否有后台或客服页面能解释当前状态?
小结
- Source of truth 是某个问题的权威数据来源,不是泛泛的“数据库”。
- 每个关键业务问题都应该指定真相来源。
- 缓存是性能工具,不是真相来源。
- 派生表和读模型可以存,但必须能从事实重建。
- 第三方系统和本地系统各自有真相边界。
- 真相跨系统时,需要对账机制。
- 前端状态不能成为安全和权限的真相来源。