写性能优化相关的内容有一段时间了,后台收到最多的一个问题就是:算法复杂度分析做得清清楚楚,为什么一上线还是慢得让人挠头?面试的时候大家都会背大O,写代码的时候也习惯了数循环层数,可真到了生产环境,一个接口的数据量从几十万涨到几百万,响应时间“唰”一下就上去了,而你拿着复杂度公式算出来的结论,跟线上表现完全对不上。
问题出在哪?出在很多团队只盯着一套度量标准在干活。算法复杂度度量的是理论增长趋势,工程性能度量的是真实运行表现,这俩根本不是一回事。我后来在项目里试着把这两套东西拼成了一个双重度量体系,用理论复杂度定方向,用工程性能指标验证结果,前后跑了大半年,效果比之前单纯靠某个单一指标来优化要靠谱得多。这篇文章就把这套体系的落地方法、踩坑记录和实操步骤完整梳理一遍,适合后端开发、算法工程师、SRE以及所有被线上性能问题折腾过的人参考。
1. 为什么单看算法复杂度会翻车:双重度量体系的由来
1.1 算法复杂度到底度量了什么
先聊一个基础问题:算法复杂度,尤其是时间复杂度,它度量的其实是“操作次数随输入规模增长的趋势”,而不是“这段代码运行起来需要几毫秒”。大O记号本身就是个渐进记号,它把常数项、低阶项、机器差异、语言差异全部抹掉了,只保留增长量级这一层信息。
这个特性在理论上非常优雅,但放到工程里就容易产生错觉。举个例子,插入排序的时间复杂度是O(n²),快速排序平均是O(n log n),理论上后者全面碾压前者。但是当n=10的时候,插入排序可能比快速排序还快,因为快速排序有递归调用、分区操作的固定开销,小规模数据下这些常数因子完全压过了量级差异。只有n涨到几千、几万甚至上百万时,O(n log n)的优势才会真正拉开。
我用一个更生活化的类比解释过很多次:大O复杂度像是判断你从A城市到B城市是走路、开车还是坐飞机,它只管交通方式这个量级;工程性能则是“今天路上堵不堵、你的车是1.0L自吸还是3.0T涡轮、高速入口排队多久”。两个维度都有用,但谁也没法替代谁。
1.2 工程性能度量管的是什么
工程性能度量的是代码在真实环境下的综合表现,包括接口延迟、吞吐量、CPU占用率、内存占用、GC停顿、线程阻塞、IO等待、缓存命中率等。它不关心你的算法在纸面上是O(n)还是O(log n),只关心这一秒、这一台机器、这一份数据分布下,代码到底跑出了什么样的数字。
这两个维度结合的逻辑其实很简单:算法复杂度告诉你“如果输入规模翻十倍,理论上会发生什么”,工程性能告诉你“现状到底有多慢、瓶颈卡在哪个环节”。一个负责预测趋势,一个负责量化现状,交叉验证,才能避免被单一指标带偏。
我整理过一个对比表格,方便团队里的小伙伴理解:
| 对比维度 | 算法复杂度 | 工程性能度量 |
|---|---|---|
| 度量对象 | 抽象的操作次数 | 真实的延迟、吞吐、资源消耗 |
| 粒度 | 增长量级 | 具体数字(毫秒、QPS、百分比) |
| 环境依赖 | 基本不依赖运行环境 | 强依赖硬件、数据分布、并发模型 |
| 擅长发现 | 规模增长带来的瓶颈 | 常量因子、锁竞争、IO等待、GC问题 |
| 典型工具 | 纸面推导、主定理 | JMH、压测工具、Profiler、监控系统 |
两套指标都有盲区。复杂度分析发现不了“同一个算法在Java里比在C++里慢3倍”这种常量因子问题,也发现不了“线程池满导致请求排队”这类并发问题;纯靠压测和监控去调优,又容易陷入“这次调好了,数据量一变又崩了”的反复循环。双重度量体系就是要把这两个视角强行绑在一起用。
1.3 “技术6”不是版本号,是六层落地结构
很多朋友看到这个标题里的“技术6”会以为是版本代号,其实我习惯把这套双重度量体系拆成六个可落地的环节,正好对应这个“6”。六层分别是:复杂度建模、微基准测量、宏观压测、容量规划、回归守护、度量闭环。前两层解决“理论和微观实测”,第三、四层解决“宏观表现和资源预估”,第五、六层解决“长期守住优化成果并持续迭代”。
这套六层结构的核心思想是:不能让理论分析和工程验证各自为政,而是让它们形成一条完整的流水线。每一层都有输入、输出和判定标准,做完一层再进入下一层。下面我把每一层具体怎么搭、用什么工具、注意什么坑,逐一展开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 六层度量体系怎么搭:从复杂度到工程性能的落地路径
2.1 先给算法“定级”:复杂度建模
第一层不是写代码,而是先把待优化的核心路径摊开,画出数据流,标注每一个关键循环、递归、排序、集合操作的输入规模,最后推导出整体复杂度。这个过程我一般放在代码评审阶段就做掉,不要等到线上报警才来建模。
实际操作中有两个高频误区。一是只数最内层循环,忽略了外层循环对规模的影响。比如有一段代码,外层遍历n个用户,内层对每个用户做关键词匹配,关键词数量是m,那么复杂度就是O(n×m),不是O(n)。如果m是100,那么实际计算量是100n,增长趋势虽然是线性的,但常数项很大,这种信息也要记录在案。
二是完全忽略均摊复杂度。Java的ArrayList扩容、HashMap的rehash,单次操作可能触发O(n)的复制,但均摊下来是O(1)。如果你在复杂度文档里把HashMap的读操作写成O(n),会过度悲观导致错误决策;写成O(1)而不说明最坏情况,又会埋雷。正确做法是把best、average、worst三种情况都标清楚。
我习惯在每个核心模块的注释里直接写一段复杂度说明,例如:
java复制/**
* 根据关键词列表过滤用户列表。
* 时间复杂度:O(n*m),n为用户数,m为关键词数。
* 空间复杂度:O(1),原地过滤,不额外申请集合。
* 注意:当 m > 50 时,建议改用倒排索引方案,见 issue #2041。
*/
public List<User> filter(User[] users, String[] keywords) { ... }
这个方法看起来很朴素,但半年后回看代码时,你会感谢当年留下了这些说明。复杂度建模的产出物,就是一张描述“每个核心路径复杂度边界”的清单,它是后续所有性能动作的理论基准。
2.2 微基准测量:抓常量因子
复杂度模型建完,进入第二层:微基准测量。这一层的目的非常明确,就是量化复杂度分析中被抹掉的常量因子。因为这些常量因子在真实业务里可能就是三倍、五倍甚至十倍的性能差距。
最常用的工具是JMH(Java Microbenchmark Harness),这是OpenJDK官方的微基准测试框架。不要用System.currentTimeMillis()包一圈循环去测,JIT分层编译、死代码消除、时钟精度这些因素会让结果完全失真。JMH通过预热、迭代、黑盒处理等方式把这些干扰控制住。
一个最简的JMH基准测试长这样:
java复制@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(1)
public class FilterBenchmark {
@Benchmark
public List<User> filterWithLoop(FilterState state) {
return FilterUtils.filterWithLoop(state.users, state.keywords);
}
@Benchmark
public List<User> filterWithIndex(FilterState state) {
return FilterUtils.filterWithIndex(state.users, state.keywords);
}
}
跑完之后,你会得到同一种输入规模下两个方案的吞吐量对比。如果复杂度分析说两者都是O(n),但JMH数据显示一个比另一个快80%,那说明常量大头在别的地方。这时要结合Profile工具看火焰图,找到真实热点,而不是继续埋头改算法的量级。
微基准有一点要提醒:它测的是“一小段代码”的局部表现,不代表整个接口的端到端延迟。所以这一层的结果只能说明“代码片段本身快不快”,不能直接当作线上结论。线上结论要交给下一层的宏观压测。
2.3 宏观压测与观测:定义工程性能基线
第三层是宏观压测,也就是把被测模块放进一个接近线上的环境里,施加真实负载,采集接口级别的指标。工具可以选择wrk、k6、JMeter等,选哪个不重要,重要的是测出来的指标要能回答三个问题:当前QPS是多少、P99延迟是多少、资源瓶颈在哪里。
压测场景设计很讲究。我一般会从低并发开始,比如10个并发线程,逐步增加到50、100、200,每档跑5分钟,记录各项指标的变化趋势。这里特别要关注P99而不仅仅是P50。P50代表多数用户的主观感受,P99则反映长尾延迟。两个指标放到一起看,才能判断是整体变慢,还是少数请求被卡住了。
采集指标的同时,一定要同时记录CPU、内存、GC、线程状态。我见过太多人只盯着延迟看,压测发现P99高,就急着优化代码,其实火焰图打开一看,CPU利用率根本不高,线程一大半BLOCKED在锁上。这种情况下算法复杂度再优秀也没用,瓶颈是并发控制。
把压测结果固化下来,就是工程性能基线。这个基线包含:并发梯度、QPS、P50/P95/P99、CPU利用率、平均GC暂停时间。以后每次优化都跟这条基线比,才有说服力。复杂度和工程性能在第二、三层的交叉验证,能帮你快速定位“理论该改”还是“工程该调”。
2.4 容量规划:把复杂度代入真实负载模型
第四层是比较容易被忽略的一层,但也是双重度量体系里最有价值的部分之一——容量规划。用一个简化模型来说明:假设单请求的CPU计算量是C,单核每秒能处理的操作数是S,那么单核的理论QPS大约是S/C。如果算法复杂度是O(n),数据量翻倍后C也翻倍,要维持同样的QPS,理论上CPU核数也要翻倍;如果算法复杂度是O(n²),数据量翻倍后C变成原来的4倍,那CPU核数需要翻4倍才能维持原来的吞吐。
这个模型本身很粗糙,因为它没有考虑锁、IO等待、GC等额外开销,但它能给你一个非常重要的数量级判断:当业务数据以某个速度增长时,当前架构还能撑多久?是需要优化算法,还是直接扩容机器?
我在做容量评估时,会结合历史数据增长曲线算一笔账。假设数据量每年增长3倍,如果接口复杂度是O(n²),一年后相同QPS需要的算力是现在的9倍;如果优化到O(n log n),同样条件下算力需求只涨3倍多一点。这种预判式的计算,比单纯“现在能撑住”要有用得多,它能在问题爆发之前就推动团队去做复杂度治理。
2.5 回归守护:把度量固化到研发流程
第五层是把前几层的成果固化到日常研发流程里,防止性能问题回流。没有这一层,前面做的基线很快就会失效——某次普通的需求变更可能就顺手把性能带崩了,而且没人察觉。
我在团队里做了两件事。第一,把关键模块的JMH基准测试挂到CI流水线上,每次MR跑一遍,如果相对基线吞吐量下降超过10%,这个MR就会被拦截,要求提交性能分析说明。这个门禁一开始执行阻力很大,因为很多开发会觉得“业务功能没变,测试也过了,凭啥不让合”。但坚持跑两个月后,大家就习惯了,而且确实拦截了好几回“看着人畜无害的复杂度退化”。
第二,集成测试里增加小规模压测,不需要每次都跑满负载,只跑一个固定并发档位,对比P99的变化。相比JMH,这个更接近真实链路,能抓到框架层面、序列化层面的性能退化。下面是一个极简的流水线配置示意:
yaml复制# ci/perf-check.yml
performance-check:
stage: test
script:
- mvn test -Dtest=PerfRegressionTest
- sh scripts/compare_jmh_result.sh # 对比历史基线,超阈值则失败
only:
- merge_requests
代码评审阶段我也会要求每个核心方法的注释里写明复杂度,并且在评审模板里加一项“本次变更是否会改变核心路径的复杂度量级”。这项要求看起来很软性,但作用不小,至少大家在写双层循环的时候会多问自己一句:这里真的需要嵌套遍历吗?
2.6 度量闭环:建立基准库与复盘机制
最后一层是沉淀。我会把每次优化前后的复杂度量级、JMH基准数据、压测基线、容量评估结果都记录到一个统一的地方,比如一个独立的Git仓库,或者其实就是一个带版本管理的Markdown文档加一个数据CSV。关键是要让后续的每一次变更都能追溯到“上次做到什么程度、为什么这么做”。
这块用表格记录就够,我放一个简化示例:
| 时间 | 模块 | 复杂度调整 | 数据规模 | P99(优化前) | P99(优化后) | QPS(优化前) | QPS(优化后) |
|---|---|---|---|---|---|---|---|
| 2025-03-12 | 用户过滤 | O(n*m) → O(n) | 80万 | 4600ms | 33ms | 32 | 1200 |
| 2025-04-02 | 报表聚合 | O(n²) → O(n log n) | 5万行 | 890ms | 120ms | 45 | 180 |
每次优化上线后1~2周,还要做一次线上数据回看,确认实际表现和压测预估是否一致。如果差异很大,通常说明压测场景设计和线上业务模型差距太大,需要回头修正压测模型。这一步虽然繁琐,却是整个体系能持续运转的关键。毕竟性能优化不是一次性的项目,而是一个持续迭代的过程。
3. 实操过程:一个真实接口的双重度量全流程
3.1 案例背景与复杂度建模
说一个我实际处理过的案例。业务方反馈,会员列表接口越来越慢,最严重时等一个报表导出要转圈十几秒。这个接口之前只有几千条会员数据,一年后涨到了80万条。上线第一次报警时,接口最慢已经到4.6秒,而且还在持续恶化。
第一步,我先拉出核心代码看复杂度。接口里有一段模糊搜索逻辑,逻辑上就是一个双层循环:外层遍历全部会员,内层逐个匹配搜索关键词。简化代码如下:
java复制public List<Member> search(List<Member> members, List<String> keywords) {
List<Member> result = new ArrayList<>();
for (Member member : members) { // n = 800,000
boolean hit = false;
for (String keyword : keywords) { // m = 100
if (member.getName().contains(keyword)
|| member.getTag().contains(keyword)) {
hit = true;
break;
}
}
if (hit) {
result.add(member);
}
}
return result;
}
这段代码非常典型:单看外层,O(n)似乎很健康;加上内层,就是O(n×m),也就是800万次字符串匹配,而字符串匹配每次又是O(L),L是字段长度。实际计算量比直觉大得多。复杂度建模的结论是:理论瓶颈在O(n×m)这个量级,关键词数量m越大,性能退化越明显,数据量n翻倍时,接口耗时大概率线性增长。
这就是双重度量体系的第一步:先定罪,再从理论上判断“该往哪个方向优化”。
3.2 压测量化工程现状
光有复杂度结论还不够,因为复杂度分析没法告诉你,当前的4.6秒里有多少是计算、有多少是锁、有多少是GC。第二步就是压测,用实际数据把工程现状钉死。
我用wrk对接口做了5分钟压测,并发50,预热2分钟后记录。测试环境的数据量同步到80万,结果如下:P50延迟约2.1秒,P95约3.2秒,P99约4.6秒,QPS约32。监控显示CPU已经接近打满,内存和GC反而比较平稳。这说明瓶颈基本上就在CPU计算这一块,不是内存分配问题,也不是锁问题。
这个交叉验证非常重要。如果只看复杂度,你会认为“O(n×m)当然是瓶颈”;如果只看监控,你会看到CPU打满、延迟高。两个结论互相印证之后,就可以放心往下做优化了。如果压测结果显示CPU只有20%、线程全部卡在锁上,那你花大把时间把O(n×m)优化成O(n)也不会有一点效果——瓶颈根本不在这条路径上。
3.3 优化方案设计:理论降级加工程落地
复杂度建模和压测都指向同一个方向后,我开始设计优化方案。这里有个常见的误区:很多人第一反应是把双层循环改成更高效的匹配算法,比如把contains换成KMP、把List换成Set,然后把复杂度从O(n×m×L)降到O(n×L)。
但在这个场景里,真正的数据特征是什么?关键词列表相对稳定(业务方维护的标签词表),而会员数据量在持续增长。与其在查询时拼命优化匹配,不如把“搜索”这个计算从查询链路里挪走。最终方案是:用离线任务给每个会员打标签,把“文本是否包含关键词”这个结果提前算好,查询时直接查标签索引。
改造后的查询逻辑变成了:
java复制public List<Member> searchByTag(List<Member> members, List<String> keywords) {
List<Member> result = new ArrayList<>();
for (Member member : members) {
if (member.intersects(keywords)) { // O(1) 位图/集合求交
result.add(member);
}
}
return result;
}
这里的复杂度直接降到了O(n),而且每一次命中判断变成了常数级操作。离线任务每天跑一次,把每个会员的标签集合更新到内存里的Bitmap索引,查询时只需要做一次集合求交。复杂度量级降下来了,工程上的存量计算也转移到了异步链路,接口的实时计算量大幅减少。
这个方案的选择也体现了双重度量体系的核心:理论维度告诉你O(n×m)到O(n)的量级变化,工程维度告诉你离线预处理比在线优化更符合当前业务的数据特征。两者缺一,都不容易做出这个决策。
3.4 双重指标对比与容量评估
优化上线后,我又用同样的压测脚本重新跑了一遍,结果如下:
| 度量维度 | 优化前 | 优化后 |
|---|---|---|
| 时间复杂度 | O(n×m) | O(n) |
| 空间复杂度 | O(1) | O(n)(Bitmap索引) |
| P50延迟 | 2100ms | 18ms |
| P99延迟 | 4600ms | 33ms |
| QPS | 32 | 约1200 |
| CPU利用率 | 接近打满 | 约40% |
可以看到,理论上从O(n×m)降到O(n)之后,工程性能的数字也出现了量级上的改善。但这里有个关键点:空间复杂度从O(1)变成了O(n),80万会员的标签索引在内存里大约占120MB。换来的是延迟下降两个数量级、QPS提升近40倍,这个权衡在业务场景下非常值。
容量评估也重新算了一笔账:按当前增长速度,明年数据量预计到240万,优化后的方案在现有机器上P99预计还能维持在80ms以内;而如果不做这个优化,按O(n×m)的模型推算,P99会涨到13秒以上,必须扩容至少3倍才能勉强压住。双重度量体系让我能拿着这份“理论推导+工程验证”的报告去说服业务方:这个优化是必须做的,不是锦上添花。
3.5 上线与回归验证
上线采用了灰度发布,先放10%流量观察一天,确认P99、错误率、GC指标都正常后再全量。全量后我在CI里补了两道防线:一是对核心匹配方法加了一个JMH基准测试,防止后续有人把复杂度改回去;二是集成测试里放了固定100万数据的接口压测用例,P99超过500ms就告警。
这里有一个小提醒:离线任务引入后,会员标签不是实时更新的。业务上要能接受“标签数据延迟一天”的代价。当时业务方评估后认为完全可接受,因为模糊搜索本身就是辅助功能,对实时性要求不高,但这点必须在方案设计阶段就确认清楚,否则上线后才发现数据对不上,很容易被认为是程序bug。
4. 常见问题与排查技巧实录
4.1 复杂度降了,线上还是慢
这是最让人崩溃的情况。代码从O(n²)优化到了O(n log n),压测看着也快了,一上线还是慢。遇到这种情况,我的第一反应是“复杂度从来就不是那个瓶颈”。
复杂度只描述了计算量,但线上接口的耗时是“计算+IO+锁+GC+网络”的总和。当你把计算量降下来后,原本被计算掩盖的锁竞争、IO等待、GC问题就可能暴露出来。排查工具优先用async-profiler抓火焰图,而不是凭直觉猜。火焰图会直接告诉你CPU时间耗在哪个函数里;如果CPU不高但线程大量BLOCKED,就抓线程转储看锁等待;如果GC频繁,就开GC日志看暂停时间。
这里面有一个需要注意的点:有些代码看着是纯计算,实际会触发大量反射、序列化、正则表达式匹配,这些操作带来的开销比普通算术运算高几个量级。复杂度分析经常把这些细节当成“常数”忽略,但工程性能可能被它们拖垮。所以复杂度降完还不快,下一步一定是Profile,而不是继续扣理论公式。
4.2 压测结果忽高忽低,怎么判断优化是否有效
压测最忌讳的就是数字不稳定。JIT还没热起来、GC刚好触发一次、宿主机上其他租户在跑任务,都足以让压测结果剧烈抖动。我第一次做JMH的时候没注意预热,同一个方法跑五遍,每遍结果都差好几倍,愣是没看出来哪个快。
解决办法有四个。一是压测前先做2~5分钟预压测,让JIT完成编译优化。二是正式记录取多轮的中位数而不是单次最大值。三是压测同时记录JVM指标,确认GC和CPU没有异常拐点。四是固定测试环境和机器,不要在开发机和自己笔记本之间比数字。
还有一个小技巧:如果两次对比性能差异不到15%,不要急着下结论,很可能是测量噪声。这时候可以看更稳定的指标——比如同复杂度下多次运行的中位数是否单调变化,或者用JMH的置信区间判断差异是否显著。
4.3 复杂度分析遇到外部调用怎么算
实际业务里,很多代码不是纯内存计算,中间会夹杂Redis缓存、数据库查询、远程RPC调用。这时候还硬套大O复杂度就会犯傻。比如一个循环里每轮查一次数据库,循环n次,你不能简单说这段代码是O(n),因为每一轮的内部操作是毫秒级的网络IO,而不是纳秒级的CPU指令。
我的处理方式是把这类调用当作“外部未知成本”,单独建模。复杂度文档上写清楚:计算部分O(n),外部IO次数n次,单次IO预计1~3ms,整体耗时量级是n×IO时间,而不是O(n)时间。如果你把IO调用误算成O(1),很容易得出“数据量翻倍没问题”的错误结论。
优化方向上也不能只盯着算法,要考虑连接池大小、批量查询、缓存策略、异步化这些工程手段。双重度量体系在这一点上特别有用:复杂度负责说“计算上是不是合理”,工程性能负责说“IO等待是不是占了绝对大头”,两个视角一结合,优化路径就很清晰了。
4.4 小数据量下优化看起来没收益
有一个很常见的挫败场景:你费劲把一个O(n²)的算法改成了O(n log n),结果测试数据只有几千条,压测下来性能几乎没差别,甚至连优化前的代码还偶尔更快。这时候别急着自我怀疑,小数据量下常数项本来就可能盖过量级优势。
正确的判断方式是用容量预判。看业务的数据增长曲线,如果半年后数据量会涨到现在的10倍,那么在那个规模下,O(n²)和O(n log n)的差距就是巨大的。我在方案评审时会给出一张类似的推导:假设当前n=5000时两者耗时接近,n=50000时O(n²)耗时是O(n log n)的10倍,n=50万时差距会拉大到100倍以上。
所以,双重度量体系里我最看重容量规划这一层,它能把“现在还看不太出来”的优化价值讲清楚,让团队愿意为半年后的稳定性买单。这比单纯拿着JMH数据说“优化后快了多少”更有说服力。
4.5 问题排查速查表
最后整理一张速查表,记录一些典型的性能问题症状、可能原因和排查手段,供实际遇到问题时对照使用。
| 现象 | 可能原因 | 排查手段 | 推荐工具 |
|---|---|---|---|
| 复杂度很低但CPU飙高 | 常量因子大、反射/序列化/正则开销高 | 抓火焰图定位热点 | async-profiler、perf |
| P99高但CPU不高 | 锁等待、线程池排队、IO阻塞 | 线程转储、看Blocked线程 | jstack、Arthas |
| 内存持续上升 | 对象创建过多、缓存无界、内存泄漏 | 堆转储分析、GC日志 | Eclipse MAT、jmap |
| GC频繁导致延迟抖动 | 大对象、晋升过快、GC参数不合理 | 查看GC日志、对象分配分析 | GCViewer、JFR |
| 数据库慢查询拖垮接口 | 循环内查询、缺少索引、N+1问题 | SQL日志、慢查询分析 | 数据库执行计划、SkyWalking |
| 数据量翻倍后性能严重劣化 | 复杂度量级过高、存在O(n²)热点 | 复杂度建模、容量预估 | 本文提到的六层流程 |
这表里的场景我基本都踩过,尤其是“P99高但CPU不高”这个,排查起来最容易走弯路。后来养成了习惯:任何性能问题排查的第一步都是同时看CPU和线程状态,先判断瓶颈在计算侧还是等待侧,再往下挖,能省掉大量无效操作。
这套双重度量体系我实际用了三年,说句实在话,它最大的价值不是帮你算出某个接口的极限QPS,而是逼着你在动手优化之前,同时回答两个问题:理论上它该不该快?工程上它为什么还不快?这两个问题一旦能对齐,很多无谓的优化动作自然就消失了。我个人还有个习惯,会把每次优化前后的复杂度量级、P99、QPS、数据量打成一个标签贴在接口文档里,后来团队翻老代码的时候都靠这个快速判断“这段代码还能不能扛得住下一波数据增长”。你也可以试试,尤其当你面对的是一个逐年增长的老系统。
