MyBatis(三)MyBatis缓存和工作原理

MyBatis缓存

MyBatis提供了一级缓存和二级缓存,并且预留了集成第三方缓存的接口。
从上面MyBatis的包结构可以很容易看出跟缓存相关的类都在cache的package里,其底层是一个Cache的接口,默认的实现类是PerpetualCache,使用一个Map<Object, Object>的哈希Map来缓存数据。此外还有很多的装饰器类,如下图所示:从包名就可以猜测出其功能,这里的缓存基本可以分为三类

  • 基本缓存:默认的是PerpetualCache,也可以自定义 RedisCache等
  • 淘汰算法缓存:FifoCache,LruCache,WeakCache,SoftCache 定义了当缓存内存不足时,淘汰的算法
  • 其他装饰器缓存:BlockingCache等

MyBatis一级缓存

一级缓存也叫本地缓存,MyBatis的一级缓存是在会话(SqlSession)层面进行缓存的。其生命周期也就是Session级别,一旦会话关闭,一级缓存也就不存在了。

MyBatis的一级缓存是默认开启的,不需要任何的配置,如果想要关闭一级缓存,就把localCacheScope设置成STATEMENT。
查看源码可以发现在DefaultSqlSession里有一个Executor属性,在SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor的构造方法里创建了一个PerpetualCache对象用于一级缓存。故而在同一个会话里面(同一个SqlSession),多次执行相同的SQL语句,会直接从PerpetualCache缓存的Map里取到缓存的结果,不会再发送 SQL 到数据库,简单的流程如下图所示

注意:使用一级缓存需要关闭二级缓存,并且将localCacheScope设置成SESSION

<!-- 控制全局缓存(二级缓存) 设置成false则为关闭二级缓存-->
<setting name="cacheEnabled" value="false"/>
<setting name="localCacheScope" value="SESSION"/>

那么MyBatis的一级缓存是以什么为key来判断某两次查询是完全相同的查询? MyBatis构造了一个CacheKey来表示每一个不同的sql

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }


  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            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();
        ....
}
public class CacheKey implements Cloneable, Serializable {

  private static final long serialVersionUID = 1146682552656046210L;

  public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

  private static final int DEFAULT_MULTIPLYER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  // 8/21/2017 - Sonarlint flags this as needing to be marked transient.  While true if content is not serializable, this is not always true and thus should not be marked transient.
  private List<Object> updateList;

  public CacheKey() {
    //得到初始的hashCode和乘数
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

 //每次添加参数,则将其保存在updateList里,然后计算新加参数的hashCode,更新最新的hashCode
 public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }
   
  //比较两个CacheKey是否一致
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;
    //hashcode,count,checksum都需要相等
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }
    //要求两个CacheKey的updateList里的每个元素都相等
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

从CacheKey的构造可以看出MyBatis认为,如果两次查询,以下条件都完全一样,那么就可以认为它们是完全相同的两次查询:

  • 传入的 statementId 
  • 查询时要求的结果集的分页范围 (rowBounds.offset和rowBounds.limit,这里是逻辑分页);
  • 本次次查询要传递给数据库的Sql语句
  • sql中的参数值
     

继续往下就可以看到MyBatis里是如何判断使用一级缓存的

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 (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            //对于select语句,flushCahe默认为false,如果配置成true,就会去清空localcache一级缓存
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }

            List list;
            try {
                ++this.queryStack;
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    //如果缓存里没有,则去查询数据库
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }

            if (this.queryStack == 0) {
                Iterator var8 = this.deferredLoads.iterator();

                while(var8.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                    deferredLoad.load();
                }

                this.deferredLoads.clear();
                //如果当前mybatis-config.xml配置的localCacheScope是STATEMENT级别,那么也清空缓存,这就是STATEMENT级别的一级缓存无法共享localCache的原因
                if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }

            return list;
        }
    }

一级缓存在当前会话执行update(insert,update,delete语句)的时候会调用clearLocalCache()方法清空缓存,但是对于其他会话下的更新不会响应,这就会导致出现数据不一致的问题

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

总结

  • Mybatis一级缓存的生命周期和SqlSession一致
  • Mybatis的一级缓存是通过PerpetualCache保存的map来做缓存的,没有更新缓存和缓存过期的机制,也没有做容量上的限定。
  • Mybatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,同时操作数据库的话,会引起脏数据
  • 可以把一级缓存的默认级别localCacheScope设定为Statement,即不使用一级缓存。

MyBatis二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个SqlSession共享。

MyBatis用了一个装饰器类来存储二级缓存数据,就是CachingExecutor。如果启用了二级缓存,MyBatis在创建Executor对象的时候对Executor进行装饰。
CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有交给真正的查询器Executor,比如SimpleExecutor来执行查询,在Executor查询的时候会再去判断一级缓存是否存在,最后会把查询到的结果缓存起来,并且返回给用户  如下图所示

开启二级缓存的方式:
步骤1: 在mybatis-config.xml配置<setting name="cacheEnabled" value="true"/> 默认该参数值是true
步骤2:在Mapper.xml中配置<cache/>标签

 #可以配置具体的参数,也可以直接使用一个<cache/>标签  
 #type 缓存实现类
 #size 最多缓存个数,默认是1024
 #eviction 回收淘汰策略 默认LRU
 #flushInterval自动刷新间隔  没有配置则默认在调用时刷新
 #readOnly 默认是false,  只读的缓存会给所有调用者返回相同实例,因此这些对象不能被修改
 #而可读写的缓存会(通过序列化)返回缓存对象的拷贝,速度上会慢一些,但是更安全 (设置成true要求对象实
 #现序列化接口)
 <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
               size="1024"  
               eviction="LRU"
               flushInterval="120000"
               readOnly="false"/>

如果开启了二级缓存,那么在创建Executor的时候会使用CachingExecutor装饰对应的Executor(装饰器模式)

DefaultSqlSessionFactory:96行
final Executor executor = configuration.newExecutor(tx, execType);

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    //根据ExecutorType创建不同的执行器
    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);
    }
    //如果开启了二级缓存,则使用CachingExecutor装饰Executor
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //这里是调用插件的方法来增强executor 后面会详细分析
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

所以最后查询方法会执行CachingExecutor的query方法,代码如下:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //先从MappedStatement中获取在配置解析时得到的cache
    //使用了装饰器模式,具体的执行链是SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
    Cache cache = ms.getCache();
    if (cache != null) {
      //判断是否需要刷新缓存
      flushCacheIfRequired(ms);
      //如果在mapper.xml里开启了二级缓存则执行下面的逻辑;否则直接调用原来的executor的query方法
      if (ms.isUseCache() && resultHandler == null) {
        //用来处理存储过程,暂时不考虑
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        //如果缓存里没有,则直接执行执行器的query方法,查询后放入缓存
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

这里MyBatis的二级缓存就是通过TransactionalCacheManager--tcm来管理的(在CachingExecutor里),获取缓存和添加缓存分别调用了对应的getObject和putObject方法,下面看下tcm的相关源码

public class TransactionalCacheManager {
  
  //缓存查询结果
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  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);
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

}

TransactionalCacheManager里持有了一个transactionalCaches的map对象,保存了Cache和用TransactionalCache包装后的Cache的映射关系。这里的TransactionalCache实现了Cache接口,CachingExecutor会默认使用TransactionalCache包装初始生成的Cache,TransactionalCacheManager的getObject和putObject实际是调用了TransactionalCache的getObject和putObject方法,源码如下:

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  //保存待添加到缓存的key,value,执行commit的时候才会真正添加到缓存
  private final Map<Object, Object> entriesToAddOnCommit;
  //保存没有命中的key  用于计算命中率
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }
  
  @Override
  public Object getObject(Object key) {
    //这里直接调用被包装cache的getObject方法获取缓存结果
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
     //如果没有缓存则添加到miss集合里
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public void putObject(Object key, Object object) {
    //将查询结果的key-value保存到entriesToAddOnCommit集合里
    entriesToAddOnCommit.put(key, object);
  }

   @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    //调用commit的时候执行flushPendingEntries方法,将缓存的key-value真正保存到cache缓存里
    flushPendingEntries();
    reset();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }


 ...
} 

那么这里的commit方法是什么时候调用的呢?猜测是在SqlSession执行commit方法的时候

-------DefaultSqlSession的commit方法
@Override
  public void commit(boolean force) {
    try {
      //对于开启了二级缓存的Executor,这里的executor是CachingExecutor
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
 
 
 -------CachingExecutor的commit方法
 @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    //调用了TransactionalCacheManager的commit方法,里面循环所有的TransactionalCache并commit
    tcm.commit();
  }


 public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }


上面的源码分析说明了为什么在使用二级缓存的时候需要commit事务才会将查询到的数据写入缓存

如果某些查询方法对数据的实时性要求很高,无法使用二级缓存,我们可以在单个Statement ID上显式关闭二级缓存(默认是true)

<select id="selectPerson" resultMap="personResultMap" useCache="false">
//在二级缓存下,如果执行update数据库更新操作,在更新前会先调用flushCacheIfRequired方法
//然后根据statement 上的 flushCache属性判断是否需要刷新缓存  
//对于insert,update,delete语句 flushCache默认值都为true,对于select默认值为false
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        this.flushCacheIfRequired(ms);
        return this.delegate.update(ms, parameterObject);
}

private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) {
            this.tcm.clear(cache);
        }
}

Mybatis的二级缓存一般只推荐在以查询为主的应用中使用,因为频繁的更新会导致缓存清空,那么缓存的意义也就不大了。此外,二级缓存比较适合在单表操作的情形下使用,如果在多个不同的namespace下都操作同一张表,因为二级缓存的范围是namespace,那么一个namespace下的更新无法同步到另外的namespace下,可能会导致出现脏数据

如果想要在多个命名空间中共享相同的缓存配置和实例。可以使用 cache-ref 元素来引用另一个Mapper的缓存。
<cache-ref namespace="com.chenpp.application.data.XXXMapper"/>

除此之外,还可以使用第三方的缓存或者自定义的缓存,比方说redis,ehcache等,使用的时候可以在Mapper.xml里的<cache>标签指定对应的缓存类型

<cache type="org.mybatis.caches.redis.RedisCache" eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> 
发布了58 篇原创文章 · 获赞 19 · 访问量 7530

猜你喜欢

转载自blog.csdn.net/qq_35448165/article/details/104442378
今日推荐