ORM - MyBatis一级缓存实现机制详解

2022年8月1日
大约 6 分钟

ORM - MyBatis一级缓存实现机制详解

一级缓存概念

Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。

图

  1. 一级缓存(local cache),即本地缓存,作用域默认为SqlSession。当Session flush或close后,该session中的所有Cache将被清空。
  2. 本地缓存不能被关闭,但可以调用clearCache()来清空本地缓存,或者改变缓存的作用域。
  3. 在 MyBatis3.1 之后,可以配置本地缓存的作用域,在 MyBatis.xml 中配置。

localCacheScope:MyBatis利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为session,这种情况下会缓存一个会话中执行的所有语句。若设置为STATEMENT,本地会话仅用在语句执行上,对相同SqlSession的不同调用将不会共享数据。

  1. 一级缓存是 sqlsession 级别的缓存。一级缓存是一直开启的,sqlsession级别的一个 map,与数据库同一次会话期间查询到的数据会放在本地缓存中。 多个一级缓存中的数据不能共用。以后如果需要获取相同的数据,直接从缓存中获取,不必再去查询数据库。
  2. 一级缓存的工作机制,同一次会话期间只要查询过的数据都会保存在当前的 SqlSession 的一个Map中。

一级缓存失效的四种情况

一级缓存失效情况(没有使用到当前一级缓存的情况,效果就是,还需要再向数据发送SQL),如下:

  1. sqlSession 不同:使用不同的 sqlSession 数据库会话,不同的 SqlSession 对应不同的一级缓存;
  2. sqlSession 相同:但查询条件不同(当前一级缓存中还没有这个数据);
  3. 如果sqlSession相同:两次查询之间执行了增删改操作(这次增删改可能对当前数据有影响);
  4. 如果sqlSession相同,手动清除了一级缓存(把缓存内容清空)。

SqlSession级别的缓存就相当于一个Map。

一级缓存的生命周期

  1. MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象。Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
  2. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
  3. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
  4. SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用

一级缓存源码分析

MyBatis的一级缓存是SqlSession级别的缓存,SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的。

PerpetualCache

一级缓存使用PerpetualCache实例实现,在BaseExecutor类中维护了两个PerpetualCache属性。

public abstract class BaseExecutor implements Executor {
    protected PerpetualCache localCache;
    protected PerpetualCache localOutputParameterCache;
}

其中,localCache属性用于缓存MyBatis查询结果,localOutputParameterCache属性用于缓存存储过程调用结果。这两个属性在BaseExecutor构造方法中进行初始化

protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
}

CacheKey

MyBatis通过CacheKey对象来描述缓存的Key值。在进行查询操作时,首先创建CacheKey对象(CacheKey对象决定了缓存的Key与哪些因素有关系)。如果两次查询操作CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。CacheKey对象通过BaseExecutor类的createCacheKey()方法创建

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
        if (parameterMapping.getMode() != ParameterMode.OUT) {
            Object value;
            String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
            value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
            value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
        } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
        }
    }
    if (configuration.getEnvironment() != null) {
        // issue #176
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

从上面的代码可以看出,缓存的Key与下面这些因素有关:

  1. Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名。
  2. 查询结果的偏移量及查询的条数。
  3. 具体的SQL语句及SQL语句中需要传递的所有参数。
  4. MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的Id属性值。

执行两次查询时,只有上面的信息完全相同时,才会认为两次查询执行的是相同的SQL语句,缓存才会生效。

BaseExecutor

接下来我们看一下BaseExecutor的query()方法相关的执行逻辑

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
    ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        // issue #601
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    return list;
}

在BaseExecutor类的query()方法中,首先根据缓存Key从localCache属性中查找是否有缓存对象,如果查找不到,则调用queryFromDatabase()方法从数据库中获取数据,然后将数据写入localCache对象中。如果localCache中缓存了本次查询的结果,则直接从缓存中获取。

需要注意的是,如果localCacheScope属性设置为STATEMENT,则每次查询操作完成后,都会调用clearLocalCache()方法清空缓存。除此之外,MyBatis会在执行完任意更新语句后清空缓存,我们可以看一下BaseExecutor类的update()方法

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
}

可以看到,MyBatis在调用doUpdate()方法完成更新操作之前,首先会调用clearLocalCache()方法清空缓存。

引用资料

  • https://zhuanlan.zhihu.com/p/361560639
  • https://www.cnblogs.com/niujifei/p/15243662.html
  • https://www.cnblogs.com/happyflyingpig/p/7739749.html