接口响应慢是后端开发中最常见的问题之一。本文从实际案例出发,记录一次用户查询接口从 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 ) { 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 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 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 logging: level: com.example.mapper: DEBUG
SQL 分析与索引优化 大多数接口慢的原因都是 SQL。
问题 SQL 定位 1 2 3 4 5 6 7 SET GLOBAL slow_query_log = 'ON' ;SET GLOBAL long_query_time = 0.5 ; 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_idWHERE u.status = 1 AND o.create_time > '2024-01-01' ;
常见索引失效场景 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 SELECT * FROM orders WHERE YEAR (create_time) = 2024 ;SELECT * FROM orders WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01' ;SELECT * FROM users WHERE name LIKE '%张%' ; SELECT * FROM users WHERE name LIKE '张%' ; SELECT * FROM users WHERE id = 1 OR phone = '13800000000' ;SELECT * FROM users WHERE id = 1 UNION SELECT * FROM users WHERE phone = '13800000000' ;SELECT * FROM users WHERE phone = 13800000000 ; 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 SELECT u.id, u.name, u.phone, o.order_count, o.total_amountFROM users uLEFT 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 ;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 ordersWHERE 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 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 Duration.ofMinutes(30 + random.nextInt(10 )) 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 spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 300000 max-lifetime: 1200000 connection-timeout: 30000 pool-name: UserHikariPool
连接数计算公式
例如: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 ; } 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); }
问题分析:
4 次数据库查询
无缓存
串行执行
优化后的代码 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); UserVO cached = getFromCache(key); if (cached != null ) { return cached; } UserVO userVO = userService.getUserDetail(id); 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) { User user = userMapper.selectById(userId); if (user == null ) { return null ; } 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(); 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 <select id ="selectById" resultType ="com.example.User" > SELECT * FROM users WHERE id = #{id} </select > <select id ="selectByUserId" resultType ="com.example.Order" > SELECT * FROM orders WHERE user_id = #{userId} </select > <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)
总结 接口优化的一般步骤:
定位瓶颈 :日志、Arthas、EXPLAIN
SQL 优化 :索引、SQL 改写
缓存 :Redis,减少数据库访问
异步 :CompletableFuture 并行查询
连接池 :合理配置 HikariCP
验证 :压测确认优化效果
优化时注意:
先定位再优化,不要猜测
小步迭代,每次只做一个改动
记录优化前后的数据对比