核心问题

当多个地方都声称自己知道答案时,谁才可信?

软件系统里经常会有多个数据形态:

  • 原始事实
  • 当前状态
  • 缓存
  • 搜索索引
  • 分析报表
  • 前端本地状态
  • 第三方系统状态
  • 后台管理页展示

它们可能都在描述同一个业务对象,但不一定同样可信。

例如课程访问权:

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_summaryLessonProgress 不一致,应该相信 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 重建。

判断一个派生表是否健康,问:

  1. 它从哪些事实生成?
  2. 什么时候更新?
  3. 更新失败怎么办?
  4. 能不能全量重建?
  5. 读它的人是否知道它可能延迟?

第三方系统的真相边界

支付、邮件、身份认证等外部服务会让 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 检查清单

设计一个数据流时,问:

  1. 对每个关键问题,权威数据在哪里?
  2. 哪些数据只是缓存?
  3. 哪些数据只是投影?
  4. 哪些数据来自第三方?
  5. 当两个来源不一致时,谁赢?
  6. 是否有对账机制?
  7. 派生数据能否重建?
  8. 缓存失效失败会造成性能问题还是业务错误?
  9. 调用方是否知道读到的数据可能延迟?
  10. 是否有后台或客服页面能解释当前状态?

小结

  1. Source of truth 是某个问题的权威数据来源,不是泛泛的“数据库”。
  2. 每个关键业务问题都应该指定真相来源。
  3. 缓存是性能工具,不是真相来源。
  4. 派生表和读模型可以存,但必须能从事实重建。
  5. 第三方系统和本地系统各自有真相边界。
  6. 真相跨系统时,需要对账机制。
  7. 前端状态不能成为安全和权限的真相来源。