MyBatis 二级缓存
一级缓存跟着 SqlSession 走,换个会话就失效;二级缓存则把数据提升到命名空间(namespace)级别,同一 Mapper 的所有 SqlSession 都能共享。它不是默认开启的——必须在 Mapper XML 里显式加 <cache/>,并且只适合缓存变化少、读多写少的数据。
开启方式
<mapper namespace="com.example.mapper.UserMapper">
<!-- 一句话开启该 namespace 的二级缓存 -->
<cache/>
...
</mapper>
<cache/> 的默认配置(官方文档给出):
| 属性 | 默认值 | 含义 |
|---|---|---|
| eviction | LRU | 回收策略,还有 FIFO、SOFT、WEAK |
| flushInterval | 不设置 | 定时刷新毫秒数,不设置 = 无定时刷新 |
| size | 1024 | 最多缓存对象个数 |
| readOnly | false | true=只读共享同一实例;false=读写拷贝返回 |
| type | 默认实现 | 可指定自定义 Cache 实现类 |
缓存是怎么流转的
查询 → 命中一级缓存 → 提交(commit)时写入二级缓存 → 其他 SqlSession 同语句命中二级缓存
关键点:数据要先经过当前会话,SqlSession 提交(或关闭)时才会进入二级缓存;事务没提交前,别的事务是看不到的。查询语句可用 useCache="false" 跳过二级缓存。
三个必须注意的坑
坑 1:对象要可序列化
默认 readOnly=false 属于读写缓存:取数据时会做一次反序列化拷贝,保证每个调用者拿到的对象互不影响。因此缓存的 POJO 必须实现 Serializable;把 readOnly 设为 true 则直接共享同一个实例(快,但谁改都会污染缓存,仅适合只读数据)。
坑 2:跨表更新会读到脏数据
二级缓存按 namespace 隔离:UserMapper 缓存了一条 JOIN 了 orders 表的查询,OrderMapper 更新了订单,UserMapper 的缓存完全不知情,下次查询就是脏数据。多表关联查询要慎开二级缓存;若多个 Mapper 操作同一批表,可用 <cache-ref namespace="..."/> 让它们共用一块缓存,一起被刷新。
坑 3:写语句会刷缓存(但按 namespace 刷) insert/update/delete 语句默认 flushCache=true,执行时会清空本 namespace 的缓存(select 默认不刷)。所以坑 2 的跨 namespace 脏读无法靠它自动解决。
全局开关与日常取舍
- settings 里的
cacheEnabled是二级缓存的总开关,默认 true;置 false 后所有<cache/>失效,一级缓存不受影响。 - 二级缓存是"进程内"缓存:多实例部署(集群)时各节点缓存不一致,官方定位不解决分布式问题。
- 生产实践:很多团队直接不用 MyBatis 二级缓存,改由 Redis 等外部缓存统一管理热点数据,避免"本地缓存+数据库+Redis"三层一致性问题。
小结
二级缓存 = 命名空间级的进程内缓存,<cache/> 开启、提交时写入、默认 LRU 回收;坑在 Serializable、namespace 隔离带来的跨表脏读与分布式不一致。低频读多写少的数据才值得开,复杂场景以官方文档为准。