云计算百科
云计算领域专业知识百科平台

extendInfos两处修复复盘-代码审查检查清单

拿到一段陌生代码,怎么快速找到风险点?一份实习期间沉淀的检查清单

声明:本文基于工作中实际处理过的两个真实缺陷案例总结而成,已对公司名称、系统架构、代码实现、涉及人员做完全改写和泛化处理,文中代码均为示意性伪代码,不对应任何真实代码库。仅作为个人技术方法论记录分享。

背景

review 代码的时候,经常会陷入"从第一行读到最后一行"的模式——读完了,但说不出这段代码到底安不安全。后来在处理两个真实的线上缺陷时,慢慢总结出一套更高效的方法:不是通读,而是带着几个具体问题去扫。这篇文章讲这套方法,并用两个真实处理过的案例(细节已做泛化)来验证它。

拿到代码,先问自己 4 个问题

  • 这段代码的入口和目标是什么?——一句话说清楚"谁调用它、正常情况下它要干成什么事",先把 happy path 在脑子里搭起来,不深究每个分支。
  • 风险集中在哪几个点?——不是每一行风险都一样大。经验上,风险会扎堆出现在这几类地方:
    • 跨进程 / 跨服务的调用(RPC、数据库写、消息队列)——网络会超时、会丢响应,“调用返回了"不等于"对方真的处理成功了”。
    • 写操作发生在另一个可能失败的操作之前——尤其是"先写 A,再做 B,B 失败了怎么办"这种时序。
    • 循环里复用同一份内存对象/状态——上一轮改过的东西,下一轮还在不在?
    • 从集合里"随便挑一个"(findFirst()/findAny()/get(0) 之类)——如果这个集合本该只有一个元素,这行代码不会出事;但如果偶尔会有多个,这行就是定时炸弹。
    • 布尔标志"最后才赋值"——中间任何一个分支提前 return,这个标志有没有被正确写进真正要返回的对象里?
  • 失败分支里做了什么?——重点看 catch / if (xxx == null) 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) 这类"从集合里随便挑一个"?如果这个集合本该只有一个元素,为什么不用直接判空/取值,而要用集合操作?
    • 有没有在循环里复用同一份对象/状态?上一轮改过的东西,这一轮开始前处理干净了吗?
    • 提交说明里说"已修复"的地方,代码是不是真的改了?(自己读一遍,不要只信描述)

    本文改写自个人工作复盘记录,已做完全脱敏处理,不对应任何真实代码库或公司系统。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » extendInfos两处修复复盘-代码审查检查清单
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!