一、缓存概述:为什么缓存对 MyBatis 如此重要?
在数据库操作中,查询操作通常占比最高,而频繁的数据库访问会导致:
- 数据库压力增大
- 网络传输开销增加
- 应用响应速度降低
缓存作为解决这些问题的关键技术,通过将频繁访问的数据存储在内存中,减少对数据库的直接访问,从而显著提升应用性能。
MyBatis 提供了完善的缓存机制,分为:
- 一级缓存:SqlSession 级别的缓存(默认开启)
- 二级缓存:Mapper 级别的缓存(需要手动开启)
本文将深入解析 MyBatis 缓存的工作原理、使用方式和最佳实践,帮助你在实际开发中合理利用缓存提升系统性能。
二、一级缓存:SqlSession 级别的缓存
一级缓存是 MyBatis 默认开启的缓存机制,其作用域为单个 SqlSession。
2.1 一级缓存工作原理
- 当通过 SqlSession 执行查询时,MyBatis 会将查询结果存储在一级缓存中
- 同一 SqlSession 内的相同查询(相同 SQL 和参数)会直接从缓存获取结果
- 当执行增删改操作或关闭 SqlSession 时,一级缓存会被清空
2.2 一级缓存演示
java
运行
@Test
public void testFirstLevelCache() {
try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询,从数据库获取
User user1 = mapper.getUserById(1);
System.out.println("第一次查询:" + user1);
// 第二次查询,从缓存获取
User user2 = mapper.getUserById(1);
System.out.println("第二次查询:" + user2);
// 比较两个对象是否相同(内存地址相同)
System.out.println("两个对象是否相同:" + (user1 == user2)); // true
}
}
日志输出:
plaintext
第一次查询执行SQL:SELECT * FROM user WHERE id = ?
第一次查询:User{id=1, username='张三', ...}
第二次查询:User{id=1, username='张三', ...}
两个对象是否相同:true
2.3 一级缓存失效场景
- 执行增删改操作:
java
运行
@Test
public void testCacheInvalidationWithCRUD() {
try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUserById(1);
System.out.println("第一次查询:" + user1);
// 执行更新操作
User updateUser = new User();
updateUser.setId(1);
updateUser.setAge(21);
mapper.updateUserSelective(updateUser);
// 缓存已失效,重新查询数据库
User user2 = mapper.getUserById(1);
System.out.println("第二次查询:" + user2);
System.out.println("两个对象是否相同:" + (user1 == user2)); // false
}
}
- 手动清空缓存:
java
运行
// 清空一级缓存
sqlSession.clearCache();
- 使用不同的 SqlSession:
java
运行
@Test
public void testDifferentSqlSession() {
// 第一个SqlSession
try (SqlSession sqlSession1 = MyBatisUtils.getSqlSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.getUserById(1);
System.out.println("SqlSession1查询:" + user1);
}
// 第二个SqlSession(与第一个不同)
try (SqlSession sqlSession2 = MyBatisUtils.getSqlSession()) {
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.getUserById(1); // 重新查询数据库
System.out.println("SqlSession2查询:" + user2);
}
}
- 查询参数或 SQL 不同:
java
运行
// 不同参数会导致缓存不命中
User user1 = mapper.getUserById(1);
User user2 = mapper.getUserById(2); // 不同参数,不使用缓存
2.4 一级缓存配置
一级缓存默认开启,且无法关闭,但可以通过以下方式间接控制:
xml
<settings>
<!-- 本地缓存范围:SESSION(默认)| STATEMENT -->
<setting name="localCacheScope" value="SESSION"/>
</settings>
SESSION:缓存对整个 SqlSession 有效(默认)STATEMENT:缓存仅对当前语句有效,执行完即清空
三、二级缓存:Mapper 级别的缓存
二级缓存是 Mapper 接口级别的缓存,作用域跨越多个 SqlSession,需要手动开启。
3.1 二级缓存工作原理
- 二级缓存与 Mapper 接口绑定,不同 Mapper 的缓存相互独立
- 当 SqlSession 关闭时,其一级缓存中的数据会被写入二级缓存
- 新的 SqlSession 查询时,会先检查二级缓存,再检查一级缓存
3.2 二级缓存开启步骤
- 全局配置开启二级缓存:
xml
<settings>
<!-- 开启二级缓存(默认就是true,可省略) -->
<setting name="cacheEnabled" value="true"/>
</settings>
- 在 Mapper 映射文件中配置缓存:
xml
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
<!-- 映射语句... -->
</mapper>
- 实体类实现序列化接口:
java
运行
public class User implements Serializable {
// 确保所有属性都可序列化
private static final long serialVersionUID = 1L;
// ...其他代码
}
3.3 二级缓存演示
java
运行
@Test
public void testSecondLevelCache() {
// 第一个SqlSession
try (SqlSession sqlSession1 = MyBatisUtils.getSqlSession()) {
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.getUserById(1);
System.out.println("SqlSession1查询:" + user1);
// 关闭SqlSession时,数据写入二级缓存
}
// 第二个SqlSession
try (SqlSession sqlSession2 = MyBatisUtils.getSqlSession()) {
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.getUserById(1); // 从二级缓存获取
System.out.println("SqlSession2查询:" + user2);
}
}
日志输出:
plaintext
SqlSession1执行SQL:SELECT * FROM user WHERE id = ?
SqlSession1查询:User{id=1, username='张三', ...}
SqlSession2查询:User{id=1, username='张三', ...} // 无SQL执行
3.4 二级缓存配置详解
cache标签支持多种属性配置:
xml
<cache
eviction="LRU" // 缓存回收策略
flushInterval="60000" // 刷新间隔(毫秒)
size="1024" // 最大缓存对象数
readOnly="false" // 是否只读
blocking="false"/> // 是否阻塞
缓存回收策略(eviction):
LRU(默认):最近最少使用,移除最长时间未使用的对象FIFO:先进先出,按对象进入缓存的顺序移除SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象
其他属性:
flushInterval:缓存自动刷新时间,默认不设置(即不自动刷新)size:缓存最多可存储的对象数量,默认 1024readOnly:true:返回缓存对象的只读引用,性能好但不安全false:返回缓存对象的拷贝(序列化实现),性能较差但安全
blocking:当缓存中没有请求的对象时,是否阻塞等待其他线程加载
3.5 控制语句对缓存的影响
可以通过useCache和flushCache属性控制单个语句对缓存的影响:
xml
<!-- 查询语句配置 -->
<select id="getUserById" resultType="User"
useCache="true" // 是否使用二级缓存
flushCache="false"> // 是否执行后清空二级缓存
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 增删改语句默认flushCache="true" -->
<update id="updateUser" flushCache="true">
UPDATE user SET ...
</update>
默认行为:
select语句:useCache=true,flushCache=falseinsert/update/delete语句:flushCache=true(执行后清空一、二级缓存)
四、缓存失效与一致性问题
缓存虽然能提升性能,但也带来了数据一致性问题,需要特别注意。
4.1 缓存失效的常见原因
- 同一 Mapper 的增删改操作:会自动清空该 Mapper 的二级缓存
- 不同 Mapper 操作同一张表:可能导致缓存不一致
- 缓存配置不当:如刷新间隔设置不合理
- 分布式环境下的缓存同步问题:多节点修改数据后缓存无法同步
4.2 保证缓存一致性的策略
-
合理设置缓存刷新策略:
- 对频繁修改的数据减少缓存时间
- 关键业务数据可禁用缓存
-
手动控制缓存刷新:
java
运行
// 修改数据后手动清除相关缓存
sqlSession.update("updateUser", user);
sqlSession.commit();
// 清除指定Mapper的缓存
sqlSession.clearCache(); // 清除一级缓存
sqlSession.getMapper(UserMapper.class).clearCache(); // 自定义方法清除二级缓存
-
使用第三方缓存框架:
- 集成 Redis、Ehcache 等分布式缓存
- 利用缓存中间件的过期策略和发布订阅功能
-
缓存粒度控制:
- 避免缓存过大的对象集合
- 优先缓存热点数据和变化频率低的数据
五、集成第三方缓存:以 Redis 为例
MyBatis 的二级缓存默认使用本地缓存,在分布式环境下存在局限。集成 Redis 可以实现分布式缓存。
5.1 添加依赖
xml
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
5.2 配置 Redis 缓存
在src/main/resources下创建redis.properties:
properties
redis.host=localhost
redis.port=6379
redis.password=
redis.timeout=2000
redis.database=0
redis.clientName=mybatis-redis-cache
5.3 在 Mapper 中使用 Redis 缓存
xml
<mapper namespace="com.example.mapper.UserMapper">
<!-- 使用Redis作为二级缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache">
<property name="eviction" value="LRU"/>
<property name="timeout" value="300000"/> <!-- 5分钟过期 -->
</cache>
<!-- ...其他映射 -->
</mapper>
六、缓存使用最佳实践
-
合理选择缓存级别:
- 高频访问、低频修改的数据:使用二级缓存
- 一次会话内多次访问的数据:依赖一级缓存
- 实时性要求高的数据:禁用缓存
-
控制缓存粒度:
- 优先缓存单条记录而非大集合
- 避免缓存包含大量数据的结果集
-
设置合理的缓存过期时间:
- 热点静态数据:可设置较长过期时间
- 频繁变化数据:设置较短过期时间或不缓存
-
分布式环境必须使用分布式缓存:
- 避免使用本地缓存导致的数据不一致
- 推荐使用 Redis 作为分布式缓存解决方案
-
缓存监控与调优:
- 监控缓存命中率
- 根据业务变化调整缓存策略
- 定期清理无效缓存
-
避免缓存穿透和雪崩:
- 缓存空结果,防止缓存穿透
- 设置随机过期时间,避免缓存雪崩
- 实现缓存降级和熔断机制
七、常见问题与解决方案
-
二级缓存不生效:
- 检查
cacheEnabled是否开启 - 确认 Mapper 文件是否配置了
<cache>标签 - 验证实体类是否实现了
Serializable接口 - 检查 SqlSession 是否正常关闭(数据需在关闭时写入二级缓存)
- 检查
-
缓存数据不一致:
- 确保增删改操作后缓存被正确清空
- 避免不同 Mapper 操作同一张表
- 合理设置缓存过期时间
-
缓存占用内存过大:
- 调整
size属性限制缓存对象数量 - 选择合适的缓存回收策略
- 减少大对象缓存
- 调整
-
分布式缓存同步问题:
- 改用 Redis 等分布式缓存
- 实现缓存更新的发布订阅机制
- 考虑使用 Canal 监听数据库变更同步缓存
总结
MyBatis 的缓存机制是提升应用性能的重要手段,一级缓存默认开启,适用于单会话内的数据复用;二级缓存需要手动开启,适用于多会话共享数据。合理使用缓存可以显著减少数据库访问,提高应用响应速度,但同时也需要注意数据一致性问题。
在实际开发中,应根据业务特点选择合适的缓存策略,对于分布式系统,建议集成 Redis 等分布式缓存框架。通过监控缓存命中率和持续调优,可以充分发挥缓存的性能优势,构建高效、稳定的应用系统。
1520

被折叠的 条评论
为什么被折叠?



