Mybatis解析-缓存原理解析

本文基于mybatis-spring 1.3.1和mybatis 3.4.4版本

mybatis提供了两级缓存,一个在事务内部使用的一级缓存,另一个可以全局使用的二级缓存。

一、一级缓存

一级缓存是在SqlSession内部使用,也就是一级缓存的最大有效范围只能在事务内部。可以使用参数“localCacheScope”设置一级缓存, 一共有两个值:SESSION和STATEMENT。默认值为SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,每次查询完之后都会清空一级缓存,使用STATEMENT相当于关闭了一级缓存。
下图是查询的执行流程:
在这里插入图片描述
每个SqlSession实例中都有一个Executor对象,Executor对象执行查询操作。
Executor有多个实现,无论哪种实现,查询的时候都会执行父类BaseExecutor的query方法。

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();
      }
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
		//如果设置localCacheScope=STATEMENT,则清空缓存
        clearLocalCache();
      }
    }
    return list;
  }
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      //查询数据库
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    //将数据库查询结果放入一级缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

查询结果保存到BaseExecutor的localCache属性,localCache便是一级缓存。

	protected PerpetualCache localCache;

下面来看一下PerpetualCache类。

public class PerpetualCache implements Cache {
  //id表示该缓存的名字
  private String id;
  //保存数据库的查询结果
  private Map<Object, Object> cache = new HashMap<Object, Object>();
}

PerpetualCache将数据库的查询结果保存在HashMap的value中。
一级缓存其实是使用HashMap实现的。
接下来看一下缓存的key是怎么创建的。
缓存的key使用CacheKey表示。mybatis使用createCacheKey创建CacheKey对象。

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接口的全限定类名+方法名
    cacheKey.update(rowBounds.getOffset());//查询数据的偏移量
    cacheKey.update(rowBounds.getLimit());//查询条数
    cacheKey.update(boundSql.getSql());//原始SQL语句
    //下面处理SQL语句的入参,将入参放入cacheKey
    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) {
      //将Environment的id放入cacheKey中
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

CacheKey集合了SQL语句、SQL参数、调用的Mapper方法名、SQL分页参数、Environment对象信息。当比较两个CacheKey是否相等时,上述这些数据都要参与比较,而且CacheKey对象哈希值也是基于这些数据生成的。因此CacheKey重写了hashCode(),equals()。
下面看一下cacheKey.update方法就可以明白hashCode(),equals()如何做的了。

  //每次向CacheKey添加数据时,都要调用该方法
  public void update(Object object) {
  	//计算入参对象的hash值
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
    
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    //计算CacheKey对象哈希值
    //hashCode()方法返回的就是hashcode值
    //multiplier = 37
    hashcode = multiplier * hashcode + baseHashCode;
	//updateList是List对象,用于保存原始数据,比较两个CacheKey是否相等,要比较updateList中的数据
    updateList.add(object);
  }

最后分析一下一级缓存何时清理。
一级缓存的清理时间点如下:

  1. 事务提交
  2. 事务回滚
  3. 执行insert/delete/update方法。清空缓存是在执行这些方法前。
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);
  }

二、二级缓存

二级缓存默认是关闭的,开启二级缓存需要两步操作。

  • 设置cacheEnabled=true,默认是true;
  • 如果是注解配置,需要在Mapper接口的类上添加注解@CacheNamespace;如果是XML配置,在XML映射文件中添加“<cache/>”,该标签位于<mapper/>标签下。不同namespace使用的缓存实例不同,可以使用<cache-ref/>标签引用其他namespace的缓存实例。比如:
<cache-ref namespace="mapper.StudentMapper"/>

mybatis为cache标签提供了多个参数:

  • type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
  • eviction: 定义回收的策略,常见的有FIFO,LRU。
  • flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
  • size: 最多缓存对象的个数。
  • readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
  • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

缓存使用Cache接口表示,Cache接口有多个实现类:
在这里插入图片描述
这些类之间嵌套装饰,形成一个嵌套链条,共同实现cache标签的设置。比如FIFO类型的回收策略就是由FifoCache实现的。这个链条的最后一个对象是PerpetualCache。PerpetualCache在一级缓存已经介绍过,其内部是一个Map对象,所以二级缓存与一级缓存一样缓存数据也是存储在map对象中。
二级缓存是由CachingExecutor处理的。
首先来看一下如何创建CachingExecutor,下面代码是SqlSession实例创建过程:

private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      //代码删减
      final Transaction tx = transactionFactory.newTransaction(connection);
      //创建Executor实例,newExecutor方法见下方
      final Executor executor = configuration.newExecutor(tx, execType);
      //创建SqlSession实例
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
 //下面是configuration.newExecutor方法的代码
 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    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);
    }
    //如果设置cacheEnabled为true,则创建CachingExecutor对象装饰其他Executor对象
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

因为CachingExecutor包装了其他Executor对象,而且位于最外层,因此SqlSession调用Executor查询数据库时,首先会调用到CachingExecutor中的方法。
下面是CachingExecutor中的查询方法:

 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //创建CacheKey对象,作为缓存的key,一级缓存中已经介绍过
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    //调用查询方法
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    //检查映射文件是否配置了<cache/>标签
    if (cache != null) {
      //如果是增删改,flushCacheIfRequired将清空缓存
      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) {
          //如果缓存中没有对应数据,则执行下层Executor对象
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //将数据库返回结果放入缓存
          tcm.putObject(cache, key, list); 
        }
        return list;
      }
    }
    //如果没有配置<cache/>标签,直接调用下层Executor对象
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

查询时首先检查二级缓存,当缓存中不存在时,才查询数据库。
从上面代码也可以看到执行增删改,会清空对应namespace下的二级缓存。
二级缓存与一级缓存还有一点不同,一级缓存只要查询结束就将结果放入缓存中,下次查询时就可以使用了,而二级缓存是将查询结果放入临时Map中,临时Map对查询不可见,当事务提交时,才将临时Map数据放入缓存中。

三、总结

  1. MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  2. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  3. 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

四、引用

聊聊MyBatis缓存机制:https://tech.meituan.com/2018/01/19/mybatis-cache.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值