做了这么多年后端,最怕听到的一句话是什么?
「线上又慢了。」
不是报错,不是崩溃,是慢。慢到用户骂街,慢到业务方发来截图标注「这里卡了3秒钟」。你打开监控一看,CPU不高,内存够用,网络带宽还剩一半——但接口就是慢。
我复盘了上百个线上性能事故,发现大部分慢接口的问题根源出奇一致。今天把这几个「坑王」逐个点名,看看你踩过几个。
坑一:N+1查询,数据中心的「隐形杀手」
什么是N+1?举个例子你就懂了。
你写了一个接口,需要展示订单列表以及每个订单对应的用户名。代码这样写:
// 伪代码
orders = db.query("SELECT * FROM orders LIMIT 20")
for order in orders:
order.username = db.query("SELECT name FROM users WHERE id = ?", order.user_id)
查一次订单列表,再循环查20次用户信息——21次数据库往返,21次网络延迟叠加。这就是经典的N+1问题。
优化方法简单到离谱:
-- 一次搞定,用JOIN
SELECT o.*, u.name as username
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LIMIT 20
一次查询,20行数据,延迟从21次叠加变成1次。线上案例:某电商列表页从平均800ms优化到45ms,就改了这一个SQL。
坑二:连接池?不用?你在逗我
有些新手写数据库代码,每次请求都新建一个连接,用完直接关闭。大概是这样:
def get_user(user_id):
conn = pymysql.connect(host="localhost", user="root", password="123456")
# ... 查询 ...
conn.close() # 关闭连接
MySQL新建一个TCP连接加上认证握手,平均耗时20-50ms。你一个接口如果QPS是100,光建连接就能把你拖垮。
正确做法:连接池,用完放回去,不关闭。
# 以Python为例
from DBUtils import PooledDB
pool = PooledDB(creator=pymysql, maxconnections=20, ...)
def get_user(user_id):
conn = pool.connection()
# ... 查询 ...
conn.close() # 放回池子,不是真的关闭
连接复用,延迟直接砍掉一个数量级。
坑三:JSON序列化——你忽视的性能黑洞
数据查出来了,要返回给前端,就得序列化。很多人的代码是这样的:
import json
def get_orders():
orders = db.query("SELECT ...")
return json.dumps(orders) # Python原生json,慢
Python原生json库的速度在业界是出了名的慢。同样数据量,换个库提升多少?
import orjson
def get_orders():
orders = db.query("SELECT ...")
return orjson.dumps(orders) # 速度是原生json的5-10倍
实测:一个返回2000条记录的接口,换了orjson后P99延迟从120ms降到18ms。不是数据库变快了,是序列化变快了。
其他语言同理:Go用json-iterator,Java用Jackson的afterburner模块,PHP用Symfony的序列化组件——都有更快的替代品,别在序列化上吃哑巴亏。
坑四:没有缓存策略,每次都打数据库
有些接口的数据是相对稳定的,比如商品分类、配置表、用户基础信息。但代码每次都老老实实查库。
正确的做法是分层缓存:
- L1缓存:进程内缓存(Go的sync.Map,Java的Caffeine)
- L2缓存:分布式缓存(Redis)
- L3数据库:最终数据源
经典场景:用户信息查询,读QPS 10000,数据库只有20QPS,剩下9980次全靠Redis扛住。
user = redis.get(f"user:{user_id}")
if not user:
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
redis.setex(f"user:{user_id}", 3600, user) # 缓存1小时
return user
当然,缓存带来了复杂度:缓存穿透、缓存击穿、缓存雪崩。这些话题值得单独开一篇,但比起慢接口,这些问题是「幸福的烦恼」。
坑五:同步阻塞,写代码顺手挖的坑
Node.js或者异步框架里,有些代码表面是异步,实际上在for循环里一个个await——这叫「异步串行」,本质跟同步没区别。
// 慢写法:串行等待
for (const orderId of orderIds) {
const detail = await fetchOrderDetail(orderId); // 每次等100ms
results.push(detail);
}
// 5个订单 = 500ms
正确做法——能并行的绝不串行:
// 快写法:Promise.all并行
const promises = orderIds.map(id => fetchOrderDetail(id));
const results = await Promise.all(promises);
// 5个订单 = 100ms(取最慢的那个)
同样5个接口,500ms变100ms,这个优化不需要加机器,不需要换架构,改几行代码而已。
坑六:日志打太多,I/O拖垮吞吐量
很多人喜欢在每个函数里打日志,info、debug、warn全开。结果呢?
一次请求打了50条日志,每条日志要写磁盘(或者通过网络发到日志收集服务)。高并发下,磁盘I/O成为瓶颈,你的代码反而被日志拖累。
建议:生产环境INFO级日志只打关键节点,DEBUG日志默认关闭,按需开启。日志是用来排查问题的,不是用来证明代码运行过的。
说点实在的:怎么系统性排查慢接口
上面的坑是常见问题,但线上情况复杂,怎么系统性地排查?记住这个顺序:
- 监控先行:先确定慢是普遍现象还是个别case,P50/P95/P99都要看
- 链路追踪:看慢在哪个环节,是数据库、缓存、外部服务,还是自己代码
- 数据库:慢查询日志+执行计划分析,90%的问题出在SQL
- 连接池:看是否有连接等待、连接泄露
- 业务逻辑:是否有不必要的循环、重复计算、同步等待
大多数时候,解决问题不需要什么高大上的技术,就是把SQL写好、把缓存用上、把循环里的串行改并行——三个改动,够你解决80%的慢接口问题了。
总结
慢接口的本质,是资源浪费。你浪费数据库连接、浪费网络往返、浪费CPU周期去反复做同样的事情。
优化这件事,有时候是技术问题,但更多时候是意识和习惯问题。希望这篇文章能让你在下次写代码的时候多想一步:这个查询能不能合并?这个同步能不能改成并行?这个数据要不要加缓存?
想清楚这些,你的接口想慢都难。
有问题欢迎留言,你踩过哪个坑,踩得最惨的那次是什么情况?