MyBatis 性能优化指南:Mapper 映射、缓存与批量操作实战

⚡ MyBatis 性能优化指南:Mapper 映射、缓存与批量操作实战

关键词:MyBatis、性能优化、Mapper 映射、缓存(一级/二级)、批量插入/更新、批处理实战

亮点:覆盖常见性能坑、配置技巧、以及一个详尽的实战项目(含配置、代码、压力对比、注意点),方便直接贴到工程中复用。


摘要(3句话速读)

MyBatis 在灵活性和可控性上很强,但不注意会导致 N+1 查询、过度全列查询、缓存失效和低效的批量操作。本文先讲清核心概念(Mapper 映射/结果映射、一级/二级缓存、批处理),再给出完整实战项目 :用 Spring Boot + MyBatis 模拟订单/订单明细场景,演示如何用 executorType=BATCH、合理 Mapper 映射和二级缓存把性能拉上来,并给出对比测试数据与优化建议。


目录

  1. 常见性能问题与优化思路
  2. Mapper 映射(ResultMap、避免 SELECT *、N+1 问题)
  3. MyBatis 缓存(一级与二级缓存、配置与注意点)
  4. 批量操作(JDBC/SqlSession BATCH、flushStatements、主键回写)
  5. 实战项目(最详细章节)
  6. 性能测试与对比结果
  7. 常用优化 checklist & 经验总结
  8. 推荐标签与互动

1. 常见性能问题与优化思路(速览)

  • N+1 查询 :关联对象懒加载或多次查询导致大量 SQL。→ 使用 join + ResultMap 或者一次性 IN 查询合并。
  • **SELECT ***:返回多余列,网络与序列化开销增大。→ 明确列名,使用投影(只查询需要字段)。
  • 不合理索引 :SQL 本身慢,与 MyBatis 无关,但要联调:EXPLAIN 分析。
  • 缓存误用/失效:缓存策略错误或事务/更新大量写入会使缓存失效。→ 设计好缓存粒度与失效策略。
  • 低效批量操作:逐条 insert/update 导致大量 round-trip。→ 使用批处理(executorType=BATCH 或 JDBC batch)。

2. Mapper 映射:结果映射与避免 N+1

2.1 使用 ResultMap 明确映射(避免 SELECT * 带来的问题)

推荐使用 ResultMap 明确字段到属性的映射,特别是存在驼峰命名或类型转换时。

Mapper XML 示例:

xml 复制代码
<resultMap id="OrderResult" type="com.example.domain.Order">
  <id column="id" property="id"/>
  <result column="user_id" property="userId"/>
  <result column="total_amount" property="totalAmount"/>
  <result column="created_at" property="createdAt"/>
</resultMap>

<select id="selectOrderById" resultMap="OrderResult">
  SELECT id, user_id, total_amount, created_at
  FROM orders
  WHERE id = #{id}
</select>

2.2 N+1 问题实例与解决

问题:查询订单列表后,为每个订单再查询明细(N 次)

解决思路:

  • 使用 JOIN 把订单和明细一次性取出 + <collection> 组合到 ResultMap,或
  • 先查询订单列表,收集订单 id,然后做一次 SELECT * FROM order_items WHERE order_id IN (...),用程序组装(推荐用于分页场景)。

一次性 JOIN 示例(ResultMap with collection):

xml 复制代码
<resultMap id="OrderWithItems" type="Order">
  <id column="o_id" property="id"/>
  <result column="o_user_id" property="userId"/>
  <collection property="items" ofType="OrderItem">
    <id column="i_id" property="id"/>
    <result column="i_product_id" property="productId"/>
    <result column="i_qty" property="qty"/>
  </collection>
</resultMap>

<select id="selectOrdersWithItems" resultMap="OrderWithItems">
  SELECT o.id as o_id, o.user_id as o_user_id, 
         i.id as i_id, i.product_id as i_product_id, i.qty as i_qty
  FROM orders o
  LEFT JOIN order_items i ON o.id = i.order_id
  WHERE o.created_at &gt;= #{from}
</select>

注意:JOIN 会导致行重复(会产生笛卡尔型重复列),ResultMap 的 <collection> 可以正确合并,但 JOIN 查询返回列较多、网络流量增加。分页时慎用 JOIN(推荐先分页拿主表 ids,再用 IN 批量加载子表)。


3. MyBatis 缓存(一级与二级缓存)

3.1 一级缓存(SqlSession 级别)

  • 默认开启,作用域为 SqlSession。一次 SqlSession 内对同一条 SQL(相同参数)会命中缓存。
  • 注意 :每次操作 insert/update/delete 会清空相关缓存(SqlSession 的缓存),所以短事务内可以受益,但跨多个 SqlSession 无法命中。

3.2 二级缓存(Mapper/namespace 级别)

  • 需要在 mapper xml 中显式开启 <cache />
  • 二级缓存是基于 namespace 的,每个 namespace(mapper)为一套缓存。
  • 支持 type(序列化实现)、eviction(LRU、FIFO、SOFT、WEAK)、flushInterval(刷新间隔)、size 等。

示例:

xml 复制代码
<cache eviction="LRU" flushInterval="600000" size="1024" readOnly="false"/>

注意事项:

  • 二级缓存会在对应 namespace 的 insert/update/delete 操作时被清空(flush),因此写多读少场景适合。
  • 对象可变性:缓存应当存不可变对象或设置 readOnly="true"。若对象会被修改,不要缓存或返回拷贝。
  • 分布式环境下二级缓存是本地 JVM 的缓存(除非集成 Redis 等集中缓存实现),生产多实例部署时需谨慎(可以集成 MyBatis-Redis-Cache 插件)。

4. 批量操作(高效插入/更新)

4.1 两种主流方式

  1. JDBC batch(SqlSession executorType=BATCH)
    优点:减少网络往返,提升批量写入性能。
  2. 单条拼接多值 INSERT(INSERT ... VALUES (...), (...), (...))
    优点:更少的 SQL 但 SQL 语句可能过长且不同 DB 对批量语法限制不同。

4.2 MyBatis BATCH 示例(推荐)

配置 Mapper(注解或 XML)通常一样,但在 SqlSession 层面需设置 ExecutorType.BATCH

Service 示例(Spring Boot):

java 复制代码
@Autowired
private SqlSessionFactory sqlSessionFactory;

public void batchInsert(List<OrderItem> items) {
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
    try {
        OrderItemMapper mapper = sqlSession.getMapper(OrderItemMapper.class);
        int i = 0;
        for (OrderItem item : items) {
            mapper.insert(item);
            if (i % 500 == 0) { // 每500条flush一次
                sqlSession.flushStatements();
            }
            i++;
        }
        sqlSession.commit();
    } catch (Exception e) {
        sqlSession.rollback();
        throw e;
    } finally {
        sqlSession.close();
    }
}

Mapper XML insert

xml 复制代码
<insert id="insert" parameterType="OrderItem" useGeneratedKeys="true" keyProperty="id">
  INSERT INTO order_items (order_id, product_id, qty, price)
  VALUES (#{orderId}, #{productId}, #{qty}, #{price})
</insert>

关键点:

  • flushStatements() 用于把缓存的语句批量下发到 DB,并返回每次执行的结果(避免内存过大)。
  • useGeneratedKeys="true" 在 batch 下对某些驱动不稳定,测试是否能正确回填主键;若不能,可使用 SELECT LAST_INSERT_ID() 或批量插入后用业务层自行生成 ID(例如雪花ID)。
  • 异常处理:批处理出现异常时,部分驱动可能只在 executeBatch 抛异常,需 rollback()

4.3 MyBatis foreach 批量 INSERT(构造单条 SQL)

XML 示例:

xml 复制代码
<insert id="batchInsert" parameterType="list">
  INSERT INTO order_items (order_id, product_id, qty, price)
  VALUES
  <foreach collection="list" item="item" separator=",">
    (#{item.orderId}, #{item.productId}, #{item.qty}, #{item.price})
  </foreach>
</insert>

优点:单条 SQL,数据库端解析一次,性能较好。缺点:SQL 过长、参数绑定数量大时可能有问题;不同 DB 参数限制不一样。


5. 【最重要】实战项目:订单 / 订单明细性能优化(完整、详细)

下面给出一个可复现的实战项目

目标:模拟订单创建与订单明细写入(高并发场景),用不同策略(逐条 insert、SqlSession BATCH、foreach SQL)对比性能,并演示 Mapper 映射与缓存优化点。

5.1 项目概述

  • 框架:Spring Boot 3.x + MyBatis-Spring-Boot-Starter
  • DB:MySQL 8.x(单机测试)
  • 表结构:ordersorder_items
  • 场景:一次创建一笔订单含 N 个明细(N 可配置),并行并发 Q 个请求(模拟高并发)

5.2 表结构(SQL)

sql 复制代码
CREATE DATABASE IF NOT EXISTS demo;
USE demo;

CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT,
  total_amount DECIMAL(10,2),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE order_items (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  order_id BIGINT,
  product_id BIGINT,
  qty INT,
  price DECIMAL(10,2)
);

5.3 项目依赖(pom.xml 片段)

xml 复制代码
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>3.0.1</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.33</version>
</dependency>

application.yml(关键配置):

yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
  mybatis:
    mapper-locations: classpath*:mapper/*.xml
    type-aliases-package: com.example.domain

5.4 Mapper 接口 & XML(简洁示例)

OrderMapper.java

java 复制代码
public interface OrderMapper {
    int insert(Order order); // useGeneratedKeys
}

OrderItemMapper.java

java 复制代码
public interface OrderItemMapper {
    int insert(OrderItem item);
    int batchInsert(@Param("list") List<OrderItem> list);
}

OrderItemMapper.xml(包含 foreach 批量插入)

xml 复制代码
<insert id="batchInsert" parameterType="list">
  INSERT INTO order_items (order_id, product_id, qty, price)
  VALUES
  <foreach collection="list" item="item" separator=",">
    (#{item.orderId}, #{item.productId}, #{item.qty}, #{item.price})
  </foreach>
</insert>

5.5 Service 层实现三种策略(逐条、SqlSession BATCH、foreach 单条 SQL)

OrderService.java(核心方法)

java 复制代码
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    // 1. 逐条插入(baseline)
    public void createOrderSingle(Order order, List<OrderItem> items) {
        orderMapper.insert(order);
        for (OrderItem it : items) {
            it.setOrderId(order.getId());
            orderItemMapper.insert(it);
        }
    }

    // 2. SqlSession BATCH
    public void createOrderBatch(Order order, List<OrderItem> items) {
        orderMapper.insert(order);
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
        try {
            OrderItemMapper mapper = sqlSession.getMapper(OrderItemMapper.class);
            for (int i = 0; i < items.size(); i++) {
                OrderItem it = items.get(i);
                it.setOrderId(order.getId());
                mapper.insert(it);
                if (i % 500 == 0) {
                    sqlSession.flushStatements();
                }
            }
            sqlSession.commit();
        } catch (Exception e) {
            sqlSession.rollback();
            throw e;
        } finally {
            sqlSession.close();
        }
    }

    // 3. foreach 单条 SQL(推荐在 items 数量适中时)
    public void createOrderBatchSql(Order order, List<OrderItem> items) {
        orderMapper.insert(order);
        for (OrderItem it : items) {
            it.setOrderId(order.getId());
        }
        orderItemMapper.batchInsert(items);
    }
}

5.6 并发测试脚本(Java 基础模拟或使用 JMeter)

简单 Java 多线程模拟:

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(50);
for (int i = 0; i < 200; i++) {
    pool.submit(() -> {
        Order order = new Order(...);
        List<OrderItem> items = buildItems(20);
        long start = System.currentTimeMillis();
        orderService.createOrderBatchSql(order, items); // 切换三种方法测试
        System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
    });
}
pool.shutdown();
pool.awaitTermination(10, TimeUnit.MINUTES);

5.7 注意点与调优参数

  • 事务控制 :批量操作应在事务中(Spring @Transactional)以保证一致性,但要注意事务内不能持有太长时间锁,影响并发吞吐。

  • JDBC batch size:适中为佳(如 200 ~ 1000),过大导致内存压力和单次事务过大,过小则批量优势不足。

  • useServerPrepStmts :MySQL 驱动 rewriteBatchedStatements=true 可以提升 batch insert 性能(使用 JDBC 参数)。

    复制代码
    jdbc:mysql://.../?rewriteBatchedStatements=true
  • 主键回写 :在 batch/foreach 下对 useGeneratedKeys 的行为因驱动而异,请测试是否能正确回填 ID。

  • 监控慢 SQL :启用 MySQL slow query log 并使用 pt-query-digest 分析。


6. 性能测试与对比(示例数据)

测试环境:MySQL 本地,单机 8 核,16GB 内存,200 并发线程,每个请求 20 个明细。

方法 平均耗时 / 请求 说明
逐条 insert(baseline) 1200 ms 每条 insert 均为单次请求,网络往返多,性能差
SqlSession BATCH (flush 500) 180 ms 批处理减少 round-trip,需注意内存/事务大小
foreach 单条 SQL 150 ms 单 SQL 多值插入,解析耗时小,整体最快(当 items 不是超大时)

结论:在多数场景 foreach 单 SQL 与 JDBC batch 性能最好;当每条记录需要复杂处理或驱动不支持大 batch 时,ExecutorType.BATCH 更安全。


7. 常用优化 checklist(发布前快速自检)

  • SQL 使用 EXPLAIN 验证索引是否命中
  • 避免 SELECT *,只选必要字段
  • 对频繁查询的维度建立合适复合索引
  • 消除 N+1,优先 IN + 一次加载子表或 JOIN(分页慎用 JOIN)
  • 批量写入采用 foreachExecutorType.BATCH,并设置合理 flush 大小
  • 开启并分析慢查询日志,使用 pt-query-digest 批量定位问题
  • 二级缓存只用于读多写少场景,控制缓存失效与对象可变性
  • 日志级别:生产关闭过多 debug SQL 打印(影响性能)
  • 连接池配置(maxActive、minIdle、validationQuery)匹配并发量

8. 常见陷阱与 FAQ

  • Q:batch 插入时 useGeneratedKeys 能否返回所有主键?
    A:取决于驱动,MySQL + Connector/J 在某些 batch 模式下能回填,但并不总是可靠,建议测试或使用雪花 ID/业务 ID。
  • Q:二级缓存适合所有查询吗?
    A:不适合高并发写场景(写会导致 flush),适合读多写少且数据可容忍延迟的场景。
  • Q:分页时应该使用 JOIN 吗?
    A:分页主表建议先分页拿主键,再用主键 IN 批量加载子表,避免 JOIN 导致行数膨胀影响分页准确性。

9. 小结(行动指南)

  1. 先用 EXPLAIN 找出慢 SQL,再改 SQL / 加索引。
  2. 解决 N+1:优先 IN 批量加载或 ResultMap JOIN(分页慎用)。
  3. 写操作量大时用 foreach 单 SQL 或 ExecutorType.BATCH;控制 batch size。
  4. 二级缓存谨慎启用(读多写少场景),关注缓存失效策略。
  5. 在 CI/CD 中加入慢查询/压测脚本,避免线上回归性能问题。
相关推荐
unicrom_深圳市由你创科技2 小时前
MySQL 全文索引进阶:中文分词配置 + 模糊查询性能优化
mysql·性能优化·中文分词
熬夜敲代码的小N2 小时前
仓颉ArrayList动态数组源码分析:从底层实现到性能优化
数据结构·python·算法·ai·性能优化
沐浴露z2 小时前
详细解析 MySQL 性能优化之【索引下推】
数据库·mysql·性能优化
F_Director2 小时前
Webpack DLL动态链接库的应用和思考
前端·webpack·性能优化
那我掉的头发算什么2 小时前
【javaEE】多线程--认识线程、多线程
java·jvm·redis·性能优化·java-ee·intellij-idea
是烟花哈3 小时前
后端开发CRUD实现
java·开发语言·spring boot·mybatis
半部论语4 小时前
MyBatis-Plus 通用 CRUD 实现原理技术文档
java·spring boot·mybatis
得物技术5 小时前
得物TiDB升级实践
数据库·性能优化·tidb
.豆鲨包6 小时前
【Android】Android内存缓存LruCache与DiskLruCache的使用及实现原理
android·java·缓存