跳转到内容
123xiao | 无名键客

《Java Web 开发中基于 Spring Boot + MyBatis 的接口性能优化实战:从慢 SQL 到线程池调优》

字数: 0 阅读时长: 1 分钟

Java Web 开发中基于 Spring Boot + MyBatis 的接口性能优化实战:从慢 SQL 到线程池调优

做 Java Web 接口时,很多性能问题一开始看起来都像“服务有点慢”,但真正排查下去,往往不是某一个点的问题,而是请求链路上多个环节一起拖后腿:SQL 慢、索引失效、MyBatis 映射不合理、连接池打满、线程池配置失衡,最后表现成接口 RT 飙升、超时、CPU 抖动、数据库负载高。

这篇文章我换一个更贴近线上排障的角度来讲:不是先讲概念,而是从一个“慢接口”的完整优化过程切入,一步步把 Spring Boot + MyBatis 场景里最常见的性能瓶颈串起来。你可以把它当成一份可落地的优化教程。


背景与问题

假设我们有一个订单查询接口:

  • 支持按用户 ID 查询订单列表
  • 可按状态过滤
  • 支持分页
  • 返回订单基础信息和金额统计

线上现象通常是这样的:

  • 平均响应时间从 80ms 涨到 800ms
  • 高峰期接口偶发超时
  • 数据库 CPU 偶尔飙高
  • 应用线程池堆积,请求排队

很多同学第一反应是“加机器”。这不是不能做,但如果 SQL 写法本身有问题、线程模型又配置得不合理,加机器通常只能延缓问题暴露。

我们先看一张典型链路图。

flowchart LR
    A[客户端请求] --> B[Spring MVC Controller]
    B --> C[Service]
    C --> D[MyBatis Mapper]
    D --> E[(MySQL)]
    E --> D
    D --> C
    C --> F[线程池异步补充查询/聚合]
    F --> C
    C --> B
    B --> A

这个链路里任何一层卡住,都会反馈到接口耗时上。


前置知识 / 环境准备

建议你具备这些基础:

  • Spring Boot 基本开发经验
  • MyBatis XML 或注解开发经验
  • MySQL 索引与 EXPLAIN 的基本使用
  • 对线程池参数有初步认知

示例环境:

  • JDK 17
  • Spring Boot 3.x
  • MyBatis Spring Boot Starter
  • MySQL 8.x
  • HikariCP
  • Maven

核心原理

接口性能优化,建议始终按这条顺序思考:

  1. 先看数据库

    • SQL 是否走索引
    • 是否出现全表扫描、回表严重、排序/临时表
    • 分页是否合理
  2. 再看 ORM/数据访问层

    • MyBatis 是否查了不必要字段
    • 是否有 N+1 查询
    • 动态 SQL 是否把索引条件写废了
  3. 再看连接池与线程池

    • 数据库连接数是否耗尽
    • 业务线程是否阻塞过多
    • 异步任务是否无边界堆积
  4. 最后看接口设计

    • 是否把多个耗时操作串行执行
    • 是否存在重复查询、重复计算
    • 是否可以做缓存或降级

一句话总结:先消灭慢 SQL,再优化并发模型。这一步顺序非常重要。我踩过的一个坑就是先去调线程池,结果线程更多了,数据库更早被打满,问题反而更严重。


逐步验证清单

实际优化时,我建议你按下面的检查顺序执行:

  • 记录接口 P50/P95/P99 响应时间
  • 打开 SQL 慢日志
  • 对核心 SQL 执行 EXPLAIN ANALYZE
  • 检查是否存在 select *
  • 检查分页是否为深分页
  • 检查是否出现 N+1 查询
  • 查看连接池活跃连接数
  • 查看线程池队列积压情况
  • 压测前后对比吞吐量和错误率

一个典型慢接口的起点

先看一个不太理想的实现。

Controller

package com.example.demo.controller;

import com.example.demo.dto.OrderQueryRequest;
import com.example.demo.dto.OrderVO;
import com.example.demo.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/api/orders")
    public List<OrderVO> list(OrderQueryRequest request) {
        return orderService.listOrders(request);
    }
}

DTO

package com.example.demo.dto;

public class OrderQueryRequest {
    private Long userId;
    private Integer status;
    private Integer pageNo = 1;
    private Integer pageSize = 20;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Integer getPageNo() {
        return pageNo;
    }

    public void setPageNo(Integer pageNo) {
        this.pageNo = pageNo;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }
}

实体与返回对象

package com.example.demo.dto;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public class OrderVO {
    private Long id;
    private Long userId;
    private Integer status;
    private BigDecimal amount;
    private LocalDateTime createTime;
    private String summary;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }

    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
}

Mapper 接口

package com.example.demo.mapper;

import com.example.demo.dto.OrderVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface OrderMapper {

    List<OrderVO> selectOrders(@Param("userId") Long userId,
                               @Param("status") Integer status,
                               @Param("offset") int offset,
                               @Param("pageSize") int pageSize);

    String selectOrderSummary(@Param("orderId") Long orderId);
}

Mapper XML:存在问题的版本

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.OrderMapper">

    <select id="selectOrders" resultType="com.example.demo.dto.OrderVO">
        select *
        from orders
        where 1 = 1
        <if test="userId != null">
            and user_id = #{userId}
        </if>
        <if test="status != null">
            and status = #{status}
        </if>
        order by create_time desc
        limit #{offset}, #{pageSize}
    </select>

    <select id="selectOrderSummary" resultType="string">
        select summary
        from orders
        where id = #{orderId}
    </select>

</mapper>

Service:存在 N+1 查询

package com.example.demo.service;

import com.example.demo.dto.OrderQueryRequest;
import com.example.demo.dto.OrderVO;
import com.example.demo.mapper.OrderMapper;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderService {

    private final OrderMapper orderMapper;

    public OrderService(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    public List<OrderVO> listOrders(OrderQueryRequest request) {
        int offset = (request.getPageNo() - 1) * request.getPageSize();
        List<OrderVO> list = orderMapper.selectOrders(
                request.getUserId(),
                request.getStatus(),
                offset,
                request.getPageSize()
        );

        for (OrderVO order : list) {
            order.setSummary(orderMapper.selectOrderSummary(order.getId()));
        }
        return list;
    }
}

这段代码的典型问题有三个:

  1. select * 读取了不必要字段
  2. order by create_time desc 如果没有合适索引,会非常慢
  3. 列表查出 20 条后,又对每条执行一次 summary 查询,形成 N+1 查询

核心原理:为什么它会慢

1. 慢 SQL 不只是“执行久”,还会拖累全局

一条 SQL 慢,通常会带来连锁反应:

  • 数据库连接占用时间变长
  • 应用线程阻塞等待结果
  • 并发一上来,连接池耗尽
  • 请求开始排队
  • 线程池积压,接口 RT 继续上升
sequenceDiagram
    participant U as User
    participant A as App
    participant P as ConnectionPool
    participant D as MySQL
    U->>A: 请求 /api/orders
    A->>P: 获取数据库连接
    P->>D: 执行慢SQL
    D-->>P: 返回结果很慢
    P-->>A: 连接长时间占用
    Note over A: 业务线程持续阻塞
    U->>A: 更多请求进入
    A->>P: 申请连接
    Note over P: 连接池趋于耗尽
    Note over A: 请求排队,RT飙升

2. MyBatis 本身不慢,慢在“你让它怎么查”

MyBatis 是很轻量的,但它不会替你优化 SQL。常见问题包括:

  • 动态 SQL 写得太灵活,导致索引失效
  • 大字段不区分场景,一次全查
  • 批量场景写成循环单查
  • 深分页仍然用 limit offset

3. 线程池不是越大越好

线程池参数如果设置过大,会出现:

  • 更多线程竞争 CPU
  • 更多线程同时打数据库
  • 上下文切换增多
  • 数据库更容易被打爆

性能优化里最常见的误区之一,就是把线程池调大当成万能药。


实战代码:从慢 SQL 到可运行优化版

下面我们给出一套更合理的实现。


第一步:给 SQL 做“减法”

建表与索引示例

create table orders (
    id bigint primary key auto_increment,
    user_id bigint not null,
    status int not null,
    amount decimal(10,2) not null,
    summary varchar(255),
    create_time datetime not null,
    update_time datetime not null
);

create index idx_user_status_ctime on orders(user_id, status, create_time desc);
create index idx_user_ctime on orders(user_id, create_time desc);

这里索引的思路是:

  • 常见过滤条件有 user_id
  • 可选条件有 status
  • 排序字段是 create_time desc

如果你的查询是“按用户 + 状态 + 时间倒序分页”,那联合索引就要尽量贴近这个访问模式。

优化后的 Mapper XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.OrderMapper">

    <resultMap id="OrderVOMap" type="com.example.demo.dto.OrderVO">
        <id property="id" column="id"/>
        <result property="userId" column="user_id"/>
        <result property="status" column="status"/>
        <result property="amount" column="amount"/>
        <result property="createTime" column="create_time"/>
        <result property="summary" column="summary"/>
    </resultMap>

    <select id="selectOrders" resultMap="OrderVOMap">
        select id, user_id, status, amount, create_time, summary
        from orders
        where user_id = #{userId}
        <if test="status != null">
            and status = #{status}
        </if>
        order by create_time desc
        limit #{offset}, #{pageSize}
    </select>

</mapper>

关键变化:

  • 去掉 select *
  • 直接一次查出 summary,避免 N+1
  • 强约束 user_id 为必传条件,减少“全表查分页”的风险

真实项目里,如果 summary 是大字段,可按场景拆成“列表轻量字段”和“详情完整字段”两套查询。

优化后的 Mapper 接口

package com.example.demo.mapper;

import com.example.demo.dto.OrderVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface OrderMapper {

    List<OrderVO> selectOrders(@Param("userId") Long userId,
                               @Param("status") Integer status,
                               @Param("offset") int offset,
                               @Param("pageSize") int pageSize);
}

优化后的 Service

package com.example.demo.service;

import com.example.demo.dto.OrderQueryRequest;
import com.example.demo.dto.OrderVO;
import com.example.demo.mapper.OrderMapper;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.List;

@Service
public class OrderService {

    private final OrderMapper orderMapper;

    public OrderService(OrderMapper orderMapper) {
        this.orderMapper = orderMapper;
    }

    public List<OrderVO> listOrders(OrderQueryRequest request) {
        Assert.notNull(request.getUserId(), "userId 不能为空");

        int pageNo = Math.max(request.getPageNo(), 1);
        int pageSize = Math.min(Math.max(request.getPageSize(), 1), 100);
        int offset = (pageNo - 1) * pageSize;

        return orderMapper.selectOrders(
                request.getUserId(),
                request.getStatus(),
                offset,
                pageSize
        );
    }
}

这里顺手做了两件很有价值的小事:

  • 限制 pageSize,防止有人一页查几千条
  • 明确 userId 必传,避免大范围扫描

第二步:用 EXPLAIN 验证,而不是“感觉变快了”

优化 SQL 之后,一定要验证执行计划。

EXPLAIN ANALYZE
select id, user_id, status, amount, create_time, summary
from orders
where user_id = 1001
  and status = 1
order by create_time desc
limit 0, 20;

重点看这些指标:

  • type 是否为 ref / range / const,尽量避免 ALL
  • key 是否命中预期索引
  • rows 扫描行数是否明显偏大
  • 是否出现 Using filesort
  • 是否出现 Using temporary

如果 status 可选,而有时只传 user_id,那就要关注不同 SQL 组合下是否都能用上合适索引。必要时可以拆成两个 SQL,而不是强行用一条“全能动态 SQL”。


第三步:处理深分页问题

pageNo 很大时,limit offset, size 会越来越慢,因为数据库需要跳过前面很多行。

不推荐的大偏移分页

select id, user_id, status, amount, create_time, summary
from orders
where user_id = 1001
order by create_time desc
limit 100000, 20;

推荐:基于游标或上次最大值翻页

select id, user_id, status, amount, create_time, summary
from orders
where user_id = 1001
  and create_time < '2026-12-01 10:00:00'
order by create_time desc
limit 20;

如果你的场景是滚动加载、无限下拉,优先考虑游标分页。后台管理系统要求跳页时,再评估是否接受大页码性能损耗。


第四步:线程池调优,不让异步变成放大器

有些接口会同时查数据库、查远程服务、做聚合。为了降低总耗时,很多人会加异步并行,这个方向本身没问题,但前提是异步任务必须有边界

线程池配置

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.*;

@Configuration
public class ExecutorConfig {

    @Bean("orderQueryExecutor")
    public ExecutorService orderQueryExecutor() {
        return new ThreadPoolExecutor(
                8,
                16,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(200),
                new ThreadFactory() {
                    private int index = 1;
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "order-query-" + index++);
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

这里我刻意没有把线程数配得很大,原因很简单:

  • 如果任务主要是数据库查询,线程太多只会增加 DB 压力
  • 有界队列比无界队列更安全
  • CallerRunsPolicy 可以起到反压作用,防止任务无限堆积

异步聚合示例

假设除了订单列表,我们还要查询用户优惠信息和统计金额汇总。

package com.example.demo.service;

import com.example.demo.dto.OrderQueryRequest;
import com.example.demo.dto.OrderVO;
import com.example.demo.mapper.OrderMapper;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;

@Service
public class OrderFacadeService {

    private final OrderMapper orderMapper;
    private final ExecutorService orderQueryExecutor;

    public OrderFacadeService(OrderMapper orderMapper, ExecutorService orderQueryExecutor) {
        this.orderMapper = orderMapper;
        this.orderQueryExecutor = orderQueryExecutor;
    }

    public OrderPageResult query(OrderQueryRequest request) {
        Assert.notNull(request.getUserId(), "userId 不能为空");

        int pageNo = Math.max(request.getPageNo(), 1);
        int pageSize = Math.min(Math.max(request.getPageSize(), 1), 100);
        int offset = (pageNo - 1) * pageSize;

        CompletableFuture<List<OrderVO>> orderFuture = CompletableFuture.supplyAsync(
                () -> orderMapper.selectOrders(request.getUserId(), request.getStatus(), offset, pageSize),
                orderQueryExecutor
        );

        CompletableFuture<BigDecimal> totalAmountFuture = orderFuture.thenApply(list ->
                list.stream()
                        .map(OrderVO::getAmount)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        );

        List<OrderVO> orders = orderFuture.join();
        BigDecimal totalAmount = totalAmountFuture.join();

        OrderPageResult result = new OrderPageResult();
        result.setOrders(orders);
        result.setPageNo(pageNo);
        result.setPageSize(pageSize);
        result.setTotalAmount(totalAmount);
        return result;
    }

    public static class OrderPageResult {
        private List<OrderVO> orders;
        private Integer pageNo;
        private Integer pageSize;
        private BigDecimal totalAmount;

        public List<OrderVO> getOrders() {
            return orders;
        }

        public void setOrders(List<OrderVO> orders) {
            this.orders = orders;
        }

        public Integer getPageNo() {
            return pageNo;
        }

        public void setPageNo(Integer pageNo) {
            this.pageNo = pageNo;
        }

        public Integer getPageSize() {
            return pageSize;
        }

        public void setPageSize(Integer pageSize) {
            this.pageSize = pageSize;
        }

        public BigDecimal getTotalAmount() {
            return totalAmount;
        }

        public void setTotalAmount(BigDecimal totalAmount) {
            this.totalAmount = totalAmount;
        }
    }
}

这个例子里,金额汇总直接基于已查出的列表计算,没有再发起额外 SQL,避免了数据库重复开销。


第五步:连接池参数要和数据库承载能力匹配

Spring Boot 默认常用的是 HikariCP。很多性能问题并不是 Hikari 本身,而是参数和数据库能力不匹配。

application.yml 示例

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-timeout: 3000
      validation-timeout: 1000

mybatis:
  mapper-locations: classpath*:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

经验上:

  • maximum-pool-size 不是越大越好,要结合数据库最大连接数、SQL 平均耗时、服务实例数一起算
  • 如果服务有 5 个实例,每个实例连接池 50,那数据库可能要扛 250 个连接
  • SQL 已经很慢时,加大连接池只会让数据库同时处理更多慢请求

一张完整优化路径图

flowchart TD
    A[接口RT高] --> B{先看慢SQL?}
    B -->|是| C[开启慢日志/抓SQL]
    C --> D[EXPLAIN ANALYZE]
    D --> E{索引命中?}
    E -->|否| F[调整索引/重写SQL]
    E -->|是| G{是否N+1/多余字段?}
    G -->|是| H[减少字段/合并查询]
    G -->|否| I{是否深分页?}
    I -->|是| J[改游标分页]
    I -->|否| K[检查连接池与线程池]
    K --> L[限制并发/有界队列/反压]
    L --> M[压测验证优化结果]

常见坑与排查

这一部分我尽量讲得“接地气”一点,因为很多问题真的很像。

坑 1:索引建了,但就是不走

常见原因:

  • 查询条件类型不一致,比如数据库是 bigint,代码里传字符串
  • 对索引列做了函数处理
  • 联合索引顺序和查询条件不匹配
  • 数据分布导致优化器判断全表扫描更划算

排查方式:

EXPLAIN
select id, user_id, status, amount, create_time
from orders
where user_id = '1001';

如果 user_id 是数值列,这种隐式类型转换就值得警惕。


坑 2:动态 SQL 写得太灵活,最后谁都救不了

例如:

<select id="badQuery" resultType="com.example.demo.dto.OrderVO">
    select id, user_id, status, amount, create_time
    from orders
    where 1 = 1
    <if test="keyword != null and keyword != ''">
        and summary like concat('%', #{keyword}, '%')
    </if>
    <if test="userId != null">
        and user_id = #{userId}
    </if>
    order by create_time desc
</select>

这里如果模糊查询经常参与,而且 %keyword% 这种写法无法利用普通索引,就容易拖慢整体查询。

建议:

  • 把全文检索需求和普通条件过滤拆开
  • 不要让一条 SQL 试图兼容所有查询场景

坑 3:把异步并发当“提速开关”

我见过一种配置,线程池开到几百,结果数据库连接池才 20。后果就是:

  • 应用层线程疯狂排队等连接
  • CPU 上下文切换增加
  • 实际吞吐并没有提升

排查指标建议看:

  • 活跃线程数
  • 线程池队列长度
  • 数据库连接池活跃连接数
  • SQL 平均执行时间
  • 接口超时比例

坑 4:分页查询返回大对象

比如列表页只展示:

  • 订单号
  • 状态
  • 金额
  • 创建时间

但 SQL 却把备注、详情 JSON、扩展字段全带回来。这会浪费:

  • 数据库 IO
  • 网络传输
  • JVM 反序列化和对象创建开销

建议做法:

  • 列表 DTO 和详情 DTO 分离
  • 列表接口只查必要字段

坑 5:只看平均耗时,不看 P95/P99

平均值常常会骗人。接口性能优化更应该关注:

  • P95
  • P99
  • 超时率
  • 错误率

因为线上用户真正感受到的“卡”,往往来自尾延迟。


安全/性能最佳实践

这一节我把最值得长期保留的习惯整理成清单。

1. 参数边界要收紧

  • pageSize 限制上限,例如不超过 100
  • 对排序字段做白名单校验,避免任意排序注入风险
  • 可选条件不要无限扩张

2. SQL 始终显式列出字段

不要依赖 select *。这样做有三个好处:

  • 减少无用数据读取
  • 避免表结构变更带来兼容问题
  • 方便做覆盖索引优化

3. 优先消灭 N+1 查询

如果是:

  • 列表 + 详情补查
  • 循环单条聚合
  • 循环查字典项

都要警惕 N+1。优先考虑:

  • 一次 JOIN
  • 一次 IN 批量查
  • 先查主数据,再批量组装

4. 线程池必须有界

推荐至少做到:

  • 有界队列
  • 明确拒绝策略
  • 根据任务类型分池
  • 监控活跃线程和队列长度

5. 连接池与数据库容量要联动设计

建议有这样一个意识:

服务实例数 × 单实例最大连接数 ≤ 数据库可承受连接数的一定比例

不要每个服务都把连接池开到几十上百,最后数据库被“合法配置”打垮。

6. 给关键接口加监控埋点

至少记录:

  • 接口总耗时
  • SQL 执行次数
  • 单次请求命中数据库次数
  • 线程池排队时间
  • 连接池等待时间

没有数据,优化就只能靠猜。

7. 压测必须模拟真实请求结构

不要只压一个“最简单参数”的 happy path。要覆盖:

  • 大页码分页
  • 热门用户数据
  • 高频状态过滤
  • 高并发突发流量

一个更稳妥的优化思路:分层定责

在团队协作里,我很推荐把性能问题按层分开看:

层次关注点常见动作
Controller参数边界、返回体大小限流、校验、裁剪字段
Service串并行策略、业务聚合合并查询、异步边界控制
MyBatisSQL 结构、映射成本去掉 select *、批量化
DB索引、执行计划、锁建索引、改分页、拆热点
Infra连接池、线程池、监控配置调优、告警、压测

这样做的好处是:排查不会乱,责任不会混。


一个最小可运行验证方案

如果你想自己快速验证,可以按下面步骤操作:

  1. 建表并插入 10 万条订单数据
  2. 先运行“慢版本”接口
  3. 用 JMeter 或 wrk 压测 100 并发
  4. 记录 RT、吞吐量、错误率
  5. 切换到优化 SQL 的版本
  6. 再对比线程池与连接池参数前后的差异

重点不要只看“有没有变快”,而要看:

  • 数据库 CPU 是否下降
  • 活跃连接是否更稳定
  • 接口尾延迟是否下降
  • 高峰期是否更不容易超时

总结

Spring Boot + MyBatis 的接口性能优化,真正有效的方法不是“盲调参数”,而是沿着调用链逐层收敛问题:

  1. 先抓慢 SQL
  2. EXPLAIN ANALYZE 验证索引与执行计划
  3. 消灭 select * 和 N+1 查询
  4. 深分页改成游标分页
  5. 线程池有界、连接池适配数据库容量
  6. 用压测和监控验证结果,而不是凭感觉

如果你只记住一句话,我建议记这个:

接口慢,先怀疑 SQL;并发差,先看资源边界;调优前先测,调优后再证。

这套方法的边界也很明确:

  • 如果瓶颈是数据库物理资源不足,仅靠代码调优不够
  • 如果业务必须做复杂统计,可能需要引入缓存、搜索引擎或离线计算
  • 如果是热点数据争用,还要进一步分析锁竞争和事务设计

但对于大多数中级 Java Web 项目来说,把 SQL、MyBatis、连接池、线程池这四层打通理解,已经能解决 70% 以上的接口性能问题。这也是最值得优先投入的优化路径。


分享到:

下一篇
《Web逆向实战:中级开发者如何定位并复现前端签名算法实现接口自动化调用》