在高并发业务场景中,数据库 IO 往往是系统性能瓶颈。MyBatis 提供的一级缓存(SqlSession 级别) 和二级缓存(Mapper 级别) 机制,能通过内存缓存高频查询数据,大幅减少数据库访问次数。本文从核心概念 、工作原理 、配置实战 、性能优化四个维度,全方位拆解 MyBatis 缓存的使用方法、失效场景及最佳实践。
一、缓存核心概念
1. 缓存的定义
缓存是将频繁访问的数据临时存储在内存中的机制,核心价值:
- 减少数据库磁盘 IO 次数,降低数据库压力;
- 内存读写速度远高于磁盘,提升查询响应效率(毫秒级→微秒级)。
2. 缓存的适用场景
并非所有数据都适合缓存,符合以下特征的数据优先使用缓存:
| 特征 | 说明 |
|---|---|
| 访问频率高 | 系统首页配置、热门商品列表、地区字典等高频查询数据 |
| 修改频率低 | 商品分类、权限角色、数据字典等静态 / 准静态数据 |
| 非核心敏感数据 | 允许短期数据不一致(订单、支付记录等核心数据需谨慎,避免脏数据) |
| 查询成本高 | 多表关联、复杂统计查询结果(缓存可规避重复计算 / 查询开销) |
3. MyBatis 缓存体系
MyBatis 内置两级缓存,同时支持集成第三方分布式缓存(如 Redis/EhCache),体系结构如下:
MyBatis 缓存体系
├── 一级缓存(SqlSession 级别,默认开启)
│ └── 作用域:单个 SqlSession,会话隔离
├── 二级缓存(Mapper/Namespace 级别,手动开启)
│ └── 作用域:同一 Mapper 的所有 SqlSession,跨会话共享
└── 第三方缓存(Redis/EhCache 等,分布式场景首选)
└── 作用域:整个应用集群,跨服务实例共享
二、一级缓存(SqlSession 会话级别)
1. 核心定义
一级缓存是SqlSession 对象级别的本地缓存,MyBatis 默认开启,无需额外配置:
- SqlSession 内部维护一个
Map集合(Key:缓存标识;Value:查询结果),存储当前会话的查询数据; - 一级缓存生命周期与 SqlSession 完全绑定:SqlSession 创建则缓存生效,关闭 / 清空则缓存失效;
- 缓存数据以对象引用形式存储(非序列化),读取性能极高。
2. 一级缓存的工作流程

3. 一级缓存的验证实战
(1)测试代码
java
package mybatis.test;
import mybatis.mapper.UserMapper;
import mybatis.pojo.User;
import mybatis.util.MyBatisUtil;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class FirstLevelCacheTest {
private InputStream in;
private SqlSession sqlSession;
private UserMapper mapper;
// 初始化(每次测试前执行)
@Before
public void init() throws IOException {
// 1. 加载 MyBatis 核心配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2. 创建 SqlSessionFactory 工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3. 获取 SqlSession(自动提交事务)
sqlSession = factory.openSession(true);
// 4. 获取 Mapper 代理对象
mapper = sqlSession.getMapper(UserMapper.class);
}
// 销毁(每次测试后执行)
@After
public void destroy() throws IOException {
// 关闭 SqlSession
sqlSession.close();
// 关闭输入流
in.close();
}
/**
* 测试一级缓存存在性:同一 SqlSession 下,相同查询条件多次查询
*/
@Test
public void testFirstLevelCache() {
// 1. 第一次查询:缓存无数据,查询数据库并写入缓存
User user1 = mapper.findById(1);
System.out.println("第一次查询结果:" + user1);
// 2. 第二次查询:缓存有数据,直接返回(不执行 SQL)
User user2 = mapper.findById(1);
System.out.println("第二次查询结果:" + user2);
// 3. 验证是否为同一对象(一级缓存返回对象引用)
System.out.println("是否为同一对象:" + (user1 == user2)); // 输出 true
}
}
(2)执行日志分析
// 第一次查询:执行 SQL,写入一级缓存
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
第一次查询结果:User{id=1, username='老王', address='北京'}
// 第二次查询:无 SQL 执行,直接读取一级缓存
第二次查询结果:User{id=1, username='老王', address='北京'}
是否为同一对象:true
4. 一级缓存的底层原理
一级缓存由 MyBatis 的 BaseExecutor(基本执行器)实现,核心逻辑在 BaseExecutor.query() 方法(简化源码):
java
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. 生成缓存 Key(SQL + 参数 + 分页 + Mapper ID 等)
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 2. 优先查询一级缓存
List<E> list = (List<E>) localCache.getObject(key);
if (list != null) {
return list;
}
// 3. 缓存无数据,查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
// 4. 将结果写入一级缓存
localCache.putObject(key, list);
return list;
}
缓存 Key 生成规则 :MappedStatement ID + SQL 语句 + 参数 + 分页参数 + 环境,确保相同查询条件生成相同 Key。
5. 一级缓存的失效场景
一级缓存并非永久有效,以下操作会触发缓存清空:
| 失效场景 | 说明 |
|---|---|
执行 update/insert/delete |
增删改修改数据,MyBatis 自动清空当前 SqlSession 缓存(避免脏数据) |
调用 sqlSession.commit() |
提交事务时清空缓存(保证事务一致性) |
调用 sqlSession.clearCache() |
手动清空当前 SqlSession 缓存 |
调用 sqlSession.close() |
关闭 SqlSession,缓存随对象销毁 |
| 不同 SqlSession 对象 | 一级缓存会话隔离,不同 SqlSession 缓存互不干扰 |
失效场景验证示例
java
@Test
public void testFirstLevelCacheInvalid() {
// 1. 第一次查询:写入缓存
User user1 = mapper.findById(1);
System.out.println("第一次查询:" + user1);
// 2. 执行更新操作:触发缓存清空
User updateUser = new User();
updateUser.setId(1);
updateUser.setUsername("老王更新");
mapper.updateUser(updateUser);
sqlSession.commit(); // 提交事务,进一步清空缓存
// 3. 第二次查询:缓存已清空,重新查询数据库
User user2 = mapper.findById(1);
System.out.println("第二次查询:" + user2);
System.out.println("是否为同一对象:" + (user1 == user2)); // 输出 false
}
三、二级缓存(Mapper/Namespace 级别)
1. 核心定义
二级缓存是Mapper 接口 / Namespace 级别的全局缓存,需手动开启,核心特征:
- 作用域覆盖同一 Mapper 的所有 SqlSession(跨会话共享);
- 生命周期与应用程序同步(应用不重启,缓存可一直存在);
- 缓存数据默认以序列化对象 存储(需实体类实现
Serializable); - 查询优先级:一级缓存 > 二级缓存 > 数据库。
2. 二级缓存的工作流程

3. 二级缓存的开启与配置
(1)全局配置(SqlMapConfig.xml)
开启二级缓存总开关(默认开启,可显式配置):
XML
<configuration>
<settings>
<!-- 开启二级缓存总开关(默认 true,可省略) -->
<setting name="cacheEnabled" value="true"/>
<!-- 可选:关闭一级缓存(不推荐,一级缓存是基础) -->
<!-- <setting name="localCacheScope" value="STATEMENT"/> -->
</settings>
<!-- 别名配置(简化映射文件) -->
<typeAliases>
<package name="mybatis.pojo"/>
</typeAliases>
</configuration>
(2)Mapper 映射文件配置
在需要开启二级缓存的 Mapper.xml 中添加 <cache/> 标签:
XML
<!-- UserMapper.xml -->
<mapper namespace="mybatis.mapper.UserMapper">
<!-- 开启当前 Mapper 的二级缓存 -->
<cache
eviction="LRU" <!-- 缓存淘汰策略:LRU(默认)/FIFO/SOFT/WEAK -->
flushInterval="60000" <!-- 缓存自动刷新时间(毫秒),默认不自动刷新 -->
size="1024" <!-- 缓存最大存储条目数,默认 1024 -->
readOnly="false"/> <!-- 是否只读:false(默认)/true -->
<!-- 查询方法:useCache="true"(默认,可省略) -->
<select id="findById" parameterType="int" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 增删改方法:flushCache="true"(默认,自动清空二级缓存) -->
<update id="updateUser" parameterType="User" flushCache="true">
UPDATE user SET username = #{username} WHERE id = #{id}
</update>
</mapper>
(3)实体类序列化(关键)
二级缓存可能序列化存储数据,实体类必须实现 Serializable 接口:
java
package mybatis.pojo;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
// getter/setter/toString
}
4. 二级缓存的验证实战
(1)测试代码
java
package mybatis.test;
import mybatis.mapper.UserMapper;
import mybatis.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
public class SecondLevelCacheTest {
// 全局唯一 SqlSessionFactory(避免重复创建)
private static SqlSessionFactory sqlSessionFactory;
// 静态初始化 SqlSessionFactory
static {
try {
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
} catch (IOException e) {
throw new RuntimeException("MyBatis 初始化失败:" + e.getMessage());
}
}
/**
* 测试二级缓存:跨 SqlSession 共享缓存数据
*/
@Test
public void testSecondLevelCache() {
SqlSession sqlSession1 = null;
SqlSession sqlSession2 = null;
try {
// 第一个 SqlSession:查询并写入二级缓存
sqlSession1 = sqlSessionFactory.openSession(true);
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.findById(1);
System.out.println("第一个 SqlSession 查询:" + user1);
sqlSession1.close(); // 关闭时,一级缓存数据同步到二级缓存
// 第二个 SqlSession:读取二级缓存
sqlSession2 = sqlSessionFactory.openSession(true);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.findById(1);
System.out.println("第二个 SqlSession 查询:" + user2);
// 验证:二级缓存返回序列化拷贝(非同一对象)
System.out.println("是否为同一对象:" + (user1 == user2)); // 输出 false
} finally {
// 关闭资源
if (sqlSession2 != null) sqlSession2.close();
}
}
}
(2)执行日志分析
// 第一个 SqlSession:执行 SQL,写入一级缓存,关闭时同步到二级缓存
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
第一个 SqlSession 查询:User{id=1, username='老王', address='北京'}
// 第二个 SqlSession:无 SQL 执行,读取二级缓存
第二个 SqlSession 查询:User{id=1, username='老王', address='北京'}
是否为同一对象:false
5. 二级缓存核心参数说明
| 参数 | 取值 | 说明 |
|---|---|---|
eviction |
LRU(默认) | 最近最少使用:移除最长时间未使用的缓存 |
| FIFO | 先进先出:按缓存加入顺序移除 | |
| SOFT | 软引用:JVM 内存不足时移除(依赖 GC) | |
| WEAK | 弱引用:GC 扫描到即移除(比 SOFT 更激进) | |
flushInterval |
数值(毫秒) | 缓存自动刷新时间,默认无(仅增删改触发刷新) |
size |
数值(默认 1024) | 缓存最大存储条目数,建议根据业务调整(避免内存溢出) |
readOnly |
false(默认) | 返回对象拷贝(序列化),安全但性能稍低 |
| true | 返回对象引用,性能高但可能导致脏数据 |
6. 二级缓存的失效场景
| 失效场景 | 说明 |
|---|---|
执行 update/insert/delete |
增删改默认 flushCache="true",清空当前 Mapper 二级缓存 |
查询方法设置 useCache="false" |
该查询不写入 / 读取二级缓存 |
| 不同 Namespace | 二级缓存按 Namespace 隔离,不同 Mapper 缓存互不干扰 |
实体类未实现 Serializable |
序列化失败,二级缓存无法生效 |
手动调用 sqlSession.clearCache() |
仅清空当前 SqlSession 一级缓存,不影响二级缓存 |
四、一级缓存 vs 二级缓存(核心对比)
| 维度 | 一级缓存(SqlSession 级别) | 二级缓存(Mapper 级别) |
|---|---|---|
| 作用域 | 单个 SqlSession | 同一 Namespace 的所有 SqlSession |
| 开启方式 | 默认开启,无需配置 | 需手动开启(全局 + Mapper 配置) |
| 数据存储 | 内存(对象引用) | 内存 / 磁盘(序列化对象) |
| 生命周期 | 随 SqlSession 销毁 | 随应用程序启停 |
| 数据一致性 | 会话内一致 | 跨会话一致,增删改触发刷新 |
| 性能 | 极高(直接取引用) | 稍低(序列化 / 反序列化) |
| 适用场景 | 单次会话内重复查询 | 跨会话高频静态数据查询 |
五、MyBatis 缓存进阶使用
1. 缓存的禁用与刷新
- 全局禁用二级缓存:
<setting name="cacheEnabled" value="false"/>; - 单个查询禁用缓存:
<select useCache="false">; - 强制刷新缓存:
<select flushCache="true">(查询时清空缓存); - 手动刷新二级缓存:调用
sqlSession.commit()(增删改自动触发)。
2. 集成第三方分布式缓存(Redis)
MyBatis 内置二级缓存为本地缓存,分布式系统中需集成 Redis 实现缓存共享:
(1)引入依赖
XML
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
(2)配置 Redis 缓存
XML
<!-- UserMapper.xml -->
<cache type="org.mybatis.caches.redis.RedisCache">
<property name="host" value="127.0.0.1"/>
<property name="port" value="6379"/>
<property name="expire" value="3600"/> <!-- 缓存过期时间(秒) -->
<property name="timeout" value="1000"/> <!-- 连接超时(毫秒) -->
</cache>
3. 缓存使用最佳实践
| 实践原则 | 说明 |
|---|---|
| 优先使用一级缓存 | 无额外开销,是性能优化的基础 |
| 二级缓存只缓存静态数据 | 避免缓存频繁修改的数据,减少脏数据风险 |
| 分布式系统用 Redis 替代二级缓存 | 本地二级缓存无法跨实例共享,Redis 保证分布式缓存一致性 |
| 避免缓存大对象 | 大对象序列化 / 反序列化开销大,建议拆分或不缓存 |
| 设置合理的过期时间 | 即使静态数据,也建议设置过期时间(避免缓存永久有效) |
| 监控缓存命中率 | 通过 MyBatis 日志或第三方工具监控,命中率低则调整缓存策略 |
六、常见问题与解决方案
1. 缓存脏数据问题
现象:缓存数据与数据库数据不一致。
原因:增删改未触发缓存刷新、多线程修改数据、只读缓存返回对象引用。
解决方案:
- 增删改操作确保
flushCache="true"; - 二级缓存设置
readOnly="false"; - 核心数据避免缓存,或缩短缓存过期时间。
2. 二级缓存不生效排查
- 检查
cacheEnabled是否为true; - 检查 Mapper.xml 是否添加
<cache/>标签; - 检查实体类是否实现
Serializable; - 检查查询方法是否设置
useCache="false"; - 检查是否执行了增删改操作(触发缓存清空)。
3. 一级缓存导致事务内数据不一致
现象:同一 SqlSession 内,先查询后更新,再次查询仍取旧数据。
解决方案:
- 更新后调用
sqlSession.clearCache()清空一级缓存; - 事务内避免重复查询,或使用
flushCache="true"强制刷新。
七、总结
MyBatis 缓存是提升查询性能的核心手段,需结合业务场景合理使用:
- 一级缓存是基础,默认开启,适用于单次会话内的重复查询;
- 二级缓存需手动配置,适用于跨会话的静态数据缓存;
- 分布式系统优先使用 Redis 替代本地二级缓存,保证缓存共享;
- 缓存的核心是 "权衡":既要提升性能,也要保证数据一致性,避免过度缓存。
通过合理配置一级 / 二级缓存,结合业务特征调整缓存策略,可在不修改业务代码的前提下,大幅降低数据库压力,提升系统响应速度。