绍一下MyBatis的缓存机制

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 和参数完全一致
最佳实践建议
  1. 明确会话边界:在事务中完成关联操作,避免跨会话数据不一致。

  2. 避免长会话:及时关闭SqlSession,防止缓存堆积导致内存溢出。

  3. 慎用大对象缓存:返回List<Entity>时注意数据量,避免 OOM。

  4. 调试技巧:启用 MyBatis 日志(log4j.logger.org.apache.ibatis=DEBUG)观察缓存命中情况

三、二级缓存全面剖析

1. 启用条件
  1. 全局开关:在mybatis-config.xml中配置:

    <settings>
      <setting name="cacheEnabled" value="true"/> <!-- 默认true,可不显式配置 -->
    </settings>
  2. Mapper级启用:在Mapper XML中添加<cache/>标签。

  3. 实体类序列化:返回的POJO需实现Serializable接口。

2. 工作流程
  1. 查询时优先访问二级缓存。

  2. 未命中则查一级缓存,仍未命中才查数据库。

  3. 数据写入时机: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清空缓存粒度太粗易误伤
分页列表设置合理 sizeeviction避免大结果集撑爆内存
防恶意攻击开启 cacheNullValue="true"结合参数校验拦截非法请求
分布式环境集成 Redis 等分布式缓存避免节点间数据不一致

调试技巧: 启用 MyBatis 日志(log4j.logger.org.apache.ibatis.cache=DEBUG),观察 Cache Hit Ratio

永远记住:二级缓存是 “空间换时间” 的权衡,用对场景是利器,用错则成隐患。

配置方案

四、生产实践注意事项

1. 缓存适用场景
场景推荐策略原因
单次请求内重复查询依赖一级缓存减少同会话内重复查询
跨请求的只读数据(如配置)启用二级缓存避免频繁访问静态数据
高频修改数据(如订单状态)禁用缓存避免脏读风险
分布式环境集成Redis/Ehcache替代默认二级缓存,解决跨节点数据一致性问题
2. 常见问题与规避方案
  • 脏读风险:多表关联查询时,关联表更新可能导致缓存数据不一致。

    → 规避:避免多表联查使用二级缓存,或通过<cache-ref>共享缓存空间(需谨慎)。

  • 缓存穿透: 频繁查询不存在的数据(如恶意攻击)。 → ​​规避​​:缓存空对象或使用布隆过滤器。

  • 分布式一致性:默认二级缓存无法跨JVM同步。

    → 规避:改用RedisCache等分布式缓存实现。

总结

  • 一级缓存:会话级“临时缓存”,自动开启,适合优化同会话内重复查询。

  • 二级缓存:跨会话“共享缓存”,需显式配置,适合低频变更的全局数据(如系统配置)。

  • 选型关键:根据数据更新频率、一致性要求及架构复杂度权衡缓存策略,分布式环境优先考虑Redis集成方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tsxchen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值