java web之mybatis(六)----缓存机制

(0) MyBatis缓存

  • MyBatis缓存分类

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

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

(1)MyBatis一级缓存

1. 一级缓存的实现流程

  • 缓存存在的意义是为了避免多次重复性的数据库查询IO操作,因此缓存执行流程的入口是查询操作;
@Override
public <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执行器中;
@Override
public <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,保证数据源相同;

@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();
  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()方法尝试获取二级缓存,如果获取成功则直接返回结果,否则尝试获取一级缓存;
@Override
public <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")
@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;
}
  •  

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来从缓存中取值;

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

@Override
public <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操作,先从二级缓存中查询是否存在相应的缓存,如果不存在从一级缓存中查询是否存在相应的缓存,如果不存在则从数据中查询,并把查询结果放入一级缓存和二级缓存中;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值