编程思维不是拆解问题,而是记录你拆掉了什么

🔑 关键词:编程思维,抽象泄漏,问题拆解,程序员思维局限,系统设计

📖 摘要:大多数讲编程思维的文章都在说「把大问题拆成小问题」,但真正让程序员踩坑的不是拆得不够细,而是拆的时候悄悄丢掉了信息却没有任何记录。这篇从一次线上事故出发,聊聊为什么抽象能力的一半应该是「记录丢失物」。

一次扣成负数的库存

图片

2016年我在一家做本地生活服务的公司,负责优惠券核销系统。那年11月10号晚上——我们做的是线下商户,流量高峰比电商平台早一天——一个小时内涌进来大概4.2万次核销请求。库存用 Redis 的 DECR 扣减,逻辑上没毛病,原子操作。

但第二天早上商户打电话来,说券核销不了了。查下来库存是 -37,还有12个用户拿着已经用过的券又核销了一次。我们六个人轮流看代码看了将近六个小时,最后发现问题根本不在 Redis,在于商户那台 POS 机在弱网环境下会自动重试,而重试请求里的 request_id 是客户端本地生成的 UUID,每次重试都会变。服务端拿到的是一个全新的、合法的、从未见过的请求。

问题出在哪?不是技术选型,也不是并发量。是我们当初把「一次核销」抽象成一个 HTTP 请求的时候,脑子里默认了一个前提:请求和用户意图是一一对应的。这个前提在办公室的 wifi 下永远成立,在城中村那家麻辣烫店的路由器上不成立。

拆解的反面不是不拆,是「知道自己拆掉了什么」

图片

市面上讲编程思维的内容,八成都在讲一个东西:分解。把大问题拆成小问题,把复杂系统拆成模块,把业务逻辑拆成函数。这话没错,但它只说了硬币的一面。

拆解这个动作,本质上是有损压缩。你把现实的混乱塞进一个模型里,就一定有些东西被挤出去了。时间被挤成时间戳,人的犹豫被挤成状态机的两个分支,网络的不可靠被挤成 try-catch。压缩本身不是问题,问题是大多数人拆完之后,就把被丢掉的那部分彻底忘了。

我后来刻意观察过身边水平差异比较大的人。初级工程师的代码,你能看出他脑子里想的是什么;高级工程师的代码,你能看出他脑子里担心的是什么。区别不在拆得多细,在于他会不会在注释里、在接口设计里、在日志字段里,反复标记「这里我做了简化,简化掉的东西可能会在什么条件下回来咬我」。

这就是我想说的那个独立观点:编程思维的核心不是分解能力,是标记丢失物的能力。分解只是手段。

图片

抽象泄漏:不是意外,是常态

Joel Spolsky 在 2002 年写过一篇短文叫《The Law of Leaky Abstractions》,大意是所有非平凡的抽象都有泄漏。这篇文章被引用得很多,但我发现大部分人引用的方式是错的——他们把它理解成「抽象有 bug,要小心」。

不是的。它说的是:抽象必然丢失信息,而被丢失的信息在特定边界条件下会自己爬回来。这不是 bug,这是抽象的物理性质。就像你不能既要地图的比例尺是一比一,又要它装进口袋。

回到我那个核销系统。我们当时抽象成了这样一条链路:用户点核销 → 发请求 → 服务端校验 → 扣库存 → 返回。丢失了什么?丢掉了「网络会重试」、丢掉了「客户端可能不具备生成幂等键的能力」、丢掉了「商户端的设备是千元安卓机而不是测试机」。这些东西在架构图上一个字都没有。

后来重构的时候我们加了三个东西:服务端生成核销流水号并要求客户端回传、库存扣减改用带版本号的 CAS、所有核销接口带上一个从用户会话派生的幂等键。数据库层面加了一张 800 万行量级的核销流水表,用来做对账兜底。这三件事没有一件是「更聪明的算法」,全都是「把当初丢掉的东西捡回来」。

图片

一个可以落地的检查清单

如果你想练这个能力,我建议在每次做完设计之后,别急着写代码,先花二十分钟回答下面四个问题。我自己用了很多年,确实管用:

第一,我这个模型假设了什么? 把假设一条条写出来,写成完整的句子,不是关键词。「我认为同一个用户不会在 500 毫秒内点两次」——写出来你才发现这假设多脆弱。

第二,这些假设在什么条件下不成立? 不要写「极端情况」这种废话,要写具体的:网络延迟超过 1 秒、客户端时钟不同步、设备电量低于 5%……

图片

第三,如果不成立,系统会怎么表现? 是报错、是静默错误、还是数据不一致?静默错误最危险,因为它不会告警。

第四,我要不要留一条缝? 也就是,要不要在接口里多留一个字段、在日志里多打一行、在数据库里多留一张表,专门用来处理假设破裂时的情况。多留的这条缝,就是你未来排查问题的抓手。

它不是万能的,说清楚边界

最后我得承认,这套东西有它管不着的地方。

图片

有些系统的复杂度不是来自抽象丢失,而是来自人的利益冲突。我参与过一个跨部门的结算系统,技术上的抽象做得挺干净,但两个业务部门对「一笔订单算谁的业绩」这件事的定义从一开始就不一致。这种情况下你再怎么标记丢失物也没用,因为丢的不是信息,是共识。另一个边界是探索性的产品,早期你根本不知道抽象该往哪做,这时候强行画状态机反而是负担,不如先写一坨能跑的东西,等模式浮现了再重构。

所以编程思维不是一个可以到处套的锤子。它的适用区间是:问题有明确目标、系统有稳定边界、但细节复杂度超出人脑负荷。在这个区间里,拆解加标记丢失物,是我见过的性价比最高的方法。出了这个区间,你可能更需要的是谈判能力,或者是把东西先做出来的勇气。

我那个核销系统的 bug,最后复盘的时候有人在文档里写了一句「根本原因是架构设计不够健壮」。我当时把这句话删了,换成了「我们假设了请求与意图一一对应,该假设在弱网下有 0.3% 左右的破裂概率」。前者听着深刻,其实什么信息都没留下。后者不优雅,但它能在下一次评审会上被一个刚入职的同事看懂,然后追问一句:那另外 99.7% 呢?

这个问题问出来的时候,说明这套思维真的传下去了。

🏷️ 标签: