公司动态
后端性能调优实战:从数据库慢查询到接口响应优化
凌晨两点告警声像一把尖刀刺穿机房的沉寂。监控大屏上用户中心接口的P99延迟从80ms一路狂飙到3.2秒数据库服务器CPU使用率烧到99%慢查询日志里躺着一条执行了整整8秒的SQL。你明明记得昨天的发布一切正常可线上数据不会说谎——性能调优从来不是锦上添花而是火线救命的实战。先从最基础也最容易被忽略的环节开始慢查询日志。慢查询日志先承认数据库出了问题很多团队连慢查询日志都没开直到故障发生才手忙脚乱。MySQL里只需设置slow_query_logON并把long_query_time设为1秒甚至更低就能捕捉到最耗时的SQL。但日志只是开始真正有价值的是explain输出。你会在那里看到typeALL、rows1000000、ExtraUsing filesort这些触目惊心的字眼。慢SQL不是一天养成的但一定会在流量高峰时给你一记响亮的耳光。不要指望DBA救场每个后端开发都应该能独立读懂执行计划。这里有个常见误区阈值设太高过滤掉了真正慢但频率高的查询。只盯着单条慢查询却忽视了每天执行一万次的“小慢查询”才是性能陷阱的常态。所以要结合pt-query-digest这类工具分析聚合找到“累积代价”最高的查询。慢查询日志是病历但诊断报告还要靠执行计划与统计信息一起下结论。索引用空间换时间但别滥用很多调优新手一上来就疯狂加索引结果线上存储膨胀写入变慢。索引的本质是排序的数据结构它让查找从全表扫描变为B树查找。但加索引不是万灵丹加错索引比不加更可怕。我们需要理解最左前缀原则如果查询条件里没有包含索引的第一个字段这个索引就形同虚设。例如在(user_id, status)上建了索引查询WHERE statusactive用不上这个索引。还有更隐蔽的陷阱隐式类型转换。MySQL在比较时会自动把字符串转成数字如果字段是varchar参数却传数字索引就会失效。比如WHERE phone 13800000000如果phone是varchar索引会失效。一条索引的生死往往取决于一个引号。另外对索引字段进行函数运算也会让索引失效如DATE(create_time) 2024-01-01应该改写为范围条件create_time 2024-01-01 AND create_time 2024-01-02。索引优化是一场与优化器的博弈你要学会用explain和optimizer trace看清它的底牌。查询重写别让数据库做无用功如果索引没问题那么问题多半出在SQL本身。很多团队习惯于SELECT把几十个字段全部捞出来然后应用层只用两个。每一次SELECT都是对网卡和内存的无声摧残。更严重的是大表分页查询LIMIT 100000, 20会让数据库扫描十万行后再丢弃解决方法是延迟关联先查子查询得到主键范围再回表取完整行。例如SELECT FROM orders o JOIN (SELECT id FROM orders WHERE status1 ORDER BY created_at DESC LIMIT 100000, 20) t ON o.id t.id这样可以将扫描量降到最低。优化SQL的终极目标不是让查询变快而是让数据库根本不需要执行它。对于多表关联如果关联字段没有索引必然导致嵌套循环。这时要审视业务是否需要实时数据或者是否可以用冗余字段来换查询速度。连接池比SQL更隐蔽的瓶颈当SQL和索引都优化得差不多了接口依然慢问题可能根本不在数据库内部而在数据库连接上。每打开一个数据库连接都要经历TCP握手和认证成本极高。连接池就是复用的关键。但连接池大小并不是越大越好。连接池太小请求排队连接池太大数据库线程切换累死甚至拖垮CPU。以HikariCP为例官方推荐的大小公式是core_count 2 spindle_count但云数据库环境下需要压测。连接池的等待时间也是关键参数connectionTimeout如果设置过短高峰期会频繁抛出连接超时异常设置过长用户会等到怀疑人生。更微妙的是应用层线程池和连接池的并发要匹配。调参的本质是让连接池在尖峰流量中既不饥饿也不浪费。如果连接池监控显示活跃连接经常打满说明数据库端或SQL端还有瓶颈如果一直空闲则可能是连接释放泄漏或配置过保守。性能调优是系统性的而不是单点自责。缓存让数据库从“拼命三郎”变成“甩手掌柜”数据库再快也快不过内存。很多热点数据比如用户信息、商品配置命中率极高完全没必要每次都去查库。引入Redis作为缓存层查询路径从“应用→DB”变成“应用→Redis→DB”。但缓存不是简单set/get就完事。缓存穿透查询不存在的数据每次都会打到数据库解决办法是缓存空值或布隆过滤器。缓存击穿热key过期瞬间大并发全部打到数据库解决办法是互斥锁或逻辑过期。缓存雪崩大量key同时过期数据库被压垮解决办法是过期时间加随机抖动。别把缓存当避风港用不好它会变成杀手。还有一个反直觉的真相有些查询的复杂度本来很低比如根据主键查一条记录但因为没有缓存在高并发下也会变成压垮数据库的最后一根稻草。性能优化不是消除慢查询而是消灭不必要的查询。缓存的价值就是在数据进入数据库之前把流量拦下来。实战中我更推荐多级缓存本地Caffeine缓存分钟级热点Redis缓存秒级数据数据库兜底让每一层都尽可能拦截。接口响应优化最后一公里的较量数据库和缓存都优化了但接口还是有几百毫秒的开销那就要审视HTTP传输层。第一JSON序列化。Jackson或Gson默认序列化会把日期序列化成时间戳或字符串如果字段冗余响应体能达到几MB。接口返回的每一KB都是用户流量钱包里的钱。应该裁剪字段使用JsonIgnore甚至用自定义序列化器。第二压缩。开启gzip或brotli可以大幅减少传输体积尤其对于移动网络。第三异步化。如果接口内部有发邮件、写日志、调用外部通知等非关键路径应该丢进消息队列或使用CompletableFuture异步执行让主线程立即返回。接口响应时间 关键路径耗时而非所有子任务的耗时总和。另外HTTP连接复用也很重要。如果每次请求都新建TCP甚至重复TLS握手延迟会翻倍。使用HTTP/2或长连接并配置合理的连接池。性能优化最后拼的是细节细节藏在协议层和应用层的缝隙里。接口响应优化是一场从数据库到用户手指的接力赛任何一环掉棒都会前功尽弃。实战复盘一条慢查询的救赎之路让我们回到开头的凌晨两点。那条8秒的SQL长这样SELECT FROM order_info WHERE user_id ? ORDER BY created_at DESC LIMIT 10表里有两千万行user_id上有普通索引EXPLAIN显示typeref但实际每行扫描了数千个订单且ORDER BY created_at触发了Using filesort。问题在于这个查询要按用户取最近10单但索引是(user_id)在用户内部created_at不是有序的。于是我们把索引改为(user_id, created_at)这样既满足等值查询又让created_at天然有序消除了排序。但新的SELECT依然要回表取出完整行耗时不低。进一步优化只查询需要的字段或者把订单摘要冗余到Redis。改造后该SQL耗时从8秒降到20毫秒接口P99从3.2秒降到150毫秒。这个案例告诉我们性能调优不是玄学而是索引、SQL、缓存、系统工程层层递进的结果。每一步都要有数据支撑每一次改动都要压测。没有度量就没有调优没有对比就没有优化。后续我们建立了完整的性能巡检机制慢查询日志周报、索引使用率监控、接口耗时分位数告警。让故障不再半夜拜访。当清晨的太阳升起告警终于平息。你关掉终端知道这场战斗不是靠运气赢的。后端性能调优的本质是减少不必要的计算和等待让每一毫秒都花在刀刃上。从慢查询到接口响应这个链路里没有一劳永逸的银弹只有持续观察、精确制导和反复验证。愿你下次看到告警时手里有枪心里有底。