Java 后端接口性能优化:从响应 500ms 优化到 50ms

接口响应慢是后端开发中最常见的问题之一。本文从实际案例出发,记录一次用户查询接口从 500ms 优化到 50ms 的全过程,涵盖 SQL 优化、缓存、索引等关键技术点。

性能瓶颈定位方法

优化前必须先定位瓶颈。盲目优化是浪费时间。

1. 添加性能日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j
@Component
public class PerformanceInterceptor implements HandlerInterceptor {

@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;

if (duration > 100) { // 超过 100ms 记录日志
log.warn("慢接口: {} {}, 耗时: {}ms, 参数: {}",
request.getMethod(),
request.getRequestURI(),
duration,
request.getParameterMap());
}
}

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
}

2. Arthas 线上诊断

1
2
3
4
5
6
7
8
9
10
11
# 启动 Arthas
java -jar arthas-boot.jar

# 查看最慢的方法调用
dashboard -n 10

# 追踪方法执行时间
trace com.example.UserService getUserDetail

# 查看方法调用堆栈
stack com.example.UserService getUserDetail

3. Spring Boot Actuator

1
2
3
4
5
6
7
8
9
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 关键路径埋点
@RestController
public class UserController {

@GetMapping("/users/{id}")
public UserVO getUser(@PathVariable Long id) {
long start = System.currentTimeMillis();
try {
return userService.getUserDetail(id);
} finally {
log.info("getUser cost: {}ms", System.currentTimeMillis() - start);
}
}
}

4. MyBatis SQL 日志

1
2
3
4
# application.yml
logging:
level:
com.example.mapper: DEBUG # 打印 SQL 及参数

SQL 分析与索引优化

大多数接口慢的原因都是 SQL。

问题 SQL 定位

1
2
3
4
5
6
7
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5; -- 超过 500ms 记录

-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log%';
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;

EXPLAIN 分析

1
2
3
4
5
6
7
8
9
EXPLAIN SELECT u.*, o.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 1 AND o.create_time > '2024-01-01';

-- 关键字段:
-- type: ALL(全表) < index < range < ref < eq_ref < const
-- key: 实际使用的索引
-- rows: 扫描行数,越少越好
-- Extra: Using filesort/Using temporary 需要优化

常见索引失效场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 1. 函数/计算导致索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2024;
-- 应改为
SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';

-- 2. LIKE 前缀匹配
SELECT * FROM users WHERE name LIKE '%张%'; -- 索引失效
SELECT * FROM users WHERE name LIKE '张%'; -- 索引有效

-- 3. OR 导致索引断裂
SELECT * FROM users WHERE id = 1 OR phone = '13800000000';
-- 应改为 UNION
SELECT * FROM users WHERE id = 1
UNION
SELECT * FROM users WHERE phone = '13800000000';

-- 4. 隐式类型转换
SELECT * FROM users WHERE phone = 13800000000; -- phone 是 varchar,索引失效
SELECT * FROM users WHERE phone = '13800000000'; -- 正确

优化后的 SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 原始:500ms
SELECT u.id, u.name, u.phone, o.order_count, o.total_amount
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount
FROM orders
WHERE status = 1
GROUP BY user_id
) o ON u.id = o.user_id
WHERE u.status = 1;

-- 优化后:50ms(添加索引 + 优化 SQL)
ALTER TABLE orders ADD INDEX idx_status_user (status, user_id);

-- 改写为两步查询,避免子查询
SELECT id, name, phone FROM users WHERE status = 1; -- 索引支持
SELECT user_id, COUNT(*), SUM(amount)
FROM orders
WHERE status = 1 AND user_id IN (1,2,3...) -- 应用层传入
GROUP BY user_id;

Redis 缓存策略

缓存是性能优化的利器。

缓存数据结构设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Service
public class UserCache {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String USER_KEY = "user:%d";

public User getUser(Long userId) {
String key = String.format(USER_KEY, userId);

// 先查缓存
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}

// 缓存未命中,查数据库
User user = userMapper.selectById(userId);

// 写入缓存,设置过期时间
if (user != null) {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
Duration.ofMinutes(30));
}

return user;
}

public void evictUser(Long userId) {
redisTemplate.delete(String.format(USER_KEY, userId));
}
}

缓存问题与解决方案

缓存穿透

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 问题:大量请求不存在的数据
public User getUser(Long userId) {
String key = String.format(USER_KEY, userId);
String cached = redisTemplate.opsForValue().get(key);

if (cached != null) {
return "NULL".equals(cached) ? null : JSON.parseObject(cached, User.class);
}

User user = userMapper.selectById(userId);

// 缓存空值,防止穿透
if (user == null) {
redisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));
} else {
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
Duration.ofMinutes(30));
}

return user;
}

缓存击穿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 问题:热点 key 过期瞬间,大量请求打到数据库
public User getUser(Long userId) {
String key = String.format(USER_KEY, userId);

// 尝试获取锁
String lockKey = "lock:user:" + userId;
String lockValue = redisTemplate.opsForValue().get(lockKey);

if (lockValue == null) {
try {
// 尝试加锁
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));

if (Boolean.TRUE.equals(acquired)) {
// 获取锁成功,查数据库
User user = loadAndCacheUser(userId);
return user;
} else {
// 等待后重试
Thread.sleep(50);
return getUser(userId);
}
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}

// 直接查缓存
String cached = redisTemplate.opsForValue().get(key);
return cached != null ? JSON.parseObject(cached, User.class) : null;
}

缓存雪崩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 问题:大量缓存同时过期
// 解决1:过期时间加随机值
Duration.ofMinutes(30 + random.nextInt(10))

// 解决2:永不过期 + 异步更新
public User getUser(Long userId) {
String key = String.format(USER_KEY, userId);
String cached = redisTemplate.opsForValue().get(key);

if (cached != null) {
User user = JSON.parseObject(cached, User.class);
// 如果接近过期,异步更新
if (user.getCacheTime() < System.currentTimeMillis() - 25 * 60 * 1000) {
asyncRefreshCache(userId);
}
return user;
}

return loadAndCacheUser(userId);
}

数据库连接池调优

连接池配置不当也会影响性能。

HikariCP 配置

1
2
3
4
5
6
7
8
9
10
# application.yml
spring:
datasource:
hikari:
minimum-idle: 5 # 最小空闲连接
maximum-pool-size: 20 # 最大连接数
idle-timeout: 300000 # 空闲超时 5 分钟
max-lifetime: 1200000 # 最大生命周期 20 分钟
connection-timeout: 30000 # 获取连接超时 30 秒
pool-name: UserHikariPool

连接数计算公式

1
连接数 = ((核心数 * 2) + 磁盘数)

例如:4 核 CPU + 1 块 SSD = 9,建议 maximum-pool-size=10~20

监控连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@Component
public class HikariPoolMonitor {

@Scheduled(fixedRate = 60000)
public void monitor() {
HikariPool pool = (HikariPool) ((DataSource) dataSource).getHikariPoolMXBean();

log.info("HikariPool - Active: {}, Idle: {}, Waiting: {}, " +
"Total: {}, Max: {}",
pool.getActiveConnections(),
pool.getIdleConnections(),
pool.getThreadsAwaitingConnection(),
pool.getTotalConnections(),
pool.getMaxConnections());
}
}

实战:用户接口优化全过程

优化前的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping("/users/{id}")
public UserVO getUser(@PathVariable Long id) {
User user = userMapper.selectById(id);
if (user == null) {
return null;
}

// N+1 查询:查完用户再查订单
List<Order> orders = orderMapper.selectByUserId(id);
List<OrderVO> orderVOs = orders.stream()
.map(orderMapper::toVO)
.collect(Collectors.toList());

// 逐个查商品
for (OrderVO orderVO : orderVOs) {
Product product = productMapper.selectById(orderVO.getProductId());
orderVO.setProductName(product.getName());
}

return toUserVO(user, orderVOs);
}

问题分析:

  1. 4 次数据库查询
  2. 无缓存
  3. 串行执行

优化后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@GetMapping("/users/{id}")
public UserVO getUser(@PathVariable Long id) {
return userCache.getUser(id);
}

@Service
public class UserCache {

@Autowired
private UserService userService;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private static final String USER_KEY = "user:%d";
private static final Duration CACHE_TIME = Duration.ofMinutes(30);

public UserVO getUser(Long userId) {
String key = String.format(USER_KEY, userId);

// 1. 查缓存
UserVO cached = getFromCache(key);
if (cached != null) {
return cached;
}

// 2. 缓存未命中,查数据库
UserVO userVO = userService.getUserDetail(id);

// 3. 写入缓存
if (userVO != null) {
redisTemplate.opsForValue().set(key, userVO, CACHE_TIME);
}

return userVO;
}
}

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

@Autowired
private OrderMapper orderMapper;

@Autowired
private ProductMapper productMapper;

@Transactional(readOnly = true)
public UserVO getUserDetail(Long userId) {
// 1. 查用户
User user = userMapper.selectById(userId);
if (user == null) {
return null;
}

// 2. 并行查订单和商品(使用 CompletableFuture)
CompletableFuture<List<Order>> ordersFuture = CompletableFuture
.supplyAsync(() -> orderMapper.selectByUserId(userId));

List<Product> products = productMapper.selectByUserId(userId);
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getId, p -> p));

// 等待订单完成
List<Order> orders = ordersFuture.join();

// 3. 组装结果
List<OrderVO> orderVOs = orders.stream()
.map(order -> {
OrderVO vo = orderMapper.toVO(order);
Product product = productMap.get(order.getProductId());
if (product != null) {
vo.setProductName(product.getName());
}
return vo;
})
.collect(Collectors.toList());

return toUserVO(user, orderVOs);
}
}

SQL 优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- UserMapper.xml -->
<select id="selectById" resultType="com.example.User">
SELECT * FROM users WHERE id = #{id}
</select>

<!-- OrderMapper.xml:一次查询订单 -->
<select id="selectByUserId" resultType="com.example.Order">
SELECT * FROM orders WHERE user_id = #{userId}
</select>

<!-- ProductMapper.xml:一次 IN 查询 -->
<select id="selectByUserId" resultType="com.example.Product">
SELECT p.* FROM products p
INNER JOIN orders o ON p.id = o.product_id
WHERE o.user_id = #{userId}
</select>

优化效果

指标 优化前 优化后
响应时间 500ms 50ms
数据库查询次数 4 次 3 次
缓存命中率 0% >90%
并行执行 是(CompletableFuture)

总结

接口优化的一般步骤:

  1. 定位瓶颈:日志、Arthas、EXPLAIN
  2. SQL 优化:索引、SQL 改写
  3. 缓存:Redis,减少数据库访问
  4. 异步:CompletableFuture 并行查询
  5. 连接池:合理配置 HikariCP
  6. 验证:压测确认优化效果

优化时注意:

  • 先定位再优化,不要猜测
  • 小步迭代,每次只做一个改动
  • 记录优化前后的数据对比