缓存是提升数据库查询性能的关键技术,MyBatis内置了两级缓存机制,能有效减少重复查询的数据库交互,降低数据库压力。
数据库查询是应用性能的常见瓶颈(磁盘IO比内存IO慢10^6倍以上),缓存通过将频繁查询的结果存储在内存中,避免重复访问数据库,从而:
MyBatis提供两级缓存,工作流程如下:
查询数据时,MyBatis的缓存查询顺序:
二级缓存 → 一级缓存 → 数据库
即先查二级缓存,若未命中则查一级缓存,仍未命中才查询数据库。
一级缓存是MyBatis的默认缓存,绑定到SqlSession
(会话),生命周期与SqlSession
一致。
SqlSession
拥有独立的一级缓存,不同SqlSession
的缓存互不影响;SqlSession
执行select
查询后,会将结果存入一级缓存;Mapper方法
+相同的参数
+相同的SQL
;SqlSession
执行insert
/update
/delete
(会清空当前SqlSession
的一级缓存)、SqlSession
关闭或提交。// 获取SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询(未命中缓存,查询数据库)
User user1 = userMapper.selectById(1);
// 第二次查询(相同SqlSession+相同参数,命中一级缓存,不查数据库)
User user2 = userMapper.selectById(1);
System.out.println(user1 == user2); // true(同一对象,从缓存获取)
sqlSession.close(); // 关闭会话,一级缓存失效
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user1 = userMapper.selectById(1);
// 执行更新操作(insert/update/delete),清空一级缓存
userMapper.updateAge(1, 25);
sqlSession.commit(); // 提交事务(触发缓存清空)
// 再次查询(缓存已清空,重新查询数据库)
User user2 = userMapper.selectById(1);
System.out.println(user1 == user2); // false(不同对象,从数据库获取)
特点 | 说明 |
---|---|
默认开启 | 无需配置,开箱即用 |
会话隔离 | 不同SqlSession的缓存独立,避免数据冲突 |
自动管理 | 增删改自动清空缓存,保证数据一致性 |
适用场景:
二级缓存是跨SqlSession
的全局缓存,绑定到Mapper接口
(同一Mapper的所有方法共享),需手动开启。
SqlSession
共享二级缓存;SqlSession
关闭(close()
)或提交(commit()
)后,一级缓存的结果会写入二级缓存;Mapper接口
+相同的方法
+相同的参数
;insert
/update
/delete
(会清空当前Mapper的二级缓存)。在mybatis-config.xml
中开启二级缓存(默认已开启,可省略):
<settings>
<setting name="cacheEnabled" value="true"/>
settings>
在需要使用二级缓存的Mapper XML
中添加
标签:
<mapper namespace="com.example.mapper.UserMapper">
<cache
eviction="LRU"
size="1024"
readOnly="false"/>
<select id="selectById" resultType="User">
SELECT id, username, age FROM user WHERE id = #{id}
select>
mapper>
标签属性说明:
eviction
:缓存淘汰策略(LRU
:移除最近最少使用;FIFO
:先进先出);flushInterval
:缓存自动刷新时间(毫秒,0表示不自动刷新);size
:最大缓存数量(过多会占用内存);readOnly
:true
(返回缓存对象本身,性能好但线程不安全);false
(返回副本,安全但性能略低)。二级缓存可能将对象写入磁盘(如使用第三方缓存),因此实体类需实现Serializable
接口:
// 实现Serializable接口
public class User implements Serializable {
private Integer id;
private String username;
private Integer age;
// getter/setter
}
// 第一个SqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.selectById(1);
sqlSession1.close(); // 关闭会话,将一级缓存写入二级缓存
// 第二个SqlSession
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 命中二级缓存(无需查询数据库)
User user2 = userMapper2.selectById(1);
sqlSession2.close();
System.out.println(user1 == user2); // false(二级缓存返回副本,readOnly=false时)
System.out.println(user1.getId().equals(user2.getId())); // true(数据一致)
特点 | 说明 |
---|---|
手动开启 | 需要在Mapper中配置 标签 |
跨会话共享 | 同一Mapper的所有SqlSession可共享缓存 |
支持序列化 | 可配置第三方缓存(如Redis)持久化缓存 |
适用场景:
若某查询不需要使用二级缓存(如实时性要求高的数据),可通过useCache="false"
禁用:
<select id="selectLatestOrder" resultType="Order" useCache="false">
SELECT * FROM `order` ORDER BY create_time DESC LIMIT 1
select>
若需在查询时强制刷新缓存(忽略现有缓存,重新查询数据库并更新缓存),可使用flushCache="true"
:
<select id="selectUserWithForceRefresh" resultType="User" flushCache="true">
SELECT * FROM user WHERE id = #{id}
select>
MyBatis的默认二级缓存是内存缓存(重启后失效),生产环境通常整合Redis等分布式缓存,实现缓存持久化和分布式共享。
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-redisartifactId>
<version>1.0.0-beta2version>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>3.7.0version>
dependency>
<cache type="org.mybatis.caches.redis.RedisCache">
<property name="host" value="localhost"/>
<property name="port" value="6379"/>
<property name="timeout" value="30000"/>
<property name="expiration" value="3600000"/>
cache>
优势:
问题:多SqlSession场景下,一级缓存可能读取到旧数据。
// SqlSession1查询数据并缓存
SqlSession sqlSession1 = sqlSessionFactory.openSession();
User user1 = sqlSession1.getMapper(UserMapper.class).selectById(1);
// SqlSession2更新数据并提交
SqlSession sqlSession2 = sqlSessionFactory.openSession();
sqlSession2.getMapper(UserMapper.class).updateAge(1, 26);
sqlSession2.commit();
sqlSession2.close();
// SqlSession1再次查询(一级缓存未更新,读取到旧数据)
User user2 = sqlSession1.getMapper(UserMapper.class).selectById(1);
System.out.println(user2.getAge()); // 25(旧值,而非更新后的26)
解决方案:
flushCache="true"
);错误:实体类未实现Serializable
接口,二级缓存报错NotSerializableException
。
解决方案:
Serializable
接口;问题:事务未提交时,其他SqlSession可能读取到未提交的缓存数据(脏读)。
原因:
解决方案:
问题:缓存大量数据(如全表查询结果),导致JVM内存溢出(OOM)。
解决方案:
size
属性,如size="1024"
);flushInterval
);总结:缓存机制的最佳实践
MyBatis的两级缓存各有适用场景,合理使用能显著提升性能,核心实践原则
:
- 一级缓存:
- 无需额外配置,充分利用其默认特性;
- 注意SqlSession的生命周期(一个请求一个SqlSession),避免脏读;
- 增删改操作后,一级缓存会自动清空,无需手动处理。
- 二级缓存:
- 仅对“读多写少、实时性要求低”的数据开启(如字典表、商品分类);
- 必须实现实体类序列化,避免缓存失败;
- 生产环境推荐整合Redis等分布式缓存,支持持久化和分布式共享;
- 对实时性要求高的查询(如订单状态),禁用二级缓存。
- 通用原则:
- 缓存粒度越小越好(优先缓存单条数据,而非全表);
- 避免缓存大对象和频繁变化的数据;
- 结合监控工具(如MyBatis Log)分析缓存命中率,优化缓存策略。
若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ