系统性能优化记录
引言
最近花了差不多两周时间给项目做了一次全面的性能优化,总算把接口响应时间从平均 800ms 降到了 200ms 左右。过程踩了不少坑,今天正好整理一下,记录下这次优化的思路和具体做法,希望能给有类似问题的朋友一些参考。
一、问题定位:先找到瓶颈在哪
刚接手的时候,团队反馈系统访问量稍微上来一点就卡得不行。我第一反应不是直接改代码,而是先弄清楚问题出在哪里。
我用了几个常用的排查工具:
- SkyWalking 做全链路追踪,看请求在各环节的耗时
- Arthas 实时观察方法执行情况
- MySQL Slow Query Log 抓慢查询
这一查不要紧,发现问题还挺严重的。一个看似简单的用户列表接口,数据库查询居然跑了 600ms,里面有 N+1 查询问题,还有不必要的全表扫描。定位到问题后,后面的优化就有方向了。
二、数据库优化:最常见的性能瓶颈
1. 索引优化
首先看查询有没有走索引。拿那个用户列表接口来说,原来的 SQL 大概是这样的:
SELECT * FROM users
WHERE status = 1
ORDER BY created_at DESC
LIMIT 20;
status 和 created_at 两个字段单独都有索引,但 WHERE 条件和 ORDER BY 组合起来就没法高效使用索引了。后来加了联合索引:
ALTER TABLE users ADD INDEX idx_status_created (status, created_at);
这一下查询时间从 600ms 降到了 80ms,效果挺明显的。
2. 解决 N+1 查询
还有个典型问题就是 N+1 查询。查用户列表的时候,每个用户还要单独查他的角色信息、权限信息,导致 20 条数据的列表发了 60 多条 SQL。
// 优化前
List<User> users = userMapper.selectByStatus(1);
for (User user : users) {
List<Role> roles = roleMapper.selectByUserId(user.getId());
user.setRoles(roles);
}
改成批量查询或者用 JOIN:
// 优化后 - 批量查询
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
Map<Long, List<Role>> roleMap = roleMapper.selectByUserIds(userIds)
.stream()
.collect(Collectors.groupingBy(Role::getUserId));
for (User user : users) {
user.setRoles(roleMap.getOrDefault(user.getId(), Collections.emptyList()));
}
3. 分页优化
对于大数据量的列表查询,用了游标分页代替传统的 OFFSET 分页:
// 传统 OFFSET - 页数越大越慢
SELECT * FROM users LIMIT 100000, 20;
// 游标分页 - 性能稳定
SELECT * FROM users
WHERE id < #{lastId}
ORDER BY id DESC
LIMIT 20;
三、缓存策略:减少数据库压力
数据库优化做完以后,响应时间降到了 200ms 左右。但我发现有些数据查询频率特别高,几乎不变,完全可以缓存起来。
1. 本地缓存
对于配置数据、字典表这些几乎不变的内容,用 Caffeine 做了本地缓存:
Cache<String, DictData> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
public DictData getDict(String type, String code) {
String key = type + ":" + code;
return cache.get(key, () -> dictMapper.selectByTypeAndCode(type, code));
}
2. 分布式缓存
用户权限信息这种需要跨服务共享的数据,用 Redis 做缓存。这里有个小技巧,就是注意缓存的失效策略:
public UserDetail getUserDetail(Long userId) {
String cacheKey = "user:detail:" + userId;
// 先查缓存
UserDetail cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 缓存未命中,查数据库
UserDetail userDetail = userMapper.selectDetailById(userId);
// 写入缓存,设置合理过期时间
if (userDetail != null) {
redisTemplate.opsForValue().set(cacheKey, userDetail, 30, TimeUnit.MINUTES);
}
return userDetail;
}
四、代码层面的优化
1. 异步处理
有些非核心的逻辑其实不需要同步执行,比如:
- 记录操作日志
- 发送通知消息
- 更新统计指标
用 CompletableFuture 改成异步执行,主流程不受影响:
public void submitOrder(Order order) {
// 主流程:保存订单
orderMapper.insert(order);
// 异步执行:记录日志
CompletableFuture.runAsync(() -> {
logService.saveOrderLog(order);
});
// 异步执行:发送通知
CompletableFuture.runAsync(() -> {
notificationService.sendOrderNotify(order);
});
}
2. 对象池复用
对于频繁创建销毁的对象,比如解析 JSON 用的 ObjectMapper,用对象池来复用:
ObjectMapper mapper = ObjectMapperFactory.getInstance();
避免每次请求都 new 一个,减少 GC 压力。
五、JVM 调优:最后的优化手段
前面几个优化做完,接口响应时间基本稳定在 150ms 左右了。但我注意到服务偶尔会有卡顿,GC 时间偏长。
看了一下 GC 日志,发现年轻代太小,频繁发生 Minor GC。后来调整了 JVM 参数:
-Xms2g -Xmx2g
-XX:NewRatio=1
-XX:SurvivorRatio=8
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
G1 垃圾收集器对响应时间敏感的业务比较友好,配合合理的停顿时间目标,整体流畅度提升了不少。
总结
这次优化做下来,有几点体会:
1. 先定位问题再动手。不要凭感觉优化,用数据说话,找到真正的瓶颈再针对性处理。
2. 数据库通常是最短木板。大部分性能问题都出在数据库查询上,索引、N+1、慢查询这些基础问题先排查一遍。
3. 缓存要合理使用。不是所有数据都要缓存,要考虑数据变更频率和缓存成本。
4. 优化是渐进的过程。不太可能一步到位,先做收益最大的改动,再逐步精细化。
系统性能优化是个持续的过程,这次改完后面还要持续监控,有问题再调整。希望这个记录对大家有帮助,有什么问题欢迎评论区交流。
系统性能优化记录
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法