MyBatis 查询缓存不生效?二级缓存与本地缓存差异分析
大家好,今天我们来聊聊 MyBatis 中的缓存机制,特别是着重分析一下大家在使用过程中经常遇到的一个问题:MyBatis 查询缓存不生效。我们会深入探讨一级缓存(本地缓存)和二级缓存的差异,以及可能导致缓存失效的各种原因,并通过具体的代码示例来演示如何正确地使用和配置 MyBatis 缓存。
一、MyBatis 缓存机制概述
MyBatis 提供了两级缓存:
-
一级缓存 (Local Cache): 也称为本地缓存,是 SqlSession 级别的缓存。 默认情况下,一级缓存是开启的,无需额外配置。当 SqlSession 关闭时,一级缓存也会被清空。
-
二级缓存 (Second Level Cache): 是 Mapper 级别的缓存,可以被多个 SqlSession 共享。 需要手动开启和配置。
二、一级缓存 (Local Cache)
2.1 工作原理
一级缓存的生命周期与 SqlSession 相同。 当在同一个 SqlSession 中执行相同的查询语句时, MyBatis 会首先从一级缓存中查找结果。 如果找到,则直接返回缓存中的数据,避免重复访问数据库。
2.2 生效条件
- 必须是同一个 SqlSession 对象。
- 查询语句完全相同 (包括 SQL 语句、参数值、分页信息等)。
- 在查询期间没有执行过任何 update、insert、delete 操作,这些操作会清空一级缓存。
2.3 示例代码
// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
// 获取 SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// 获取 Mapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询,从数据库获取数据
User user1 = userMapper.selectUserById(1);
System.out.println("First query: " + user1);
// 第二次查询,从一级缓存获取数据
User user2 = userMapper.selectUserById(1);
System.out.println("Second query: " + user2);
// 更新操作,会清空一级缓存
userMapper.updateUser(user1);
sqlSession.commit(); //提交事务,更新数据库
// 第三次查询,重新从数据库获取数据
User user3 = userMapper.selectUserById(1);
System.out.println("Third query: " + user3);
} finally {
sqlSession.close();
}
//UserMapper接口定义
public interface UserMapper {
User selectUserById(int id);
void updateUser(User user);
}
2.4 导致一级缓存失效的原因
- 不同的 SqlSession: 使用不同的 SqlSession 对象进行查询。
- 不同的查询语句: 查询语句不一致(即使逻辑相同,但 SQL 语句的字符串不同也算)。
- 参数不同: 查询参数值不同。
- 执行了 update、insert、delete 操作: 这些操作会清空一级缓存,因为 MyBatis 无法保证缓存数据的有效性。
- 手动清空缓存: 调用
sqlSession.clearCache()方法。
三、二级缓存 (Second Level Cache)
3.1 工作原理
二级缓存是 Mapper 级别的缓存,可以被多个 SqlSession 共享。 当开启二级缓存后,查询结果会被缓存在 Mapper 对应的缓存区域中。 不同的 SqlSession 可以从同一个 Mapper 的缓存中获取数据。
3.2 生效条件
- 开启二级缓存: 需要在 MyBatis 配置文件中开启二级缓存,并在 Mapper.xml 文件中配置
<cache>元素。 - POJO 类需要实现 Serializable 接口: 因为缓存数据需要序列化到磁盘或网络中。
- SqlSession 关闭或提交: 只有当 SqlSession 关闭或提交时,一级缓存中的数据才会同步到二级缓存。
- 查询语句完全相同: 与一级缓存相同,查询语句必须完全一致。
- Mapper 接口方法上没有配置
flushCache="true": 如果配置了flushCache="true",每次执行该方法都会清空二级缓存。 - 没有配置
useCache="false": 如果配置了useCache="false",则禁用该查询的缓存。
3.3 配置方法
3.3.1 全局开启二级缓存 (mybatis-config.xml)
<configuration>
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 其他配置 -->
</configuration>
3.3.2 Mapper 接口配置二级缓存 (UserMapper.xml)
<mapper namespace="com.example.mapper.UserMapper">
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
<select id="selectUserById" resultType="com.example.model.User">
SELECT * FROM users WHERE id = #{id}
</select>
<update id="updateUser" parameterType="com.example.model.User">
UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
</update>
</mapper>
3.3.3 POJO 类实现 Serializable 接口
import java.io.Serializable;
public class User implements Serializable {
private int id;
private String name;
private String email;
// 构造方法、Getter 和 Setter 方法
}
3.4 <cache> 元素属性说明
| 属性 | 说明 | 默认值 |
|---|---|---|
| eviction | 缓存回收策略: – LRU:最近最少使用策略 (Least Recently Used) – FIFO:先进先出策略 (First In First Out) – SOFT:软引用策略 – WEAK:弱引用策略 |
LRU |
| flushInterval | 刷新间隔(毫秒): 缓存多长时间清空一次。不配置则不刷新。 | 不刷新 |
| size | 缓存存放多少个元素。 | 1024 |
| readOnly | 是否只读: – true: 所有调用者都将共享缓存对象的相同实例。 缓存对象不可修改。 – false:调用者将得到缓存对象的拷贝。 |
false |
3.5 示例代码
// 创建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
// 第一次 SqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
try {
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.selectUserById(1);
System.out.println("First query (SqlSession1): " + user1);
sqlSession1.close(); // 关闭 SqlSession1,将数据同步到二级缓存
} finally {
sqlSession1.close();
}
// 第二次 SqlSession
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.selectUserById(1);
System.out.println("Second query (SqlSession2): " + user2); // 从二级缓存获取数据
sqlSession2.close();
} finally {
sqlSession2.close();
}
// 第三次 SqlSession,执行更新操作
SqlSession sqlSession3 = sqlSessionFactory.openSession();
try {
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
User user3 = userMapper3.selectUserById(1);
user3.setName("Updated Name");
userMapper3.updateUser(user3); // 更新操作,会清空二级缓存
sqlSession3.commit(); // 提交事务,更新数据库
sqlSession3.close();
} finally {
sqlSession3.close();
}
// 第四次 SqlSession
SqlSession sqlSession4 = sqlSessionFactory.openSession();
try {
UserMapper userMapper4 = sqlSession4.getMapper(UserMapper.class);
User user4 = userMapper4.selectUserById(1);
System.out.println("Fourth query (SqlSession4): " + user4); // 重新从数据库获取数据
sqlSession4.close();
} finally {
sqlSession4.close();
}
3.6 导致二级缓存失效的原因
- 未开启二级缓存:
cacheEnabled设置为false。 - Mapper.xml 文件中未配置
<cache>元素。 - POJO 类未实现 Serializable 接口。
- SqlSession 未关闭或提交: 一级缓存中的数据无法同步到二级缓存。
- Mapper 接口方法上配置了
flushCache="true": 每次执行该方法都会清空二级缓存。 - 配置了
useCache="false": 禁用该查询的缓存。 - 执行了 update、insert、delete 操作: 默认情况下,这些操作会清空与该 Mapper 相关的二级缓存。 可以使用
<cache flushInterval="xxx">,然后设置flushCache=false,来避免update, insert, delete操作清空缓存,设置flushInterval来定时清理缓存。
3.7 如何验证二级缓存是否生效
- 日志输出: MyBatis 会在日志中输出缓存命中信息。 可以通过配置 MyBatis 日志来查看是否使用了缓存。
- 数据库监控: 监控数据库的查询次数。 如果二级缓存生效,相同的查询语句应该只执行一次。
- 使用 MyBatis 提供的 Cache 接口: 可以通过
Configuration.getCache(namespace)获取 Cache 对象,并调用getSize()方法来查看缓存大小。
四、一级缓存与二级缓存的差异对比
| 特性 | 一级缓存 (Local Cache) | 二级缓存 (Second Level Cache) |
|---|---|---|
| 作用范围 | SqlSession 级别 | Mapper 级别 |
| 共享性 | 不共享 | 多个 SqlSession 共享 |
| 生效条件 | 默认开启 | 需要手动开启和配置 |
| 生命周期 | 与 SqlSession 相同 | 与应用程序相同 |
| 数据同步 | 无需同步 | SqlSession 关闭或提交时同步 |
| 存储介质 | 内存 | 内存或磁盘 |
五、常见的缓存问题及解决方案
5.1 问题:更新数据库后,缓存数据未同步
原因:
- 没有正确提交事务,导致数据没有写入数据库。
- 使用了错误的缓存配置,导致缓存没有被清空。
解决方案:
- 确保在更新数据库后提交事务。
- 检查 Mapper 接口方法上的
flushCache属性是否设置为true。 - 确保相关的二级缓存配置正确。
5.2 问题:缓存数据过期或失效
原因:
- 缓存的
flushInterval设置过短,导致缓存频繁刷新。 - 缓存的
size设置过小,导致缓存数据被频繁回收。 - 数据库中的数据发生了变化,但缓存没有及时更新。
解决方案:
- 调整
flushInterval和size参数,根据实际情况进行设置。 - 考虑使用第三方缓存解决方案,例如 Redis 或 Memcached,以获得更强大的缓存管理能力。
- 使用 MyBatis 的缓存刷新机制,确保缓存数据与数据库数据保持同步。
5.3 问题:并发场景下的缓存问题
原因:
- 多个线程同时访问和修改缓存数据,导致数据不一致。
解决方案:
- 如果
readOnly设置为false, 确保 POJO 类是线程安全的。 - 使用悲观锁或乐观锁来控制对缓存数据的并发访问。
- 考虑使用分布式缓存解决方案,以支持高并发场景。
六、选择合适的缓存策略
选择合适的缓存策略需要考虑以下因素:
- 数据访问模式: 如果数据读取频繁,更新较少,则适合使用缓存。
- 数据一致性要求: 如果对数据一致性要求很高,则需要谨慎使用缓存,并采取相应的同步机制。
- 系统性能需求: 缓存可以提高系统性能,但也会增加系统的复杂性。 需要根据实际情况进行权衡。
七、集成第三方缓存
MyBatis 允许集成第三方缓存,例如 Ehcache、Redis、Memcached 等。 通过集成第三方缓存,可以获得更强大的缓存管理能力,例如:
- 更大的缓存容量
- 更灵活的缓存策略
- 分布式缓存支持
具体集成方法可以参考 MyBatis 官方文档和第三方缓存的文档。
八、关于缓存使用的建议
- 不要过度依赖缓存: 缓存虽然可以提高性能,但也会增加系统的复杂性。
- 监控缓存的性能: 定期监控缓存的命中率和性能指标,确保缓存正常工作。
- 理解缓存的局限性: 缓存并不能解决所有性能问题。 需要综合考虑各种因素,选择合适的优化方案。
九、缓存失效的原因总结和应对
以上我们详细讨论了 MyBatis 缓存机制,一级缓存和二级缓存的差异,以及可能导致缓存失效的各种原因。希望通过今天的分享,大家能够对 MyBatis 缓存有更深入的理解,并能够在实际项目中正确地使用和配置 MyBatis 缓存,从而提高系统的性能和效率。记住,理解缓存的工作原理和限制是解决缓存问题的关键。在遇到缓存不生效的情况时,需要仔细检查配置、代码和数据操作,才能找到问题的根源。