mybatis源码(九)mybatis一级缓存的使用

mybatis源码(九)mybatis一级缓存的使用

mybatis缓存分为两种:一级缓存和二级缓存

1.一级缓存:是sqlSession级别的缓存,同时mybatis的一级缓存不支持关闭

  例如通过<association>和<collection>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。

  a.mybatis一级缓存localCacheScope属性

    • SESSION :缓存对整个sqlSession有效。只有当执行了update语句的时候,缓存才会被清除
    • STATEMENT :缓存仅对当前执行的语句有效。当语句执行完毕后,缓存才会被清除

  SqlSession提供了面向用户的API,真正执行操作的是Executor组件。

  b.mybatis一级缓存实现原理:

  mybatis的一级缓存实现了Cache接口。里面维护了一个map集合。

  cache接口的源码如下:

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key Can be any object but usually it is a {@link CacheKey}
   * @param value The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * As of 3.3.0 this method is only called during a rollback 
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that 
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null 
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be 
   * available instead of hitting the database.
   *
   * 
   * @param key The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance
   */  
  void clear();

  /**
   * Optional. This method is not called by the core.
   * 
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();
  
  /** 
   * Optional. As of 3.2.6 this method is no longer called by the core.
   *  
   * Any locking needed by the cache must be provided internally by the cache provider.
   * 
   * @return A ReadWriteLock 
   */
  ReadWriteLock getReadWriteLock();

}
View Code

  mybaits一级缓存PerpetualCache类的源码如下,可以看到缓存就是一个map集合

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<Object, Object>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}
View Code

  Cache接口的实现类如下:

  

   Cache接口使用装饰者模式,可以对某些Cache进行增强,比如说如下测试代码

@Test
public void testCache() {
    final int N = 100000;
    Cache cache = new PerpetualCache("default");
    cache = new LruCache(cache);
    cache = new FifoCache(cache);
    cache = new SoftCache(cache);
    cache = new WeakCache(cache);
    cache = new ScheduledCache(cache);
    cache = new SerializedCache(cache);
    cache = new SynchronizedCache(cache);
    cache = new TransactionalCache(cache);
    for (int i = 0; i < N; i++) {
        cache.putObject(i, i);
        ((TransactionalCache) cache).commit();
    }
    System.out.println(cache.getSize());
}
View Code

  也可以通过CacheBuilder生成器模式创建缓存对象,代码如下:

@Test
public void testCacheBuilder() {
    final int N = 100000;
    Cache cache = new CacheBuilder("com.blog4java.mybatis.example.mapper.UserMapper")
            .implementation( PerpetualCache.class)
            .addDecorator(LruCache.class)
            .clearInterval(10 * 60L)
            .size(1024)
            .readWrite(false)
            .blocking(false)
            .properties(null)
            .build();
    for (int i = 0; i < N; i++) {
        cache.putObject(i, i);
    }
    System.out.println(cache.getSize());
}
View Code

  mybatis中一级缓存的执行代码如下:

  BaseExecutor的部分源码如下:

public abstract class BaseExecutor implements Executor {

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

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  // Mybatis一级缓存对象
  protected PerpetualCache localCache;
  // 存储过程输出参数缓存
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack;
  private boolean closed;

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

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

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取BoundSql对象,BoundSql是对动态SQL解析生成的SQL语句和参数映射信息的封装
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 创建CacheKey,用于缓存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 调用重载的query()方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);


  @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()); // Mapper Id
    cacheKey.update(rowBounds.getOffset()); // 偏移量
    cacheKey.update(rowBounds.getLimit()); // 条数
    cacheKey.update(boundSql.getSql()); // SQL语句
    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);
      }
    }
    // Environment Id
    if (configuration.getEnvironment() != null) {
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

  

从上面的代码中。缓存key可能与下面的因素有关
  1.mapper的id。由mapper的命名空间的+<select|insert|update|delete> 的id值组合在一起
  2.查询结果的偏移量和查询的条数
  3.具体的sql语句及sql语句中需要传递的参数
  4.mybatis的主配置文件中。通过<envienoment>标签配置的环境信息的对应的id的属性值

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

执行查询的方法

@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 {
        // 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
        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()方法清空缓存。

  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。

3.mybatis支持使用redis、ehcache作为二级缓存

原文地址:https://www.cnblogs.com/yingxiaocao/p/13587927.html