MyBatis 缓存机制详解:提升性能的关键技术

一、缓存概述:为什么缓存对 MyBatis 如此重要?

在数据库操作中,查询操作通常占比最高,而频繁的数据库访问会导致:

  • 数据库压力增大
  • 网络传输开销增加
  • 应用响应速度降低

缓存作为解决这些问题的关键技术,通过将频繁访问的数据存储在内存中,减少对数据库的直接访问,从而显著提升应用性能。

MyBatis 提供了完善的缓存机制,分为:

  • 一级缓存:SqlSession 级别的缓存(默认开启)
  • 二级缓存:Mapper 级别的缓存(需要手动开启)

本文将深入解析 MyBatis 缓存的工作原理、使用方式和最佳实践,帮助你在实际开发中合理利用缓存提升系统性能。

二、一级缓存:SqlSession 级别的缓存

一级缓存是 MyBatis 默认开启的缓存机制,其作用域为单个 SqlSession。

2.1 一级缓存工作原理

  1. 当通过 SqlSession 执行查询时,MyBatis 会将查询结果存储在一级缓存中
  2. 同一 SqlSession 内的相同查询(相同 SQL 和参数)会直接从缓存获取结果
  3. 当执行增删改操作或关闭 SqlSession 时,一级缓存会被清空

2.2 一级缓存演示

java

运行

@Test
public void testFirstLevelCache() {
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        
        // 第一次查询,从数据库获取
        User user1 = mapper.getUserById(1);
        System.out.println("第一次查询:" + user1);
        
        // 第二次查询,从缓存获取
        User user2 = mapper.getUserById(1);
        System.out.println("第二次查询:" + user2);
        
        // 比较两个对象是否相同(内存地址相同)
        System.out.println("两个对象是否相同:" + (user1 == user2)); // true
    }
}

日志输出

plaintext

第一次查询执行SQL:SELECT * FROM user WHERE id = ?
第一次查询:User{id=1, username='张三', ...}
第二次查询:User{id=1, username='张三', ...}
两个对象是否相同:true

2.3 一级缓存失效场景

  1. 执行增删改操作

java

运行

@Test
public void testCacheInvalidationWithCRUD() {
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        
        User user1 = mapper.getUserById(1);
        System.out.println("第一次查询:" + user1);
        
        // 执行更新操作
        User updateUser = new User();
        updateUser.setId(1);
        updateUser.setAge(21);
        mapper.updateUserSelective(updateUser);
        
        // 缓存已失效,重新查询数据库
        User user2 = mapper.getUserById(1);
        System.out.println("第二次查询:" + user2);
        System.out.println("两个对象是否相同:" + (user1 == user2)); // false
    }
}
  1. 手动清空缓存

java

运行

// 清空一级缓存
sqlSession.clearCache();
  1. 使用不同的 SqlSession

java

运行

@Test
public void testDifferentSqlSession() {
    // 第一个SqlSession
    try (SqlSession sqlSession1 = MyBatisUtils.getSqlSession()) {
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        User user1 = mapper1.getUserById(1);
        System.out.println("SqlSession1查询:" + user1);
    }
    
    // 第二个SqlSession(与第一个不同)
    try (SqlSession sqlSession2 = MyBatisUtils.getSqlSession()) {
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = mapper2.getUserById(1); // 重新查询数据库
        System.out.println("SqlSession2查询:" + user2);
    }
}
  1. 查询参数或 SQL 不同

java

运行

// 不同参数会导致缓存不命中
User user1 = mapper.getUserById(1);
User user2 = mapper.getUserById(2); // 不同参数,不使用缓存

2.4 一级缓存配置

一级缓存默认开启,且无法关闭,但可以通过以下方式间接控制:

xml

<settings>
    <!-- 本地缓存范围:SESSION(默认)| STATEMENT -->
    <setting name="localCacheScope" value="SESSION"/>
</settings>
  • SESSION:缓存对整个 SqlSession 有效(默认)
  • STATEMENT:缓存仅对当前语句有效,执行完即清空

三、二级缓存:Mapper 级别的缓存

二级缓存是 Mapper 接口级别的缓存,作用域跨越多个 SqlSession,需要手动开启。

3.1 二级缓存工作原理

  1. 二级缓存与 Mapper 接口绑定,不同 Mapper 的缓存相互独立
  2. 当 SqlSession 关闭时,其一级缓存中的数据会被写入二级缓存
  3. 新的 SqlSession 查询时,会先检查二级缓存,再检查一级缓存

3.2 二级缓存开启步骤

  1. 全局配置开启二级缓存

xml

<settings>
    <!-- 开启二级缓存(默认就是true,可省略) -->
    <setting name="cacheEnabled" value="true"/>
</settings>
  1. 在 Mapper 映射文件中配置缓存

xml

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存 -->
    <cache/>
    
    <!-- 映射语句... -->
</mapper>
  1. 实体类实现序列化接口

java

运行

public class User implements Serializable {
    // 确保所有属性都可序列化
    private static final long serialVersionUID = 1L;
    // ...其他代码
}

3.3 二级缓存演示

java

运行

@Test
public void testSecondLevelCache() {
    // 第一个SqlSession
    try (SqlSession sqlSession1 = MyBatisUtils.getSqlSession()) {
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        User user1 = mapper1.getUserById(1);
        System.out.println("SqlSession1查询:" + user1);
        // 关闭SqlSession时,数据写入二级缓存
    }
    
    // 第二个SqlSession
    try (SqlSession sqlSession2 = MyBatisUtils.getSqlSession()) {
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = mapper2.getUserById(1); // 从二级缓存获取
        System.out.println("SqlSession2查询:" + user2);
    }
}

日志输出

plaintext

SqlSession1执行SQL:SELECT * FROM user WHERE id = ?
SqlSession1查询:User{id=1, username='张三', ...}
SqlSession2查询:User{id=1, username='张三', ...} // 无SQL执行

3.4 二级缓存配置详解

cache标签支持多种属性配置:

xml

<cache 
    eviction="LRU"    // 缓存回收策略
    flushInterval="60000" // 刷新间隔(毫秒)
    size="1024"       // 最大缓存对象数
    readOnly="false"  // 是否只读
    blocking="false"/> // 是否阻塞

缓存回收策略(eviction)

  • LRU(默认):最近最少使用,移除最长时间未使用的对象
  • FIFO:先进先出,按对象进入缓存的顺序移除
  • SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
  • WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象

其他属性

  • flushInterval:缓存自动刷新时间,默认不设置(即不自动刷新)
  • size:缓存最多可存储的对象数量,默认 1024
  • readOnly
    • true:返回缓存对象的只读引用,性能好但不安全
    • false:返回缓存对象的拷贝(序列化实现),性能较差但安全
  • blocking:当缓存中没有请求的对象时,是否阻塞等待其他线程加载

3.5 控制语句对缓存的影响

可以通过useCacheflushCache属性控制单个语句对缓存的影响:

xml

<!-- 查询语句配置 -->
<select id="getUserById" resultType="User" 
        useCache="true"       // 是否使用二级缓存
        flushCache="false">   // 是否执行后清空二级缓存
    SELECT * FROM user WHERE id = #{id}
</select>

<!-- 增删改语句默认flushCache="true" -->
<update id="updateUser" flushCache="true">
    UPDATE user SET ...
</update>

默认行为

  • select语句:useCache=trueflushCache=false
  • insert/update/delete语句:flushCache=true(执行后清空一、二级缓存)

四、缓存失效与一致性问题

缓存虽然能提升性能,但也带来了数据一致性问题,需要特别注意。

4.1 缓存失效的常见原因

  1. 同一 Mapper 的增删改操作:会自动清空该 Mapper 的二级缓存
  2. 不同 Mapper 操作同一张表:可能导致缓存不一致
  3. 缓存配置不当:如刷新间隔设置不合理
  4. 分布式环境下的缓存同步问题:多节点修改数据后缓存无法同步

4.2 保证缓存一致性的策略

  1. 合理设置缓存刷新策略

    • 对频繁修改的数据减少缓存时间
    • 关键业务数据可禁用缓存
  2. 手动控制缓存刷新

java

运行

// 修改数据后手动清除相关缓存
sqlSession.update("updateUser", user);
sqlSession.commit();
// 清除指定Mapper的缓存
sqlSession.clearCache(); // 清除一级缓存
sqlSession.getMapper(UserMapper.class).clearCache(); // 自定义方法清除二级缓存
  1. 使用第三方缓存框架

    • 集成 Redis、Ehcache 等分布式缓存
    • 利用缓存中间件的过期策略和发布订阅功能
  2. 缓存粒度控制

    • 避免缓存过大的对象集合
    • 优先缓存热点数据和变化频率低的数据

五、集成第三方缓存:以 Redis 为例

MyBatis 的二级缓存默认使用本地缓存,在分布式环境下存在局限。集成 Redis 可以实现分布式缓存。

5.1 添加依赖

xml

<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>

5.2 配置 Redis 缓存

src/main/resources下创建redis.properties

properties

redis.host=localhost
redis.port=6379
redis.password=
redis.timeout=2000
redis.database=0
redis.clientName=mybatis-redis-cache

5.3 在 Mapper 中使用 Redis 缓存

xml

<mapper namespace="com.example.mapper.UserMapper">
    <!-- 使用Redis作为二级缓存 -->
    <cache type="org.mybatis.caches.redis.RedisCache">
        <property name="eviction" value="LRU"/>
        <property name="timeout" value="300000"/> <!-- 5分钟过期 -->
    </cache>
    <!-- ...其他映射 -->
</mapper>

六、缓存使用最佳实践

  1. 合理选择缓存级别

    • 高频访问、低频修改的数据:使用二级缓存
    • 一次会话内多次访问的数据:依赖一级缓存
    • 实时性要求高的数据:禁用缓存
  2. 控制缓存粒度

    • 优先缓存单条记录而非大集合
    • 避免缓存包含大量数据的结果集
  3. 设置合理的缓存过期时间

    • 热点静态数据:可设置较长过期时间
    • 频繁变化数据:设置较短过期时间或不缓存
  4. 分布式环境必须使用分布式缓存

    • 避免使用本地缓存导致的数据不一致
    • 推荐使用 Redis 作为分布式缓存解决方案
  5. 缓存监控与调优

    • 监控缓存命中率
    • 根据业务变化调整缓存策略
    • 定期清理无效缓存
  6. 避免缓存穿透和雪崩

    • 缓存空结果,防止缓存穿透
    • 设置随机过期时间,避免缓存雪崩
    • 实现缓存降级和熔断机制

七、常见问题与解决方案

  1. 二级缓存不生效

    • 检查cacheEnabled是否开启
    • 确认 Mapper 文件是否配置了<cache>标签
    • 验证实体类是否实现了Serializable接口
    • 检查 SqlSession 是否正常关闭(数据需在关闭时写入二级缓存)
  2. 缓存数据不一致

    • 确保增删改操作后缓存被正确清空
    • 避免不同 Mapper 操作同一张表
    • 合理设置缓存过期时间
  3. 缓存占用内存过大

    • 调整size属性限制缓存对象数量
    • 选择合适的缓存回收策略
    • 减少大对象缓存
  4. 分布式缓存同步问题

    • 改用 Redis 等分布式缓存
    • 实现缓存更新的发布订阅机制
    • 考虑使用 Canal 监听数据库变更同步缓存

总结

MyBatis 的缓存机制是提升应用性能的重要手段,一级缓存默认开启,适用于单会话内的数据复用;二级缓存需要手动开启,适用于多会话共享数据。合理使用缓存可以显著减少数据库访问,提高应用响应速度,但同时也需要注意数据一致性问题。

在实际开发中,应根据业务特点选择合适的缓存策略,对于分布式系统,建议集成 Redis 等分布式缓存框架。通过监控缓存命中率和持续调优,可以充分发挥缓存的性能优势,构建高效、稳定的应用系统。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值