形式 1:状态修复。形式 2:浪费报告。
心脏计量按时钟运行。不是根据需要,也不是根据变化,而是根据定时器。
两种形式,同一个根源:一个定时任务代替了正确的设计。
形式 1:状态修复
状态转换失败完成原子性地完成。相反,一个在延迟后运行的后台任务进行了协调。用户在协调窗口期间看到损坏的状态。
GitHub 示例(2026-04-08): 一个拉取请求的上游存储库变为私有。GitHub 尝试了一次状态转换:关闭 PR,更新分支状态,清除合并状态。该转换没有原子性地完成。PR 状态显示为 'branch-forced-closed' 和 '无法加载合并状态' 同时。一个 Sidekiq 后台任务几分钟后运行并完成了协调。观察者在协调窗口期间看到损坏的状态。
心脏计量:Sidekiq 任务按计划运行。它没有因为 GitHub 检测到损坏的状态而运行;它是因为时钟触发了运行。一个在实时观看 PR 的用户在下一个任务执行之前看到与自己相矛盾的 PR。
形式 2:浪费报告
一个报告或汇总按固定间隔从头开始重新计算。没有缓存检查。没有幂等性保护。没有增量更新。每次执行:一个全扫描。
示例:一个晚上的 cron 任务,重新计算每个用户的总购买金额,通过从开始时刻扫描所有订单。一个每日分析任务,重新生成一个从原始事件日志中生成的仪表板。一个每周摘要电子邮件,查询活动表中的每一行。
每个都按是否在上次执行以来数据发生变化而运行。每个都在只有最后 24 小时包含新数据的情况下全历史进行全扫描。每个都将预定的重复代替了增量设计。
共享的根
心脏计量不能关于自己的状态说实话。它只知道时钟。形式 1:状态修复任务在 T+5 分钟运行,regardless of 是否在 T+0 分钟状态是损坏的。形式 2:报告任务在凌晨 2 点运行,regardless of 是否在昨天以来任何数据发生变化。
时钟不携带关于需要做什么的信息。一个事件携带了该信息:'一个状态转换刚刚失败了','新订单刚刚到达了'。心脏计量丢弃了该信息,并将其替换为一个调度。
资本流失
A Metered Heart 将活跃资本抽干:工程师在出现故障时待命。侵蚀社会信任:用户看到不一致的数据并报告缺陷,这些缺陷会自动解决。放大其他 MOAD:一个用于修复状态的操作可能会扫描所有记录以查找故障状态,通常会包含 MOAD-0001(O(N²)扫描)。一个报告作业可能会触发 MOAD-0005(缓存拥堵)。 MOAD-0009 会放大其他缺陷。
共同根源
1 和 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() # step 2: now consistent
# 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() # both fields commit atomically; never half-written
表单 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点。它只更新受影响的用户。它只读取新订单,而不读取所有时间的所有订单。夜间作业消失。
为什么表单 1 暴露了一个不完整的过渡
表单 1 仪表盘揭示了一个状态转换未完成。修复作业存在是因为工程师注意到了损坏的状态并添加了一个协调机制,而不是修复转换本身。修复作业:在一个损坏的架构决策上打补丁。
MOAD-0009 作为放大器
MOAD-0009 放大了其他 MOAD。一个用于修复状态的扫描所有记录的修复作业:MOAD-0001 (O(N) 或 O(N²) 的扫描每次运行)。一个重新计算一切的报告作业:MOAD-0005 (缓存冲击开始时触发作业并访问上游时发生)。MOAD-0009 本身不仅造成损害;它按计划传递了其他 MOAD。
诊断与重新设计
一组团队在凌晨2点运行一个定期任务。这个任务扫描所有订单,为所有用户重新计算每个用户的总购买金额。这个任务需要4个小时。到6点,仪表板显示最新总额。2点到6点之间,仪表板显示昨天的总额。