MyBatis的缓存机制是其提升数据库查询性能的核心设计,通过一级缓存和二级缓存两级结构减少数据库访问压力。以下结合其工作原理、配置方式及生产实践进行系统解析:
一、缓存架构概述
MyBatis采用分层缓存设计,形成“应用层-数据库层”的缓冲体系:
-
一级缓存(本地缓存):SqlSession级别,默认开启,生命周期与会话绑定。
-
二级缓存(全局缓存):Mapper/Namespace级别,需手动开启,支持跨会话共享。
缓存查询顺序:二级缓存 → 一级缓存 → 数据库,优先从高层缓存获取数据。
二、一级缓存深度解析
1. 核心特性
-
作用域:仅限同一SqlSession内,不同会话缓存隔离。
-
存储结构:基于PerpetualCache(HashMap实现),键由SQL语句、参数、分页等生成。
-
生命周期:随SqlSession创建而创建,关闭或提交时销毁。
2. 缓存生效与失效场景
| 场景 | 是否命中缓存 | 说明 |
|---|---|---|
| 同会话重复相同查询 | ✅ | 直接返回缓存结果,不访问数据库 |
执行INSERT/UPDATE/DELETE | ❌ | 清空当前会话所有缓存(无论是否提交事务) |
手动调用clearCache() | ❌ | 主动清空缓存 |
| 查询参数或SQL不同 | ❌ | 缓存键不匹配 |
3. Spring整合下的特殊行为
-
非事务环境:每次操作新建SqlSession,一级缓存无效。
-
事务环境:同一事务绑定同一SqlSession,缓存生效。
4. 案例结合讲解
4.1基础案例:同会话重复查询的缓存生效
场景描述
用户管理模块中,同一个请求内多次查询同一用户信息(如根据 ID 查用户)。
public void getUserById(int userId) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询:访问数据库
User user1 = mapper.getUserById(userId); // 输出 SQL 日志
// 第二次查询:命中一级缓存(无 SQL 日志)
User user2 = mapper.getUserById(userId);
System.out.println(user1 == user2); // true(同一对象引用)
}
}
对照知识点
-
缓存存储结构:一级缓存使用 HashMap存储,Key 由SQL语句 + 参数 + 分页参数哈希生成。
-
缓存生效条件:相同 SQL、相同参数、同一SqlSession。
-
对象引用一致:返回的是缓存对象的引用,因此user1和user2指向同一内存地址。
💡 开发者常见误区:误以为返回的是数据副本,实际是同一对象。若修改
user1的属性,user2也会同步变化!
4.2缓存失效案例:更新操作清除缓存
场景描述
查询用户信息后,更新该用户数据,再次查询需获取最新结果。
public void updateAndGetUser(int userId) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUserById(userId); // 查数据库
// 更新用户信息
user1.setName("张三");
mapper.updateUser(user1);
sqlSession.commit(); // 提交事务
// 再次查询:缓存已清空,重新查数据库
User user2 = mapper.getUserById(userId); // 输出 SQL 日志
}
}
对照知识点
-
失效触发条件:执行INSERT/UPDATE/DELETE或调用sqlSession.commit()/rollback()会清空当前会话所有缓存。
-
设计意图:避免脏读,确保数据一致性。
-
特殊注意:即使更新操作失败(如 SQL 报错),只要调用commit(),缓存仍会被清除。
✅ 正确实践:事务提交后,若需继续操作,应重新查询获取最新数据。
4.3缓存隔离案例:多会话间缓存不共享
场景描述
多线程环境下,线程 A 和线程 B 分别查询同一用户信息。
public void multiSessionQuery(int userId) {
// 会话 1
try (SqlSession session1 = sqlSessionFactory.openSession()) {
User user1 = session1.getMapper(UserMapper.class).getUserById(userId); // 输出 SQL
}
// 会话 2(新建 SqlSession)
try (SqlSession session2 = sqlSessionFactory.openSession()) {
User user2 = session2.getMapper(UserMapper.class).getUserById(userId); // 再次输出 SQL
}
}
对照知识点
-
作用域隔离:一级缓存绑定SqlSession实例,不同会话缓存互不干扰。
-
生命周期:SqlSession关闭 → 缓存销毁。
-
并发安全:每个线程应使用独立SqlSession,避免并发冲突。
⚠️ 风险提示:若在 Spring 中未配置事务,每次 DAO 调用会创建新
SqlSession,导致一级缓存失效(需通过@Transactional绑定会话)。
4.4Spring 整合案例:事务对缓存的影响
场景描述
在 Spring 管理的服务层方法中,多次调用同一查询。
@Service
public class UserService {
@Autowired private UserMapper userMapper;
@Transactional // 关键:声明事务
public void testCacheInSpring() {
User user1 = userMapper.getUserById(1); // 查数据库
User user2 = userMapper.getUserById(1); // 命中缓存(无 SQL)
}
// 无事务时,每次调用新建 SqlSession
public void testWithoutTransaction() {
User user1 = userMapper.getUserById(1); // 查数据库
User user2 = userMapper.getUserById(1); // 再次查数据库(缓存失效)
}
}
对照知识点
-
事务绑定会话:Spring 的 @Transactional会在事务内复用同一SqlSession。
-
无事务场景:默认每次 DAO 调用新建 SqlSession,缓存无效。
-
性能优化点:事务范围过大可能导致缓存堆积,建议事务粒度控制在必要操作内。
总结:一级缓存使用要点与避坑指南
| 场景 | 缓存是否生效 | 原因与解决方案 |
|---|---|---|
| 同会话重复查询 | ✅ | 利用缓存减少 DB 访问 |
| 会话中执行增删改 | ❌ | 提交后清空缓存,需重新查询 |
| 跨线程/跨请求查询 | ❌ | 不同会话隔离,考虑用二级缓存或 Redis |
| Spring 无事务方法 | ❌ | 通过 @Transactional 绑定会话 |
| 查询参数或 SQL 变化 | ❌ | 确保 SQL 和参数完全一致 |
最佳实践建议:
-
明确会话边界:在事务中完成关联操作,避免跨会话数据不一致。
-
避免长会话:及时关闭SqlSession,防止缓存堆积导致内存溢出。
-
慎用大对象缓存:返回List<Entity>时注意数据量,避免 OOM。
-
调试技巧:启用 MyBatis 日志(
log4j.logger.org.apache.ibatis=DEBUG)观察缓存命中情况
三、二级缓存全面剖析
1. 启用条件
-
全局开关:在mybatis-config.xml中配置:
<settings> <setting name="cacheEnabled" value="true"/> <!-- 默认true,可不显式配置 --> </settings> -
Mapper级启用:在Mapper XML中添加<cache/>标签。
-
实体类序列化:返回的POJO需实现Serializable接口。
2. 工作流程
-
查询时优先访问二级缓存。
-
未命中则查一级缓存,仍未命中才查数据库。
-
数据写入时机:SqlSession关闭或提交后,一级缓存数据同步至二级缓存
3. 高级配置选项
<cache
eviction="LRU" <!-- 回收策略(LRU/FIFO/SOFT/WEAK) -->
flushInterval="60000" <!-- 自动刷新间隔(毫秒) -->
size="1024" <!-- 缓存对象最大数量 -->
readOnly="true" <!-- 是否只读(默认false,安全但性能略低) -->
/>
策略说明:
LRU(默认):移除最久未使用的对象
FIFO:按进入顺序移除readOnly="true":返回缓存实例本身,性能高但需确保对象不被修改。
4. 失效场景
-
执行同Namespace下的增删改操作(清空该命名空间所有缓存)。
-
配置
flushInterval超时或size超限触发回收。
5. 案例结合讲解
5.1跨会话共享数据:配置中心的优化案例
场景描述
系统配置表(如 sys_config)数据极少变更,但几乎所有业务模块启动时都需加载。频繁查询数据库会导致性能瓶颈。
<!-- 开启二级缓存并设置1小时刷新 -->
<mapper namespace="com.example.ConfigMapper">
<cache eviction="LRU" flushInterval="3600000" readOnly="true"/>
<select id="getConfigByKey" resultType="Config">
SELECT * FROM sys_config WHERE key = #{key}
</select>
</mapper>
// 服务A的查询
SqlSession sessionA = factory.openSession();
Config configA = sessionA.getMapper(ConfigMapper.class).getConfigByKey("timeout");
sessionA.close(); // 关闭会话,数据写入二级缓存
// 服务B的查询(不同线程)
SqlSession sessionB = factory.openSession();
Config configB = sessionB.getMapper(ConfigMapper.class).getConfigByKey("timeout"); // ✅ 命中二级缓存
对照知识点
-
共享原理:二级缓存绑定 namespace(此处为ConfigMapper)
-
生效条件:SqlSession关闭或提交后,数据才从一级缓存同步到二级缓存
-
只读优势:readOnly="true"避免拷贝对象,提升性能(适合配置类数据)
💡 避坑提醒:若此处忘记关闭
sessionA,则sessionB无法命中缓存!
5.2缓存失效:订单更新引发的数据不一致
场景描述
用户修改订单状态后,其他模块查询订单仍返回旧数据,因二级缓存未及时清除。
// 订单查询
Order order1 = orderMapper.getOrder(1001); // 首次查询,数据存入二级缓存
// 更新订单状态(同一Mapper)
orderMapper.updateOrderStatus(1001, "SHIPPED");
sqlSession.commit(); // 提交事务
// 再次查询
Order order2 = orderMapper.getOrder(1001); // ❌ 缓存已清空,重新查库
对照知识点
-
失效触发:同namespace下的增删改操作会清空整个二级缓存(如OrderMapper下的update清空所有Order查询缓存)
-
设计缺陷:粒度太粗!即使修改订单1001,也会清空其他订单的缓存
-
一致性风险:若其他系统直接改库,MyBatis 无法感知,导致脏读
✅ 解决方案:高频更新业务(如订单)禁用二级缓存,或用 Redis 等支持细粒度失效的缓存
5.3缓存穿透:攻击导致的性能雪崩
场景描述
接口被恶意攻击,频繁请求不存在的数据(如 user_id = -1),每次均穿透缓存查询数据库。
代码与修复
<!-- 原配置(存在穿透风险) -->
<select id="getUserById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 修复方案:缓存空对象 -->
<cache>
<property name="cacheNullValue" value="true"/> <!-- 开启空值缓存 -->
</cache>
对照知识点
-
穿透原因:MyBatis 默认不缓存null结果,导致恶意请求反复查库
-
解决机制:cacheNullValue="true"会将null也缓存,后续请求直接返回
-
额外防御:对非法参数(如id<=0)在服务层校验拦截
5.4参数调优:电商商品列表的缓存优化
场景描述
商品列表页(ProductMapper)访问频繁,但数据量较大需防止内存溢出。
<mapper namespace="com.example.ProductMapper">
<!-- 限制缓存500个对象,30分钟刷新,LRU淘汰策略 -->
<cache eviction="LRU" flushInterval="1800000" size="500"/>
<select id="listHotProducts" resultType="Product" useCache="true">
SELECT * FROM product WHERE is_hot = 1 <!-- 高频访问 -->
</select>
<select id="getProductDetail" resultType="ProductDetail" useCache="false">
<!-- 商品详情页更新频繁,禁用缓存 -->
</select>
</mapper>
对照知识点
-
容量控制:size="500"限制缓存对象数量,避免内存溢出(尤其大对象)
-
策略选择:eviction="LRU"优先淘汰冷门商品数据
-
按需启用:useCache="false"对实时性要求高的查询禁用缓存
总结:二级缓存使用速查表
| 场景 | 推荐方案 | 核心注意事项 |
|---|---|---|
| 静态数据(配置/字典) | 开启缓存 + readOnly="true" | 提交事务后缓存才生效 |
| 高频更新业务(订单/库存) | 禁用缓存 或 用 Redis | 清空缓存粒度太粗易误伤 |
| 分页列表 | 设置合理 size 和 eviction | 避免大结果集撑爆内存 |
| 防恶意攻击 | 开启 cacheNullValue="true" | 结合参数校验拦截非法请求 |
| 分布式环境 | 集成 Redis 等分布式缓存 | 避免节点间数据不一致 |
调试技巧: 启用 MyBatis 日志(
log4j.logger.org.apache.ibatis.cache=DEBUG),观察Cache Hit Ratio永远记住:二级缓存是 “空间换时间” 的权衡,用对场景是利器,用错则成隐患。
配置方案
四、生产实践注意事项
1. 缓存适用场景
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 单次请求内重复查询 | 依赖一级缓存 | 减少同会话内重复查询 |
| 跨请求的只读数据(如配置) | 启用二级缓存 | 避免频繁访问静态数据 |
| 高频修改数据(如订单状态) | 禁用缓存 | 避免脏读风险 |
| 分布式环境 | 集成Redis/Ehcache | 替代默认二级缓存,解决跨节点数据一致性问题 |
2. 常见问题与规避方案
-
脏读风险:多表关联查询时,关联表更新可能导致缓存数据不一致。
→ 规避:避免多表联查使用二级缓存,或通过<cache-ref>共享缓存空间(需谨慎)。
-
缓存穿透: 频繁查询不存在的数据(如恶意攻击)。 → 规避:缓存空对象或使用布隆过滤器。
-
分布式一致性:默认二级缓存无法跨JVM同步。
→ 规避:改用RedisCache等分布式缓存实现。
总结
-
一级缓存:会话级“临时缓存”,自动开启,适合优化同会话内重复查询。
-
二级缓存:跨会话“共享缓存”,需显式配置,适合低频变更的全局数据(如系统配置)。
-
选型关键:根据数据更新频率、一致性要求及架构复杂度权衡缓存策略,分布式环境优先考虑
Redis集成方案

3万+

被折叠的 条评论
为什么被折叠?



