java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis N+1查询、一级缓存踩坑

MyBatis中N+1 查询、一级缓存踩坑解决方案

作者:长大1988

本文主要介绍了MyBatis中N+1 查询、一级缓存踩坑解决方案,结合执行原理和典型场景,给出JOIN嵌套映射、批量查询等解决方案,助你根治性能问题,避免数据错误,生产环境必备

MyBatis 上手容易,但一旦系统并发上来,慢 SQL、CPU 飙高、数据莫名不对,往往都能追溯到两个老熟人:

N+1 查询问题

一级缓存误用问题

这两个坑,一个影响性能,一个影响正确性。本文将结合执行原理 → 典型场景 → 解决方案 → 实战建议,彻底拆干净。

一、N+1 查询问题:最隐蔽的性能杀手

1️⃣ 什么是 N+1 查询?

举个最常见的例子:查询订单列表,并关联查询每个订单的用户信息。

错误写法(典型 N+1)

// 1. 查订单
List<Order> orders = orderMapper.selectAll();

// 2. 遍历订单,查用户
for (Order order : orders) {
    User user = userMapper.selectById(order.getUserId());
    order.setUser(user);
}

SQL 执行情况:

1 条:select * from orders
N 条:select * from user where id = ?

👉 总共 N+1 条 SQL

orders = 1000 时,直接 1001 次 DB 访问

2️⃣ 为什么 N+1 这么致命?

特征非常明显:

单次接口请求,SQL 数量异常多,但每条 SQL 都很快。

3️⃣ MyBatis 中 N+1 的高发场景

✅ 场景一:association / collection 的select属性

<resultMap id="orderMap" type="Order">
    <id property="id" column="id"/>
    <association property="user"
                 column="user_id"
                 select="com.xxx.UserMapper.selectById"/>
</resultMap>

⚠️ 这就是官方文档里最容易踩的 N+1 陷阱

每映射一条 Order,就会触发一次 selectById

✅ 场景二:懒加载(lazyLoadingEnabled)

<setting name="lazyLoadingEnabled" value="true"/>

懒加载 ≠ 解决问题,它只是推迟 N+1 的发生时机

4️⃣ N+1 的标准解决方案

✅ 方案一(最推荐):JOIN + 嵌套映射(一次 SQL)

<select id="selectOrdersWithUser" resultMap="orderUserMap">
    select o.id, o.order_no, u.id as user_id, u.name
    from orders o
    left join user u on o.user_id = u.id
</select>
<resultMap id="orderUserMap" type="Order">
    <id property="id" column="id"/>
    <association property="user" javaType="User">
        <id property="id" column="user_id"/>
        <result property="name" column="name"/>
    </association>
</resultMap>

1 条 SQL

✅ 性能好

✅ 生产环境首选

✅ 方案二:批量查询(次优)

List<Order> orders = orderMapper.selectAll();

Set<Long> userIds = orders.stream()
        .map(Order::getUserId)
        .collect(Collectors.toSet());

Map<Long, User> userMap =
        userMapper.selectByIds(userIds)
                  .stream()
                  .collect(Collectors.toMap(User::getId, u -> u));

orders.forEach(o -> o.setUser(userMap.get(o.getUserId())));

✅ SQL 数量:2 条

✅ 适合复杂查询或跨数据源场景

✅ 方案三:开启二级缓存(辅助手段,不是根治)

⚠️ 二级缓存只能缓解,不能消除 N+1

5️⃣ N+1 排查小技巧

黄金法则:

一个接口,SQL 超过 5 条,就要警惕 N+1。

二、一级缓存:最容易“出 Bug”的地方

1️⃣ MyBatis 一级缓存是什么?

同一个 SqlSession
同一个 SQL
同一个参数
→ 第二次直接走缓存

2️⃣ 一级缓存的典型踩坑场景

❌ 坑一:事务中读到“脏数据”

@Transactional
public void updateUser() {
    User u1 = userMapper.selectById(1L);
    userMapper.updateName(1L, "newName");
    User u2 = userMapper.selectById(1L); // 还是旧值!
}

原因:

这不是 Bug,是设计如此

❌ 坑二:Spring 环境下缓存“时灵时不灵”

很多人以为:

Spring + MyBatis 一级缓存一直生效

真相是:

场景一级缓存
方法内多次同 SQL
跨 @Transactional 方法
Mapper 方法独立调用

因为 Spring 每次执行完 Mapper 方法后,close SqlSession。

❌ 坑三:分布式环境下数据不一致

⚠️ 一级缓存完全不适合分布式

3️⃣ 一级缓存的源码级真相(重点)

MyBatis 一级缓存失效条件:

只读事务 + 多次查询 = 一级缓存命中

4️⃣ 一级缓存的正确使用姿势

✅ 原则一:不要把一级缓存当“一致性保障”

一级缓存是性能优化手段,不是数据一致性手段

✅ 原则二:必要时手动清缓存

sqlSession.clearCache();

或在 XML 中:

<select id="selectById" flushCache="true">

✅ 原则三:Spring 项目慎用长事务

长事务 = 一级缓存长期存活 = 脏读风险

@Transactional
public void process() {
    // 大量逻辑 + 多次查询
}

✅ 拆小事务

✅ 读写分离

✅ 查询走从库

5️⃣ 一级缓存 vs 二级缓存

对比项一级缓存二级缓存
作用域SqlSessionMapper (namespace)
默认开启关闭
分布式
一致性更弱
推荐了解即可慎用

👉 生产环境建议:

一级缓存:了解原理,避免误判

二级缓存:能不用就不用

三、综合实战建议(强烈推荐)

✅ MyBatis 性能与安全最佳实践

  1. 禁止 association / collection 使用 select
  2. 复杂对象用 JOIN + resultMap
  3. 一个接口 SQL ≤ 5 条
  4. 事务尽量短小
  5. 不依赖一级缓存做业务判断
  6. 分布式场景禁用 MyBatis 缓存
  7. 缓存统一交给 Redis

四、一张图总结

N+1 查询
├── 表现:SQL 数量爆炸
├── 根因:逐条关联查询
└── 解法:JOIN / 批量查询
一级缓存
├── 表现:数据“明明改了却没变”
├── 根因:SqlSession 内缓存命中
└── 解法:理解作用域 + 避免强依赖

五、一句话总结

N+1 是性能问题,一级缓存是正确性问题。 ​

N+1 靠 JOIN 根治,一级缓存靠“别信它”规避。

到此这篇关于MyBatis中N+1 查询、一级缓存踩坑解决方案的文章就介绍到这了,更多相关MyBatis中N+1 查询、一级缓存踩坑内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文