《SpringBoot性能调优:从10秒到100毫秒的实战技巧》

27 阅读13分钟

《SpringBoot性能调优:从10秒到100毫秒的实战技巧》

每天5分钟,掌握一个SpringBoot核心知识点。大家好,我是SpringBoot指南的小坏。前几天我们聊了日志、监控、健康检查,今天来点更刺激的——让你的SpringBoot应用飞起来!

一、先看一个真实案例

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

我朋友的公司,去年上线了一个新系统:

上线前测试:响应时间 200毫秒
上线第一天:响应时间 800毫秒
一个月后:响应时间 5秒
三个月后:用户投诉,响应时间 10秒

老板下了死命令:“一周内优化到500毫秒内!”

优化过程

  • 第1天:加缓存 → 降到8秒
  • 第2天:优化SQL → 降到3秒
  • 第3天:加索引 → 降到1秒
  • 第4天:线程池调优 → 降到500毫秒
  • 第5天:JVM调优 → 降到200毫秒

最终效果

  • 服务器从20台降到8台
  • 每月节省服务器成本:6万元
  • 用户满意度从40%提升到95%

今天,我就把这一整套优化方案分享给你!

二、第一步:找到瓶颈在哪

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

优化前先别急着改代码,先搞清楚慢在哪

2.1 最简单的性能测试

pom.xml里加个依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.yml里加配置:

management:
  endpoints:
    web:
      exposure:
        include: metrics,httptrace,prometheus

访问:http://localhost:8080/actuator/metrics/http.server.requests

你会看到类似的数据:

{
  "请求总数": 15234,
  "平均响应时间": "5.2s",  // 太慢了!
  "最大响应时间": "23.1s", // 有接口特别慢
  "错误率": "3.2%"
}

2.2 找出最慢的接口

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。 用这个命令找最慢的接口:

# 查看所有接口响应时间排序
curl http://localhost:8080/actuator/metrics/http.server.requests | jq '.measurements[] | select(.statistic=="TOTAL_TIME")'

通常会发现问题集中在:

  1. 查询用户列表(关联5张表,没分页)
  2. 导出Excel(一次查10万条数据)
  3. 第三方接口调用(没设超时时间)

三、数据库优化:80%的慢都是SQL的锅

3.1 先看慢查询日志

MySQL开启慢查询

-- 查看当前配置
SHOW VARIABLES LIKE '%slow_query%';

-- 开启慢查询日志(生产环境慎用)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 超过1秒就算慢

-- 查看慢查询
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;

3.2 最常见的SQL优化场景

场景1:没有索引

-- ❌ 错误:100万条数据全表扫描
SELECT * FROM users WHERE phone = '13800138000';

-- ✅ 正确:加索引
ALTER TABLE users ADD INDEX idx_phone (phone);
-- 查询时间:从2秒 → 0.02秒

场景2:分页查询太深

-- ❌ 错误:查第10000页
SELECT * FROM orders LIMIT 10000, 20;  -- 扫描10020条

-- ✅ 正确:用id分页
SELECT * FROM orders WHERE id > 10000 ORDER BY id LIMIT 20;
-- 查询时间:从1.5秒 → 0.01秒

场景3:关联查询太多

// ❌ 错误:一次关联5张表
@Query("SELECT o FROM Order o " +
       "JOIN FETCH o.user u " +
       "JOIN FETCH o.address a " +
       "JOIN FETCH o.items i " +
       "JOIN FETCH i.product p " +
       "WHERE o.id = :id")
Order findOrderWithDetails(@Param("id") Long id);

// ✅ 正确:分批查询或只查需要字段
@Query("SELECT new OrderDTO(o.id, o.status, u.name) " +
       "FROM Order o JOIN o.user u " +
       "WHERE o.id = :id")
OrderDTO findOrderBasic(@Param("id") Long id);

3.3 连接池配置

连接池配置不对,性能直接掉一半:

# application.yml
spring:
  datasource:
    hikari:
      # 连接池大小 = (核心数 * 2) + 有效磁盘数
      maximum-pool-size: 20      # 最大连接数
      minimum-idle: 10           # 最小空闲连接
      connection-timeout: 30000  # 连接超时30秒
      idle-timeout: 600000       # 空闲连接10分钟后关闭
      max-lifetime: 1800000      # 连接最大生存时间30分钟
      
      # 性能关键参数
      connection-test-query: SELECT 1  # 测试连接有效性的SQL
      validation-timeout: 5000         # 验证超时5秒

四、缓存优化:让响应快10倍

4.1 加Redis缓存

先加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

最简单的缓存

@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    // 加缓存:商品详情
    public Product getProduct(Long id) {
        String key = "product:" + id;
        
        // 1. 先查缓存
        Product product = redisTemplate.opsForValue().get(key);
        if (product != null) {
            return product;
        }
        
        // 2. 缓存没有,查数据库
        product = productRepository.findById(id).orElse(null);
        if (product != null) {
            // 3. 写入缓存,设置30分钟过期
            redisTemplate.opsForValue().set(
                key, 
                product, 
                30, TimeUnit.MINUTES
            );
        }
        
        return product;
    }
}

效果

  • 第一次查询:数据库 50毫秒
  • 后续查询:Redis 5毫秒
  • 性能提升:10倍!

4.2 Spring Cache注解(更简单)

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Configuration
@EnableCaching  // 开启缓存
public class CacheConfig {
    
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.builder(factory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)))  // 默认30分钟
            .build();
    }
}

// 使用缓存
@Service
public class UserService {
    
    @Cacheable(value = "users", key = "#id")  // 自动缓存
    public User getUser(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    @CachePut(value = "users", key = "#user.id")  // 更新缓存
    public User updateUser(User user) {
        return userRepository.save(user);
    }
    
    @CacheEvict(value = "users", key = "#id")  // 删除缓存
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

4.3 缓存雪崩/穿透/击穿解决方案

@Service
public class SafeCacheService {
    
    // 1. 缓存雪崩:大量缓存同时过期
    // 解决:设置不同的过期时间
    public void setWithRandomExpire(String key, Object value) {
        // 基础30分钟 + 随机0-5分钟
        int randomMinutes = new Random().nextInt(5);
        redisTemplate.opsForValue().set(
            key, value, 
            30 + randomMinutes, TimeUnit.MINUTES
        );
    }
    
    // 2. 缓存穿透:查询不存在的数据
    // 解决:缓存空值
    public Product getProductSafely(Long id) {
        String key = "product:" + id;
        Product product = redisTemplate.opsForValue().get(key);
        
        if (product != null) {
            // 如果是特殊标记的空值,直接返回null
            if (product.getId() == -1) {
                return null;
            }
            return product;
        }
        
        // 查数据库
        product = productRepository.findById(id).orElse(null);
        
        if (product == null) {
            // 缓存空值,防止穿透
            Product emptyMarker = new Product();
            emptyMarker.setId(-1L);
            redisTemplate.opsForValue().set(key, emptyMarker, 5, TimeUnit.MINUTES);
            return null;
        }
        
        // 缓存正常数据
        redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
        return product;
    }
    
    // 3. 缓存击穿:热点key过期
    // 解决:互斥锁
    public Product getProductWithLock(Long id) {
        String key = "product:" + id;
        String lockKey = "lock:product:" + id;
        
        // 先查缓存
        Product product = redisTemplate.opsForValue().get(key);
        if (product != null) {
            return product;
        }
        
        // 尝试获取锁
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        
        try {
            if (locked != null && locked) {
                // 拿到锁,查数据库
                product = productRepository.findById(id).orElse(null);
                if (product != null) {
                    redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
                }
            } else {
                // 没拿到锁,等待100毫秒后重试
                Thread.sleep(100);
                return getProductWithLock(id);
            }
        } finally {
            // 释放锁
            redisTemplate.delete(lockKey);
        }
        
        return product;
    }
}

五、JVM调优:让内存更聪明

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

5.1 查看当前JVM状态

启动时加参数:

java -jar app.jar -Xmx2g -Xms2g -XX:+PrintGCDetails

或者用命令行工具:

# 查看进程
jps -l

# 查看堆内存
jmap -heap <pid>

# 查看GC情况
jstat -gc <pid> 1000 10  # 每秒看一次,看10次

5.2 常用JVM参数

# 启动脚本 start.sh
#!/bin/bash

java -jar app.jar \
  -Xmx2g -Xms2g \           # 堆内存2G
  -Xmn1g \                  # 新生代1G
  -XX:MaxMetaspaceSize=256m \  # 元空间256M
  -XX:MaxDirectMemorySize=256m \  # 直接内存256M
  -XX:+UseG1GC \            # 使用G1垃圾回收器
  -XX:MaxGCPauseMillis=200 \  # 目标停顿时间200ms
  -XX:+PrintGCDetails \     # 打印GC详情
  -XX:+PrintGCDateStamps \
  -Xloggc:logs/gc.log \     # GC日志文件
  -XX:+HeapDumpOnOutOfMemoryError \  # OOM时生成dump
  -XX:HeapDumpPath=logs/heapdump.hprof

5.3 内存泄漏排查

如果发现内存一直增长,可能是内存泄漏:

# 1. 生成堆内存快照
jmap -dump:live,format=b,file=heap.hprof <pid>

# 2. 用MAT工具分析(下载地址:https://eclipse.dev/mat/)
# 打开heap.hprof,看哪个对象占用最多

常见内存泄漏场景

  1. 静态集合static Map 一直往里放对象
  2. 连接未关闭:数据库连接、文件流
  3. 监听器未注销:注册了监听器但没移除
  4. 内部类引用外部类:匿名内部类持有外部类引用

六、异步处理:别让用户等你

6.1 线程池配置

@Configuration
public class ThreadPoolConfig {
    
    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数 = CPU核心数
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        // 最大线程数 = 核心数 * 2
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        // 队列容量
        executor.setQueueCapacity(1000);
        // 线程名前缀
        executor.setThreadNamePrefix("async-task-");
        // 拒绝策略:调用者运行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

// 使用异步
@Service
public class OrderService {
    
    @Async("taskExecutor")  // 指定线程池
    public CompletableFuture<Order> createOrderAsync(OrderRequest request) {
        // 耗时操作:发邮件、发短信、写日志
        sendEmail(request.getEmail());
        sendSms(request.getPhone());
        
        // 返回结果
        Order order = processOrder(request);
        return CompletableFuture.completedFuture(order);
    }
}

6.2 @Async常见问题

问题1:同一个类内调用不生效

// ❌ 错误:同一个类内调用
@Service
public class UserService {
    
    public void updateUser(User user) {
        // 同一个类内调用,不会异步
        sendNotification(user);
    }
    
    @Async
    public void sendNotification(User user) {
        // 不会异步执行!
    }
}

// ✅ 正确:通过代理调用
@Service
public class UserService {
    
    @Autowired
    private UserService self;  // 注入自己
    
    public void updateUser(User user) {
        // 通过代理调用
        self.sendNotification(user);  // 这会异步执行
    }
    
    @Async
    public void sendNotification(User user) {
        // 异步执行
    }
}

问题2:需要等待所有异步任务完成

@Service
public class BatchService {
    
    @Async
    public CompletableFuture<String> processItem(String item) {
        // 处理单个项目
        return CompletableFuture.completedFuture("processed:" + item);
    }
    
    public void processAll(List<String> items) {
        List<CompletableFuture<String>> futures = items.stream()
            .map(this::processItem)
            .collect(Collectors.toList());
        
        // 等待所有任务完成
        CompletableFuture<Void> allDone = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        );
        
        // 获取所有结果
        List<String> results = allDone.thenApply(v -> 
            futures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList())
        ).join();
        
        System.out.println("处理完成,共" + results.size() + "条");
    }
}

七、HTTP优化:网络传输更高效

7.1 启用Gzip压缩

# application.yml
server:
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
    min-response-size: 1024  # 大于1KB才压缩

效果

  • JSON响应大小:从100KB → 20KB
  • 传输时间:减少80%

7.2 连接池配置

# RestTemplate连接池
http:
  client:
    max-total: 200           # 最大连接数
    default-max-per-route: 50 # 每个路由最大连接数
    connect-timeout: 5000    # 连接超时5秒
    read-timeout: 10000      # 读取超时10秒
    
# Feign客户端配置
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 10000
        loggerLevel: basic

7.3 启用HTTP/2

server:
  http2:
    enabled: true

HTTP/2优势

  • 多路复用:一个连接同时传输多个请求
  • 头部压缩:减少传输数据量
  • 服务器推送:服务端可以主动推送资源

八、实战案例:电商系统优化全流程

假设有一个查询用户订单的接口:

优化前:15秒

// 优化前代码
@GetMapping("/user/{userId}/orders")
public List<Order> getUserOrders(@PathVariable Long userId) {
    // 1. 查用户信息(1秒)
    User user = userRepository.findById(userId);
    
    // 2. 查所有订单(5秒,没分页)
    List<Order> orders = orderRepository.findByUserId(userId);
    
    // 3. 查每个订单的商品(N+1查询)
    for (Order order : orders) {
        List<OrderItem> items = orderItemRepository.findByOrderId(order.getId());
        order.setItems(items);
    }
    
    // 4. 查每个商品的详情(又是N+1)
    for (Order order : orders) {
        for (OrderItem item : order.getItems()) {
            Product product = productRepository.findById(item.getProductId());
            item.setProduct(product);
        }
    }
    
    return orders;  // 总耗时:15秒!
}

优化后:200毫秒

// 优化后代码
@Service
public class OrderService {
    
    @Cacheable(value = "userOrders", key = "#userId + ':' + #page + ':' + #size")
    public Page<OrderVO> getUserOrders(Long userId, int page, int size) {
        // 1. 只查需要字段 + 分页(100毫秒 → 10毫秒)
        Page<Order> orders = orderRepository.findSimpleOrders(userId, PageRequest.of(page, size));
        
        // 2. 批量查询商品,避免N+1(5秒 → 50毫秒)
        List<Long> productIds = orders.stream()
            .flatMap(order -> order.getItems().stream())
            .map(OrderItem::getProductId)
            .distinct()
            .collect(Collectors.toList());
        
        Map<Long, Product> productMap = productRepository.findByIdIn(productIds)
            .stream()
            .collect(Collectors.toMap(Product::getId, p -> p));
        
        // 3. 组装VO,不返回全部字段(传输数据减少80%)
        return orders.map(order -> convertToVO(order, productMap));
    }
    
    @Async
    @CacheEvict(value = "userOrders", key = "#userId + '*'")  // 清除相关缓存
    public void refreshUserOrders(Long userId) {
        // 异步刷新缓存
    }
}

// 对应的SQL优化
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    // 优化前:SELECT * FROM orders WHERE user_id = ?
    // 优化后:只查需要的字段 + 分页
    @Query("SELECT new OrderSummary(o.id, o.orderNo, o.totalAmount, o.status) " +
           "FROM Order o WHERE o.userId = :userId")
    Page<OrderSummary> findSimpleOrders(@Param("userId") Long userId, Pageable pageable);
    
    // 批量查询,避免N+1
    @Query("SELECT p FROM Product p WHERE p.id IN :ids")
    List<Product> findByIdIn(@Param("ids") List<Long> ids);
}

优化总结

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

优化点优化前优化后提升
数据库查询N+1查询,15秒批量查询,150毫秒100倍
数据传输返回全部字段只返回需要字段减少80%
缓存无缓存Redis缓存热数据0毫秒
分页无分页分页查询内存减少90%
异步同步处理异步刷新响应时间0毫秒

九、性能优化检查清单

每次上线前,检查这10项:

  1. ✅ SQL是否有索引?
    执行时间超过100ms的SQL都要看

  2. ✅ 是否避免N+1查询?
    JOIN FETCH或批量查询

  3. ✅ 是否合理分页?
    列表接口必须分页,默认每页20条

  4. ✅ 是否加了缓存?
    频繁查询的数据加Redis缓存

  5. ✅ 缓存是否安全?
    防雪崩、穿透、击穿

  6. ✅ 线程池配置是否合理?
    核心线程数 = CPU核心数

  7. ✅ 异步任务是否处理?
    发邮件、发短信等走异步

  8. ✅ JVM参数是否调优?
    堆内存、GC算法设置

  9. ✅ HTTP是否压缩?
    启用Gzip压缩

  10. ✅ 慢查询是否有监控?
    监控接口响应时间,超过1秒告警

十、今日思考题

场景:你的电商系统有一个"猜你喜欢"功能,需要:

  1. 根据用户历史行为推荐商品
  2. 每次推荐100个商品
  3. 响应时间要求在200毫秒内
  4. 每天有1000万次请求

问题

  1. 你会如何设计这个系统?
  2. 数据库、缓存、算法如何配合?
  3. 如何保证高性能和高可用?

在评论区分享你的架构设计,点赞最高的送《高性能MySQL》+《Redis设计与实现》纸质书!


明天预告:《SpringBoot安全防护:防止你的应用被黑客"光顾"》—— 从认证授权到防攻击,一次性讲清楚!

性能优化工具包:关注公众号回复"性能优化",获取SQL优化脚本、JVM调优模板、性能测试工具!


掘金小贴士:

💡 互动设计

  1. 话题讨论:你做过最成功的性能优化是什么?
  2. 投票:你们系统最慢的接口响应时间是多少?
  3. 经验征集:分享你的调优经验,抽3位送《Java性能权威指南》

🎁 粉丝福利

  1. 关注后回复"调优工具",获取性能监控工具包
  2. 转发到3个技术群,截图领《高并发系统设计》纸质书
  3. 评论区抽奖:送5个《阿里Java开发手册》实体版

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。