基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

MyBatis的二级缓存是如何工作的?

知识点图片

MyBatis二级缓存是Mapper级别的共享缓存,跨SqlSession生效。它在会话关闭或提交后将数据存入,适合读多写少场景,但需注意分布式环境下的一致性问题。

我们来详细、清晰地剖析一下MyBatis的二级缓存是如何工作的。

一、 什么是MyBatis二级缓存?

首先,要理解二级缓存,我们需要先对比一下一级缓存。

  • 一级缓存(Local Cache)

    • 作用域SqlSession 级别。它是默认开启且无法关闭的。
    • 生命周期:与 SqlSession 的生命周期相同。当 SqlSession 被创建时,一个新的一级缓存被创建;当 SqlSession 被关闭或清空时,它里面的一级缓存也随之销毁。
    • 工作方式:在同一个 SqlSession 中,执行完全相同的SQL查询(相同的Statement ID、相同的参数等),第一次查询会从数据库获取数据并放入缓存,后续的查询会直接从这个缓存中获取,不再请求数据库。
    • 共享性:一级缓存是不共享的。不同的 SqlSession 实例之间,它们的一级缓存是相互隔离的。
  • 二级缓存(Global Cache)

    • 作用域Mapper Namespace 级别,也就是一个Mapper映射文件。
    • 生命周期:与应用程序的生命周期相同。一旦数据被缓存,它可以被该Namespace下的所有 SqlSession 共享。
    • 工作方式:当一个 SqlSession 提交了事务(session.commit())或者关闭了(session.close())之后,它的一级缓存中的数据才会被刷新到二级缓存中。这样,其他新的 SqlSession 就可以访问到这些被缓存的数据。
    • 共享性:二级缓存是SqlSession 共享的。

一句话总结:一级缓存是SqlSession内部的“私有缓存”,二级缓存是多个SqlSession可以共享的“公共缓存”。


二、 二级缓存的工作原理

MyBatis的二级缓存工作流程可以用经典的装饰器模式(Decorator Pattern)来解释。

  1. Executor 体系结构
    MyBatis的核心操作由Executor接口执行。它有几个实现类:

    • SimpleExecutor: 每执行一次 updateselect,就开启一个 Statement 对象,用完立刻关闭。
    • ReuseExecutor: 重复使用 Statement 对象。
    • BatchExecutor: 批量执行SQL。
  2. CachingExecutor 的介入
    当你启用了二级缓存后,MyBatis会创建一个CachingExecutor实例。这个CachingExecutor包装(装饰)一个真实的Executor(比如 SimpleExecutor)。

    因此,执行流程变成了:
    SQL请求 -> CachingExecutor -> SimpleExecutor -> 数据库

  3. 查询流程(Cache Hit / Cache Miss)
    当一个查询请求到来时:

    • 步骤 1: 查询二级缓存
      CachingExecutor首先会根据查询请求(包括Statement ID、参数、分页信息、SQL语句等)生成一个唯一的CacheKey
    • 步骤 2: 判断缓存命中
      CachingExecutor会用这个CacheKey去二级缓存中查找数据。
      • 缓存命中(Cache Hit):如果找到了对应的数据,它会直接将数据返回给调用者,不会再执行后续的SimpleExecutor,也不再查询数据库。
      • 缓存未命中(Cache Miss):如果没有找到数据,请求会继续传递给被它包装的SimpleExecutor
    • 步骤 3: 查询数据库并缓存结果
      SimpleExecutor会像往常一样去查询数据库,获取结果。
    • 步骤 4: 结果存入一级缓存
      查询到的结果首先会被放入一级缓存SqlSession级别)。
    • 步骤 5: 将结果推送到二级缓存
      SqlSession关闭(session.close())或提交(session.commit())时,CachingExecutor会检查一级缓存中的数据,并将这些数据刷新(放入)到二级缓存中。这一步是关键,它使得数据能够被其他SqlSession访问。
  4. 更新/插入/删除操作(缓存刷新)
    当执行insertupdatedelete操作时,为了保证数据的一致性,CachingExecutor清空(flush)Namespace下的所有二级缓存条目。这是默认行为,可以通过在statement标签上设置flushCache="false"来改变,但通常不建议这么做。


三、 如何开启和配置二级缓存

开启二级缓存需要满足以下几个条件:

  1. 全局开启缓存
    mybatis-config.xml中,确保cacheEnabled设置为true(这是默认值)。

    xml
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
  2. 在Mapper XML中声明缓存
    在需要开启二级缓存的Mapper映射文件(如 UserMapper.xml)中,添加<cache/>标签。

    xml
    <mapper namespace="com.example.mapper.UserMapper">
        <!-- 开启该namespace的二级缓存 -->
        <cache/>
    
        <select id="getUserById" resultType="com.example.model.User">
            SELECT * FROM users WHERE id = #{id}
        </select>
        <!-- ...其他语句 -->
    </mapper>
  3. 实体类必须序列化
    所有需要被二级缓存的POJO(实体类)都必须实现java.io.Serializable接口。
    原因:二级缓存的数据可能存储在内存、硬盘或者像Redis这样的分布式缓存中。序列化是将Java对象转换为字节流的过程,这样才能在不同的存储介质中进行读写和传输。即使是默认的内存缓存,为了实现readOnly=false(返回一个对象的拷贝而非引用),也需要通过序列化和反序列化来创建副本。

    java
    import java.io.Serializable;
    
    public class User implements Serializable {
        private static final long serialVersionUID = 1L; // 建议添加
        private Integer id;
        private String name;
        // ... getters and setters
    }
  4. SqlSession必须关闭或提交
    查询操作所在的SqlSession必须被commit()close(),其一级缓存中的数据才会被刷新到二级缓存。

    java
    try (SqlSession session = sqlSessionFactory.openSession()) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        User user = mapper.getUserById(1);
        // ...
    } // try-with-resources 会自动调用 session.close()
    // 此时,user对象才会被放入二级缓存

四、 <cache/> 标签的详细配置

<cache/>标签提供了丰富的属性来定制缓存策略:

xml
<cache
    eviction="LRU"      <!-- 驱逐策略默认LRU (最近最少使用) -->
    flushInterval="60000" <!-- 刷新间隔,单位毫秒。不设置则无间隔 -->
    size="1024"         <!-- 缓存对象数量,默认1024 -->
    readOnly="false"    <!-- 是否只读。默认为false -->
    blocking="false"    <!-- 是否阻塞,默认为false -->
/>
  • eviction: 缓存驱逐策略。
    • LRU (Least Recently Used) - 最近最少使用(默认)。
    • FIFO (First In First Out) - 先进先出。
    • SOFT - 软引用,基于垃圾回收器状态和软引用规则。
    • WEAK - 弱引用,更积极地被垃圾回收。
  • flushInterval: 自动刷新缓存的时间间隔,单位毫秒。如果设置了,缓存会每隔指定时间被清空一次。
  • size: 缓存中最多可以存储的对象数量。
  • readOnly:
    • true (只读):缓存会直接返回对象的引用。这样做性能更高,但不安全,因为多个调用者可能会修改同一个对象,导致数据混乱。
    • false (读写,默认):缓存会返回对象的拷贝(通过序列化和反序列化实现)。这样做更安全,但性能开销稍大。

五、 使用场景和注意事项

适合使用的场景:

  1. 读多写少的数据,如系统配置、字典数据、国家地区列表等。
  2. 数据变化频率低,对实时性要求不高的数据。
  3. 查询结果集不会过大,否则会占用大量内存。

注意事项(坑):

  1. 脏读问题:二级缓存是基于Namespace的。如果多个Namespace操作同一张表,一个Namespace的更新操作只会清空自己的缓存,而不会清空其他Namespace的缓存,这可能导致脏读。
    • 解决方案:使用<cache-ref>标签,让多个Namespace共享同一个缓存配置和实例。
    xml
    <!-- 在 OrderMapper.xml 中 -->
    <cache-ref namespace="com.example.mapper.UserMapper"/>
  2. 分布式环境下的数据不一致:MyBatis自带的二级缓存是基于JVM本地内存的。在分布式或集群环境下,每个应用节点都有自己的二级缓存,一个节点的更新无法通知其他节点,会导致数据不一致。
    • 解决方案:集成第三方的分布式缓存框架,如Redis、Ehcache等。这需要自定义实现MyBatis的Cache接口。社区已经有很多成熟的集成方案(如mybatis-redis)。
  3. 配置复杂性:需要仔细规划哪些Mapper需要开启缓存,以及它们之间的依赖关系,避免因缓存刷新不及时导致的数据问题。

总结

MyBatis二级缓存是一个强大的性能优化工具,但它是一把双刃剑。正确理解其Namespace级别作用域SqlSession共享的特性,以及事务提交/关闭时才生效的机制至关重要。在使用时,必须充分评估业务场景,特别是在复杂和分布式环境下,要警惕其可能带来的数据一致性问题。

00:00
00:00