MyBatis二级缓存解密:深入探究缓存机制与应用场景

简介: MyBatis二级缓存解密:深入探究缓存机制与应用场景

在上文《探秘MyBatis缓存原理:Cache接口与实现类源码分析》中,我们已经介绍了 MyBatis 的 Cache 接口以及对应的实现类。其中的 PerpetualCache 是 MyBatis 缓存的最基础的实现类,底层通过 HashMap 存储数据,其他的实现类都属于装饰器,基于 PerpetualCache 的各个方面进行增强,各个实现类的理论和实现我们学习过后,本文我们就来探究一下,MyBatis 真正的缓存机制是怎么样的!

MyBatis 缓存机制

MyBatis 为提高其数据库查询性能,提供了两层缓存机制(只针对查询做缓存),包括一级缓存和二级缓存。

  1. ⼀级缓存:将查询到的数据存储到 SqlSession 中,所以只对本 SqlSession 有效。范围比较小,只对于一次 SQL 会话。
  2. ⼆级缓存:将查询到的数据存储到 SqlSessionFactory 中。范围比较大,针对于整个数据库级别。

MyBatis 也可以集成其它第三方缓存:比如基于 Java 开发的 EhCache、基于 C 语言开发的 Memcache等。

二级缓存

二级缓存也叫全局缓存。数据存放在 SqlSessionFactory 的 Configuration 中,只要是同一个工厂中对象创建的 SqlSession,在进行查询时都能共享数据。一般在项目中只有一个 SqlSessionFactory 对象,所以二级缓存的数据是全项目共享的,它可以在多个会话之间共享缓存数据,有效减少数据库访问次数,提高系统性能和响应速度。

默认情况下,MyBatis 在解析 SQL XML 文件的时候,会为每个 XML 文件创建一个二级缓存,这个 XML 中的多个查询结果都共用这同一个 Cache,当然也可以通过  标签来配置多个 XML 复用同一个 Cache,通常会在多表查询的时候会指定同一个 Cache,这是为了避免脏数据。

因为在对数据库进行更新的时候会清理当前 MappedStatement 引用的 Cache,但是如果别的 XML 文件中的 MappedStatement 的查询也使用到了这个表,那么别的 XML 的 Cache 中就会出现脏数据了。

但是如果都指定同一个 Cache,那么在更新的时候会频繁的清理当前的 Cache,而且是全部清理,这样显然不是很好,所以我们有必要更改 MyBatis 底层的缓存清除策略。

  1. 作用范围:跨会话缓存,二级缓存是跨多个 SqlSession 的缓存机制,可以在不同的会话之间共享缓存数据。
  2. 生命周期:应用级别缓存,二级缓存的生命周期与应用程序的生命周期相同,可以在整个应用程序中共享数据。
  3. 默认开启:二级缓存可以通过 MyBatis 配置文件中的控制,默认就是 true,可以理解为默认开启,但是还是需要对应的 Mapper XML 配置标签后才能生效。
  4. 缓存策略:二级缓存的配置通常配置在映射文件(Mapper XML 文件)的  标签内部。配置 元素,可用于配置二级缓存的属性,如缓存类型、缓存的大小、刷新间隔等。在映射文件添加  标签,该映射文件下的所有方法都支持二级缓存。该标签有 size 属性,可以设置缓存中的对象数量,默认是 1024 个。
  5. 缓存命中:当执行查询时,MyBatis 会先检查二级缓存中是否存在相应的结果。如果存在,则直接从缓存中返回结果,而不需要访问数据库。
  6. 缓存失效:对于更新、插入或删除操作,会导致相应的缓存失效,需要重新查询更新后的结果,同一个 XML 中的 SQL 是共用同一个缓存的,失效的时候是指这个 XML 中的多个查询 SQL 都会失效,多个 MappedStatement 引用的同一个 Cache。
  7. 数据同步问题:由于二级缓存是跨会话的,可能会出现数据不一致的情况。在更新或删除数据时,需要及时清除相应的缓存,以保持数据的一致性。

二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是:二级缓存 -> 一级缓存 -> 数据库。

验证二级缓存

MyBatis 配置文件(可省略)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 此项配置可以省略,因为默认就是 true -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <typeAliases>
        <typeAlias type="world.xuewei.mybatis.entity.Account" alias="Account"/>
    </typeAliases>
    <environments default="default">
        <environment id="default">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://**.**.**/learn?useSSL=false&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mappers/AccountMapper.xml"/>
    </mappers>
</configuration>
Mapper 映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 配置 namespace -->
<mapper namespace="world.xuewei.mybatis.dao.AccountDao">
    <!-- 需要明确指定此标签,表示开启二级缓存 -->
    <cache/>
    
    <!-- useCache="true" 可以省略 -->
    <select id="getAll" resultType="Account" useCache="true">
        select * from account
    </select>
</mapper>

在 MyBatis 的映射 XML 中配置 cache 或者 cache-ref 都可以。

  1. 标签用于声明这个 namespace 使用二级缓存,并且可以自定义配置。
  • type:cache 使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
  • eviction:定义回收的策略,常见的有 FIFO,LRU。
  • flushInterval:配置一定时间自动刷新缓存,单位是毫秒。
  • size:最多缓存对象的个数,默认 1024 个。
  • readOnly:是否只读,若配置可读写,则需要对应的实体类能够序列化。
  • blocking:若缓存中找不到对应的 key,是否会一直 blocking,直到有对应的数据进入缓存。
  1. 代表引用别的命名空间的 Cache 配置,两个命名空间的操作使用的是同一个 Cache。
    例如:
测试实体(Serializable)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {
    private Integer id;
    private String name;
    private String password;
}

数据库实体必须要实现 Serializable 接口,否则会抛出异常:Error committing transaction. Cause: org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: world.xuewei.mybatis.entity.Account

测试类
public class Cache2Test {
    private SqlSession sqlSession1;
    private SqlSession sqlSession2;
    @Before
    public void before() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession1 = sessionFactory.openSession();
        sqlSession2 = sessionFactory.openSession();
    }
    /**
     * 验证二级缓存,多个 SqlSession 有效
     */
    @Test
    public void testGetAll() {
        AccountDao accountDao1 = sqlSession1.getMapper(AccountDao.class);
        AccountDao accountDao2 = sqlSession2.getMapper(AccountDao.class);
        accountDao1.getAll().forEach(System.out::println);
        // 处理这里很关键,SqlSession 提交或关闭的时候才会将数据保存到二级缓存
        sqlSession1.commit();
        System.out.println("======================================");
        accountDao2.getAll().forEach(System.out::println);
        sqlSession2.commit();
    }
}

通过观察日志的打印情况,可以看出,在第二次执行相同的查询方法时,就已经是从缓存中直接拿到的数据了,并且打印出了缓存的命中率 0.5。

清理二级缓存

执行 update、insert 或 delete 操作
@Test
public void testGetAll() {
    AccountDao accountDao1 = sqlSession1.getMapper(AccountDao.class);
    AccountDao accountDao2 = sqlSession2.getMapper(AccountDao.class);
    accountDao1.getAll().forEach(System.out::println);
    accountDao1.delete(1);
    sqlSession1.commit();
    System.out.println("======================================");
    accountDao2.getAll().forEach(System.out::println);
    sqlSession2.commit();
}
配置 flushCache=“true”
<select id="getAll" resultType="Account" flushCache="true">
    select * from account
</select>

关闭二级缓存

<settings>
    <!-- 默认是 true -->
    <setting name="cacheEnabled" value="false"/>
</settings>

实现原理

CachingExecutor 创建时机

开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示:

这是一个典型的装饰器模式,通过 CachingExecutor 为 Executor 增强缓存的功能。在源码中如何提现的呢?首先根据我们原有的知识,Configuration 类是 MyBatis 的第一大核心类,这个类有一个职能就是可以创建其他的核心对象,包括 Executor:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
 executorType = executorType == null ? defaultExecutorType : executorType;
 executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
 Executor executor;
 // 创建一个原始的 Executor,类型可能为 BatchExecutor、ReuseExecutor、SimpleExecutor,默认为 SimpleExecutor。
 if (ExecutorType.BATCH == executorType) {
     executor = new BatchExecutor(this, transaction);
 } else if (ExecutorType.REUSE == executorType) {
     executor = new ReuseExecutor(this, transaction);
 } else {
     executor = new SimpleExecutor(this, transaction);
 }
 // cacheEnabled 默认就是 true
 if (cacheEnabled) {
     // 通过 CachingExecutor 来装饰原始的 Executor
     executor = new CachingExecutor(executor);
 }
 executor = (Executor) interceptorChain.pluginAll(executor);
 return executor;
}
二级缓存实现流程

SqlSession 将 SQL 语句的处理和执行交给 Executor 去处理,如果是查询语句,那么肯定会交给 Executor#query 方法处理,而这里的 Executor 就是刚刚得到的使用 CachingExecutor 装饰后的 Executor。所以我们可以将关注点放在 CachingExecutor#query 方法,以下是 CachingExecutor 的源码:

public class CachingExecutor implements Executor {
  
    // 非常典型的装饰器模式
    private final Executor delegate;
    // 存储缓存的工具,稍后我们会仔细分析
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();
    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this);
    }
    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        // 插入、更新、删除操作会清空当前 MappedStatement 的缓存
        flushCacheIfRequired(ms);
        return delegate.update(ms, parameterObject);
    }
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // 创建缓存的 Key,其实就是调用 BaseExecutor 的方法,和一级缓存的 Key 一样。
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    @Override
    public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
        // 这种查询方式也会清空 MappedStatement 缓存
        flushCacheIfRequired(ms);
        return delegate.queryCursor(ms, parameter, rowBounds);
    }
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
        Cache cache = ms.getCache();
        // 判断 Mapper XML 中是否配置了 <cache/> 标签
        if (cache != null) {
            // 判断是否需要刷新缓存
            flushCacheIfRequired(ms);
            // 此查询 SQL 是否配置了 useCache="true" 属性
            if (ms.isUseCache() && resultHandler == null) {
                // 用来处理存储过程的,暂时不用考虑
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                // 从二级缓存中拿,会把获取值的职责一路传递,最终到 PerpetualCache。如果没有查到,会把 key 加入 Miss 集合,这个主要是为了统计命中率。
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    // 没拿到就查询数据库
                    list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    // 然后存放在二级缓存,等待提交(注意此时还没有保存到二级缓存,还需要一个提交的操作)
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        // 没有配置 <cache/> 则直接调用原始 Executor 查询数据库
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    }
    private void flushCacheIfRequired(MappedStatement ms) {
        // 判断 Mapper XML 中是否配置了 <cache/> 标签
        Cache cache = ms.getCache();
        // 如果配置了 <cache/> 并且此查询语句配置了 flushCache="true" 就会清空缓存
        if (cache != null && ms.isFlushCacheRequired()) {      
            // 清空缓存
            tcm.clear(cache);
        }
    }
  // 省略其他方法...
}

在上面的 query 方法中的 Cache cache = ms.getCache();是获取当前 MappedStatement 配置的缓存对象。

本质上是装饰器模式的使用(无限套娃),具体的装饰链是:

SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。

以下是具体这些 Cache 实现类的介绍,他们的组合为 Cache 赋予了不同的能力。

  • SynchronizedCache:同步 Cache,实现比较简单,直接使用 synchronized 修饰方法。
  • LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了 DEBUG 模式,则会输出命中率日志。
  • SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的 Copy,用于保存线程安全。
  • LruCache:采用了 Lru 算法的 Cache 实现,移除最近最少使用的 Key/Value。
  • PerpetualCache:作为为最基础的缓存类,底层实现比较简单,直接使用了 HashMap。

在上面的 query 方法中,有这样的逻辑:

if (ms.isUseCache() && resultHandler == null) {
  // ...
    List<E> list = (List<E>) tcm.getObject(cache, key);
    if (list == null) {
        list = delegate.<E> query(...);
        tcm.putObject(cache, key, list); // issue #578 and #116
    }
    return list;
}

表达的含义是先调用 tcm 的 getObject 查询缓存,如果能查询到数据直接返回,否则调用 delegate 的查询,并将结果保存在 tcm。那么这个 tcm 可以说就是用来存储数据的。找到 tcm 的定义如下:

private final TransactionalCacheManager tcm = new TransactionalCacheManager();

查询源码后可以看到:TransactionalCacheManager 维护了一个 transactionalCaches 这个 HashMap,键就是每个 MappedStatement 的 Cache,值就是 TransactionalCache。这个 TransactionalCache 也是 PerpetualCache 的装饰器,提供了事务性缓存的功能。事务性缓存确保缓存中的数据与数据库中的数据保持一致,只有在事务成功提交后,缓存中的数据才会被更新或者删除。

关于 TransactionalCache 的介绍可以查询这篇文章:《探秘MyBatis缓存原理:Cache接口与实现类源码分析

public class TransactionalCacheManager {
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }
  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      // 注意这里!当第一次添加数据的时候,会用 TransactionalCache 再次装饰一下 MappedStatement 的 Cache 当做 Map 的 value
      // 这也就是 TransactionalCache 是怎么和 MappedStatement 的 Cache 产生联系的原因
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }
}
二级缓存创建过程

刚刚我们获取到了一个套娃的 PerpetualCache,那么这个 PerpetualCache 是什么时候创建的呢?这个就要联系到我们之前学到的知识《MyBatis初探:揭示初始化阶段的核心流程与内部机制》。

在构建 MappedStatement 的时候是由 XMLMapperBuilder 负责处理,在其核心的配置解析的方法(XMLMapperBuilder#configurationElement)中就由一步是去解析和标签的。

稍微追踪一下源码即可发现,缓存的创建是由 MapperBuilderAssistant 类完成,这是一个典型的构建者模式:

public Cache useNewCache(Class<? extends Cache> typeClass,
                         Class<? extends Cache> evictionClass,
                         Long flushInterval,
                         Integer size,
                         boolean readWrite,
                         boolean blocking,
                         Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        // 默认底层是 PerpetualCache
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        // 默认换出策略是 LruCache
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        // 刷新时间
        .clearInterval(flushInterval)
        // 存储数据大小
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

这些属性其实就是对应的标签的属性。

所以说,缓存存储对象 cache 是在 MyBatis 初始化 MappedStatement 的时候就创建好了,创建好后存储在 MappedStatement 对象,由 XMLMapperBuilder 负责解析,MapperBuilderAssistant 负责创建。

接下来我们仔细看一下 CacheBuilder 的 build 方法是如何构建出这个 Cache 对象的:

public Cache build() {
    // 设置默认的缓存实现,如果 <cache> 标签没有指定 type,那么就用 PerpetualCache + LruCache
    setDefaultImplementations();
    // 通过反射来创建缓存对象
    Cache cache = newBaseCacheInstance(implementation, id);
    // 读取 <cache> 下的 <property> 增加额外的参数(内置缓存不用,通常用于搭配自定义缓存,例如:Redis、Ehcache 等)
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
            // 添加装饰器,可以通过 <cache> 标签的 eviction 属性指定
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 根据 <cache> 标签配置的属性,设置装饰器
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}
private void setDefaultImplementations() {
    if (implementation == null) {
        // 默认就是 PerpetualCache + LruCache
        implementation = PerpetualCache.class;
        if (decorators.isEmpty()) {
            decorators.add(LruCache.class);
        }
    }
}
private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        if (size != null && metaCache.hasSetter("size")) {
            // 设置缓存大小,其实就是赋值给 PerpetualCache 的 size 属性,默认 1024
            metaCache.setValue("size", size);
        }
        if (clearInterval != null) {
            // 支持定时清理的缓存
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        if (readWrite) {
            // 自动序列化的缓存
            cache = new SerializedCache(cache);
        }
        // 添加日志功能的缓存
        cache = new LoggingCache(cache);
        // 同步的缓存
        cache = new SynchronizedCache(cache);
        if (blocking) {
            // 阻塞式的缓存
            cache = new BlockingCache(cache);
        }
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}
MyBatis 缓存的查询顺序

先说结论:查询的时候先查询二级缓存,查不到再去查询一级缓存,再查不到则会查询数据库。

再默认的情况下(开启二级缓存),我们得到的 Executor 是 CachingExecutor,而这个类就是二级缓存的负责类。当用户执行查询操作的时候,会先由 CachingExecutor 的 query 处理,处理过程就是先查缓存,然后没查到的话调用委托的 Executor delegate 来处理查询,而这个 delegate 就是 SimpleExecutor,一级缓存的负责类就是这个 SimpleExecutor 的父类,即 BaseExecutor,所以结合一级缓存和二级缓存的实现原理可以得出上面这个结论。

相关文章
|
7天前
|
缓存 Linux
linux系统缓存机制
linux系统缓存机制
|
16天前
|
缓存 数据库 开发者
Django视图中的缓存机制:提升页面加载速度
【4月更文挑战第15天】本文介绍了Django的缓存机制在提升页面加载速度中的作用。Django提供视图缓存和片段缓存,通过`cache_page`装饰器和`CacheMixin`实现视图级别的缓存,使用`{% cache %}`模板标签实现页面片段缓存。开发者可选择不同的缓存后端,并在设置中配置缓存参数。同时,注意合理控制缓存粒度、及时更新和管理缓存,以优化用户体验和网站性能。
|
18天前
|
缓存 Java 数据库连接
MyBatis三级缓存实战:高级缓存策略的实现与应用
MyBatis三级缓存实战:高级缓存策略的实现与应用
35 0
MyBatis三级缓存实战:高级缓存策略的实现与应用
|
18天前
|
存储 缓存 Java
探秘MyBatis缓存原理:Cache接口与实现类源码分析
探秘MyBatis缓存原理:Cache接口与实现类源码分析
35 2
探秘MyBatis缓存原理:Cache接口与实现类源码分析
|
2月前
|
存储 缓存 算法
深入探究LRU缓存机制:优化内存利用与提升性能
深入探究LRU缓存机制:优化内存利用与提升性能
159 1
|
2月前
|
存储 缓存 Java
什么!?实战项目竟然撞到阿里面试的原题!???关于MyBatis Plus的缓存机制
什么!?实战项目竟然撞到阿里面试的原题!???关于MyBatis Plus的缓存机制
|
2月前
|
缓存 Java 数据库连接
mybatis 数据库缓存的原理
MyBatis 是一个流行的 Java 持久层框架,它封装了 JDBC,使数据库交互变得更简单、直观。MyBatis 支持两级缓存:一级缓存(Local Cache)和二级缓存(Global Cache),通过这两级缓存可以有效地减少数据库的访问次数,提高应用性能。
282 1
|
2月前
|
存储 缓存 Java
【MyBaits】4、延迟加载、MyBatis 的缓存
【MyBaits】4、延迟加载、MyBatis 的缓存
22 0
|
2月前
|
缓存 NoSQL 安全
【Redis】缓存穿透
【Redis】缓存穿透
30 0
|
5天前
|
缓存 NoSQL Redis
深度解析Redis的缓存双写一致性
【4月更文挑战第20天】
30 1