深入理解MyBatis(六)—MyBatis的缓存机制

来源:互联网 发布:灵犀一动知乎 编辑:程序博客网 时间:2024/06/06 11:08

深入理解MyBatis(六)—MyBatis的缓存机制

频繁的数据库查询操作是非常耗费性能的额,因为数据库查询底层依靠文件存储机制的IO操作,而IO操作的速度相比于内存作要慢几个量级,因此更好的解决方案是把相同的查询语句的结果存储在内存中,下次再查询时直接从内存中读取;

个人主页:tuzhenyu’s page
原文地址:深入理解MyBatis(六)—MyBatis的缓存机制

(0) MyBatis缓存

  • MyBatis缓存分类

    • 一级缓存:一级缓存是SqlSession级别的缓存,在同一个会话session中对于相同的查询,会从缓存中返回结果而不是查询数据库;

    • 二级缓存:二级缓存是Mapper级别的,定义在Mapper文件中标签并需要开启此标签;多个Mapper文件可以共用一个缓存,依赖标签配置;

(1)MyBatis一级缓存

1. 一级缓存的实现流程

  • 缓存存在的意义是为了避免多次重复性的数据库查询IO操作,因此缓存执行流程的入口是查询操作;
@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {  try {    MappedStatement ms = configuration.getMappedStatement(statement);    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);  } catch (Exception e) {    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);  } finally {    ErrorContext.instance().reset();  }}
  • sqlSession将具体的sql操作委托给Executor执行器,缓存信息也被维护在Executor执行器中;
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {  BoundSql boundSql = ms.getBoundSql(parameterObject);  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

cacheKey的定义

  • 一级缓存存储在BaseExecutor对象中的localCache属性中,localCache的实现类是perpetualCache,其底层是用HashMap存储缓存对象,CacheKey对象作为HashMap的key,缓存对象作为HashMap作为value;因此CacheKey对象的hashcode将决定存储位置;
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 int multiplier;  private int hashcode;  private long checksum;  private int count;  private List<Object> updateList;  public CacheKey() {    this.hashcode = DEFAULT_HASHCODE;    this.multiplier = DEFAULT_MULTIPLYER;    this.count = 0;    this.updateList = new ArrayList<Object>();  }  public CacheKey(Object[] objects) {    this();    updateAll(objects);  }  public int getUpdateCount() {    return updateList.size();  }  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);  }  public void updateAll(Object[] objects) {    for (Object o : objects) {      update(o);    }  }  @Override  public boolean equals(Object object) {    if (this == object) {      return true;    }    if (!(object instanceof CacheKey)) {      return false;    }    final CacheKey cacheKey = (CacheKey) object;    if (hashcode != cacheKey.hashcode) {      return false;    }    if (checksum != cacheKey.checksum) {      return false;    }    if (count != cacheKey.count) {      return false;    }    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;  }  @Override  public int hashCode() {    return hashcode;  }  @Override  public String toString() {    StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);    for (Object object : updateList) {      returnValue.append(':').append(ArrayUtil.toString(object));    }    return returnValue.toString();  }  @Override  public CacheKey clone() throws CloneNotSupportedException {    CacheKey clonedCacheKey = (CacheKey) super.clone();    clonedCacheKey.updateList = new ArrayList<Object>(updateList);    return clonedCacheKey;  }}
  • 调用createCacheKey()方法创建查询语句的唯一标示cacheKey,创建cacheKey主要依据以下几个条件:

    • MappedStatement的id也就是select标签所在mapper文件的namespace+select的id相同

    • MappedStatement中的boundSql中的sql语句相同

    • RowBounds的offset属性和limit()属性相同;

    • 遍历输入参数列表必须满足每个参数相同;

    • 获取Environment的id,保证数据源相同;

@Overridepublic 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();  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) {    cacheKey.update(configuration.getEnvironment().getId());  }  return cacheKey;}
  • 调用update()方法将唯一性判断条件加入cache对象中,并根据每个条件的hashcode更新cacheKey的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获取缓存

  • 调用query()方法尝试获取二级缓存,如果获取成功则直接返回结果,否则尝试获取一级缓存;
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)    throws SQLException {  Cache cache = ms.getCache();  if (cache != null) {    flushCacheIfRequired(ms);    if (ms.isUseCache() && resultHandler == null) {      ensureNoOutParams(ms, parameterObject, boundSql);      @SuppressWarnings("unchecked")      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;    }  }  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
  • 调用query()重写方法尝试获取一级缓存,如果失败则从数据库中查询
@SuppressWarnings("unchecked")@Overridepublic <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;}

2. 一级缓存的生命周期

  • MyBatis创建数据库会话sqlSession时,会初始化Executor执行器,Executor对象初始化过程中会创建PerpetualCache对象作为一级缓存;

  • 当会话结束也就是调用session.close()方法时,会释放Executor对象,同时也会释放PerpetualCache对象;一级缓存不可用;

  • 数据库会话调用clearCache()方法,会清空PerpetualCache对象,对象仍可用;

  • sqlSession中执行了update操作(update,insert,delete)都会清空PerpetualCache对象;

3. 一级缓存的性能

  • MyBatis的一级缓存简单的采用HashMap来存储缓存对象,没有对HashMap的容量大小进行限制,如果一直使用同一个session进行查询操作,可能会出现OOM错误;MyBatis不对HashMap大小进行限制的原因是session存在的时间较短,同时只要进行update操作缓存就会被清空,另外可以通过clearCache()方法手动清空缓存;

  • 一级缓存是一种粗粒度的缓存机制,没有过期机制同时一旦执行updata操作所有的缓存都将被清空;

  • MyBatis认为的完全相同的查询,不是指使用sqlSession查询时传递给算起来Session的所有参数值完完全全相同,你只要保证statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就可以了。

(2)MyBatis二级缓存

1. 二级缓存的实现流程

  • 二级缓存的入口在上文提到的query()方法;二级缓存读取在CachingExecutor类中,一级缓存的读取在BaseExecutor中;二级缓存的存取优先级高于一级缓存;

    • 尝试从MappedStatement中获取cache对象,只有使用标签或者标签标记使用缓存的Mapper.xml或Mapper接口才会有二级缓存,即cache对象不为空;

    • 根据sql操作的flushCache属性来确定是否清空缓存;

    • 根据sql操作的useCache属性来确定时候使用缓存;

    • 根据上面生成的cacheKey来从缓存中取值;

    • 如果没有缓存就从数据库中查询并将结果放入缓存中;

@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)    throws SQLException {  Cache cache = ms.getCache();  if (cache != null) {    flushCacheIfRequired(ms);    if (ms.isUseCache() && resultHandler == null) {      ensureNoOutParams(ms, parameterObject, boundSql);      @SuppressWarnings("unchecked")      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;    }  }  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

二级缓存的初始化

  • 根据Mapper.xml配置文件初始化缓存标签cache和cache-ref

    • type属性是设置为”PERPETUAL”是指缓存存储方式使用PerpetualCache类,底层由HashMap实现;

    • eviction属性设置为”LRU”是指缓存容量管理算法采用LRU算法即最近最少使用算法;

      • eviction属性主要包括LRU最近最少使用算法,FIFO先进先出算法,Scheduled时间间隔清空算法
private void cacheElement(XNode context) throws Exception {    if (context != null) {      String type = context.getStringAttribute("type", "PERPETUAL");      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);      String eviction = context.getStringAttribute("eviction", "LRU");      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);      Long flushInterval = context.getLongAttribute("flushInterval");      Integer size = context.getIntAttribute("size");      boolean readWrite = !context.getBooleanAttribute("readOnly", false);      boolean blocking = context.getBooleanAttribute("blocking", false);      Properties props = context.getChildrenAsProperties();      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);    }}
  • 根据属性的设置创建缓存

    • 判断缓存存储是否为PerpecutalCache,如果是则采用装饰器模式装饰cache,给PerpecutalCache加上LRU功能;如果缓存存储采用第三方存储或者自定义存储只将cache装饰为LoggingCache,未定义数据定期清除功能,淘汰过期数据功能;

    • 调用setStandardDecorators进行cache的参数设置;

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)        .implementation(valueOrDefault(typeClass, PerpetualCache.class))        .addDecorator(valueOrDefault(evictionClass, LruCache.class))        .clearInterval(flushInterval)        .size(size)        .readWrite(readWrite)        .blocking(blocking)        .properties(props)        .build();    configuration.addCache(cache);    currentCache = cache;    return cache;}
public Cache build() {    setDefaultImplementations();    Cache cache = newBaseCacheInstance(implementation, id);    setCacheProperties(cache);    // issue #352, do not apply decorators to custom caches    if (PerpetualCache.class.equals(cache.getClass())) {      for (Class<? extends Cache> decorator : decorators) {        cache = newCacheDecoratorInstance(decorator, cache);        setCacheProperties(cache);      }      cache = setStandardDecorators(cache);    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {      cache = new LoggingCache(cache);    }    return cache;}

2. 二级缓存的启用的条件

  • 缓存全局开关:在config.xml配置文件中,设定cacheEnabled属性的值为true;

  • select语句所在的Mapper.xml文件中,配置了或者标签;

  • 该select语句的参数useCache=true;

3. 二级缓存存在的问题

  • 二级缓存是以namespace为单位的,不同namespace下的操作互不影响;如果多个namespace同时操作一个表就会造成多个namepace下的缓存不一致从而出现脏数据;比如在一个namepace对一个表进行了update操作,而其他namespace没有刷新缓存就会造成脏数据;

  • 多表联合查询语句,命名空间不是同一个的话,一旦表有update操作就会出现数据未更新的脏数据现象;

select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;

如果上述语句的命名空间在MapperA中,如果tableB出现了update操作,命名空间MapperB会清空缓存而命名空间MapperA不会清空;如果再从MapperA查询就会出现脏数据;

(3)MyBatis一级缓存和二级缓存区别

  • 生命周期不同:一级缓存是Session级别的,一次会话结束就会被清空;二级缓存是Configuration级别的初始化时候创建;

  • 开启机制不同:一级缓存是默认支持的缓存用户不能进行定制;二级缓存用户需要手动开启

  • 存储机制不同:一级缓存存储在PerpetualCache中,而二级缓存存储默认存储在PerpetualCache中,也可以存储在第三方缓存和自定义缓存中;

(4)总结

MyBatis的缓存机制是为了进行减少消耗性能的数据库IO操作,先从二级缓存中查询是否存在相应的缓存,如果不存在从一级缓存中查询是否存在相应的缓存,如果不存在则从数据中查询,并把查询结果放入一级缓存和二级缓存中;

阅读全文
1 0
原创粉丝点击