TL;DR
- 场景:MyBatis 持久层开发,需要跨 SqlSession 共享热点数据,进一步减少数据库访问
- 结论:二级缓存基于 Mapper namespace 共享,commit/增删改自动清空,必须配合 Serializable 序列化
- 产出:掌握二级缓存原理、配置步骤、commit 清空机制与 useCache/flushCache 高级控制

核心关键词 :MyBatis、二级缓存、Mapper namespace、cacheEnabled、<cache>、commit 清空、Serializable 序列化、useCache、flushCache、MyBatis 3.5
版本矩阵
| 功能 | 状态 | 说明 |
|---|---|---|
| 二级缓存默认行为 | ⚠️ 需手动开启 | cacheEnabled 默认 true,但 Mapper 需显式声明 <cache/> 才生效 |
| Mapper namespace 作用域 | ✅ 已验证 | 多个 SqlSession 共享同一 namespace 的缓存 |
| 同 namespace 共享缓存区 | ✅ 已验证 | 多个 Mapper 文件 namespace 相同时会缓存到同一区域 |
| commit 自动清空缓存 | ✅ 已验证 | INSERT/UPDATE/DELETE + commit 触发 namespace 缓存清空 |
| Serializable 强制要求 | ✅ 已验证 | 缓存对象需实现 Serializable 以支持反序列化到不同介质 |
| useCache 控制单条 SQL | ✅ 已验证 | 设置 false 可禁用该 select 的二级缓存,默认 true |
| flushCache 自动刷新 | ✅ 已验证 | 增删改默认 flushCache=true,无需手动设置 |
| 自定义 Cache 实现 | ✅ 已验证 | 通过 type 参数指定实现了 Cache 接口的自定义类 |
| MyBatis 3.5.x 支持 | ✅ 已验证 | 2020 年发布至今持续维护,缓存机制稳定 |
二级缓存
二级缓存原理和一级缓存的原理是一样的,第一次查询,将数据放入到数据库中,第二次查询就会去缓存中取,但是一级缓存是基于 SqlSession 的,而二级缓存是基于 Mapper 的 namespace 的,也就是说,多个 SqlSession 可以共享一个 Mapper 中的二级缓存,并且如果两个 Mapper 的 namespace 相同的话,即使是两个 Mapper,也会缓存到相同的区域中。 MyBatis 提供了两级缓存机制:一级缓存和二级缓存。一级缓存是 SqlSession 级别的,作用域仅限于当前会话,默认开启。而二级缓存是跨 SqlSession 的,作用域为 Mapper 映射文件级别,需要手动配置开启。

工作原理
二级缓存是一个共享的缓存区域,存储的查询结果可以被多个 SqlSession 复用,从而减少数据库查询操作,提高性能。其核心工作流程如下:
查询时的流程:
- 先从二级缓存中查找数据。
- 如果二级缓存未命中,则去一级缓存中查找。
- 一级缓存也未命中,则执行 SQL 查询数据库,并将结果存入一级缓存和二级缓存。
更新时的流程:
- 当某个操作(如 INSERT、UPDATE 或 DELETE)导致数据发生改变时,MyBatis 会自动清空与该 Mapper 相关的二级缓存。
优缺点
优点:
- 减少数据库访问次数:提高系统性能。
- 跨 SqlSession 共享:对频繁查询的数据特别有用。
缺点:
- 数据一致性问题:如果数据库数据发生变化,缓存数据可能不一致。
- 内存占用增加:缓存需要消耗内存存储数据。
- 复杂度增加:需要额外管理缓存的清理和更新。
开启缓存
二级缓存默认是关闭的,我们需要开启,需要修改 sqlMapConfig.xml:
xml
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
对应的截图如下所示:
其次,我们需要到对应的 Mapper 中,进行缓存的开启,这里我们用 UserMapper 作为例子,在其中加入:
xml
<!--开启二级缓存-->
<cache></cache>
对应的截图如下所示:
可以看到 Mapper.xml 中文件就这么一个空的标签,这里其实是可以加一些参数的,如果不加的话,会用默认的实现类。我们也可以实现 Cache 接口来用我们自己的缓存类。(使用 type 参数来更换实现类)
注意事项
由于开启了二级缓存之后,还需要将缓存中的 model 对象实现 Serializable 接口,为了将缓存数据取出执行反序列化操作,因为二级缓存的数据可能会有不同的介质,不一定是在内存中存储的,如果是到 Redis、磁盘等,就需要进行序列化和反序列化。
java
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WzkUser implements Serializable {
private int id;
private String username;
private String password;
private Date birthday;
private List<WzkOrder> orderList;
private List<WzkRole> roleList;
}
对应的截图如下所示: 
编写代码
这里我们测试 二级缓存。
java
public class WzkicuCache03 {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
List<WzkUser> users1 = userMapper1.findAll();
sqlSession1.close();
List<WzkUser> users2 = userMapper2.findAll();
sqlSession2.close();
}
}
测试代码
运行上述的代码,控制台的输出结果如下:
shell
24/11/13 10:52:09 DEBUG UserMapper.findAll: ==> Preparing: select *,o.id oid from wzk_user u left join wzk_orders o on u.id=o.uid;
24/11/13 10:52:09 DEBUG UserMapper.findAll: ==> Parameters:
24/11/13 10:52:09 DEBUG UserMapper.findAll: <== Total: 3
对应的截图如下所示:
开启了二级缓存之后,可以看到 两个不同的 SqlSession,第二次查询依然不会发出 SQL。
编写代码
这里我们通过 commit 清空缓存来进行测试
java
public class WzkicuCache04 {
public static void main(String[] args) throws IOException {
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(inputStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
List<WzkUser> users1 = userMapper1.findAll();
sqlSession1.close();
WzkUser wzkUser = WzkUser
.builder()
.id(1)
.username("wzk-update")
.password("123-update")
.build();
userMapper2.updateById(wzkUser);
sqlSession2.commit();
sqlSession2.close();
List<WzkUser> users3 = userMapper3.findAll();
sqlSession3.close();
}
}
对应的截图如下所示: 
测试代码
执行上述的代码,结果如下:
shell
24/11/13 10:58:50 DEBUG UserMapper.updateById: ==> Preparing: UPDATE wzk_user SET username=?, password=? WHERE id = ?
24/11/13 10:58:50 DEBUG UserMapper.updateById: ==> Parameters: wzk-update(String), 123-update(String), 1(Integer)
24/11/13 10:58:50 DEBUG UserMapper.updateById: <== Updates: 1
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG pooled.PooledDataSource: Returned connection 1183888521 to pool.
24/11/13 10:58:50 DEBUG mapper.UserMapper: Cache Hit Ratio [icu.wzk.mapper.UserMapper]: 0.0
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Opening JDBC Connection
24/11/13 10:58:50 DEBUG pooled.PooledDataSource: Checked out connection 1183888521 from pool.
24/11/13 10:58:50 DEBUG jdbc.JdbcTransaction: Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4690b489]
24/11/13 10:58:50 DEBUG UserMapper.findAll: ==> Preparing: select *,o.id oid from wzk_user u left join wzk_orders o on u.id=o.uid;
24/11/13 10:58:50 DEBUG UserMapper.findAll: ==> Parameters:
对应的截图如下所示:
可以看到,commit 之后,二级缓存也被清空了,所以后续的查询还是发出了 SQL。
额外配置
MyBatis 中还配以 useCache 和 flushCache 等配置项目,useCache 是用来设置是否禁用二级缓存的,在 statement 中设置 useCache = false 可以禁用当前 select 语句的二级缓存,即每次查询都会发送 SQL 去查询,默认情况是 true,即该 SQL 使用二级缓存:
xml
<select id="findAll" resultMap="userMap" useCache="false">
select *,o.id oid from wzk_user u left join wzk_orders o on u.id=o.uid;
</select>
一般情况下,执行完 commit 都需要刷新缓存,flushCache = true 表示刷新缓存,这样可以避免数据库脏读,所以不需要设置,默认即可。
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 开启二级缓存后报序列化错误 | model 对象未实现 Serializable | 检查实体类声明 | 实体类加 implements Serializable |
| 不同 SqlSession 第二次查询仍发 SQL | Mapper 中未配置 <cache/> 标签 |
检查对应 Mapper.xml | 添加 <cache/> 标签开启 namespace 缓存 |
| 缓存命中率始终为 0 | sqlMapConfig.xml 未启用 cacheEnabled | 检查全局配置 | 设置 <setting name="cacheEnabled" value="true"/> |
| commit 后数据未刷新到缓存 | 缺少 commit 操作 | 检查 sqlSession.commit() 是否调用 | 增删改后必须执行 commit 才会清空二级缓存 |
| 期望走缓存却发 SQL | 该 select 配置了 useCache=false | 检查 select 标签 | 移除 useCache=false 或按需保留 |
| 多个 Mapper 缓存互相污染 | namespace 命名冲突 | 检查 mapper namespace 是否重复 | 用 <cache-ref namespace="..."/> 显式共享 |
| 缓存与数据库不一致 | 高并发下其他服务修改了数据库 | 检查数据源是否有写操作绕开 MyBatis | 引入分布式缓存失效通知或缩短 flushInterval |
作者:武子康的个人博客