说说 MyBatis 的缓存机制?
- 标签:后端、MyBatis
🏛 业务架构师
MyBatis 的缓存分两级,我按是否建议启用讲:
- 一级缓存(SqlSession 级,默认开启):同一 SqlSession 内相同查询直接命中本地缓存。Spring 集成下每次请求通常是新 SqlSession,一级缓存几乎不生效;但在同一事务内(同一 SqlSession)它是活的——带来的经典坑:事务内两次相同查询之间别的事务提交了更新,第二次读到的还是缓存旧值。我的规约:关键判断不要依赖同一事务内的重复查询,或在两次查询间 clearCache;
- 二级缓存(namespace/mapper 级,默认关闭):跨 SqlSession 共享,序列化存磁盘。理论上美好,实践上我不建议开启——
- 多表关联查询时,A 表缓存无法感知 B 表更新,脏读风险(订单缓存里有商品名,商品改价了缓存不知道);
- 分布式环境下各节点缓存各自为政,不一致被放大;
- 失效粒度粗(整个 namespace 清空),命中率低。
我的架构结论:缓存应该上移,不要留在 ORM 层。统一放到 Redis 层有三个理由:失效策略可控(按 key 精准失效、TTL 兜底)、可观测(命中率、大 key 监控)、架构统一(容量规划与治理一套体系)。ORM 层缓存是"看起来免费"的隐性风险,出了脏读排查链路还特别绕。
一句话:MyBatis 定位是 SQL 映射工具,让它专心干活;缓存作为独立的基础设施能力,集中治理——分层职责清楚,坑就少。
🏗 系统架构师
MyBatis 缓存分两级,我按"作用域 + 失效机制 + 生产建议"讲:
- 一级缓存(SqlSession 级):HashMap 存在 Executor 里,同一 SqlSession 内相同查询直接命中,任何 update/insert/delete、或 sqlSession 提交/关闭即清空。默认开启。坑:Spring 集成下每次请求通常是新 SqlSession,一级缓存基本形同虚设;而同一 Session 内多次查询之间若有别的会话改了数据,会读到脏数据——分布式/多会话场景我直接
localCacheScope=STATEMENT关掉它; - 二级缓存(namespace/mapper 级):跨 SqlSession,结果写到 namespace 级缓存,事务提交后才生效。失效粒度粗:任一 insert/update/delete 清空整个 namespace。跨 namespace 通过
<cache-ref>共享,但极易造成数据不一致。
我的生产建议:多表关联查询 + 更新频繁的场景,二级缓存的"改一处废全部"得不偿失,实践中我默认关闭二级缓存,查询缓存需求交给 Redis(应用层控制 key 与失效策略,可观测、可灰度、可精准失效)——DB 层缓存只留给极静态的数据(字典表、配置)。
架构原则:缓存失效的粒度越贴近数据变更的粒度越好,namespace 级失效在关联查询面前过于钝,这是我不信任 ORM 级缓存的根本原因。
💻 全栈开发专家
一级缓存(SqlSession 级):同一 SqlSession 内相同查询直接命中本地 map,默认开启。但在 Spring 集成下基本失效——每个 Mapper 调用(无事务)都是新 SqlSession,"同 session 二次查询"的场景几乎不存在;有 @Transactional 时事务内共享 session 才生效,此时要注意事务内先查后改的可见性问题。
二级缓存(namespace/mapper 级):跨 SqlSession、按 namespace 共享,需手动开启(<cache/> 或 @CacheNamespace)。我对它的态度:生产一般不开,三个硬伤:
- 多表 join 跨 namespace 失效:A 的 mapper join 了 B 的表,更新 B 不会清 A 的缓存,脏数据;
- 分布式下各自为政:多实例本地缓存互不知晓,脏读面更大;
- 以 result 整对象缓存,更新粒度粗。
yaml
# 需要跨会话缓存时我用 Redis 替代(Spring Cache 抽象)
@Cacheable(cacheNames = "user", key = "#id")
public User getById(Long id) { ... }面试加分:讲清"一级缓存在 Spring 下失效"这个细节,比背两级缓存定义值钱得多;再补一句本地缓存我会用 Caffeine + Redis 组多级,MyBatis 二级缓存属于历史产物。