常见的性能优化策略

常见的性能优化策略

性能优化之前我们需要对请求经历的各个环节进行分析,排查出可能出现性能瓶颈的地方,定位问题。


一、性能优化自检清单

1. SQL 语句是否存在问题?

核心原则: 避免慢 SQL,减少数据库层面的瓶颈。

常见问题与示例:

  • 避免 SELECT * 只查询需要的字段,减少数据传输量和内存占用。
    -- 反例
    SELECT * FROM orders WHERE user_id = 1001;
    -- 正例
    SELECT order_id, amount, status FROM orders WHERE user_id = 1001;
    
  • 避免在 WHERE 子句中对索引列使用函数或运算: 这会导致索引失效,退化为全表扫描。
    -- 反例:索引失效
    SELECT * FROM users WHERE YEAR(create_time) = 2024;
    -- 正例:利用索引范围扫描
    SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
    
  • 避免大表的 JOIN 和子查询: 大表关联会生成临时表,占用大量内存和磁盘。
    -- 反例:多表嵌套子查询
    SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE city = 'Beijing');
    -- 正例:改用 JOIN 或 EXISTS
    SELECT o.* FROM orders o JOIN users u ON o.user_id = u.id WHERE u.city = 'Beijing';
    
  • 分页深度优化: OFFSET 越大越慢,可用游标分页替代。
    -- 反例:深分页性能极差
    SELECT * FROM orders ORDER BY id LIMIT 10 OFFSET 1000000;
    -- 正例:记录上次最大 ID,用游标方式分页
    SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
    
  • 批量操作替代逐条操作: 循环单条插入/更新会产生大量网络 IO。
    -- 反例
    INSERT INTO logs (msg) VALUES ('a');
    INSERT INTO logs (msg) VALUES ('b');
    -- 正例:批量插入
    INSERT INTO logs (msg) VALUES ('a'), ('b'), ('c');
    
  • 使用 EXPLAIN 分析执行计划: 上线前用 EXPLAIN 检查 SQL 的扫描方式、是否命中索引、预估行数等。

2. 是否需要升级硬件?

核心原则: 硬件升级是"兜底方案",在软件优化做到位后才考虑。

常见升级场景与示例:

  • 内存升级: 数据库 Buffer Pool 太小导致频繁磁盘 IO(如 MySQL 的 innodb_buffer_pool_size 建议设为物理内存的 60%-80%)。
  • CPU 核心数增加: 计算密集型任务(如图片压缩、视频转码)单机处理不过来,考虑增加核心数或使用更高主频的 CPU。
  • 磁盘升级: 机械硬盘换 SSD 可以将随机读写 IOPS 从 ~200 提升到 50000+,对数据库性能提升巨大。
  • 网络带宽: 微服务之间调用频繁、数据量大时,万兆网卡或内网专线可以显著降低网络延迟。

3. 系统是否需要缓存?

核心原则: 缓存是用空间换时间,适合读多写少的场景。

常见缓存策略与示例:

  • 本地缓存(进程内): 适用于单机高频读取的热数据。
    // Guava Cache 示例
    LoadingCache<String, User> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> userService.queryById(key));
    
  • 分布式缓存(Redis): 适用于多实例共享的缓存,如 Session、验证码、排行榜。
    // Spring Redis 示例
    @Cacheable(value = "user", key = "#userId", unless = "#result == null")
    public User getUserById(Long userId) {
        return userMapper.selectById(userId);
    }
    
  • 多级缓存架构: 本地缓存(L1)+ Redis(L2)+ 数据库(L3),层层拦截,降低数据库压力。
  • 缓存穿透防护: 对不存在的 Key 也缓存空值,或使用布隆过滤器。
    // 缓存空值防穿透
    String cached = redis.get(key);
    if (cached != null) {
        return "".equals(cached) ? null : deserialize(cached);
    }
    User user = db.query(key);
    redis.set(key, user == null ? "" : serialize(user), 5, TimeUnit.MINUTES);
    
  • 缓存雪崩防护: 给缓存过期时间加上随机偏移量,避免同时失效。
    // 基础过期 10 分钟 + 0~60 秒随机偏移
    int ttl = 600 + ThreadLocalRandom.current().nextInt(60);
    redis.set(key, value, ttl, TimeUnit.SECONDS);
    

4. 系统架构本身是否有问题?

核心原则: 架构决定了系统的性能上限,代码层面优化再多也弥补不了架构缺陷。

常见问题与示例:

  • 单体应用拆分为微服务: 将用户、订单、支付等模块独立部署,按需扩容。例如大促时只需扩容订单服务。
  • 同步调用改为异步: 非核心链路使用消息队列解耦。
    // 反例:下单同步调用
    下单 → 生成订单 → 扣减库存 → 发送短信 → 发送邮件 → 更新积分(全部同步,链路过长)
    
    // 正例:非核心操作异步化
    下单 → 生成订单 → 扣减库存 → 返回成功
                                ├→ MQ → 发送短信服务
                                ├→ MQ → 发送邮件服务
                                └→ MQ → 更新积分服务
    
  • 引入 CDN: 静态资源(JS/CSS/图片)通过 CDN 就近分发,减少源站压力。
  • API 网关限流与降级: 使用 Sentinel / Gateway 对接口做限流、熔断、降级,防止雪崩。
  • 读写分离: 主库写、从库读,读多写少场景下数据库压力大幅降低。

5. 系统是否存在死锁?

核心原则: 死锁会导致线程阻塞、连接耗尽,系统表现为"假死"。

常见场景与解决方案:

  • 经典死锁 — 两个事务以不同顺序加锁:
    -- 事务 A                          -- 事务 B
    BEGIN;                              BEGIN;
    UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 锁 id=1
                                        UPDATE account SET balance = balance + 50 WHERE id = 2;   -- 锁 id=2
    UPDATE account SET balance = balance + 100 WHERE id = 2;  -- 等待 id=2 → 死锁!
                                        UPDATE account SET balance = balance - 50 WHERE id = 1;   -- 等待 id=1 → 死锁!
    
    解决: 所有事务按相同的顺序(如 id 升序)访问资源。
  • 应用层锁顺序不一致: 多线程同时操作两个对象,获取锁的顺序不同。
    // 反例:线程 A 锁 o1 再锁 o2,线程 B 锁 o2 再锁 o1
    // 正例:统一定义全局锁顺序,例如按对象 hashCode 排序
    
  • 排查工具: 开启 MySQL 的 innodb_print_all_deadlocks 将死锁日志输出到错误日志;JVM 使用 jstack 查看线程阻塞状态。

6. 数据库索引使用是否合理?

核心原则: 索引是数据库性能的第一道防线。

常见问题与示例:

  • 缺失索引导致全表扫描:
    -- orders 表有百万数据,user_id 没有索引
    SELECT * FROM orders WHERE user_id = 1001;  -- 全表扫描,耗时 2s+
    -- 添加索引后
    CREATE INDEX idx_user_id ON orders(user_id);  -- 走索引,耗时 <10ms
    
  • 联合索引遵循最左前缀原则:
    -- 联合索引 (city, age, name)
    CREATE INDEX idx_city_age_name ON users(city, age, name);
    
    -- 能命中索引
    SELECT * FROM users WHERE city = 'Beijing';
    SELECT * FROM users WHERE city = 'Beijing' AND age = 25;
    -- 不能命中索引(跳过了 city)
    SELECT * FROM users WHERE age = 25 AND name = 'Tom';
    
  • 避免索引失效的写法:
    -- 左模糊查询会导致索引失效
    SELECT * FROM users WHERE name LIKE '%tom';    -- 全表扫描
    SELECT * FROM users WHERE name LIKE 'tom%';    -- 可以走索引
    
    -- OR 连接的条件中有非索引列也会导致全表扫描
    SELECT * FROM users WHERE id = 1 OR name = 'Tom';  -- 若 name 无索引则全表扫描
    
  • 避免过多索引: 每个索引都会增加写入成本(INSERT/UPDATE/DELETE 要维护索引),建议单表索引不超过 5 个。
  • 使用覆盖索引减少回表: 查询字段都包含在索引中时,无需回表读取数据行。
    -- 索引 (user_id, amount)
    SELECT user_id, amount FROM orders WHERE user_id = 1001;  -- 覆盖索引,Using index
    

7. 系统是否存在内存泄漏?

核心原则: Java 有 GC 自动回收,但错误的编码模式仍然会导致对象无法被回收。

常见场景与示例:

  • 静态集合类持有对象引用:
    // 反例:List 被 static 持有,元素永远不会被 GC
    public class CacheHolder {
        private static final List<Object> cache = new ArrayList<>();
        public void add(Object obj) {
            cache.add(obj);  // 只进不出,内存持续增长
        }
    }
    // 正例:使用 WeakReference 或定期清理,或改用 Guava Cache 等有驱逐策略的缓存
    
  • 未关闭的资源:
    // 反例:连接未关闭
    Connection conn = dataSource.getConnection();
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT * FROM users");
    // 如果异常发生,conn 和 stmt 不会被关闭
    
    // 正例:try-with-resources 自动关闭
    try (Connection conn = dataSource.getConnection();
         Statement stmt = conn.createStatement();
         ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
        // 处理结果
    }
    
  • ThreadLocal 未清理:
    // 反例:线程复用(线程池)时 ThreadLocal 值不会自动清除
    private static ThreadLocal<User> currentUser = new ThreadLocal<>();
    public void handleRequest(User user) {
        currentUser.set(user);
        // 处理请求...
        // 忘记 remove → 线程池复用时持有旧对象
    }
    
    // 正例:在 finally 中清理
    public void handleRequest(User user) {
        try {
            currentUser.set(user);
            // 处理请求...
        } finally {
            currentUser.remove();
        }
    }
    
  • 排查工具: 使用 jmap -dump 导出堆内存快照,用 MAT(Memory Analyzer Tool)或 VisualVM 分析大对象和 GC Root 引用链。

8. 耗时操作是否进行了异步处理?

核心原则: 主链路只保留必须同步完成的操作,其余异步化。

常见场景与示例:

  • 邮件/短信通知:
    // 反例:同步发送,阻塞主线程 200ms+
    public void createOrder(Order order) {
        orderMapper.insert(order);
        emailService.send(order);     // 耗时 200ms
        smsService.send(order);       // 耗时 150ms
    }
    
    // 正例:异步发送
    @Async
    public void createOrder(Order order) {
        orderMapper.insert(order);
        rabbitTemplate.convertAndSend("notification.exchange", "email", order);
        rabbitTemplate.convertAndSend("notification.exchange", "sms", order);
    }
    
  • 使用 CompletableFuture 编排并行任务:
    // 反例:串行调用,总耗时 = 接口A + 接口B + 接口C
    User user = userService.getUser(userId);       // 100ms
    List<Order> orders = orderService.getOrders(userId);  // 200ms
    Wallet wallet = walletService.getWallet(userId);     // 150ms
    
    // 正例:并行调用,总耗时 = max(100, 200, 150) = 200ms
    CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(userId));
    CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.getOrders(userId));
    CompletableFuture<Wallet> walletFuture = CompletableFuture.supplyAsync(() -> walletService.getWallet(userId));
    CompletableFuture.allOf(userFuture, orderFuture, walletFuture).join();
    
  • 日志异步写入: 使用 Logback 的 AsyncAppender 或 Log4j2 的异步 Logger,避免日志 IO 阻塞业务线程。
  • 大数据量导出异步化: CSV/Excel 导出涉及百万行数据时,提交异步任务,完成后通知用户下载。

二、补充优化策略

9. JVM 参数调优

核心原则: 合理的 JVM 配置可以让 GC 更高效,减少 STW(Stop-The-World)停顿。

关键配置与示例:

# 堆内存设置
-Xms4g -Xmx4g                    # 初始和最大堆大小设为相同,避免动态扩缩带来的开销
-Xmn2g                            # 新生代大小,一般设为堆的 1/3 ~ 1/2
-XX:MetaspaceSize=256m            # 元空间初始大小
-XX:MaxMetaspaceSize=512m         # 元空间最大值

# GC 策略选择
-XX:+UseG1GC                      # 推荐 G1 收集器(JDK 9+ 默认)
-XX:MaxGCPauseMillis=200          # 目标最大 GC 停顿时间 200ms
-XX:G1HeapRegionSize=8m           # G1 分区大小

# GC 日志(排查必备)
-Xlog:gc*:file=gc.log:time,uptime,level,tags

排查手段:

  • jstat -gcutil <pid> 1000 实时观察各内存区占用和 GC 频率。
  • GC 日志分析工具:GCEasy、GCViewer。
  • 频繁 Full GC 通常意味着内存泄漏或堆太小。

10. 连接池配置优化

核心原则: 连接池大小不是越大越好,过大会导致数据库端资源争抢。

配置示例(HikariCP,Spring Boot 默认):

spring:
  datasource:
    hikari:
      maximum-pool-size: 20         # 最大连接数,建议公式:CPU核心数 * 2 + 磁盘数
      minimum-idle: 5               # 最小空闲连接
      connection-timeout: 3000      # 获取连接超时 3s
      idle-timeout: 600000          # 空闲连接最大存活 10min
      max-lifetime: 1800000         # 连接最大存活 30min(需小于数据库的 wait_timeout)
      leak-detection-threshold: 60000  # 连接泄漏检测阈值 60s

同样适用于 Redis 连接池、HTTP 连接池(如 OkHttp、Apache HttpClient)。


11. 序列化与压缩优化

核心原则: 微服务间大量数据传输时,序列化效率和数据大小直接影响性能。

常见策略:

  • 序列化框架选择:
    • JSON(Jackson/Gson):可读性好,性能一般,适合对外 API。
    • Protobuf / Kryo / Hessian:二进制序列化,体积小、速度快,适合内部 RPC。
  • 开启 GZIP 压缩:
    # Spring Boot 响应压缩
    server:
      compression:
        enabled: true
        mime-types: application/json,application/xml,text/html
        min-response-size: 1024      # 大于 1KB 才压缩
    
  • 数据库大字段按需查询: TEXT/BLOB 字段避免随主查询一起加载,延迟到需要时再查。

12. 线程池参数调优

核心原则: 避免使用 Executors 创建无界线程池,合理配置核心参数。

配置示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                             // 核心线程数(CPU密集型: CPU核心数+1,IO密集型: CPU核心数*2)
    20,                             // 最大线程数
    60L, TimeUnit.SECONDS,          // 非核心线程空闲存活时间
    new LinkedBlockingQueue<>(500), // 有界队列,防止 OOM
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:调用方线程执行,起到限流作用
);

排查手段: 暴露线程池监控指标(活跃线程数、队列大小、完成任务数)到 Prometheus + Grafana。


13. 接口幂等性与重试机制

核心原则: 网络不稳定时,合理的重试机制可以提升系统可靠性,但必须保证幂等性。

常见方案:

  • Token 机制: 请求前获取唯一 Token,服务端消费 Token 后标记已处理。
  • 数据库唯一索引: 利用唯一约束防止重复插入。
    CREATE UNIQUE INDEX uk_order_no ON orders(order_no);
    
  • Redis SETNX 实现分布式锁:
    Boolean acquired = redisTemplate.opsForValue()
        .setIfAbsent("lock:order:" + orderNo, "1", 10, TimeUnit.SECONDS);
    if (!acquired) {
        throw new DuplicateRequestException("请勿重复提交");
    }
    try {
        // 处理业务
    } finally {
        redisTemplate.delete("lock:order:" + orderNo);
    }
    

14. 数据预加载与懒加载

核心原则: 根据业务场景选择合适的加载策略,平衡启动速度和运行时性能。

  • 预加载(Eager Loading): 系统启动时提前加载热点数据到缓存。
    @PostConstruct
    public void init() {
        List<Config> configs = configMapper.selectAll();
        configs.forEach(c -> localCache.put(c.getKey(), c.getValue()));
    }
    
  • 懒加载(Lazy Loading): 首屏只加载必要数据,其余数据滚动或点击时按需加载。
    首屏请求:
      GET /api/home  →  只返回 banner、推荐列表(10条)
    滚动加载:
      GET /api/home/products?page=2  →  用户下滑时再请求下一页
    

三、性能优化优先级法则

SQL优化 / JVM调优 / 数据库与中间件参数调优
    > 硬件升级(内存、CPU、SSD)
        > 业务逻辑优化 / 缓存策略
            > 读写分离 / 集群 / 负载均衡
                > 分库分表(最后手段,成本最高)

核心理念: 先做低成本高收益的软件优化,最后再考虑高成本的架构级改造。80% 的性能问题都可以通过 SQL 优化和合理缓存解决。

posted @ 2026-06-26 22:24  wnbzw  阅读(3)  评论(0)    收藏  举报