MyBatis之缓存机制详解
- 一、MyBatis缓存的基本概念
- 1.1 缓存的核心价值
- 1.2 MyBatis的两级缓存体系
- 二、一级缓存(SqlSession级别缓存)
- 2.1 工作原理
- 2.2 实战案例:一级缓存演示
- 2.2.1 基础用法(默认开启)
- 2.2.2 一级缓存失效场景
- 2.3 一级缓存的特点与适用场景
- 三、二级缓存(Mapper级别缓存)
- 3.1 工作原理
- 3.2 二级缓存的开启与配置
- 3.2.1 全局配置(可选)
- 3.2.2 Mapper接口开启缓存
- 3.2.3 实体类序列化(必须)
- 3.3 实战案例:二级缓存演示
- 3.4 二级缓存的特点与适用场景
- 四、二级缓存的高级配置
- 4.1 禁用特定查询的二级缓存
- 4.2 强制刷新二级缓存
- 4.3 整合第三方缓存(如Redis)
- 4.3.1 引入依赖(Redis+MyBatis-Redis)
- 4.3.2 配置Redis缓存
- 五、缓存使用的常见问题与避坑指南
- 5.1 一级缓存导致的脏读问题
- 5.2 二级缓存的序列化问题
- 5.3 缓存与事务的一致性问题
- 5.4 过度使用缓存导致内存溢出
缓存是提升数据库查询性能的关键技术,MyBatis内置了两级缓存机制,能有效减少重复查询的数据库交互,降低数据库压力。
一、MyBatis缓存的基本概念
1.1 缓存的核心价值
数据库查询是应用性能的常见瓶颈(磁盘IO比内存IO慢10^6倍以上),缓存通过将频繁查询的结果存储在内存中,避免重复访问数据库,从而:
- 减少数据库连接和SQL执行次数;
- 降低数据库服务器压力;
- 提升应用响应速度(从内存读取比数据库查询快100倍以上)。
1.2 MyBatis的两级缓存体系
MyBatis提供两级缓存,工作流程如下:
- 一级缓存(SqlSession级别):默认开启,缓存当前会话(SqlSession)的查询结果;
- 二级缓存(Mapper级别):需手动开启,缓存Mapper接口的查询结果,可被多个SqlSession共享。
查询数据时,MyBatis的缓存查询顺序:
二级缓存 → 一级缓存 → 数据库
即先查二级缓存,若未命中则查一级缓存,仍未命中才查询数据库。
二、一级缓存(SqlSession级别缓存)
一级缓存是MyBatis的默认缓存,绑定到SqlSession
(会话),生命周期与SqlSession
一致。
2.1 工作原理
- 缓存范围:每个
SqlSession
拥有独立的一级缓存,不同SqlSession
的缓存互不影响; - 缓存时机:
SqlSession
执行select
查询后,会将结果存入一级缓存; - 命中条件:相同的
Mapper方法
+相同的参数
+相同的SQL
; - 失效场景:
SqlSession
执行insert
/update
/delete
(会清空当前SqlSession
的一级缓存)、SqlSession
关闭或提交。
2.2 实战案例:一级缓存演示
2.2.1 基础用法(默认开启)
// 获取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(); // 关闭会话,一级缓存失效
2.2.2 一级缓存失效场景
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(不同对象,从数据库获取)
2.3 一级缓存的特点与适用场景
特点 | 说明 |
---|---|
默认开启 | 无需配置,开箱即用 |
会话隔离 | 不同SqlSession的缓存独立,避免数据冲突 |
自动管理 | 增删改自动清空缓存,保证数据一致性 |
适用场景:
- 单会话内的频繁查询(如同一请求中多次查询相同用户信息);
- 读多写少的场景(避免频繁查询数据库)。
三、二级缓存(Mapper级别缓存)
二级缓存是跨SqlSession
的全局缓存,绑定到Mapper接口
(同一Mapper的所有方法共享),需手动开启。
3.1 工作原理
- 缓存范围:同一Mapper接口的所有
SqlSession
共享二级缓存; - 缓存时机:
SqlSession
关闭(close()
)或提交(commit()
)后,一级缓存的结果会写入二级缓存; - 命中条件:相同的
Mapper接口
+相同的方法
+相同的参数
; - 失效场景:Mapper接口执行
insert
/update
/delete
(会清空当前Mapper的二级缓存)。
3.2 二级缓存的开启与配置
3.2.1 全局配置(可选)
在mybatis-config.xml
中开启二级缓存(默认已开启,可省略):
<settings><setting name="cacheEnabled" value="true"/> <!-- 全局二级缓存开关 -->
</settings>
3.2.2 Mapper接口开启缓存
在需要使用二级缓存的Mapper XML
中添加<cache>
标签:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper"><!-- 开启二级缓存 --><cache eviction="LRU" <!-- 淘汰策略:LRU(最近最少使用) -->flushInterval="60000" <!-- 自动刷新间隔(毫秒,60秒) -->size="1024" <!-- 最大缓存条目 -->readOnly="false"/> <!-- 是否只读(false:缓存对象副本) --><!-- 查询语句(默认使用二级缓存) --><select id="selectById" resultType="User">SELECT id, username, age FROM user WHERE id = #{id}</select>
</mapper>
<cache>
标签属性说明:
eviction
:缓存淘汰策略(LRU
:移除最近最少使用;FIFO
:先进先出);flushInterval
:缓存自动刷新时间(毫秒,0表示不自动刷新);size
:最大缓存数量(过多会占用内存);readOnly
:true
(返回缓存对象本身,性能好但线程不安全);false
(返回副本,安全但性能略低)。
3.2.3 实体类序列化(必须)
二级缓存可能将对象写入磁盘(如使用第三方缓存),因此实体类需实现Serializable
接口:
// 实现Serializable接口
public class User implements Serializable {private Integer id;private String username;private Integer age;// getter/setter
}
3.3 实战案例:二级缓存演示
// 第一个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(数据一致)
3.4 二级缓存的特点与适用场景
特点 | 说明 |
---|---|
手动开启 | 需要在Mapper中配置<cache> 标签 |
跨会话共享 | 同一Mapper的所有SqlSession可共享缓存 |
支持序列化 | 可配置第三方缓存(如Redis)持久化缓存 |
适用场景:
- 多会话共享的高频查询(如商品分类、字典表等不常变化的数据);
- 读多写少的场景(避免频繁更新导致缓存失效)。
四、二级缓存的高级配置
4.1 禁用特定查询的二级缓存
若某查询不需要使用二级缓存(如实时性要求高的数据),可通过useCache="false"
禁用:
<select id="selectLatestOrder" resultType="Order" useCache="false">SELECT * FROM `order` ORDER BY create_time DESC LIMIT 1
</select>
4.2 强制刷新二级缓存
若需在查询时强制刷新缓存(忽略现有缓存,重新查询数据库并更新缓存),可使用flushCache="true"
:
<select id="selectUserWithForceRefresh" resultType="User" flushCache="true">SELECT * FROM user WHERE id = #{id}
</select>
4.3 整合第三方缓存(如Redis)
MyBatis的默认二级缓存是内存缓存(重启后失效),生产环境通常整合Redis等分布式缓存,实现缓存持久化和分布式共享。
4.3.1 引入依赖(Redis+MyBatis-Redis)
<dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-redis</artifactId><version>1.0.0-beta2</version>
</dependency>
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.7.0</version>
</dependency>
4.3.2 配置Redis缓存
<!-- UserMapper.xml:指定缓存实现为Redis -->
<cache type="org.mybatis.caches.redis.RedisCache"><property name="host" value="localhost"/> <!-- Redis主机 --><property name="port" value="6379"/> <!-- Redis端口 --><property name="timeout" value="30000"/> <!-- 超时时间 --><property name="expiration" value="3600000"/> <!-- 缓存过期时间(毫秒) -->
</cache>
优势:
- 缓存持久化(应用重启后缓存不丢失);
- 分布式共享(多实例应用共享缓存);
- 支持缓存过期策略(自动清理过期数据)。
五、缓存使用的常见问题与避坑指南
5.1 一级缓存导致的脏读问题
问题:多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)
解决方案:
- 避免长生命周期的SqlSession(如Web应用中,一个请求对应一个SqlSession);
- 对实时性要求高的查询,禁用一级缓存(通过
flushCache="true"
); - 使用二级缓存(跨SqlSession共享,更新后会同步)。
5.2 二级缓存的序列化问题
错误:实体类未实现Serializable
接口,二级缓存报错NotSerializableException
。
解决方案:
- 确保所有存入二级缓存的实体类实现
Serializable
接口; - 若使用第三方缓存(如Redis),需保证对象可被序列化(如避免循环引用)。
5.3 缓存与事务的一致性问题
问题:事务未提交时,其他SqlSession可能读取到未提交的缓存数据(脏读)。
原因:
- 一级缓存在事务内生效,未提交的更新不会刷新其他SqlSession的缓存;
- 二级缓存仅在事务提交后才更新,避免此问题。
解决方案:
- 优先使用二级缓存(事务提交后才写入,保证数据一致性);
- 关键业务(如支付)避免依赖缓存,直接查询数据库。
5.4 过度使用缓存导致内存溢出
问题:缓存大量数据(如全表查询结果),导致JVM内存溢出(OOM)。
解决方案:
- 限制缓存大小(
size
属性,如size="1024"
); - 设置缓存过期时间(
flushInterval
); - 避免缓存大集合(如分页查询,只缓存当前页数据);
- 使用分布式缓存(如Redis),利用外部内存存储缓存。
总结:缓存机制的最佳实践
MyBatis的两级缓存各有适用场景,合理使用能显著提升性能,核心实践原则
:
- 一级缓存:
- 无需额外配置,充分利用其默认特性;
- 注意SqlSession的生命周期(一个请求一个SqlSession),避免脏读;
- 增删改操作后,一级缓存会自动清空,无需手动处理。
- 二级缓存:
- 仅对“读多写少、实时性要求低”的数据开启(如字典表、商品分类);
- 必须实现实体类序列化,避免缓存失败;
- 生产环境推荐整合Redis等分布式缓存,支持持久化和分布式共享;
- 对实时性要求高的查询(如订单状态),禁用二级缓存。
- 通用原则:
- 缓存粒度越小越好(优先缓存单条数据,而非全表);
- 避免缓存大对象和频繁变化的数据;
- 结合监控工具(如MyBatis Log)分析缓存命中率,优化缓存策略。
若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ