系统性能优化记录

引言

最近花了差不多两周时间给项目做了一次全面的性能优化,总算把接口响应时间从平均 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;

statuscreated_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. 优化是渐进的过程。不太可能一步到位,先做收益最大的改动,再逐步精细化。

系统性能优化是个持续的过程,这次改完后面还要持续监控,有问题再调整。希望这个记录对大家有帮助,有什么问题欢迎评论区交流。