MyBatis 给我们提供了一级缓存和二级缓存机制来提高查询效率和减少数据库交互次数,一级缓存主要用于单次数据库会话内的查询优化,而二级缓存则着眼于整个应用层面的数据复用。
一级缓存(Local Cache)
-
特点:
- 一级缓存是 SqlSession 级别的缓存,也称为本地缓存,每个 SqlSession 内部都有一个本地缓存。
- 在同一个 SqlSession 生命周期内有效,即在打开一个 SqlSession 到关闭 SqlSession 这个过程中,执行相同 SQL 语句时,MyBatis 会先查找一级缓存中是否有对应结果,如果有就直接返回,不再查询数据库。
- 当在同一个 SqlSession 内执行了任何修改操作(增删改),会立即清空一级缓存,以保证数据一致性。
- 当 SqlSession 关闭时,一级缓存会被清空。
-
工作原理:
- MyBatis 使用一个 HashMap 来存储一级缓存,键值是基于 SQL 语句(包括参数)生成的一个唯一标识。
- 第一次查询时,结果会被存入缓存;第二次执行相同的查询时,若缓存未失效,则直接从缓存中获取结果。
二级缓存(Second Level Cache)
-
特点:
- 二级缓存是全局性的,跨越了 SqlSession 的边界,不同的多个 SqlSession 可以共享相同的二级缓存区域。
- 二级缓存需要在全局配置文件中开启,并且在具体的 Mapper 映射文件中通过
<cache>
标签进行详细配置才能生效。 - 二级缓存通常基于 Mapper 级别进行管理,意味着只有同一个 Mapper 下的查询结果才会被缓存在一起。
- 当不同 SqlSession 执行相同的 SQL 查询时,如果开启了二级缓存并且相关数据存在于缓存中,则可以从二级缓存中获取结果,从而避免再次查询数据库。
-
工作原理:
- 当一个 SqlSession 关闭后,其一级缓存中的数据可能会被移至二级缓存中(如果配置了二级缓存)。
- 新的 SqlSession 在执行查询时,如果查询条件与已经存在于二级缓存中的数据匹配,MyBatis 会直接从二级缓存中获取数据,避免直接访问数据库。
- 二级缓存可以配置各种策略,比如缓存过期时间、缓存淘汰算法(LRU、FIFO 等)、是否支持读写等。
-
注意事项:
- 二级缓存带来的潜在问题是数据一致性,尤其在多线程或多用户环境下,当数据库发生更新时,如果没有恰当的缓存同步机制,可能会导致脏读问题。
- 若要使用二级缓存,需要确保被缓存的相关的实体类(POJO)实现了
Serializable
接口,以便在必要时进行序列化和反序列化。 - 在执行增删改操作时,MyBatis 并不会自动更新二级缓存,开发人员需要手动处理缓存同步,可通过设置
<cache>
标签的flushInterval
属性定期刷新缓存,或者在插入、更新或删除操作后调用clearCache()
清除缓存。
- 开启二级缓存:
- 在 MyBatis 的全局配置文件(mybatis-config.xml)中,默认情况下
cacheEnabled
属性值为true
,表示全局缓存开启。 - 但在实际应用中,还需要在具体 Mapper 的 XML 映射文件或者对应的 Mapper 接口中显式地声明使用二级缓存,即在
<mapper>
标签内添加<cache>
子标签来配置二级缓存。 - 可以配置二级缓存的各种属性,如缓存大小、缓存的并发策略(read-write 或 read-only)、缓存的回收策略(LRU、FIFO 等)以及缓存的存活时间等。
- 在 MyBatis 的全局配置文件(mybatis-config.xml)中,默认情况下
全局配置开启二级缓存: 首先,你需要在 MyBatis 的全局配置文件(通常是 mybatis-config.xml
)中开启二级缓存。在 Spring Boot 中,你可以在 resources 目录下创建这个文件,或者在 application.properties 或 application.yml 中配置 MyBatis 的配置路径。在 mybatis-config.xml
文件中设置 cache-enabled
属性为 true
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration><settings><!-- 开启二级缓存 --><setting name="cacheEnabled" value="true"/><!-- 其他配置项... --></settings>
</configuration>
Mapper 中启用二级缓存: 仅仅开启全局的二级缓存还不够,你还需要在各个需要用到二级缓存的 Mapper 映射文件中启用缓存。在对应的 Mapper XML 文件中加入 <cache>
标签:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.YourMapper"><!-- 启用二级缓存 --><cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/><!-- 其他SQL映射... -->
</mapper>
在 <cache>
标签内,你可以配置二级缓存的各种属性,例如驱逐策略(eviction)、刷新间隔(flushInterval)、缓存大小(size)以及是否只读(readOnly)等。
二级缓存属性:
-
enabled
- 默认值:true (如果在
<cache>
标签内未指定,则该 Mapper 是否启用二级缓存) - 说明:决定是否启用该 Mapper 对应的二级缓存。
- 默认值:true (如果在
-
eviction
- 默认值:LRU(Least Recently Used 最近最少使用)
- 说明:缓存对象的驱逐策略,用于决定当缓存达到最大容量时如何剔除旧的条目。可选值有:
- LRU - 最近最少使用的对象将被剔除。
- FIFO - 先进先出(First In First Out)策略,最早放入缓存的对象将被首先剔除。
- SOFT - 软引用策略,依据 JVM 的垃圾回收机制决定何时剔除。
- WEAK - 弱引用策略,类似于软引用,但在 GC 时更易被回收。
-
flushInterval
- 默认值:0(不自动刷新)
- 说明:缓存刷新间隔,单位是毫秒。如果设置了非零值,那么缓存将在指定时间间隔后自动刷新,这意味着所有缓存数据都会失效,下次查询时会重新从数据库加载。
-
size
- 默认值:无限制
- 说明:缓存的最大容量,超过这个数量的对象将触发驱逐策略。
-
readOnly
- 默认值:false
- 说明:指示缓存是否只读。如果设置为 true,那么当对象被从缓存中读取时,MyBatis 不会跟踪其状态变化,也不会在更新操作后清空缓存。这样可以提高性能,但也有可能导致缓存数据与数据库数据不一致的问题。如果设置为 false,则会对缓存对象的状态变化进行跟踪,以确保缓存数据与数据库保持一致。
-
blockFlush
- 默认值:取决于具体实现
- 说明:是否阻塞刷新操作,某些缓存实现可能会支持异步刷新,此属性可控制是否等待刷新完成后再继续。
-
blockRead
- 默认值:取决于具体实现
- 说明:是否在读取操作时阻塞,当缓存正在刷新时,是否等待刷新完成后再读取。
-
serialize
- 默认值:true(在某些实现中可能是必需的)
- 说明:决定是否序列化缓存对象。由于二级缓存通常涉及跨线程和跨进程共享数据,因此存储的对象必须是可序列化的。
- 还可以通过
<cache>
标签中的type
属性指定具体的缓存实现类,以使用自定义的缓存插件或第三方比如Redis缓存库。
举个例子:
可以根据需求创建个mapper,比如<cache>
标签开启了 EmployeeMapper 的二级缓存,并指定了缓存的驱逐策略(这里是 LRU)、缓存大小以及是否只读:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.EmployeeMapper"><!-- 启用二级缓存 --><cache eviction="LRU" size="1024" readOnly="false"/><!-- 查询映射 --><select id="selectEmployeeById" resultType="com.example.model.Employee" useCache="true">SELECT * FROM employee WHERE id = #{id}</select><!-- 更新、插入、删除映射也要包含进来,因为它们会影响缓存 --><!-- ... -->
</mapper>
然后这样使用:
// 创建两个 SqlSession 实例
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();try {// 第一次查询EmployeeMapper mapper1 = session1.getMapper(EmployeeMapper.class);Employee emp1 = mapper1.selectEmployeeById(1);// 输出第一次查询结果// 第二次查询,假设使用了另一个 SqlSessionEmployeeMapper mapper2 = session2.getMapper(EmployeeMapper.class);Employee emp2 = mapper2.selectEmployeeById(1);// 因为二级缓存已启用,emp1 和 emp2 应该指向同一份缓存数据// 更新操作emp1.setName("New Name");mapper1.updateEmployee(emp1);session1.commit(); // 更新提交后,二级缓存中的对应数据应当失效// 手动清空受影响的二级缓存,然而,这种方法并不精细,因为它清空了整个 Mapper 的二级缓存,而不是仅针对被修改的特定记录。sqlSession.clearCache();// 再次查询,此时应从数据库获取最新的数据Employee emp3 = mapper2.selectEmployeeById(1);// emp3 应该反映了数据库中更新后的名称
} finally {session1.close();session2.close();
}