MyBatis 的缓存机制是其核心性能优化手段之一,目的是减少数据库查询次数,降低IO开销,提升查询效率。其设计遵循“分层缓存”理念,分为 一级缓存(SqlSession 级别) 和 二级缓存(Mapper 级别),同时支持集成第三方缓存(如 Redis)实现分布式场景下的缓存共享。
一级缓存是 SqlSession 实例级别的缓存,即同一个 SqlSession 内执行相同的查询操作,只会第一次访问数据库,后续直接从内存中获取结果。
一级缓存的 Key 由以下 4 部分组成,缺一不可:
Key = HashMap<Object, Object> {
"MappedStatementId": Mapper接口全类名+方法名(如com.example.mapper.UserMapper.selectById),
"SQL语句": 最终执行的SQL(含动态SQL拼接结果),
"参数": 查询参数(如id=1001),
"环境信息": 数据库连接环境(如数据源ID、事务状态)
}sqlSession.clearCache())。sqlSession.close():关闭 SqlSession 时,一级缓存会被销毁;insert()/update()/delete()):MyBatis 会自动清除当前 SqlSession 内的所有一级缓存(避免数据不一致);sqlSession.clearCache() 方法,主动清空当前 SqlSession 的缓存;// 1. 获取 SqlSession(默认不自动提交事务)
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 2. 第一次查询:访问数据库,结果存入一级缓存
User user1 = userMapper.selectById(1001);
System.out.println(user1); // 数据库查询
// 3. 第二次查询:参数相同,直接从一级缓存获取
User user2 = userMapper.selectById(1001);
System.out.println(user2); // 缓存命中,无数据库查询
// 4. 执行更新操作:触发一级缓存清空
userMapper.updateName(1001, "新名字");
sqlSession.commit();
// 5. 第三次查询:缓存已失效,重新访问数据库
User user3 = userMapper.selectById(1001);
System.out.println(user3); // 数据库查询
// 6. 关闭 SqlSession:一级缓存销毁
sqlSession.close();二级缓存是 Mapper 接口级别的缓存,即同一个 Mapper 接口下的所有 SqlSession 共享缓存(跨 SqlSession 共享)。例如,UserMapper 的二级缓存可被多个 SqlSession 访问,适合查询频繁、修改少的场景。
Serializable 接口(默认缓存会序列化存储,避免对象引用问题)。在 mybatis-config.xml 中配置 cacheEnabled(默认值为 true,可省略):
<configuration>
<settings>
<!-- 开启二级缓存总开关(关闭后所有 Mapper 的二级缓存都失效) -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>在对应的 Mapper.xml 中添加 <cache> 标签,开启当前 Mapper 的二级缓存:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存,配置缓存参数 -->
<cache
eviction="LRU" <!-- 缓存回收策略(默认 LRU) -->
flushInterval="60000" <!-- 缓存过期时间(毫秒,60秒) -->
size="1024" <!-- 缓存最大条目数(默认 1024) -->
readOnly="true" <!-- 是否只读(默认 false) -->
blocking="false" <!-- 是否阻塞(缓存未命中时等待其他线程查询结果,默认 false) -->
/>
<!-- 单个查询方法可单独控制是否使用二级缓存(默认使用) -->
<select id="selectById" resultType="User" useCache="true">
select * from user where id = #{id}
</select>
<!-- 增删改方法可控制是否触发缓存清空(默认触发) -->
<update id="updateName" flushCache="true">
update user set name = #{name} where id = #{id}
</update>
</mapper>参数名 | 取值范围 | 作用说明 |
|---|---|---|
eviction | LRU(默认)/FIFO/SOFT/WEAK | 缓存回收策略:- LRU:最近最少使用(移除最长时间未被访问的缓存);- FIFO:先进先出(按缓存添加顺序移除);- SOFT:软引用(JVM内存不足时移除);- WEAK:弱引用(垃圾回收时直接移除)。 |
flushInterval | 数值(毫秒) | 缓存自动过期时间,默认无过期(一直有效)。 |
size | 正整数 | 缓存最大存储条目数,超出后按回收策略移除旧缓存(避免内存溢出)。 |
readOnly | true/false(默认) | - true:缓存返回对象本身(性能高,但线程不安全,需确保对象不被修改);- false:缓存返回对象的拷贝(通过序列化,线程安全,性能略低)。 |
blocking | true/false(默认) | - true:缓存未命中时,其他线程需等待当前线程查询数据库并写入缓存(避免缓存穿透);- false:缓存未命中时,所有线程均访问数据库(可能导致并发查询)。 |
二级缓存默认会对查询结果进行序列化存储(即使 readOnly="true"),因此实体类必须实现 Serializable 接口,否则会抛出 NotSerializableException 异常:
// 实体类实现 Serializable 接口
public class User implements Serializable {
private Long id;
private String name;
// getter/setter...
}insert()/update()/delete() 操作(无论是否修改目标数据),都会清空该 Mapper 的所有二级缓存;sqlSession.clearCache()(仅清空当前 SqlSession 的一级缓存)或 sqlSessionFactory.getConfiguration().getCache("namespace").clear()(清空指定 Mapper 的二级缓存);cacheEnabled="false" 或 Mapper 未配置 <cache> 标签;select 标签设置 useCache="false"(如实时性要求高的查询);insert/update/delete 标签设置 flushCache="false"(不推荐,会导致数据不一致);Serializable 接口:缓存写入失败,无法命中;// 1. 第一个 SqlSession:查询并写入二级缓存
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1001); // 数据库查询,结果存入一级缓存+二级缓存
sqlSession1.close(); // 关闭 SqlSession 时,一级缓存销毁,二级缓存保留
// 2. 第二个 SqlSession:共享二级缓存
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1001); // 二级缓存命中,无数据库查询
sqlSession2.close();
// 3. 第三个 SqlSession:执行更新操作,触发二级缓存清空
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
mapper3.updateName(1001, "新名字");
sqlSession3.commit(); // 提交事务,清空 UserMapper 的二级缓存
sqlSession3.close();
// 4. 第四个 SqlSession:缓存失效,重新查询
SqlSession sqlSession4 = sqlSessionFactory.openSession();
UserMapper mapper4 = sqlSession4.getMapper(UserMapper.class);
User user4 = mapper4.selectById(1001); // 二级缓存已失效,数据库查询
sqlSession4.close();对比维度 | 一级缓存(SqlSession 级别) | 二级缓存(Mapper 级别) |
|---|---|---|
开启方式 | 默认开启,无需配置 | 默认关闭,需全局+Mapper 配置 |
作用范围 | 单个 SqlSession(线程私有) | 同一个 Mapper 接口(跨 SqlSession) |
存储介质 | 内存 HashMap(非序列化) | 内存 HashMap/磁盘/第三方缓存(需序列化) |
实体类要求 | 无序列化要求 | 必须实现 Serializable 接口 |
失效触发 | 关闭 SqlSession、增删改、手动清空 | 增删改操作、缓存过期、手动清空 |
数据一致性 | 高(会话内隔离,修改后缓存清空) | 中(跨会话共享,需依赖失效机制) |
适用场景 | 单次会话内重复查询(如表单校验) | 多会话共享查询(如字典数据、静态数据) |
线程安全 | 安全(线程私有) | 安全(内部锁机制) |
核心结论:一级缓存是“会话内优化”,二级缓存是“跨会话优化”,二者协同工作(查询时先查一级缓存→再查二级缓存→最后查数据库)。
MyBatis 的默认二级缓存是内存级缓存,存在以下局限性:
因此,实际开发中常集成 Redis、Ehcache 等第三方缓存替代默认二级缓存,MyBatis 提供了 Cache 接口,支持自定义缓存实现。
<!-- MyBatis 缓存接口 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- Redis 客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.6</version>
</dependency>
<!-- 序列化工具(可选,用于对象序列化) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.41</version>
</dependency>import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 自定义 Redis 缓存实现
*/
public class RedisCache implements Cache {
// 缓存 ID(对应 Mapper 的 namespace)
private final String id;
// Redis 连接池
private final JedisPool jedisPool;
// 读写锁(保证线程安全)
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 构造方法:MyBatis 会自动传入 Mapper 的 namespace 作为 id
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache ID cannot be null");
}
this.id = id;
// 初始化 Redis 连接池(实际开发中应通过配置文件读取参数)
this.jedisPool = new JedisPool("localhost", 6379);
}
@Override
public String getId() {
return id; // 缓存唯一标识(必须返回 Mapper 的 namespace)
}
// 存入缓存:key 是 MyBatis 生成的缓存 Key,value 是查询结果
@Override
public void putObject(Object key, Object value) {
try (Jedis jedis = jedisPool.getResource()) {
// 序列化 key 和 value(使用 fastjson2)
String redisKey = key.toString();
String redisValue = com.alibaba.fastjson2.JSON.toJSONString(value);
// 存入 Redis,设置过期时间(30分钟)
jedis.setex(redisKey, 1800, redisValue);
}
}
// 获取缓存:根据 key 查询 Redis
@Override
public Object getObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
String redisKey = key.toString();
String redisValue = jedis.get(redisKey);
if (redisValue == null) {
return null;
}
// 反序列化:根据实际返回类型转换(此处简化处理,实际需结合 TypeHandler)
return com.alibaba.fastjson2.JSON.parseObject(redisValue, Object.class);
}
}
// 移除缓存(MyBatis 内部很少调用,可空实现)
@Override
public Object removeObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.del(key.toString());
}
}
// 清空缓存(增删改操作时触发)
@Override
public void clear() {
try (Jedis jedis = jedisPool.getResource()) {
// 模糊删除当前 Mapper 的所有缓存(key 前缀为 id + ":")
jedis.keys(id + ":*").forEach(jedis::del);
}
}
// 获取缓存大小(可选实现)
@Override
public int getSize() {
try (Jedis jedis = jedisPool.getResource()) {
return Math.toIntExact(jedis.keys(id + ":*").size());
}
}
// 获取读写锁(MyBatis 用于并发控制)
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}<mapper namespace="com.example.mapper.UserMapper">
<!-- 配置 Redis 缓存实现(替代默认缓存) -->
<cache type="com.example.cache.RedisCache">
<!-- 可自定义缓存参数(通过构造方法或 setter 注入) -->
</cache>
<!-- 查询方法使用 Redis 缓存 -->
<select id="selectById" resultType="User" useCache="true">
select * from user where id = #{id}
</select>
</mapper>namespace + SQL + 参数 生成 Key,避免不同 Mapper 缓存冲突;TypeHandler 优化);select * from user u join order o on u.id = o.user_id),因为关联表的增删改操作无法触发当前 Mapper 的缓存清空,会导致数据不一致;Serializable 接口(二级缓存/第三方缓存均需序列化);null 的数据也存入缓存(设置短期过期),防止恶意查询不存在的 ID 击垮数据库;setnx)控制并发查询。flushInterval):根据数据更新频率调整,如字典表可设置 1 小时,新闻列表可设置 5 分钟;size):避免缓存过大导致内存溢出,建议设置为 1024~4096;readOnly="true"):如果查询结果无需修改,开启只读模式可提升性能(避免对象拷贝);cacheEnabled + Mapper 标签 <cache> + 实体类 Serializable;Cache 接口,集成 Redis 等第三方缓存的步骤;MyBatis 缓存机制的核心是“平衡性能与数据一致性”,实际开发中需根据业务场景选择合适的缓存策略,避免盲目开启二级缓存导致的问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。