在高并发业务场景中,数据库查询往往是系统性能的瓶颈。频繁的磁盘IO操作不仅会增加数据库压力,还会导致接口响应延迟。MyBatis作为主流的持久层框架,内置了强大的查询缓存机制,通过将高频查询数据缓存到内存中,减少与数据库的直接交互,从而显著提升查询效率。本文将从缓存的核心概念入手,全面拆解MyBatis一级缓存与二级缓存的实现原理、配置方法、失效场景,并梳理缓存查询的核心逻辑,助力开发者精准掌握缓存使用技巧。
一、缓存基础:读懂性能优化的核心逻辑
在深入MyBatis缓存之前,我们先理清缓存的基础概念------什么是缓存?为什么需要缓存?以及哪些数据适合缓存?这是理解后续框架缓存机制的前提。
1.1 缓存的本质:内存中的"数据中转站"
简单来说,缓存就是将数据存储在内存中的临时容器。对于系统中用户频繁查询的数据,我们可以提前将其加载到缓存(内存)中。当用户再次查询该数据时,无需从磁盘上的关系型数据库文件中读取,直接从缓存中获取即可。这种"内存读取替代磁盘读取"的方式,能极大缩短数据查询时间,有效解决高并发系统的性能瓶颈。
1.2 缓存的核心价值:降开销、提效率
使用缓存的核心目的的是减少与数据库的交互次数:每一次数据库查询都伴随着磁盘IO、网络传输等开销,高频次查询会让数据库不堪重负。而缓存将高频数据驻留内存,不仅能降低数据库的负载压力,还能减少系统整体开销,让接口响应速度大幅提升。
1.3 缓存的适用场景:选对数据是关键
并非所有数据都适合放入缓存,盲目使用缓存反而可能导致数据一致性问题。最适合缓存的数据需满足两个核心条件:频繁被查询 且不常发生变更。例如:系统中的字典数据、商品分类信息、用户基础配置等。反之,高频变更的数据(如订单实时状态、用户余额)则不适合缓存,否则容易出现"缓存数据与数据库数据不一致"的问题。
二、MyBatis缓存体系:一级缓存与二级缓存的分层设计
MyBatis内置了两级缓存机制,分别是一级缓存(本地缓存)和二级缓存(全局缓存),两者在作用范围、生命周期和使用方式上存在显著差异。默认情况下,MyBatis仅开启一级缓存,二级缓存需要手动配置启用。这种分层设计既能保证单次会话的查询效率,又能支持多会话共享数据,兼顾了性能与灵活性。
2.1 一级缓存:SqlSession级别的本地缓存
一级缓存又称本地缓存,其作用范围限定在同一个SqlSession内。也就是说,在与数据库的同一次会话期间,若多次执行相同的查询操作,MyBatis会将第一次查询的结果存入一级缓存。后续的查询请求会直接从缓存中获取数据,无需再次向数据库发送SQL语句。

实战测试:验证一级缓存有效性
我们通过一段测试代码直观感受一级缓存的效果,核心逻辑是在同一个SqlSession中两次查询相同ID的用户数据,观察是否仅执行一次SQL:
java
public class UserTest {
private InputStream in = null;
private SqlSession session = null;
private UserDao mapper = null;
@Test
public void findById() throws IOException {
// 1. 加载核心配置文件,构建SqlSessionFactory
in = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 2. 创建SqlSession(同一会话)
session = factory.openSession();
mapper = session.getMapper(UserDao.class);
// 3. 第一次查询ID=1的用户
User user1 = mapper.findById(1);
System.out.println(user1.toString());
System.out.println("-----------------");
// 4. 第二次查询相同ID的用户
User user2 = mapper.findById(1);
System.out.println(user2.toString());
// 5. 验证两个对象是否为同一实例(缓存命中)
System.out.println(user1 == user2); // 输出结果:true
// 6. 释放资源
session.close();
in.close();
}
}
测试结果分析:运行代码后,控制台仅打印一次SQL查询日志,且user1与user2的地址值相同(user1 == user2为true)。这说明第二次查询并未访问数据库,而是直接复用了一级缓存中的数据,验证了一级缓存的有效性。
一级缓存失效的4种核心场景
一级缓存的有效性依赖于"同一SqlSession"和"查询条件不变"等前提,以下4种情况会导致一级缓存失效,需要重点注意:
-
SqlSession不同:一级缓存是SqlSession级别的,不同SqlSession之间的缓存相互隔离。若使用不同的SqlSession查询相同数据,会重新执行SQL并创建新的缓存。
-
查询条件不同:即使在同一个SqlSession中,若两次查询的条件不同(如第一次查ID=1,第二次查ID=2),MyBatis会认为是不同的查询请求,不会复用缓存,而是直接查询数据库。
-
两次查询间执行增删改操作:若在同一个SqlSession中,两次查询之间执行了INSERT、UPDATE、DELETE等写操作,MyBatis会自动清空一级缓存。这是因为写操作可能改变数据库数据,清空缓存能避免后续查询获取到过时的缓存数据,保证数据一致性。
-
手动清除一级缓存:通过调用SqlSession的clearCache()方法,可以手动清空当前SqlSession的一级缓存,后续查询会重新从数据库获取数据。
2.2 二级缓存:SqlSessionFactory级别的全局缓存
与一级缓存不同,二级缓存的作用范围是SqlSessionFactory级别。也就是说,通过同一个SqlSessionFactory创建的所有SqlSession,都可以共享二级缓存中的数据。当一个SqlSession关闭或提交后,其一级缓存中的数据会被写入二级缓存,供其他SqlSession复用。这种跨会话的缓存共享能力,能进一步提升系统的查询性能。
二级缓存启用:4个核心条件
二级缓存默认关闭,需要通过层层配置才能启用,核心条件有4个,缺一不可:
-
全局配置开启缓存:在MyBatis核心配置文件(SqlMapConfig.xml)中,通过settings标签设置cacheEnabled属性为true,开启全局二级缓存开关。
-
映射文件声明缓存:在对应的Mapper映射文件(如UserDao.xml)中,添加<cache/>标签,声明当前Mapper接口的方法支持二级缓存。
-
实体类实现序列化接口:二级缓存底层需要将数据序列化后存储(如写入磁盘或分布式缓存),因此查询结果对应的实体类必须实现Serializable接口。
-
SqlSession关闭或提交后生效:一级缓存中的数据只有在SqlSession关闭(close())或提交(commit())后,才会被写入二级缓存,供其他SqlSession使用。
实战配置:一步步启用二级缓存
结合上述条件,我们通过具体配置和代码演示二级缓存的启用过程:
步骤1:全局配置开启二级缓存(SqlMapConfig.xml)
XML
<configuration>
<!-- 全局参数配置:开启二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 其他配置(数据源、映射器等)省略 -->
</configuration>
步骤2:Mapper映射文件声明缓存(UserDao.xml)
XML
<mapper namespace="com.dao.UserDao">
<!-- 声明使用二级缓存 -->
<cache/>
<!-- 查询方法配置,如根据ID查询用户 -->
<select id="findById" parameterType="int" resultType="com.pojo.User">
select * from user where id = #{id}
</select>
</mapper>
步骤3:实体类实现Serializable接口(User.java)
序列化的核心作用是将对象状态转换为字节流,便于缓存存储和传输;反序列化则是将字节流恢复为对象。实现Serializable接口是二级缓存的硬性要求:
java
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
// getter、setter方法省略
// toString方法省略
}
步骤4:测试二级缓存(跨SqlSession共享数据)
java
@Test
public void testSecondLevelCache() throws IOException {
// 1. 加载配置文件,创建SqlSessionFactory(核心:同一工厂)
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 2. 创建两个不同的SqlSession
SqlSession sqlSession1 = factory.openSession();
SqlSession sqlSession2 = factory.openSession();
// 3. 第一个SqlSession查询数据
UserDao mapper1 = sqlSession1.getMapper(UserDao.class);
User user1 = mapper1.findById(1);
System.out.println(user1.toString());
// 关闭SqlSession1:将一级缓存数据写入二级缓存,同时清空一级缓存
sqlSession1.close();
System.out.println("-----------------");
// 4. 第二个SqlSession查询相同数据
UserDao mapper2 = sqlSession2.getMapper(UserDao.class);
User user2 = mapper2.findById(1);
System.out.println(user2.toString());
sqlSession2.close();
// 验证:二级缓存存储的是数据,而非对象实例
System.out.println(user1 == user2); // 输出结果:false
resourceAsStream.close();
}
二级缓存核心结论与失效场景
测试结果分析:运行代码后,控制台仅打印一次SQL查询日志,说明第二个SqlSession的查询复用了二级缓存中的数据。但需要注意的是,user1 == user2的结果为false,这揭示了二级缓存的核心特性:二级缓存存储的是数据本身,而非对象实例。当第二个SqlSession查询时,会从二级缓存中读取数据并重新创建User对象,因此两个对象的地址值不同。
二级缓存失效场景:与一级缓存类似,两次查询之间执行任意增删改操作,会导致一级缓存和二级缓存同时被清空,后续查询需重新访问数据库。这是MyBatis保障数据一致性的重要设计。
二级缓存高级配置:定制缓存策略
默认的<cache/>标签使用MyBatis的默认缓存策略,我们也可以通过标签属性定制缓存的收回策略、刷新间隔、存储容量等。常用配置参数如下:
-
eviction(收回策略):指定缓存数据的淘汰规则,默认值为LRU。
-
LRU(最近最少使用):移除最长时间未被使用的缓存对象(默认);
-
FIFO(先进先出):按对象进入缓存的顺序依次淘汰;
-
SOFT(软引用):基于JVM软引用规则淘汰对象,仅在内存不足时淘汰;
-
WEAK(弱引用):基于JVM弱引用规则淘汰对象,只要发生垃圾回收就会淘汰。
-
-
flushInterval(刷新间隔):缓存自动刷新的时间间隔,单位为毫秒。默认不设置,即仅在执行增删改操作时刷新缓存。
-
size(引用数目):缓存可存储的对象最大数量,默认值为1024。需根据服务器内存大小合理设置,避免内存溢出。
-
readOnly(只读属性):指定缓存是否为只读模式,默认值为false。
-
true(只读):所有查询者获取的是缓存对象的同一实例,性能优异,但对象不可修改(修改会导致所有查询者获取到错误数据);
-
false(读写):通过序列化返回缓存对象的拷贝,性能略差,但安全性高(修改拷贝不会影响原缓存数据),默认推荐使用。
-
定制化配置示例:
XML
<!-- 配置FIFO收回策略、60秒刷新间隔、最大存储512个对象、只读模式 -->
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
三、MyBatis缓存查询顺序:自上而下的优先级逻辑
当MyBatis执行查询操作时,会按照"二级缓存 → 一级缓存 → 数据库"的顺序查找数据,这种自上而下的优先级逻辑能最大程度复用缓存数据,减少数据库交互。具体流程如下:
-
查询请求发起后,首先访问二级缓存。由于二级缓存是全局共享的,若存在对应数据(缓存命中),则直接返回数据,无需后续操作;
-
若二级缓存未命中(无对应数据或缓存失效),则访问当前SqlSession的一级缓存;
-
若一级缓存命中,则返回数据;若一级缓存也未命中,则向数据库发送SQL查询;
-
数据库查询结果返回后,会先存入当前SqlSession的一级缓存,供本次会话后续查询复用;
-
当当前SqlSession关闭或提交时,一级缓存中的数据会被写入二级缓存,供其他SqlSession共享。
核心记忆点:二级缓存是"全局共享池",一级缓存是"会话私有池",查询时优先查全局池,再查私有池,最后查数据库。

四、总结:MyBatis缓存使用的核心要点
MyBatis的两级缓存机制是其性能优化的重要手段,掌握以下核心要点,能帮助你在实际开发中合理使用缓存:
-
一级缓存默认开启,无需配置,作用于SqlSession内部,适用于单次会话的高频查询;
-
二级缓存需手动配置(全局开关+映射声明+序列化),作用于SqlSessionFactory,适用于多会话共享高频数据的场景;
-
增删改操作会清空一、二级缓存,这是保障数据一致性的关键设计,无需手动处理;
-
二级缓存存储的是数据而非对象实例,多次查询会创建新的对象实例;
-
缓存并非"万能优化方案",仅适用于"高频查询、低频变更"的数据,避免因缓存导致的数据一致性问题。
通过合理运用MyBatis的缓存机制,能有效降低数据库压力,提升系统响应速度。在实际开发中,还可以结合Redis等分布式缓存框架扩展二级缓存的能力(如分布式系统的缓存共享),进一步优化高并发场景下的性能。