摘要:
昨晚生产环境突发告警,某核心查询接口P99耗时直接打满。排查过程极其惊险,最后发现竟是几行“看似人畜无害”的代码惹的祸。本文不讲虚的理论,直接复盘这次事故中揪出的5个性能杀手,建议收藏自查!
1. 杀手一:失效的复合索引 (The Silent Index Killer)
很多兄弟以为加了索引就万事大吉,但在高并发下,索引失效是致命的。
事故代码:
-- 这是一个典型的索引失效案例 SELECT * FROM orders WHERE status = 1 AND DATE_FORMAT(create_time, '%Y-%m-%d') = '2023-12-12';深度解析:在索引列上使用函数(
DATE_FORMAT),会导致MySQL放弃走B+树索引,直接进行全表扫描。优化方案:将计算转移到参数侧,保持列的纯净。
-- 优化后:Range Query SELECT * FROM orders WHERE status = 1 AND create_time BETWEEN '2023-12-12 00:00:00' AND '2023-12-12 23:59:59';
2. 杀手二:循环中的RPC调用 (The N+1 Problem)
这是新手最容易犯的错误,甚至很多高级开发在写业务逻辑复杂时也会忽略。
场景还原:此时不仅慢,还会把下游服务(User Service)打挂。
// 错误示范:在循环中调用远程接口/数据库 List<Order> orders = orderMapper.selectList(); for (Order order : orders) { // 每一次循环都发起一次网络IO User user = userService.getUserById(order.getUserId()); order.setUserName(user.getName()); }优化方案:批量查询 (Batch Query) + 内存映射 (Map)。先收集所有ID,一次RPC查回,再在内存中组装。
3. 杀手三:无限制的Executors
这也是面试常问的“为什么不建议使用JDK自带的Executors”。
隐患:
Executors.newFixedThreadPool()的等待队列是LinkedBlockingQueue,默认长度是Integer.MAX_VALUE。一旦消费慢于生产,内存会被直接撑爆,触发Full GC,系统卡死。必杀技:手动创建
ThreadPoolExecutor,明确指定队列长度和拒绝策略。
4. 杀手四:深分页深渊 (Deep Paging)
当表数据达到百万级,LIMIT 1000000, 10会慢得让人怀疑人生。
原理:数据库需要扫描 1,000,010 行数据,然后丢弃前 100万行。
解法:使用“游标法”或“延迟关联”。
-- 延迟关联优化 SELECT a.* FROM orders a INNER JOIN (SELECT id FROM orders LIMIT 1000000, 10) b ON a.id = b.id;
5. 总结:性能优化Checklist
[ ] SQL解释计划(EXPLAIN)必看,关注
type是否为ALL。[ ] 严禁在循环中进行IO操作(DB/RPC)。
[ ] 日志打印要克制,大对象禁止直接
JSON.toJSONString()。[ ] 线程池必须自定义,严禁无界队列。
兄弟们,你们在生产环境遇到过最离谱的性能问题是什么?评论区聊聊,在这个充满Bug的世界里互相取暖!