第8章 MyBatis 缓存与插件机制(超详细版)¶
一、前言:为什么要有缓存和插件?¶
MyBatis 在执行 SQL 时,可能会反复查询相同的数据或执行相似的逻辑。
为了解决这些性能与扩展性问题,MyBatis 提供了:
- 缓存机制(Cache) —— 提高性能,减少数据库访问;
- 插件机制(Interceptor) —— 扩展底层行为,例如 SQL 日志、分页、性能分析等。
二、缓存机制原理概述¶
1️⃣ 类比理解:银行取钱模型¶
没有缓存时: 每次取钱都要跑到银行柜台。
有缓存时: 第一次取钱时柜台记下你的余额,下一次直接告诉你结果,不再查数据库。
2️⃣ 缓存分层结构¶
- 一级缓存(SqlSession 级别)
- 默认开启。
- 作用范围是一次会话(
SqlSession)。
- 二级缓存(Mapper 命名空间级别)
- 需手动开启。
- 在同一命名空间内多个会话共享。
三、一级缓存(Local Cache)¶
1️⃣ 工作原理¶
- 默认开启,作用范围是当前
SqlSession。 - 第一次查询后结果会存入缓存(key 为 SQL + 参数 + 映射环境)。
- 相同查询会直接从缓存中取结果,而不再访问数据库。
2️⃣ 示例¶
try (SqlSession session = factory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User u1 = mapper.selectUserById(1);
User u2 = mapper.selectUserById(1);
System.out.println(u1 == u2); // true,命中一级缓存
}
3️⃣ 缓存失效的情况¶
- 执行了
update/insert/delete(会刷新缓存)。 - 手动调用
session.clearCache()。 - 不同的
SqlSession实例。 - 查询参数或 SQL 环境不同。
四、二级缓存(Global Cache)¶
1️⃣ 概念与特点¶
- 命名空间(Mapper)级别的缓存。
- 不同
SqlSession可以共享。 - 生命周期更长,但需要开发者显式启用。
2️⃣ 开启步骤¶
(1)在全局配置中启用缓存:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
(2)在 Mapper 文件中声明:
<mapper namespace="com.example.mapper.UserMapper">
<cache eviction="LRU" flushInterval="600000" size="512" readOnly="false"/>
</mapper>
(3)开启序列化:
- 参与缓存的实体类必须实现
Serializable接口。
public class User implements Serializable {
private Integer id;
private String name;
private Integer age;
}
3️⃣ <cache> 标签属性¶
-
eviction:缓存清理策略,默认LRU。可选:LRU、FIFO、SOFT、WEAK。- LRU(Least Recently Used,最近最少使用)
- 优先移除最近最久未被访问的对象。
- 访问数据时刷新其时间戳。
- ✅ 优点:命中率高,是默认策略。
- ⚠️ 适用场景:热点数据明显的系统。
- FIFO(First In First Out,先进先出)
- 最早放入缓存的对象最先被清理。
- ✅ 优点:实现简单,性能稳定。
- ⚠️ 缺点:可能淘汰刚被访问过的对象。
- SOFT(软引用策略)
- 使用 Java 的
SoftReference实现,内存不足时释放缓存。 - ✅ 优点:防止内存溢出。
- ⚠️ 适用场景:内存敏感系统,缓存非关键数据。
- 使用 Java 的
- WEAK(弱引用策略)
- 使用 Java 的
WeakReference实现,只要 GC 运行就会回收对象。 - ✅ 优点:极致节省内存。
- ⚠️ 缺点:命中率极低,缓存存活时间短。
- 使用 Java 的
- LRU(Least Recently Used,最近最少使用)
-
flushInterval:刷新间隔(毫秒)。默认无自动刷新。 size:最多可缓存的对象数。readOnly:是否只读(true表示返回相同实例)。blocking:若缓存命中失败是否阻塞其他线程读取。
4️⃣ <cache-ref> 共享缓存¶
<cache-ref namespace="com.example.mapper.OrderMapper"/>
表示当前 Mapper 复用另一个 Mapper 的缓存。
5️⃣ 二级缓存执行顺序¶
- 查询二级缓存是否有结果;
- 若无 → 查询数据库并写入一级缓存;
- 会话关闭或提交时 → 一级缓存数据写入二级缓存。
五、缓存清理与优化建议¶
缓存失效的触发条件:
- 执行更新操作(会刷新缓存)。
- Mapper 中
flushCache="true"。
优化建议:
- 不要缓存频繁变动的数据(例如库存、余额)。
- 对静态数据(如省份、配置项)可长期缓存。
- 结合 Redis 等外部缓存提高可靠性。
六、插件机制(Interceptor)¶
1️⃣ 插件的作用¶
插件允许你在 MyBatis 执行 SQL 的四大核心阶段插入自定义逻辑,常见用途:
- 打印 SQL 日志;
- 统计 SQL 执行时间;
- 自动分页;
- 动态切换数据源。
2️⃣ 原理概述¶
MyBatis 在执行 SQL 时,会创建以下 4 类核心对象:
- Executor:执行 SQL 语句;
- StatementHandler:管理 SQL 语句;
- ParameterHandler:设置参数;
- ResultSetHandler:处理查询结果。
插件通过 动态代理(JDK Proxy),在这些对象方法调用前后注入逻辑。
MyBatis Object → [ Plugin Proxy ] → [ Original Object ]
七、自定义插件示例:日志拦截器¶
1️⃣ 插件代码¶
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class LogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed(); // 执行原方法
long end = System.currentTimeMillis();
System.out.println("[MyBatis SQL 执行耗时]:" + (end - start) + " ms");
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
System.out.println("插件属性:" + properties);
}
}
2️⃣ 在 mybatis-config.xml 注册¶
<plugins>
<plugin interceptor="com.example.plugin.LogInterceptor">
<property name="level" value="DEBUG"/>
</plugin>
</plugins>
3️⃣ 输出结果¶
[MyBatis SQL 执行耗时]:12 ms
说明:
@Intercepts:声明拦截器。@Signature:指定要拦截的接口、方法、参数类型。plugin():包装原始对象,返回代理对象。setProperties():读取配置参数。
八、常见插件案例¶
- 分页插件(PageHelper / 自定义)
- 拦截
Executor.query()方法,修改 SQL 为SELECT ... LIMIT ...。
- 拦截
- 性能监控插件
- 记录每条 SQL 的执行时间并打印慢查询警告。
- 字段加解密插件
- 拦截参数和结果集,在入库前加密、出库后解密。
九、缓存 vs 插件:差异对比¶
| 对比项 | 缓存 | 插件 |
|---|---|---|
| 目的 | 提升性能 | 扩展功能 |
| 生命周期 | SqlSession / Mapper | 全局 |
| 开启方式 | 自动 / 手动 | 在配置中注册 |
| 常见应用 | 结果复用、数据查询 | 分页、日志、加密、动态路由 |
| 实现原理 | 内存存储 + 命名空间管理 | 动态代理 + 拦截器链 |
十、小结¶
- 一级缓存自动开启,作用范围为 SqlSession。
- 二级缓存需手动启用,作用范围为 Mapper 命名空间。
- 插件可拦截四大核心对象方法,灵活扩展 MyBatis 行为。
- 缓存 = 性能优化,插件 = 功能增强。
✅ 记忆口诀:
- 一级缓存自己用(会话级)
- 二级缓存大家用(共享级)
- 插件像“插电扩展口”,能让 MyBatis 做更多事。