在 for 循环中用查询,相同参数返回的结果却不同?MyBatis 缓存的坑...

一、问题出现场景

问题:

我使用的 Mybatis-plus 的 selectByIds 查询,发现明明我传的参数是 [1,2,3],但是结果却是 [1,2,3,4]???

直接在 selectByIds 那一行打断点,发现他是真的返回的 [1,2,3,4]。

自己把 SQL 拿出来填上参数然后去数据库执行,结果是正确的为:[1,2,3]。

使用 @SpringBootTest 进行集成测试 Mybatis-plus 的 selectByIds,发现结果变正常了是 [1,2,3]…

在这里插入图片描述

业务场景:

可以理解成:一个 API 有一个响应结果实体类,一个实体类包含了多个字段,这些字段又可能是另一个实体类,并且还可能存在循环依赖情况。

为了简化代码的理解难度,没有选择把一批次的 API 对应的信息一次性查出来,然后通过 Map 来进行处理。而是每个 API 都单独处理。

简化示例代码如下:

可以先看看,能不能看出存在的问题。NPE 请忽略。
参数是: [[1,2,3], [1,2,3]]

@Transactional
public void queryAndUpdateCategory(List<List<Long>> categoryIdsList) {
	List<List<Category>> results = new ArrayList<>();

	// query
	for (List<Long> categoryIds : categoryIdsList) {
		// get current list
		List<Category> result = mapper.selectByIds(categoryIds);

		// get parent list
		List<Long> parentIds = result.stream().map(Category::getParentId).toList();
		List<Category> parents = mapper.selectByIds(parentIds);

		// merge
		result.addAll(parents);

		// results add
		results.add(result);
	}

	// update:这里忽略不执行
	// this.update(results);
}

二、问题出现的原因

这是因为 MyBatis 存在一种减少数据库查询次数、提升应用性能的机制,就是 MyBatis 缓存

缓存分为一级缓存和二级缓存,一级缓存默认是开启的。

一级缓存的简单模型是 Map<CacheKey, Result>,问题就在这里:缓存中存储的是查询结果对象的引用(Reference),而不是深拷贝(Deep Copy)

一级缓存的触发条件:

  • 在同一个 SqlSession 中,执行了完全相同的 SQL 语句(这里 @Transactional 导致查询都在同一个 SqlSession 中)。
  • 中间没有发生过 INSERT、UPDATE、DELETE 操作(这些操作会清空一级缓存)。

第一次查询 [1,2,3]:

// categoryIds: [1,2,3]
// 会去数据库里面执行SQL:SELECT WHERE id IN (1,2,3);
List<Category> result = mapper.selectByIds(categoryIds); // result:[1,2,3]

// parents: [4]
// 这里修改了 result
result.addAll(parents); // result:[1,2,3,4]

第二次查询 [1,2,3]:

// categoryIds: [1,2,3]
// 这个时候因为是同一个SQL,直接就去缓存里面拿之前那个修改后的 result,并不会再次执行 SQL
List<Category> result = mapper.selectByIds(categoryIds); // result:[1,2,3,4]

三、解决方案

知道原因后解决办法就很多:

  1. 不修改 MyBatis 查询返回的结果,把结果拷贝一份

    List<SwaggerApi> result = mapper.selectByIds(categoryIds);
    ===>
    List<Category> result = new ArrayList<>(mapper.selectByIds(categoryIds));
    
  2. 缩短 SqlSession 生命周期

    在 for 循环中,每次操作使用独立的 SqlSession(或独立的 @Transactional 方法)。
    这样缓存不会跨次共享,但会牺牲性能。

  3. 重构代码

    重构代码,一般情况下也不会出现重复查询的情况,可以考虑重构代码。

  4. 关闭一级缓存

    关不了

四、相关

1、一级缓存(Local Cache)

  • 优点:

    • 性能高:缓存位于 JVM 内存中,访问速度快,无网络开销。
    • 默认开启:无需配置,MyBatis 自动启用,开箱即用。
    • 自动管理:执行更新操作(INSERT、UPDATE、DELETE)后,缓存会自动清空,保证单次会话内的数据一致性。
    • 减少数据库访问:在同一个 SqlSession 中重复查询相同数据时,直接返回缓存结果,降低数据库压力。
  • 局限性:

    • 作用域有限:仅在当前 SqlSession 内有效,不同请求之间无法共享。
    • 生命周期短:随 SqlSession 的关闭而销毁,无法跨请求缓存数据。
    • 存在缓存污染风险:缓存存储的是对象引用。如果业务代码修改了查询返回的 List 或实体对象,会导致缓存数据被意外修改,后续查询返回错误结果。
    • 无过期机制:缓存不会自动过期,只能通过更新操作或 SqlSession 关闭来失效。
    • 在复杂逻辑中易失效:如在循环中执行更新操作,会清空缓存,影响后续查询性能。
  • 适用场景:

    • 适用于单次请求内多次查询相同数据的场景,例如在一个 Service 方法中多次调用 Mapper 查询同一记录。

2、二级缓存(Second Level Cache)

  • 优点:

    • 跨 SqlSession 共享:缓存作用于 Mapper 的 namespace 级别,同一个命名空间下的所有 SqlSession 可以共享缓存。
    • 提升整体查询效率:适合读多写少的场景,能有效减少对数据库的重复查询。
    • 可配置性强:支持设置缓存策略(如 LRU)、缓存大小、刷新间隔等参数。
    • 与一级缓存协同工作:查询时优先检查二级缓存,再查一级缓存,最后访问数据库,形成缓存层级。
  • 局限性:

    • 需手动开启:必须在 Mapper XML 中配置 或使用 @CacheNamespace 注解,且实体类需实现 Serializable 接口。
    • 不支持分布式部署:缓存仍存储在本地 JVM 内存中。在多实例部署环境下,各节点缓存无法同步,会导致数据不一致。
    • 数据一致性差:如果数据库被外部系统修改,MyBatis 无法感知,缓存数据会长期不更新。
    • 缓存粒度粗:执行任意更新操作都会清空整个 namespace 的缓存,影响范围大。
    • 功能有限:不支持过期时间(TTL)、持久化、高可用等高级特性。
    • 可能引发内存问题:若缓存数据量过大且未限制大小,可能导致 JVM 内存溢出。
  • 适用场景:

    • 适用于单机部署、数据变更频率低的场景,如字典表、配置信息等静态数据的缓存。

总结与建议
一级缓存是 MyBatis 内置的轻量级缓存,适合优化单次请求内的重复查询,开发者应避免修改其返回结果以防缓存污染。
二级缓存虽能跨会话共享,但受限于本地存储,在分布式系统中存在严重一致性问题,不推荐在集群环境下使用
现代应用推荐方案:关闭 MyBatis 二级缓存,使用 Redis 等分布式缓存系统来实现跨节点的数据共享与高可用,一级缓存可作为本地加速的补充。

特性一级缓存二级缓存
默认开启✅ 是❌ 否(需配置)
作用域SqlSessionMapper (namespace)
共享性同一 SqlSession同一 namespace 下所有 SqlSession
存储位置JVM 内存(本地)JVM 内存(本地)
分布式支持❌ 无❌ 无
数据一致性✅ 较好(UPDATE 自动清空)⚠️ 差(外部变更不可知)
性能✅ 极高(无网络)✅ 高(本地内存)
配置复杂度✅ 无⚠️ 需配置 <cache/>
适用场景单次请求内重复查询单机应用,读多写少的静态数据
主要风险缓存污染(修改返回值)数据不一致、内存溢出
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值