拿到一段陌生代码,怎么快速找到风险点?一份实习期间沉淀的检查清单
声明:本文基于工作中实际处理过的两个真实缺陷案例总结而成,已对公司名称、系统架构、代码实现、涉及人员做完全改写和泛化处理,文中代码均为示意性伪代码,不对应任何真实代码库。仅作为个人技术方法论记录分享。
背景
review 代码的时候,经常会陷入"从第一行读到最后一行"的模式——读完了,但说不出这段代码到底安不安全。后来在处理两个真实的线上缺陷时,慢慢总结出一套更高效的方法:不是通读,而是带着几个具体问题去扫。这篇文章讲这套方法,并用两个真实处理过的案例(细节已做泛化)来验证它。
拿到代码,先问自己 4 个问题
- 跨进程 / 跨服务的调用(RPC、数据库写、消息队列)——网络会超时、会丢响应,“调用返回了"不等于"对方真的处理成功了”。
- 写操作发生在另一个可能失败的操作之前——尤其是"先写 A,再做 B,B 失败了怎么办"这种时序。
- 循环里复用同一份内存对象/状态——上一轮改过的东西,下一轮还在不在?
- 从集合里"随便挑一个"(findFirst()/findAny()/get(0) 之类)——如果这个集合本该只有一个元素,这行代码不会出事;但如果偶尔会有多个,这行就是定时炸弹。
- 布尔标志"最后才赋值"——中间任何一个分支提前 return,这个标志有没有被正确写进真正要返回的对象里?
下面用两个真实处理过的案例(业务细节已经改写、代码已经改写为示意伪代码)走一遍这 4 问。
案例一:单次操作里的"写了但没兜底"
场景(已泛化):系统里有一个人工触发的"更换合作方"流程——已经生成的一笔订单,运营在后台手动切换到另一个供应方。切换时会先跟新供应方做一次"预检"调用,预检返回一些新的凭证信息,紧接着系统要用这份凭证正式创建新订单。
修复前的代码大致是这样(示意):
PrecheckResult precheck = supplierClient.precheck(order);
if (precheck.isSuccess()) {
// 直接创单,precheck 返回的凭证信息被完全丢弃
createOrder(order);
}
问题:预检返回的凭证信息(下游供应方创单必需)完全没有被保存下来,导致正式创单时用的还是旧凭证,下游校验失败。
用检查清单第 2 条扫一遍:保存凭证这一步,本质是一次跨服务的远程写(写入订单的扩展信息表),命中"风险集中点"第一条。继续往下看,写完之后:
saveCredential(order, precheck.getCredential()); // 跨服务写
Result result = createOrder(order); // 紧接着做另一件可能失败的事
if (!result.isSuccess()) {
return fail("创单失败"); // 没有任何撤销动作
}
这命中了检查清单第 2 条的第二类风险:“先写 A,再做 B,B 失败了怎么办”——这里完全没有回滚逻辑。如果创单失败了,凭证已经落库这个事实没人管,会一直残留在订单上。
教训:“跨服务写 + 写在另一个可能失败的操作之前 + 失败分支没有撤销动作”——这三件事同时出现,就是一个"跨步骤远程写、无补偿"的信号,不需要懂这段代码的业务细节,看到这个形状就该警觉。
案例二:循环把同一个风险放大了
场景(已泛化):另一个自动化流程——订单创建成功后,系统自动尝试几个更优惠的候选方案,同一次流程里可能连续试好几个候选,命中就用它创单,全部不命中则退回默认方案兜底。这次修复由同事主导首版实现。
这次风险模式和案例一相同,但因为是循环,检查清单第 2 条第三类风险(“循环里复用同一份状态”)被激活了:
for (Candidate candidate : candidates) {
if (candidate.hasExtraInfo()) {
snapshot = snapshotState(order); // 先拍快照
saved = saveCandidateInfo(order, candidate); // 再写
}
result = createOrder(order);
if (result.isSuccess()) { break; }
if (saved && !restoreState(order, snapshot)) { // 失败了要恢复
// ……
}
}
同一个订单对象贯穿整个循环——上一个候选失败了如果不撤销,下一个候选就是在"上一个候选残留的错误状态"上继续。这就是为什么同样是"写在创单之前",循环场景的后果比单次场景严重得多:单次写错一次只影响这一次尝试;循环里写错一次,会串到后面每一个候选,最后甚至可能污染兜底路径本身。
这条修复线一共经过 5 轮迭代,正好演示"审查是分层的,不是一次就能想全":
| 第 1 版 | 加了写,但没管失败分支 | 只做到第 1 问(目标),漏了第 3 问(失败分支) |
| 第 2 版 | 补了"先拍快照、失败就恢复"的补偿机制 | 补齐第 3 问 |
| 第 3 版 | 发现"取第一条"这个动作在两个地方分别做,选的可能不是同一条 | 检查清单第 2 条第四类风险(findFirst/get(0))——这条不是靠通读代码看出来的,是靠"如果同一订单有 2 条记录会怎样"这种反问逼出来的 |
| 第 4/5 版 | 复审时发现"读取端也在随便挑一条"、“第三方成功但本地记账失败被误判为整体失败” | 前者是同一个检查清单条目在不同代码位置的重复出现;后者是第 2 条第五类风险(布尔标志最后才赋值) |
其中有一轮特别值得记录:提交信息里写了"已修复某问题",但独立复审读代码后发现根本没改到那一行代码。“提交说明说修复了"不等于"代码真的改了”,看代码要自己核实,不能只信描述——这条经验本身也值得写进检查清单。
两个案例放一起看
同一个风险模板(“跨服务写 + 时序 + 无补偿”)在系统里出现了两次,一次经过多轮独立复审逐步补齐,另一次当时没被抓到、只是在复盘时顺带发现。不是因为写法不一样,是因为受到的审查次数不一样。这说明:
- 代码本身的"好坏"不是一次性状态,是"被看过几遍、被反问过几次"的结果。
- 反问的方式很具体:“如果这一步网络超时会怎样”、“如果这个集合里有 2 条会怎样”、“如果循环跑两轮会怎样”。
- 最有杠杆的事不是记住某两段具体代码,而是把这套"往哪儿反问"的习惯用到接下来看的每一段代码上。
可带走的检查清单
拿到一段代码,问这 5 个问题,答不上来的地方就是要深挖的地方:
- 这段代码正常情况下要干成什么事?(一句话说清楚)
- 哪几行是跨服务/跨进程调用?调用失败/超时时,前面已经做过的事情要不要撤销?
- 有没有"先写 A、再做 B"的时序?B 失败了,A 有没有人管?
- 有没有 findFirst()/findAny()/get(0) 这类"从集合里随便挑一个"?如果这个集合本该只有一个元素,为什么不用直接判空/取值,而要用集合操作?
- 有没有在循环里复用同一份对象/状态?上一轮改过的东西,这一轮开始前处理干净了吗?
- 提交说明里说"已修复"的地方,代码是不是真的改了?(自己读一遍,不要只信描述)
本文改写自个人工作复盘记录,已做完全脱敏处理,不对应任何真实代码库或公司系统。
网硕互联帮助中心




评论前必须登录!
注册