公司动态

后端开发三年,这些性能优化经验最实用

📅 2026/8/22 9:20:25
后端开发三年,这些性能优化经验最实用
凌晨两点十七分监控大屏跳出红色告警。支付接口P99延迟从80ms飙到3800ms数据库CPU打满连接池被占光。我盯着屏幕上不断攀升的红色曲线脑子里只有一个念头——这是本周第三次了。三年后端生涯真正让我长记性的不是教科书上的理论而是每一次被线上事故按在地上摩擦之后总结出的实战经验。下面这些性能优化的心得每一条都踩过坑每一条都值钱。索引的真相很多人以为建了索引就万事大吉索引不是越多越好索引是拿写性能换读性能的交易。我曾经维护过一张订单表因为历史原因堆了十几个索引结果每次插入都要维护所有索引树写入时延从2ms飙升到25ms。更可怕的是慢查询不但没消失反而变多了——MySQL优化器在多索引之间反复横跳选出了错误的执行计划。建索引的核心不是数量而是理解你的数据访问模式。复合索引遵循最左前缀原则顺序写反等于白建。where条件里同时出现user_id和create_time索引就建(user_id, create_time)不要反过来。像性别这种区分度低于20%的字段建了索引优化器也大概率不走白白消耗磁盘空间和写入性能。我在生产环境改索引的习惯是先看执行计划确认索引真的被命中再观察全量数据分布最后在凌晨低峰期执行在线DDL。DBA同事说的一句话让我记到现在索引是给查询指路的路标太多司机反而不知道往哪开。缓存是把双刃剑刚学会Redis那阵子恨不得把所有数据都塞进去。直到某次大促活动缓存里放了十分钟的库存数据用户下单成功却扣不到库存客诉电话把客服部打爆。缓存穿透的解法不是布隆过滤器而是先想清楚数据到底有多热。一个数据如果每天查询量不到几百次而数据库每秒能扛几万次你加缓存图什么真正要防的是缓存雪崩和击穿。穿透我会用空值缓存加短过期时间兜底但解决雪崩问题的核心思路是——热点数据的过期时间必须加随机抖动否则同一秒集体失效数据库直接跪下。还有更隐蔽的坑缓存与数据库的一致性。我们曾经在更新数据库后删缓存赶上Redis网络抖动导致删除失败旧数据残留在缓存里六个小时。后来改成先更新数据库再通过消息队列异步删缓存配上重试机制问题才彻底解决。缓存不是加速器是定时炸弹拆弹的方式就是让它尽量短命。连接池的隐形杀手连接池不会报错但会让你在凌晨三点被扣钱。这是我和同事排查一次接口超时事故后总结的教训。服务负载看起来并不高CPU只有20%但接口时延就是从1.2秒涨到3秒。查了半天发现代码里有段逻辑用HttpClient调第三方接口没设置连接超时和读取超时默认值居然是0——无限等待。外部依赖一挂所有线程卡在等待上连接池被占满后续请求全部排队雪球越滚越大。真正的性能杀手往往不是你的代码逻辑而是你摸不透的外部依赖。解决方式很朴素但有效所有网络调用必须显式设置超时连接池要预留20%的冗余容量核心业务线程池不能被任何子任务耗尽。排查这类问题最趁手的工具是jstack连续打几次线程栈看它们在等什么。线程栈里堆着几十个同类阻塞调用基本就是连接或线程被某个外部调用堵住了。别一上来就调大连接池先把你项目里所有网络调用的超时参数过一遍性能问题可能凭空消失一半。慢SQL的排查方法论慢SQL的排查顺序永远是先看执行计划再谈优化。我曾花两天时间给一张表加索引调整多次毫无效果最后explain一看——SQL里有个函数包裹了索引字段索引直接失效。create_time 2024-01-01 和 DATE(create_time) 2024-01-01性能差了整整二十倍。另一个高频坑是隐式类型转换。用户表id字段是varchar传入参数是数字MySQL自动做了类型转换索引再次失效。写SQL时参数类型与字段类型的精确匹配比字段命名大小写更值得较真。分页深翻页同理limit 100000, 20这种写法会让数据库扫十万行再扔掉数据改用上一页最大id作为游标性能提升是几何级别的。工具层面慢查询日志要开但不要全量开。我们按天汇总慢查询日志解析出Top 10的SQL分类处理。优化优先级永远是——先干掉执行频率高且慢的再管偶尔慢一次的因为前者在放大系统整体延迟后者只是背景噪音。批处理的威力有一次做账单导出功能用户选一个月代码就循环查几百条明细再一条条写Excel。数据量一大接口直接超时。后来把所有查询合并成两条SQL一次性取出全部明细在内存处理耗时从30秒降到400毫秒。分批提交比一次性提交快十倍这不是魔法是减少锁等待。批量操作的底层逻辑是减少网络往返和事务开销。同样1000条数据循环插入要等1000次网络握手、日志刷盘、锁竞争排队批量提交只需要一次事务。MySQL写入场景下批量insert的性价比高得惊人。但批量不是无脑的大——每批大小要实测找到吞吐量拐点不是越大越快。我们压测过单批500条左右最优超过1000条反而下降因为事务日志和内存占用比例失衡。生产环境改批量操作必须灰度先对低流量接口试再铺开到核心链路。锁的粒度决定天花板一个并发扣库存的功能最初用synchronized包住整个方法每秒TPS只有300。后来改成乐观锁用update ... where stock 0加上版本号TPS直接干到2500。悲观锁解决的是理论问题乐观锁解决的是工程问题没有绝对好坏只有场景匹配度。扣减类操作适合乐观锁加重试因为冲突率低银行转账这类强一致场景悲观锁反而更可靠。真正的优化方向是缩小锁粒度——别人锁整张表你锁一个商品ID别人锁整个Redis你锁一个哈希槽位。分布式锁的超时时间别设太短业务执行超过超时时间会导致锁提前释放多个线程同时进入临界区那才是安全事故。日志不是越多越好日志是系统的神经系统别让它变成血栓。我们有个服务在INFO级别打印了每次请求的完整响应体有的响应体几百KB一天下来日志文件写了几十个GB磁盘IO直接被打满接口响应时间翻倍。把响应体日志改成DEBUG级别后磁盘IO降了70%。容易被忽视的还有日志刷盘频率。log4j2的异步日志队列buffer太小生产高峰期会疯狂丢日志。调大环形缓冲区把日志从同步改成异步CPU的sys占用率肉眼可见下降。性能优化有时候不是加机器是减少没必要的资源浪费日志级别的设置就是最划算的一笔账。另外日志里永远不要打印敏感字段或超大对象JSON序列化大对象的成本比你想的高得多。压测才是照妖镜压测不是跑个脚本看数字是找到系统崩溃前的那根稻草。上线前的压测中我们曾看到吞吐量一直上涨就以为系统很健康结果GC日志显示Full GC每秒触发三次stop-the-world的停顿时间占了运行时间的20%。吞吐量是假的垃圾回收停顿才是真相。压测过程重点盯三个指标GC频率和停顿时间、线程池活跃度、连接池使用率。任何一项先到瓶颈系统表现都会断崖式下跌。不压测就直接上线的系统就像没试水的泳池——你以为底下是地板跳下去才发现是深渊。压测最容易暴露的是慢接口的雪崩效应。单接口压测一切正常一旦混入慢SQL或外部超时线程池耗尽的连锁反应能让整个服务瘫痪。所以压测场景必须包含故障注入——模拟下游服务延迟看看你的服务能否自愈。这是在模拟你最怕的事故也是你唯一能提前演练事故的窗口。写在最后三年时光我从一个乱加索引、滥用缓存的愣头青变成了每次上线前都要过一遍执行计划和压测报告的老鸟。性能优化最怕的不是慢而是不知道慢在哪。建立可观测性体系把每个环节的耗时都暴露在监控之下才是一切优化的起点。这些经验并不高深但每一条都从凌晨的告警电话和客户的咆哮中换来。希望读到这里的你少踩几个坑多睡几个安稳觉。