第09章 缓存机制
深入理解 MyBatis 的一级缓存和二级缓存机制
学习进度:36% 代码演示按钮 💻 查看完整代码 - 在线IDE体验
🎯 学习目标
📚
理解核心概念
掌握本章的核心概念和基本原理
🔧
掌握实践技能
学会相关的配置方法和实践技巧
💡
了解应用场景
明确在实际项目中的应用场景和最佳实践
学习目标
- 深入理解MyBatis缓存机制的工作原理
- 掌握一级缓存和二级缓存的配置与使用
- 学会缓存的生效条件和失效机制
- 了解自定义缓存的实现方法
- 掌握分布式缓存的集成方案
- 学会缓存性能优化和最佳实践
缓存机制概述
缓存是提高应用性能的重要手段,MyBatis提供了完善的缓存机制。通过缓存,可以减少数据库访问次数,提高查询性能。MyBatis支持一级缓存和二级缓存两种缓存级别。
缓存的作用
- 减少数据库访问:避免重复查询相同数据
- 提高响应速度:从内存获取数据比数据库查询快
- 降低数据库负载:减少数据库连接和查询压力
- 改善用户体验:更快的响应时间
MyBatis缓存层次
- 一级缓存:SqlSession级别,默认开启
- 二级缓存:Mapper级别,需要手动配置
- 自定义缓存:可以集成第三方缓存框架
一级缓存
一级缓存是SqlSession级别的缓存,在同一个SqlSession中,相同的查询会从缓存中获取结果。
一级缓存的特点
- 默认开启,无法关闭
- 作用域为SqlSession
- 基于HashMap实现
- 生命周期与SqlSession相同
一级缓存的工作原理
// 演示一级缓存的工作
public class FirstLevelCacheDemo {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = MyBatisUtils.getSqlSessionFactory();
SqlSession session = factory.openSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询,从数据库获取
System.out.println("=== 第一次查询 ===");
User user1 = mapper.selectUserById(1L);
System.out.println("用户1: " + user1.getName());
// 第二次查询,从一级缓存获取
System.out.println("=== 第二次查询 ===");
User user2 = mapper.selectUserById(1L);
System.out.println("用户2: " + user2.getName());
// 验证是否为同一个对象
System.out.println("是否为同一对象: " + (user1 == user2));
} finally {
session.close();
}
}
}
一级缓存失效的情况
- SqlSession关闭:缓存随之清空
- 执行增删改操作:会清空当前SqlSession的缓存
- 手动清除:调用clearCache()方法
- 查询参数不同:不同参数的查询不会命中缓存
// 演示一级缓存失效
public void testFirstLevelCacheInvalidation() {
SqlSession session = factory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询
User user1 = mapper.selectUserById(1L);
// 执行更新操作,缓存失效
mapper.updateUser(new User(2L, "Updated Name", 25, "[email protected]"));
// 再次查询,需要重新从数据库获取
User user2 = mapper.selectUserById(1L);
session.close();
}
二级缓存
二级缓存是Mapper级别的缓存,可以在多个SqlSession之间共享。
二级缓存的特点
- 默认关闭,需要手动开启
- 作用域为Mapper(Namespace)
- 可以跨SqlSession共享
- 需要实体类实现Serializable接口
开启二级缓存
1. 全局配置
<!-- 在mybatis-config.xml中开启二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
2. Mapper配置
<!-- 在UserMapper.xml中配置缓存 -->
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache/>
<!-- 或者使用详细配置 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="false"/>
<select id="selectUserById" resultType="User">
SELECT id, name, age, email FROM users WHERE id = #{id}
</select>
</mapper>
3. 实体类配置
// 实体类必须实现Serializable接口
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private Integer age;
private String email;
// 构造方法、getter、setter等
}
缓存配置属性
| 属性 | 描述 | 默认值 | 可选值 |
|---|---|---|---|
| eviction | 缓存回收策略 | LRU | LRU、FIFO、SOFT、WEAK |
| flushInterval | 刷新间隔(毫秒) | 无 | 正整数 |
| size | 缓存对象数量 | 1024 | 正整数 |
| readOnly | 是否只读 | false | true、false |
缓存回收策略详解
- LRU:最近最少使用,移除最长时间不被使用的对象
- FIFO:先进先出,按对象进入缓存的顺序来移除
- SOFT:软引用,基于垃圾回收器状态和软引用规则移除对象
- WEAK:弱引用,更积极地基于垃圾收集器状态和弱引用规则移除对象
二级缓存的使用示例
// 演示二级缓存的工作
public class SecondLevelCacheDemo {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = MyBatisUtils.getSqlSessionFactory();
// 第一个SqlSession
SqlSession session1 = factory.openSession();
try {
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.selectUserById(1L);
System.out.println("Session1查询: " + user1.getName());
} finally {
session1.close(); // 关闭session1,数据进入二级缓存
}
// 第二个SqlSession
SqlSession session2 = factory.openSession();
try {
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.selectUserById(1L); // 从二级缓存获取
System.out.println("Session2查询: " + user2.getName());
} finally {
session2.close();
}
}
}
控制缓存行为
1. 禁用特定查询的缓存
<select id="selectUserById" resultType="User" useCache="false">
SELECT id, name, age, email FROM users WHERE id = #{id}
</select>
2. 控制缓存刷新
<insert id="insertUser" flushCache="true">
INSERT INTO users (name, age, email) VALUES (#{name}, #{age}, #{email})
</insert>
<update id="updateUser" flushCache="true">
UPDATE users SET name=#{name}, age=#{age}, email=#{email} WHERE id=#{id}
</update>
<delete id="deleteUser" flushCache="true">
DELETE FROM users WHERE id = #{id}
</delete>
自定义缓存
MyBatis允许使用自定义缓存实现,可以集成Redis、Ehcache等第三方缓存框架。
实现Cache接口
public class CustomCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new ConcurrentHashMap<>();
public CustomCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
System.out.println("缓存存储: " + key);
}
@Override
public Object getObject(Object key) {
Object value = cache.get(key);
System.out.println("缓存获取: " + key + " -> " + (value != null ? "命中" : "未命中"));
return value;
}
@Override
public Object removeObject(Object key) {
System.out.println("缓存移除: " + key);
return cache.remove(key);
}
@Override
public void clear() {
System.out.println("清空缓存");
cache.clear();
}
@Override
public int getSize() {
return cache.size();
}
}
使用自定义缓存
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<!-- 使用自定义缓存 -->
<cache type="com.example.mybatis.cache.CustomCache"/>
<select id="selectUserById" resultType="User">
SELECT id, name, age, email FROM users WHERE id = #{id}
</select>
</mapper>
Redis缓存集成
在分布式环境中,可以使用Redis作为二级缓存的存储介质。
Redis缓存实现
public class RedisCache implements Cache {
private final String id;
private static JedisPool jedisPool;
static {
// 初始化Redis连接池
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(10);
jedisPool = new JedisPool(config, "localhost", 6379);
}
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
try (Jedis jedis = jedisPool.getResource()) {
String keyStr = id + ":" + key.toString();
String valueStr = serialize(value);
jedis.setex(keyStr, 3600, valueStr); // 1小时过期
}
}
@Override
public Object getObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
String keyStr = id + ":" + key.toString();
String valueStr = jedis.get(keyStr);
return valueStr != null ? deserialize(valueStr) : null;
}
}
@Override
public Object removeObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
String keyStr = id + ":" + key.toString();
return jedis.del(keyStr);
}
}
@Override
public void clear() {
try (Jedis jedis = jedisPool.getResource()) {
Set<String> keys = jedis.keys(id + ":*");
if (!keys.isEmpty()) {
jedis.del(keys.toArray(new String[0]));
}
}
}
@Override
public int getSize() {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.keys(id + ":*").size();
}
}
// 序列化和反序列化方法
private String serialize(Object obj) {
// 实现对象序列化
return JSON.toJSONString(obj);
}
private Object deserialize(String str) {
// 实现对象反序列化
return JSON.parseObject(str, Object.class);
}
}
配置Redis缓存
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<!-- 使用Redis缓存 -->
<cache type="com.example.mybatis.cache.RedisCache"
eviction="LRU"
flushInterval="600000"
size="1000"
readOnly="false"/>
</mapper>
缓存最佳实践
1. 缓存策略选择
- 读多写少的数据适合使用缓存
- 频繁变更的数据不适合长时间缓存
- 大对象缓存需要考虑内存占用
- 关联查询结果缓存需要谨慎处理
2. 缓存配置优化
- 合理设置缓存大小和过期时间
- 选择合适的回收策略
- 根据业务场景选择readOnly属性
- 监控缓存命中率和性能指标
3. 缓存一致性
- 及时清除过期缓存
- 处理好增删改操作对缓存的影响
- 考虑分布式环境下的缓存同步
- 避免缓存雪崩和缓存穿透
4. 性能监控
- 监控缓存命中率
- 观察缓存大小和内存使用
- 分析缓存失效原因
- 定期清理无效缓存
缓存问题与解决方案
Q: 缓存不生效怎么办?
A: 检查全局配置是否开启、Mapper是否配置cache标签、实体类是否实现Serializable接口。
Q: 如何避免缓存数据不一致?
A: 合理设置flushCache属性,在增删改操作后及时清除相关缓存。
Q: 二级缓存序列化异常?
A: 确保所有相关实体类都实现了Serializable接口,包括关联对象。
Q: 缓存占用内存过大?
A: 调整缓存大小配置,选择合适的回收策略,避免缓存大对象。
Q: 分布式环境缓存同步问题?
A: 使用Redis等分布式缓存,或者实现缓存失效通知机制。
实践练习
练习1:一级缓存验证
编写代码验证一级缓存的生效和失效条件。
练习2:二级缓存配置
配置二级缓存,测试不同SqlSession间的缓存共享。
练习3:自定义缓存实现
实现一个带有统计功能的自定义缓存。
练习4:Redis缓存集成
集成Redis作为二级缓存,测试分布式缓存效果。
练习5:缓存性能测试
对比有缓存和无缓存的性能差异,分析缓存命中率。
运行示例程序
步骤1:导入项目
将chapter-09-cache.json导入到IntelliJ IDEA中。
步骤2:配置环境
确保MySQL和Redis服务正常运行,执行init.sql创建测试数据。
步骤3:运行基础示例
运行CacheDemo.java,观察一级缓存和二级缓存的工作效果。
步骤4:观察缓存日志
查看控制台输出,理解缓存的命中和失效机制。
步骤5:测试自定义缓存
运行自定义缓存示例,观察缓存的存储和获取过程。
步骤6:Redis缓存测试
配置Redis缓存,测试分布式缓存的效果。
步骤7:运行测试用例
执行CacheTest.java中的测试用例,验证各种缓存场景。
本章小结
📖 本章小结
本章我们学习了 缓存机制 的相关内容,包括:
- 核心概念和基本原理
- 配置方法和实践技巧
- 应用场景和最佳实践
- 常见问题和解决方案
下一章节 🎉
恭喜完成本章学习!
你已经掌握了 缓存机制 的核心知识
第10章插件开发继续学习 MyBatis 的更多特性和应用⏱️预计 30 分钟📊中等难度开始学习 → ← 第08章 📚 课程目录 第10章 →