有一类bug 在 MyBatis 项目里出现的频率不低查询结果和数据库里的数据不一致但日志里没有打出第二次 SQL程序也没抛异常一切看起来都很正常。出现这种情况有相当一部分原因指向同一个地方——MyBatis 的一级缓存。它一直开着但你可能从没有意识到MyBatis 有两层缓存一级缓存本地缓存和二级缓存全局缓存。一级缓存绑定在 SqlSession 上每次创建新的 SqlSession就会创建对应的本地缓存。在同一个 Session 内执行相同的查询语句第二次起不会再打数据库直接从缓存里返回。这个行为是默认开启的而且一级缓存无法被真正关闭——cacheEnabledfalse只影响二级缓存对一级缓存没有作用。你能做的只是通过localCacheScope把它的生效范围从SESSION降为STATEMENT后者相当于每次查询结束就清空。大多数情况下这个行为是透明的。当 Spring 事务开启时同一个事务内的所有 MyBatis Mapper 实例共享同一个 SqlSession事务结束后 SqlSession 才会被销毁。这意味着在一个带Transactional的方法里你对同一条记录查了两次控制台只会打出一条 SQL——第二次走的是缓存。真正会踩坑的场景问题不出在查了两次这个动作上而出在下面两种情况里。第一种查出来的对象被修改了。一级缓存存的不是值的拷贝而是对象引用。如果你拿到查询结果后修改了对象的属性那等于直接修改了缓存里的对象后续再查同一条记录返回的就是那个被改过的对象而不是数据库里的真实数据。Transactional public void processOrder(Long orderId) { Order order orderMapper.selectById(orderId); // 业务逻辑里修改了对象属性但没有 update 进数据库 order.setStatus(PROCESSING); // 后续某个地方再查一次拿到的仍然是被改过的对象 Order check orderMapper.selectById(orderId); // check.getStatus() PROCESSING而不是数据库里的原始值 }这类 bug 排查起来很难受因为你看日志SQL 只打了一条没有任何报错数据库里的值也没有被污染——问题只存在于内存里而且只在事务存活期间。第二种跨 Session 的写操作导致的不一致。一级缓存的有效范围是 SqlSession 内部其他线程或其他服务节点对数据库做了更新本 Session 内的缓存感知不到查出来的数据依然是旧的。这在高并发写入场景或者分布式部署的情况下是一个实实在在的数据一致性风险。Spring Boot 里怎么配数据的查询顺序是二级缓存 → 一级缓存 → 数据库。如果你想让一级缓存在每次 SQL 执行后就失效可以把localCacheScope设为STATEMENTmybatis: configuration: local-cache-scope: STATEMENT设成STATEMENT后每次查询结束缓存立即清空效果上相当于禁用了一级缓存。代价是每次查询都会打库没有了同一事务内的重复查询优化。对于大多数业务服务来说这个代价是可以接受的毕竟事务内重复查同一条记录的场景本来就不多。如果不想全局修改只想针对某个查询强制绕过缓存可以在 Mapper XML 里给对应的select加上flushCachetrueselect idselectById resultTypeOrder flushCachetrue SELECT * FROM t_order WHERE id #{id} /select这样只有这个查询每次都会清空缓存再走数据库不影响其他语句。二级缓存更要谨慎相比一级缓存二级缓存的坑往往更难排查。二级缓存是全局缓存跨 SqlSession 共享需要在mybatis-config.xml里开启cacheEnabledtrue同时在对应的 Mapper XML 里加cache /标签才会生效。二级缓存在多表查询时的问题更突出。假设你的订单查询涉及order表和user表二级缓存是按 Mapper namespace 级别隔离的。user表发生了更新UserMapper的缓存会被清空但OrderMapper里依赖了user数据的查询结果不会自动失效脏数据就这样产生了。在分布式环境下MyBatis 的默认缓存实现是基于本地内存的多个节点之间缓存无法共享建议直接把二级缓存关掉cacheEnabledfalse需要缓存的地方改用 Redis 等分布式缓存手动管理成本更低安全性更高。判断标准很简单用一条原则来决定你的项目要不要动 MyBatis 缓存配置如果你的项目是单机、低并发且不会在代码里修改 Mapper 返回的对象一级缓存的默认行为基本不会给你惹麻烦。但只要有以下任一情况——多节点部署、高并发写入、业务代码里会修改查询结果对象、事务内有条件判断依赖最新数据——都建议把localCacheScope改成STATEMENT把二级缓存显式关掉不要依赖默认值。MyBatis 的缓存设计本身没有问题它是为了解决特定场景的性能问题而存在的。出问题的原因往往是开发者不知道它默认开着或者知道它存在但没想清楚它和 Spring 事务配合时的边界。把这个边界搞清楚就不容易被它坑了。
MyBatis 一级缓存默默开着,这个坑你踩过吗?
发布时间:2026/7/2 9:37:11
有一类bug 在 MyBatis 项目里出现的频率不低查询结果和数据库里的数据不一致但日志里没有打出第二次 SQL程序也没抛异常一切看起来都很正常。出现这种情况有相当一部分原因指向同一个地方——MyBatis 的一级缓存。它一直开着但你可能从没有意识到MyBatis 有两层缓存一级缓存本地缓存和二级缓存全局缓存。一级缓存绑定在 SqlSession 上每次创建新的 SqlSession就会创建对应的本地缓存。在同一个 Session 内执行相同的查询语句第二次起不会再打数据库直接从缓存里返回。这个行为是默认开启的而且一级缓存无法被真正关闭——cacheEnabledfalse只影响二级缓存对一级缓存没有作用。你能做的只是通过localCacheScope把它的生效范围从SESSION降为STATEMENT后者相当于每次查询结束就清空。大多数情况下这个行为是透明的。当 Spring 事务开启时同一个事务内的所有 MyBatis Mapper 实例共享同一个 SqlSession事务结束后 SqlSession 才会被销毁。这意味着在一个带Transactional的方法里你对同一条记录查了两次控制台只会打出一条 SQL——第二次走的是缓存。真正会踩坑的场景问题不出在查了两次这个动作上而出在下面两种情况里。第一种查出来的对象被修改了。一级缓存存的不是值的拷贝而是对象引用。如果你拿到查询结果后修改了对象的属性那等于直接修改了缓存里的对象后续再查同一条记录返回的就是那个被改过的对象而不是数据库里的真实数据。Transactional public void processOrder(Long orderId) { Order order orderMapper.selectById(orderId); // 业务逻辑里修改了对象属性但没有 update 进数据库 order.setStatus(PROCESSING); // 后续某个地方再查一次拿到的仍然是被改过的对象 Order check orderMapper.selectById(orderId); // check.getStatus() PROCESSING而不是数据库里的原始值 }这类 bug 排查起来很难受因为你看日志SQL 只打了一条没有任何报错数据库里的值也没有被污染——问题只存在于内存里而且只在事务存活期间。第二种跨 Session 的写操作导致的不一致。一级缓存的有效范围是 SqlSession 内部其他线程或其他服务节点对数据库做了更新本 Session 内的缓存感知不到查出来的数据依然是旧的。这在高并发写入场景或者分布式部署的情况下是一个实实在在的数据一致性风险。Spring Boot 里怎么配数据的查询顺序是二级缓存 → 一级缓存 → 数据库。如果你想让一级缓存在每次 SQL 执行后就失效可以把localCacheScope设为STATEMENTmybatis: configuration: local-cache-scope: STATEMENT设成STATEMENT后每次查询结束缓存立即清空效果上相当于禁用了一级缓存。代价是每次查询都会打库没有了同一事务内的重复查询优化。对于大多数业务服务来说这个代价是可以接受的毕竟事务内重复查同一条记录的场景本来就不多。如果不想全局修改只想针对某个查询强制绕过缓存可以在 Mapper XML 里给对应的select加上flushCachetrueselect idselectById resultTypeOrder flushCachetrue SELECT * FROM t_order WHERE id #{id} /select这样只有这个查询每次都会清空缓存再走数据库不影响其他语句。二级缓存更要谨慎相比一级缓存二级缓存的坑往往更难排查。二级缓存是全局缓存跨 SqlSession 共享需要在mybatis-config.xml里开启cacheEnabledtrue同时在对应的 Mapper XML 里加cache /标签才会生效。二级缓存在多表查询时的问题更突出。假设你的订单查询涉及order表和user表二级缓存是按 Mapper namespace 级别隔离的。user表发生了更新UserMapper的缓存会被清空但OrderMapper里依赖了user数据的查询结果不会自动失效脏数据就这样产生了。在分布式环境下MyBatis 的默认缓存实现是基于本地内存的多个节点之间缓存无法共享建议直接把二级缓存关掉cacheEnabledfalse需要缓存的地方改用 Redis 等分布式缓存手动管理成本更低安全性更高。判断标准很简单用一条原则来决定你的项目要不要动 MyBatis 缓存配置如果你的项目是单机、低并发且不会在代码里修改 Mapper 返回的对象一级缓存的默认行为基本不会给你惹麻烦。但只要有以下任一情况——多节点部署、高并发写入、业务代码里会修改查询结果对象、事务内有条件判断依赖最新数据——都建议把localCacheScope改成STATEMENT把二级缓存显式关掉不要依赖默认值。MyBatis 的缓存设计本身没有问题它是为了解决特定场景的性能问题而存在的。出问题的原因往往是开发者不知道它默认开着或者知道它存在但没想清楚它和 Spring 事务配合时的边界。把这个边界搞清楚就不容易被它坑了。