⚡ MyBatis 性能优化指南:Mapper 映射、缓存与批量操作实战
关键词:MyBatis、性能优化、Mapper 映射、缓存(一级/二级)、批量插入/更新、批处理实战
亮点:覆盖常见性能坑、配置技巧、以及一个详尽的实战项目(含配置、代码、压力对比、注意点),方便直接贴到工程中复用。

摘要(3句话速读)
MyBatis 在灵活性和可控性上很强,但不注意会导致 N+1 查询、过度全列查询、缓存失效和低效的批量操作。本文先讲清核心概念(Mapper 映射/结果映射、一级/二级缓存、批处理),再给出完整实战项目 :用 Spring Boot + MyBatis 模拟订单/订单明细场景,演示如何用 executorType=BATCH、合理 Mapper 映射和二级缓存把性能拉上来,并给出对比测试数据与优化建议。
目录
- 常见性能问题与优化思路
- Mapper 映射(ResultMap、避免 SELECT *、N+1 问题)
- MyBatis 缓存(一级与二级缓存、配置与注意点)
- 批量操作(JDBC/SqlSession BATCH、flushStatements、主键回写)
- 实战项目(最详细章节)
- 性能测试与对比结果
- 常用优化 checklist & 经验总结
- 推荐标签与互动
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 >= #{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 两种主流方式
- JDBC batch(SqlSession executorType=BATCH)
优点:减少网络往返,提升批量写入性能。 - 单条拼接多值 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(单机测试)
- 表结构:
orders、order_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) - 批量写入采用
foreach或ExecutorType.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. 小结(行动指南)
- 先用
EXPLAIN找出慢 SQL,再改 SQL / 加索引。 - 解决 N+1:优先
IN批量加载或 ResultMap JOIN(分页慎用)。 - 写操作量大时用
foreach单 SQL 或ExecutorType.BATCH;控制 batch size。 - 二级缓存谨慎启用(读多写少场景),关注缓存失效策略。
- 在 CI/CD 中加入慢查询/压测脚本,避免线上回归性能问题。