Mybatis中的一级二级缓存扫盲
思维导图:
MyBatis 提供了一级缓存和二级缓存机制,用于提高数据库查询的性能,减少对数据库的访问次数。(本质上是减少IO次数)。
一级缓存
1. 概念
一级缓存也称为会话缓存,它是基于 SqlSession 的缓存。在同一个 SqlSession 中,执行相同的 SQL 查询时,MyBatis 会优先从一级缓存中获取结果,而不是再次访问数据库。
2. 工作原理
2.1缓存结构:
在 SqlSession 内部,一级缓存是一个 PerpetualCache 对象,它本质上是一个 HashMap,键是根据查询的 SQL 语句、参数、环境等信息生成的唯一标识,值是查询结果。
查询流程:当调用 SqlSession 的查询方法时,MyBatis 会先将查询的 SQL 语句、参数等信息组合成一个唯一的缓存键。然后在 PerpetualCache 这个 HashMap 中查找该键对应的值。如果找到了,就直接返回该值;如果没找到,就会执行 SQL 查询,将查询结果存入 PerpetualCache 中,下次再执行相同查询时就可以直接从缓存中获取结果。
2.2 缓存命中的条件
相同的 SqlSession:必须是在同一个 SqlSession 实例中执行相同的查询,一级缓存才会生效。
相同的 SQL 语句:查询的 SQL 语句必须完全相同,包括 SQL 中的参数占位符和参数值。
相同的环境:查询的环境(如数据库连接、事务等)也必须相同。
代码示例:
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 java.io.InputStream;public class FirstLevelCacheDetailExample {public static void main(String[] args) throws Exception {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);// 创建第一个 SqlSessiontry (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {// 第一次查询User user1 = sqlSession1.selectOne("com.example.UserMapper.selectUserById", 1);System.out.println("第一次查询结果: " + user1);// 第二次查询,使用相同的 SqlSessionUser user2 = sqlSession1.selectOne("com.example.UserMapper.selectUserById", 1);System.out.println("第二次查询结果: " + user2);// 两次查询结果相同,说明使用了一级缓存System.out.println("两次查询结果是否相同: " + (user1 == user2));// 执行更新操作sqlSession1.update("com.example.UserMapper.updateUser", new User(1, "New Name"));// 第三次查询User user3 = sqlSession1.selectOne("com.example.UserMapper.selectUserById", 1);System.out.println("第三次查询结果: " + user3);// 由于执行了更新操作,一级缓存已清空,user3 是重新查询数据库得到的结果System.out.println("第一次查询结果和第三次查询结果是否相同: " + (user1 == user3));}}
}
在上述代码中,前两次查询使用相同的 SqlSession 和相同的查询条件,所以第二次查询会从一级缓存中获取结果。而执行更新操作后,一级缓存被清空,第三次查询会重新访问数据库。
3. 缓存失效情况
- 不同的 SqlSession:每个 SqlSession 都有自己独立的一级缓存,不同的 SqlSession 之间的缓存是不共享的。
- 执行 insert、update、delete 操作:当在同一个 SqlSession 中执行这些操作时,会清空该 SqlSession 的一级缓存,以保证数据的一致
4. 优缺点
优点:
- 提高性能:在同一个 SqlSession 中多次执行相同查询时,避免了重复的数据库访问,减少了数据库的负载,提高了查询性能。
- 简单易用:一级缓存是 MyBatis 内置的,无需额外配置,默认开启,使用方便。
缺点:
- 作用范围小:只在同一个 SqlSession 中有效,不同的 SqlSession 之间无法共享缓存,限制了缓存的使用范围。
- 数据一致性问题:如果在同一个 SqlSession 中执行了 insert、update、delete 操作,会清空该 SqlSession 的一级缓存,但如果在不同的 SqlSession 中对同一数据进行了修改,一级缓存可能会返回旧数据,导致数据不一致。
二级缓存
1. 工作原理
缓存结构:
二级缓存也是基于 PerpetualCache 实现的,但它是基于 SqlSessionFactory 的。每个 Mapper 可以有自己独立的二级缓存,也可以多个 Mapper 共享同一个二级缓存。
查询流程:当一个 SqlSession 执行查询操作时,MyBatis 会先检查该 Mapper 对应的二级缓存中是否存在该查询结果。如果存在,则直接从二级缓存中获取结果;如果不存在,则执行 SQL 查询,并将查询结果存入二级缓存中。在多个 SqlSession 之间,只要它们是由同一个 SqlSessionFactory 创建的,就可以共享二级缓存。
2. 配置详解
全局配置:在 mybatis-config.xml 中开启二级缓存的全局开关。
<settings><setting name="cacheEnabled" value="true"/>
</settings>
Mapper 配置:在 Mapper 映射文件中配置缓存。
<mapper namespace="com.example.UserMapper"><!-- 开启二级缓存,并配置相关属性 --><cacheeviction="LRU" <!-- 缓存淘汰策略,这里使用最近最少使用策略 -->flushInterval="60000" <!-- 缓存刷新间隔,单位为毫秒 -->size="512" <!-- 缓存的最大对象数 -->readOnly="true" /> <!-- 是否只读 --><select id="selectUserById" resultType="com.example.User">SELECT * FROM users WHERE id = #{id}</select>
</mapper>
- eviction:缓存淘汰策略,常见的有 LRU(最近最少使用)、FIFO(先进先出)等。
- flushInterval:缓存刷新间隔,指定多长时间清空一次缓存。
- size:缓存的最大对象数,当缓存中的对象数量超过该值时,会根据淘汰策略淘汰部分对象。
- readOnly:是否只读。如果设置为 true,则缓存中的对象是只读的,MyBatis 会直接返回缓存中的对象,不会进行序列化和反序列化操作,性能较高;如果设置为 false,则每次返回缓存中的对象时都会进行序列化和反序列化操作,保证返回的对象是一个新的实例,但性能相对较低。
3. 示例代码及分析
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 java.io.InputStream;public class SecondLevelCacheDetailExample {public static void main(String[] args) throws Exception {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);// 第一个 SqlSessiontry (SqlSession sqlSession1 = sqlSessionFactory.openSession()) {User user1 = sqlSession1.selectOne("com.example.UserMapper.selectUserById", 1);System.out.println("第一个 SqlSession 查询结果: " + user1);}// 第二个 SqlSessiontry (SqlSession sqlSession2 = sqlSessionFactory.openSession()) {User user2 = sqlSession2.selectOne("com.example.UserMapper.selectUserById", 1);System.out.println("第二个 SqlSession 查询结果: " + user2);// 两次查询结果相同,说明使用了二级缓存System.out.println("两次查询结果是否相同: " + (user1 == user2));}// 执行更新操作try (SqlSession sqlSession3 = sqlSessionFactory.openSession()) {sqlSession3.update("com.example.UserMapper.updateUser", new User(1, "New Name"));sqlSession3.commit(); // 提交事务,清空二级缓存}// 第三个 SqlSessiontry (SqlSession sqlSession4 = sqlSessionFactory.openSession()) {User user3 = sqlSession4.selectOne("com.example.UserMapper.selectUserById", 1);System.out.println("第三个 SqlSession 查询结果: " + user3);// 由于执行了更新操作,二级缓存已清空,user3 是重新查询数据库得到的结果System.out.println("第一个查询结果和第三个查询结果是否相同: " + (user1 == user3));}}
}
在上述代码中,第一个 SqlSession 执行查询后,结果会存入二级缓存。第二个 SqlSession 执行相同查询时,会从二级缓存中获取结果。执行更新操作并提交事务后,二级缓存会被清空,第三个 SqlSession 执行查询时会重新访问数据库。
3. 缓存失效情况
- 执行 insert、update、delete 操作:当执行这些操作时,会清空该 Mapper 对应的二级缓存,以保证数据的一致性。
- 缓存刷新策略:可以通过配置缓存的刷新策略,如 flushInterval 来定期清空缓存。
4. 优缺点
优点
- 作用范围大:多个 SqlSession 可以共享二级缓存,减少了数据库的访问次数,提高了系统的整体性能。
- 可配置性强:可以通过配置不同的缓存淘汰策略、刷新间隔等参数,满足不同的业务需求。
缺点
- 配置复杂:需要在全局配置和 Mapper 映射文件中进行配置,相对一级缓存来说配置较为复杂。
- 数据一致性问题:如果在不同的 Mapper 中对同一数据进行了修改,可能会导致二级缓存中的数据不一致,需要手动清空缓存或使用更复杂的缓存刷新策略。
5.一级缓存和二级缓存的比较
作用范围:一级缓存是基于 SqlSession 的,作用范围较小;二级缓存是基于 SqlSessionFactory 的,作用范围较大。
缓存共享:一级缓存不共享,每个 SqlSession 有自己独立的缓存;二级缓存可以在多个 SqlSession 之间共享。
开启方式:一级缓存默认开启;二级缓存需要手动配置开启。
通过合理使用一级缓存和二级缓存,可以有效提高 MyBatis 应用的性能。但在使用缓存时,需要注意数据的一致性问题,避免出现脏数据。
总结
MyBatis 的一级缓存和二级缓存各有优缺点,在实际应用中需要根据具体的业务场景合理使用。一级缓存适用于在同一个 SqlSession 中多次执行相同查询的场景,而二级缓存适用于多个 SqlSession 之间共享缓存的场景。同时,需要注意缓存的使用可能会导致数据一致性问题,需要在业务逻辑中进行相应的处理。