跳转到内容

缓存与插件链

MyBatis 的缓存不是一个全局 Map:一级缓存属于执行器,二级缓存属于 mapper namespace,并通过事务包装延迟真正提交。插件也不是事件总线,而是对四类核心对象套 JDK 代理。

先给答案:缓存和插件都在改写执行链,但一个保存结果,一个改变过程

Section titled “先给答案:缓存和插件都在改写执行链,但一个保存结果,一个改变过程”

一级/二级缓存的核心问题是“相同查询能否复用结果,以及结果应该跟多大的作用域绑定”;插件的核心问题是“在不修改主流程的情况下,能否观察或改写某个阶段”。两者都包裹 Executor,却不能混为同一种扩展:缓存关心 key、生命周期和失效,插件关心拦截点、责任链顺序和参数变形。

因此缓存命中时,后面的 JDBC、结果集映射甚至部分插件逻辑可能根本不会执行;插件如果修改了 SQL 或分页参数,又可能改变缓存 key 的正确性。排查“缓存读到旧数据”时,必须同时看更新语句的清理范围和插件是否改变了实际执行对象。

查询 -> CachingExecutor -> namespace cache
`-> BaseExecutor -> localCache
更新 / commit / rollback -> 清理 localCache
`-> TransactionalCache commit/rollback
  • BaseExecutor 创建 PerpetualCache 作为 localCache(mybatis-3/src/main/java/org/apache/ibatis/executor/BaseExecutor.java:51-72)。
  • BaseExecutor#query 查询本地 cache,未命中才进入数据库(mybatis-3/src/main/java/org/apache/ibatis/executor/BaseExecutor.java:143-181)。
  • CachingExecutor#query 先构造 key,再查询 TransactionalCacheManager(mybatis-3/src/main/java/org/apache/ibatis/executor/CachingExecutor.java:86-110)。
  • TransactionalCache 把结果放入待提交集合,不立即写入最终 cache(mybatis-3/src/main/java/org/apache/ibatis/cache/decorators/TransactionalCache.java:35-77)。
  • CachingExecutor#commit 在 delegate 提交成功后提交二级缓存(mybatis-3/src/main/java/org/apache/ibatis/executor/CachingExecutor.java:119-123)。
  • rollback 路径清理 pending entries,避免回滚数据泄露(mybatis-3/src/main/java/org/apache/ibatis/cache/TransactionalCacheManager.java:42-58)。
  • Configuration 创建 Executor、StatementHandler、ParameterHandler、ResultSetHandler 时调用 pluginAll(mybatis-3/src/main/java/org/apache/ibatis/session/Configuration.java:690-752)。
  • InterceptorChain#pluginAll 按注册顺序逐层包装目标(mybatis-3/src/main/java/org/apache/ibatis/plugin/InterceptorChain.java:28-38)。
  • Plugin#wrap 仅在 @Intercepts 签名匹配时创建代理(mybatis-3/src/main/java/org/apache/ibatis/plugin/Plugin.java:31-54)。
  • 命中方法后,Plugin#invoke 把调用包装成 Invocation 交给 interceptor(mybatis-3/src/main/java/org/apache/ibatis/plugin/Plugin.java:56-61)。

替代方案:查询完成后立即写 namespace cache。 为什么不行:当前事务随后回滚时,其他 session 会看到未提交数据。 证据:CachingExecutor#commit 在 delegate 提交成功后调用 tcm.commit(),rollback 路径则调用 tcm.rollback()(CachingExecutor.java:119-137)。

场景 现象 原因 规避
二级缓存对象不可序列化 写缓存时报错 默认装饰器可能包含序列化缓存 DTO 实现序列化或换实现
跨 namespace 更新 读到旧数据 cache 失效以 namespace 为边界 配置 cache-ref 或刷新
插件签名错误 插件不生效 @Intercepts 不匹配方法 检查接口、方法和参数类型
插件顺序改变 SQL 结果不同 代理按注册顺序嵌套 固化配置并测试

事务性缓存写入适合任何读缓存与业务提交必须一致的系统;按声明签名过滤代理,则能避免对所有对象调用都付出拦截成本。

面试锚点

  • 一级缓存和二级缓存分别属于谁?
  • 二级缓存为什么不能立即写入?
  • MyBatis 插件为什么只能拦截四类对象?