资讯详情

修复旧代码排序问题复盘:从业务规则确认到稳定排序重构

📅 2026/10/22 18:27:49 | 华诺云谱 👁 阅读
修复旧代码排序问题复盘:从业务规则确认到稳定排序重构
接手这个“41号旧代码排序问题的修复”任务之前我以为就是个普通的Bug修复打开文件改几行比较逻辑跑通测试收工。真做起来才发现老代码里的排序问题从来不只是一个“顺序不对”它背后往往连着业务规则、历史补丁、算法稳定性、甚至数据模型的账。这篇文章我就把这次排查、修复、验证的全过程复盘一遍既算给自己留个存档也给正在跟老代码搏斗的同行一点可参考的思路。项目背景很简单某跨平台系统里有一个历史遗留模块内部编号叫41号负责对一批内容做排序后输出给下游。这个模块被多次接手、打过不少补丁最近业务方反馈“结果顺序不符合预期”而且线上表现时好时坏。这个场景估计很多维护过老系统的人都熟——代码还没动需求方已经把“怎么个不对法”描述了好几种有时候是优先级高的排在后面有时候是权重差不多的两条数据顺序来回跳还有时候是某些数据忽然排到最前面。如果你也遇到类似的情况我的建议是先别急着翻开源码。排序问题最怕“看着代码猜原因”因为老代码往往已经被改得面目全非你看到的逻辑未必是线上真实运行的逻辑。先把问题现象钉死把业务预期弄清楚再做技术定位这条路虽然慢但不会白走。1. 接到需求先搞清楚“排序问题”到底是什么1.1 问题表象不是“排错序”那么简单运营同学给的反馈看起来很简单“我配置了一条高优先级内容应该排在第一屏结果它跑到第三屏去了。”“特惠商品比普通商品权重高但普通商品反而排在前面。”听起来像是排序规则失效但真正复现之后我发现问题远不止“排反了”。我先把同一批线上数据拿出来跑了一遍当时的排序逻辑然后连续多次执行对比输出结果。结果发现了一个很典型的“排序震荡”现象输入完全一样跑了三遍输出的顺序每次都不一样尤其是那些业务上认为“权重差不多”的数据前后顺序一直在跳。这说明问题不只是比较规则写错了排序本身的稳定性可能也有问题。这时候我意识到这是一个“旧代码修复”任务里典型的多层问题业务规则层面、比较器逻辑层面、算法实现层面可能都埋着雷。如果只修其中一层另一层随时会炸。1.2 先确认业务预期别急着看代码看任何旧代码之前我都习惯先做一件看似“不技术”的事把需求方的预期规则用一页纸写下来并且拉上业务方逐条确认。比如这个41号模块业务方说“按综合权重排”但这个“综合权重”到底由什么构成是热度、时间、人工分还是别的字段各自占比多少不回不知道一回发现连业务方内部都口径不一。有人说是“热度占大头”有人说“更新时间更重要”还有人说“人工干预分应该排在最后兜底”。这就是老系统里最隐蔽的成本——规则本身早就在多次业务变动中被稀释了代码只是把这种混乱固化下来而已。最终我们把规则收敛成明确的一条排序链先按综合权重大到小排权重相同再按业务优先级小到大排再相同按最近更新时间新到旧排最后按数据ID稳定收尾。有了这条明确规则后面所有修复工作才有基准。注意如果需求方自己都说不清“正确顺序”那你修好的代码大概率会在验收时被打回来。用一页纸把规则写清楚、请业务方签字确认是在保护自己。2. 定位根因旧代码里的排序是怎么“坏”掉的2.1 逐层下探锁定异常比较逻辑确认规则之后我开始看模块41的实现。入口函数不复杂从库里拉一批数据塞进一个私有排序方法然后返回结果。问题集中在排序方法内部。这段代码的原始实现估计有一定年头了用的是典型的“双层循环 自定义比较”模式——本质上是一个手写的冒泡排序比较函数里塞了大量业务判断。代码里到处是魔法数字权重公式的系数写在硬编码里有的补丁直接用注释标注“临时兼容业务方需求后续再优化”然后这个“临时”就临时了两三年。我先把比较函数逐条拆出来对照业务规则立刻发现三个问题第一权重公式的系数跟确认过的规则对不上。代码里热度、时间、人工分的系数写的是0.3、0.3、0.4但业务预期是0.5、0.3、0.2。也就是说人工分被过度放大热度的影响被削弱高热度内容自然排不上去。这种错误不靠逐行核对很难发现因为数字本身看起来“很像那么回事”。第二代码只处理了“按权重比较”权重相同的情况直接返回0根本没有进入“业务优先级”、“更新时间”这些次级规则。这就是为什么很多权重差不多的数据顺序“随缘”——它们根本没有进入第二轮比较。第三字段空值没有被统一处理。某条数据的人工分是空的时候比较结果取决于空值在表达式中被转成了0还是被当成了异常运行结果在不同环境下不一样。2.2 旧排序方案的几个典型坑继续往前挖我发现更深层的问题在于手写冒泡排序和比较器本身的“契约违规”。先说说冒泡排序。虽然冒泡排序在比较次数、交换次数上都很直观但它的问题是性能不行时间复杂度稳定在O(n²)。当时模块的数据量只有几百条跑起来还能接受现在数据量涨到几千甚至上万每次排序都要做大量比较接口耗时肉眼可见地上升。我做了个简单压测旧代码在5000条数据时排序耗时在几十毫秒到上百毫秒之间抖动而且越往后越不稳定——因为权重公式错误导致比较结果自相矛盾冒泡过程可能出现多余的交换。更隐蔽的坑是比较器的“传递性”不成立。什么叫传递性你拿A和B比B和C比A和C比三次比较的结果必须保持一致否则排序算法会陷入一种逻辑上的“死循环”。旧的系数错误导致实际计算出的数据比较结果可能是A比B大、B比C大、但C又比A大形成一个闭环。这种状态下无论用什么排序算法结果都不可预测这就是“线上排序结果时好时坏”的根源。我试过用生活里的例子跟同事解释这个坑三个人站成一排规则是“后面的人要比前面的人高”但你比较两个人的时候有时候看鞋底有时候看头发结果A比B高、B比C高、C又比A高那这队怎么都排不齐。代码里也一样比较规则内部矛盾排序结果自然乱套。3. 修复方案有限重构而不是推翻重写3.1 方案选型对比“最小改动”和“局部重构”看完代码摆在我面前有两条路最小改动修复或者局部重构。最小改动的诱惑很大只把权重系数改对、补上第二层第三层比较逻辑、把空值处理统一看起来改动很小测试范围也小。但问题在于手写冒泡排序本身是性能瓶颈而且它所在的模块已经积累了好几个补丁每次修改都是往旧逻辑上再盖一层下一个人接手时只会更痛苦。我最终选择了“有限重构”保留模块入口接口、数据模型不变只把排序方法内部重写——换掉手写冒泡排序改用标准库的稳定排序同时把比较逻辑抽成一个独立、规范的比较器。这里要解释一下为什么选“稳定排序”。业务规则里有一条权重相同的两条数据要按照业务优先级、更新时间、ID依次决定顺序。而业务优先级、更新时间都相同的极端情况下我们希望保留数据原有的相对顺序避免前端页面出现“同一批数据每次刷新顺序不一样”的现象。大多数标准库默认的排序是稳定排序比如Java里对对象排序用的是归并排序变体能保证比较器判等时保持原顺序正好符合这个场景。稳定排序的意义在企业业务里“相同优先级保持原有顺序”看起来是个小细节但直接影响用户观感。之前线上数据顺序来回跳一部分原因就是手写冒泡虽然理论稳定但比较器自相矛盾导致结果不可控。换成稳定排序 规范比较器就从源头上杜绝了这个隐患。3.2 排序规则梳理先定规矩再写比较器动手写代码之前我把排序规则整理成一张明细表这个表后来也直接成了测试用例的对照依据排序层级字段方向说明1综合权重降序热度×0.5 更新时间×0.3 人工分×0.22业务优先级升序数值越小优先级越高3最近更新时间降序最新的排前面4数据ID升序兜底保证顺序完全确定这张表看起来简单实际上写比较器的时候有不少细节要处理。最大的坑是“综合权重”的算法它是三个字段的加权和但三个字段都可能为空。我定了两条补位规则空值统一按0处理如果三个字段全部为空权重就是0这类数据统一排到最后不再参与次级比较。另外要提醒的是方向问题。业务优先级是“数值越小越靠前”写比较器时一定记得把Integer.compare(a, b)的返回值取反更新时间是“越新越靠前”也要注意方向。这些方向错误在老代码里特别常见因为旧逻辑里面经常“先升序后倒序”来回反着写最后谁也不知道哪个才是线上真实生效的规则。3.3 落地实现稳定排序 规范化比较器比较器实现我放在一个独立方法里方便后续维护和单元测试。示意代码如下// 简化示意仅保留核心逻辑 private double calcWeight(Item item) { double hot item.getHotScore() ! null ? item.getHotScore() : 0d; double update item.getUpdateTime() ! null ? item.getUpdateTime().getTime() : 0d; double manual item.getManualScore() ! null ? item.getManualScore() : 0d; // 系数以最终业务确认为准热度0.5、时间0.3、人工分0.2 return hot * 0.5 update * 0.3 manual * 0.2; } // 比较规则按“排序规则表”逐层比较 ComparatorItem sortComparator (a, b) - { int cmp Double.compare(calcWeight(b), calcWeight(a)); if (cmp ! 0) return cmp; cmp Integer.compare(a.getBizPriority(), b.getBizPriority()); if (cmp ! 0) return -cmp; // 数值越小优先级越高故取其反方向 cmp Long.compare(b.getUpdateTime().getTime(), a.getUpdateTime().getTime()); if (cmp ! 0) return cmp; return Long.compare(a.getId(), b.getId()); }; // 换成稳定排序 ListItem sortedList new ArrayList(list); sortedList.sort(sortComparator);需要说明的是这里用sort()而非Collections.sort()其实不影响核心逻辑关键是排序算法要稳定。旧代码暴露出来的日志是它自己实现了一套有缺陷的双层循环我把它替换成标准库排序之后不仅比较逻辑清晰了性能也顺带改善了。在动手改之前我还给这个排序方法补了一个详细的注释写明规则来源和系数的业务含义。这样下一次有人来看这段代码不翻需求文档也能知道“这个0.5是热度权重不是随便拍脑袋写的”。4. 测试与验证把“修好”变成“真的没问题”4.1 测试用例设计覆盖边界与业务场景修完代码之后真正的重头戏才开始。老代码修复最怕的不是“写不出来”而是“改完了没人知道它还影响什么”。我把测试分成三类边界用例、业务场景用例、回归对比用例。边界用例包括空列表、单元素列表、所有元素权重都相同、所有元素所有比较字段都相同、字段为空的数据。这些用例看起来基础但恰恰是旧代码最容易翻车的地方。比如所有权重相同的情况下旧代码直接返回0后续排序规则完全不生效新代码则能正确进入业务优先级、更新时间、ID的逐层比较。业务场景用例就按那张排序规则表构造故意设计几组数据让它们权重相同、但优先级不同优先级也相同、但更新时间不同所有字段都相同只靠ID兜底。每组都明确写出“期望顺序”再断言排序结果一致。这部分测试最好直接转化成自动化用例以后任何人再动这个模块都能快速发现回归。4.2 新旧行为对比与性能回归这个模块本质是一个“线上影响面较大”的功能单纯靠单元测试还不能让我放心上线。我做了一个线上数据回放取过去一周线上真实使用的一批样本数据分别用旧逻辑和新逻辑跑一遍输出排序结果做逐条diff。对比下来的差异点基本都能用规则解释高热度内容的排名明显提前了因为权重公式的系数被修正一些原先“顺序随机跳”的数据在新逻辑下顺序固定下来空值数据的兜底位置也稳定到末尾。让我特别在意的是一类“预期外差异”——某些以前排在很前面的低热度数据现在掉到后面去了。这类差异肉眼一看像是修复“引入的回归”其实就是旧逻辑原本的错误结果但只要肉眼解释不清楚验收就可能有争议。所以我顺手整理了一份差异说明文档把每一类变化和背后的规则对应起来作为评审材料。性能方面我也做了对比。旧算法在5000条数据量的排序耗时峰值接近上百毫秒新逻辑用标准库排序后基本稳定在10毫秒以内性能提升非常明显。这个结果对41号模块本身可能影响不大但考虑到下游还有分页、再加工操作省下来的时间对整条链路都有好处。4.3 上线步骤与灰度观察上线步骤我刻意分了三步走先在测试环境验证再在非核心链路灰度最后全量放开。灰度期间专门盯两个指标一是接口耗时确认没有因排序逻辑变化导致性能回退二是下游展示层的“顺序平均位置”之类的业务指标确认排序结果符合之前的差异说明文档。灰度观察期结束没有收到异常反馈我才把流量全部切到新逻辑上。上线之后我还留了一个线上日志开关持续记录排序样本数据。这么做是为了万一遇到新的业务规则变化可以直接从日志里重现当时线上场景不用再去翻已经滚过去的数据。5. 复盘与经验沉淀旧代码修复的正确姿势5.1 常见问题速查表这次修复之后我把排序类老代码的常见问题整理成了一张速查表方便项目组里其他同事遇到类似问题时快速对照。现象可能根因排查方向排序结果时好时坏比较器不满足传递性/一致性逐步检查比较函数是否自相矛盾相同权重顺序乱跳缺少次级比较规则或排序不稳定补齐逐层比较逻辑确认用稳定排序重点数据排不上去排序权重公式系数与业务预期不符按业务确认权重构成和系数数据量一大就变慢手写排序算法或比较函数过重换标准库排序检查算法复杂度空值数据位置不可控字段空值处理不一致统一空值默认值和兜底位置修复后结果“变了”可能是旧逻辑本身错未按规则排序保留旧结果和新结果diff向业务解释差异5.2 实操心得这次修复踩过的坑和总结出的方法最后分享几个这次修复过程中真正让我觉得“值回票价”的经验。第一改排序前不要只盯排序代码。41号模块表面上只是一个排序函数但它读取的数据模型、字段类型、空值状态都直接决定排序行为。我一开始以为问题只在比较器后来发现还有两个字段因为历史原因存的是不同格式必须统一处理之后才能参与计算。第二比较器要满足“反身性、对称性、传递性”这套规范。我在这篇里反复强调这一点因为它是很多排序问题最隐蔽的源头。写比较器时不要光看“看起来对不对”最好构造一个能制造循环比较的脏数据来测一下。第三改动范围再小也要有“产出物”。这个产出物可以是排序规则表、差异说明文档、自动化测试用例。它们不一定在代码里体现但会让评审、上线、后续接手的人都省力很多。这次我把排序规则表直接贴在方法注释里过了几周有人改这个模块基本没有反复找我确认过逻辑。第四对外“解释排序结果变化”比“证明排序技术正确”更重要。修复前的错误结果已经在线上稳定输出了一段时间业务侧可能已经习惯了某种顺序。突然一变哪怕变对了也要给下游一个明确的解释口径否则会被当成新Bug。这次“41号旧代码排序问题的修复”前后用了不到两周大头时间花在规则确认和差异对比上真正改代码的时间反而很短。回头看挺赚的——不仅修好了一个具体问题也摸清了这类排序问题在旧系统里反复出现的共性路径。以后再碰到“老代码排序有问题”的活儿我心里就门清了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑