English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

访客
1 / ?
返回课程列表

形式 1:状态修复。形式 2:浪费资源的报告。

有节律的心按时钟跳动。不根据需求,不根据变化,只根据定时器。

两种形式,一个根源:用定时任务替代正确的设计。

形式 1:状态修复

状态转换未能原子完成。没有修复转换本身,而是让后台任务延迟运行并进行对账。用户在对账窗口期内会看到损坏的状态。

GitHub 示例(2026-04-08): 某个拉取请求的上游仓库被设为私有。GitHub 尝试执行状态转换:关闭 PR、更新分支状态、清除合并状态。该转换未能原子完成。PR 状态同时显示“branch-forced-closed”和“Merge status cannot be loaded”。数分钟后,Sidekiq 后台任务执行并完成协调。观察者在窗口期内看到不一致的状态。

有节律的心脏: Sidekiq 任务按固定计划运行。它并非因为 GitHub 检测到状态损坏才执行,而是因为定时器触发。实时关注 PR 的用户在下一次任务执行前,会看到自相矛盾的 PR。

形式 2:浪费型报告

报告或聚合任务按固定间隔从头重新计算。没有缓存检查、没有幂等性保护、也没有增量更新。每次执行都是全量扫描。

示例:一个夜间 cron 任务,每次都扫描自有史以来的全部订单来重新计算每个用户的总购买金额;一个每日分析任务,从原始事件日志重新生成仪表盘;一个每周摘要邮件,查询活动表中的每一行。

无论自上次执行以来数据是否发生变化,这些任务都会触发。即便只有最近 24 小时有新数据,它们仍会扫描全部历史。它们用定时重复取代了增量设计。

共同根源

有节律的心脏无法真实反映自身状态。它只知道时钟。形式 1:状态修复任务在 T+5 分钟运行,而不管 T+0 时的状态是否已损坏。形式 2:报告任务在凌晨 2 点运行,而不管自昨天以来数据是否发生变化。

时钟不携带关于需要做什么的信息。事件才携带这些信息:“状态转换刚刚失败”、“新订单刚刚到达”。计量心将这些信息丢弃,并用一个计划表取而代之。

资本损耗

计量心会损耗活资本:工程师需随时待命处理破损状态事件。它侵蚀社会信任:用户看到不一致的数据,并报告那些会自行恢复的缺陷。它放大其他 MOAD:一个扫描全部记录以查找破损状态的状态修复任务,往往包含 MOAD-0001(O(N²) 扫描)。一个重新计算冷数据的报告任务可能触发 MOAD-0005(缓存雪崩)。MOAD-0009 会加剧其他缺陷。

共享根源

Form 1 与 Form 2 在表面上看起来不同:一个修复状态,一个重新计算数据。但它们的根本原因彼此相连。

Form 1 和 Form 2 共享同一个根本原因。用一句话描述它。然后从你用过的软件中各举一个例子。

因变化而触发,而非因时钟

事件驱动设计在状态发生变化时触发。状态变化即事件。事件即触发器。

形式 1:原子性状态转换取代修复作业。

如果一次状态转换会让系统处于损坏的中间状态,那么缺陷存在于该转换本身,而非缺少修复作业。应修复转换,使其以原子方式(或事务方式)完成。当转换以原子方式完成时,损坏状态便不会出现,修复作业也就无事可做。

# 缺陷:非原子转换导致损坏状态
def close_pr_on_repo_private(pr_id):
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed'   # 步骤 1:部分状态
pr.save()                             # 立即对用户可见
# ... 其他步骤可能失败 ...
pr.merge_status = 'not_applicable'
pr.save()                             # 步骤 2:现在已一致
# Sidekiq 任务在步骤 2 失败时进行协调
# 修复:原子性状态转换;中间状态不可见
def close_pr_on_repo_private(pr_id):
with db.transaction():
pr = PR.get(pr_id)
pr.status = 'branch-forced-closed'
pr.merge_status = 'not_applicable'
pr.save()   # 两个字段原子提交;绝不会出现半写入

形式 2:增量更新取代完整重算。

一份从头开始重算的报表之所以触发,是因为「旧数据 + 新数据 = 新结果」。但「旧结果 + 增量 = 相同的新结果」,且仅通过增量计算即可得到。事件:新数据到达。触发器:仅针对新数据更新聚合。

# 缺陷:按计划全量重算
def nightly_totals_job():
for user in all_users():
total = sum(o.amount for o in user.orders)  # 扫描所有时间
user.total_purchases = total
user.save()

# 修复:事件驱动增量更新
def on_order_placed(order):
order.user.total_purchases += order.amount   # 仅增量
order.user.save()

增量更新在订单到达时触发,而非凌晨 2 点。它只更新受影响的用户,只读取新订单,而非全部历史订单。夜间任务随之消失。

为什么 Form 1 揭示了断裂的转换

Form 1 计量心跳揭示了一次未完成的状态转换。之所以存在修复任务,是因为工程师发现了损坏的状态并添加了协调机制,而不是修复转换本身。修复任务只是对错误架构决策的修补。

MOAD-0009 作为放大器

MOAD-0009 会放大其他 MOAD。一个扫描所有记录以查找损坏状态的状态修复任务:MOAD-0001(每次运行 O(N) 或 O(N²) 扫描)。一个从头重新计算所有内容的报告任务:MOAD-0005(任务启动并命中温热上游时引发缓存雪崩)。MOAD-0009 不仅自身造成危害,还会按计划交付其他 MOAD。

诊断与重构

某团队在凌晨 2 点运行夜间 cron 任务。该任务扫描所有用户的所有订单,并从头重新计算每个用户的总购买金额。任务耗时 4 小时。到早上 6 点,仪表盘显示最新总额。在凌晨 2 点至 6 点之间,仪表盘显示的是昨日的总额。

这是 MOAD-0009 的哪种形式?应该由什么事件触发重新计算?哪种中间数据结构能让更新变得增量式?