最近项目上线一个新功能,用户反馈点一下卡三秒,领导一看监控,接口平均响应时间从200ms飙到1.2s,立马开了个会:"这不行啊,得优化。"
我一查日志,好家伙,一个接口查了17次数据库,还全是单条查询,循环里套SQL。
于是,我开始了一波接口性能优化的折腾,总结下来,还真有几个小技巧挺管用的,分享出来,都是实战踩坑经验。
1. 能一次查的,别分好几次查
场景:用户详情页要显示他最近5个订单,原本是先查用户,再循环查5次订单。
问题:5次数据库查询,网络来回开销大,连接池压力也大。
优化 :直接用IN
语句一次查出来。
sql
-- 优化前(循环5次)
SELECT * FROM orders WHERE id = 1;
SELECT * FROM orders WHERE id = 2;
...
-- 优化后
SELECT * FROM orders WHERE id IN (1,2,3,4,5);
效果:接口从800ms降到200ms。
2. 别在循环里查数据库
场景:批量处理用户信息,每个用户都要查一下他的积分等级。
代码长这样:
java
for (User user : users) {
Grade grade = gradeService.getGradeByUserId(user.getId());
user.setGrade(grade);
}
问题:100个用户就查100次数据库,慢得离谱。
优化:先把所有用户ID拿出来,一次性查,再用Map匹配。
java
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
Map<Long, Grade> gradeMap = gradeService.getGradesByUserIds(userIds);
for (User user : users) {
user.setGrade(gradeMap.get(user.getId()));
}
效果:从3秒降到400ms。
3. 大数据量别一次性捞上来
场景 :导出10万条订单数据,直接SELECT * FROM orders
,内存直接爆了。
问题:数据库压力大,应用内存溢出。
优化:分页查询+流式处理。
java
// 用游标,一条条处理,不全加载到内存
try (Cursor<Order> cursor = orderMapper.selectAllWithCursor()) {
for (Order order : cursor) {
exportToExcel(order);
}
}
效果:导出顺利了,内存稳定,就是硬盘写得响。
4. 加Redis缓存
场景:商品详情页,每次打开都查数据库,QPS一高,数据库CPU直接90%+。
优化:加Redis缓存,第一次查库,后面走缓存。
java
String cacheKey = "product:" + productId;
Product product = redis.get(cacheKey);
if (product == null) {
product = db.getProductById(productId);
redis.setex(cacheKey, 300, product); // 缓存5分钟
}
注意:缓存穿透、雪崩也得防,比如空值缓存、随机过期时间。
效果:接口从300ms降到50ms,数据库压力小了大半。
5. 接口返回别啥都给
场景:用户中心接口返回了用户所有信息,包括积分、等级、历史订单、偏好设置......前端只用了头像和昵称。
问题:数据传输大,序列化慢,网络耗时。
优化:按需返回,或者提供字段过滤参数。
json
// 前端加个参数
GET /user/123?fields=name,avatar
后端只查需要的字段,减少IO和传输。
效果:响应大小从20KB降到2KB,移动端用户都说"快了"。
6. 异步能救急
场景:用户注册后要发邮件、发短信、记录日志、同步数据到其他系统......全都同步执行,注册接口要等5秒。
优化:非核心流程扔进消息队列或线程池异步处理。
java
// 注册成功后
userService.register(user);
// 丢进线程池,不等
asyncTaskExecutor.submit(() -> {
sendWelcomeEmail(user);
syncToCRM(user);
logRegisterEvent(user);
});
效果:注册接口从5秒降到300ms,用户体验立竿见影。
7. 合理使用索引,但别过度
场景 :订单列表按时间查,created_at
没索引,每次都是全表扫描。
优化 :给created_at
加索引。
sql
ALTER TABLE orders ADD INDEX idx_created_at (created_at);
注意:索引不是越多越好,写操作会变慢,占用空间。
效果:查询从2秒降到50ms。
8. 避免大事务
场景:一个接口里更新500条记录,全包在一个事务里,锁表时间长,别人查都查不了。
优化:拆成小批次提交,比如每50条提交一次。
java
for (int i = 0; i < list.size(); i++) {
updateOneRecord(list.get(i));
if (i % 50 == 0) {
sqlSession.commit();
}
}
效果:没人再抱怨数据库卡死了。
9. 日志别打太多
场景:每个请求都把入参、出参、中间变量全打成DEBUG日志。
问题:磁盘IO高,写日志拖慢接口。
优化:生产环境关掉DEBUG,关键信息打INFO,异常才打堆栈。
java
// 别这么干
log.debug("请求参数: {}", hugeJson);
log.debug("处理结果: {}", hugeResult);
// 改成
log.info("用户 {} 下单成功, 订单ID: {}", userId, orderId);
效果:日志文件小了80%,机器负载也降了。
最后
优化不是一蹴而就的,我那个卡三秒的接口,最后结合了缓存、批量查询、异步处理,硬是压到了200ms以内。
领导看了监控图,点点头:"还行。"
我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!