一个€18,000的财务漏洞,根源不是代码写错,而是没人问过:这个字段该活多久?
周六早上的对账现场
![]()
2026年4月18日,上午。我和Hélène并排看屏幕。她管了十九年学校账目,笔记本摊在桌上;我的是电子表格。她指着一行——陶艺专业三年级,普通学生——皱起眉头。
我的系统显示她欠€1,159。Hélène的还款计划写的是€2,262。
我们拿纸笔核对。十一个月分期,每月€205.65。Hélène是对的。
问题出在contacts.montant_total这个字段。它在3月导入时从当时的总额一次性写入,此后代码库里没有任何东西更新它。三周后,Hélène手动在新的echeances表里加了一笔分期——这个字段纹丝不动。仪表板读的是3月的数据,3月时那数据是对的。
我跑了个diff:488个联系人的montant_total是NULL,但echeances_inscription里有1,789条记录;72个联系人差额超过€1。累计差额约€18,000的"幽灵债务"。
「Et combien d'autres comme ça」——还有多少这样的——Hélène问,比我冷静。她早习惯了软件自作主张。
bug不在求和公式,不在迁移脚本,不在定时任务。bug在于:从来没人决定过contacts.montant_total应该是什么——某个时刻的快照?某个总和的缓存?还是应该始终反映当前还款计划?
字段因为当时需要就插进去了。让它保持正确的机制从没被写过,因为那个问题从没被问过。
这篇文章,就是关于那个问题。
正方:"不要重复数据"为什么行不通
每个工程师都学过一条朴素规则:不要重复数据。但在生产环境里,这条规则会失效。
物化视图(materialized view)是重复。contacts.dernier_contact_at(最后联系时间)是重复。cours.places_prises(已占座位数)是重复。字面执行这条规则,你会禁掉所有这些东西——包括那些你绝对需要的。
实际有效的规则更精细:每个看起来像重复的存储值,在创建或保留之前,必须被归类为实时值(Live)、快照(Snapshot)或缓存(Cache)。每个类别有各自的实现契约。没有类别的重复,就是潜伏的bug。
这是我现在每次迁移评审都要检查的规则。值得展开说说。
反方:三种分类真的够用吗?
有人可能会质疑:把派生值强行塞进三个盒子,会不会漏掉边缘情况?比如"准实时"(near-live)——延迟几秒可以接受?或者"带失效策略的缓存"——TTL(生存时间)长短不一?
这种质疑有道理。三种分类确实粗糙。但粗糙是它的设计意图。
分类的目的不是精确描述技术实现,而是强制做出显式决策。当你写下"这是Cache"时,你被迫回答:谁来刷新?什么触发?过期后怎么办?当你写下"这是Snapshot"时,你被迫接受:它不会自动更新,调用方要知道自己读的是历史。
模糊的中间地带——"差不多实时吧"——恰恰是bug的温床。contacts.montant_total的灾难,就源于它卡在中间:看起来像实时值(总欠款),行为像快照(一次性写入),维护成本像缓存(需要刷新逻辑但从没写)。
三种分类的粗暴,是对"不做决定"的惩罚。
我的判断:先分类,再实现
回到€18,000的漏洞。修复它不只是改代码,而是补一个十九年前就该做的决定。
如果当时问了"这是什么类型",答案会导向三种截然不同的实现:
→ Live(实时值):不要存这个字段。每次查询时从echeances实时计算。实现简单,读取稍慢,但永远正确。
→ Snapshot(快照):保留字段,但改名——montant_total_at_import。调用方明确知道自己读的是3月的数据,不会误用。
→ Cache(缓存):保留字段,但写刷新机制。触发条件可能是echeances表变更、定时任务、或手动刷新。关键是:有代码负责让它保持正确。
实际发生的第四种情况——"存了,但没人管"——不在选项里。这就是bug。
为什么这件事值得你现在检查
这个规则的价值不在理论,在评审清单。下次你审迁移脚本时,看到新增字段,问一句:
这是Live、Snapshot还是Cache?
如果提交者答不上来,打回去。不是刁难,是避免十八个月后某个Hélène对着€18,000差额皱眉。
你的代码库里有多少montant_total?去跑个diff吧。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.