浅谈Mybatis中session的一级缓存的实现原理


最近由于受工作中业务需要和现有工程中dao层非orm思想的影响,觉得在有些业务场景下,并不一定非要去使用ORM框架,毕竟写大量的实体类也是一件麻烦的事,于是着手编写一个非ORM框架。初步完成后,底层的session并没能像mybatis那样能支持session的一级缓存(虽然在和Spring整合之后,Mybatis的session的一级缓存并没起什么作用),so,通过看源码大致了解一哈Mybatis中session的一级缓存实现。

Mybatis是一个很轻量也很强大的ORM框架(这并不影响我学习来开发非ORM框架),但她的一级缓存的实现则不是那么复杂。首先我们知道,Mybatis在每次开启数据库会话时,都会创建一个sqlsession对象,下面看一段默认sessionfactory创建默认session时的代码

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
上述代码是DefaultSessionFactory通过数据源创建session的方法,另一种通过Connection创建session的方式与此大同小异,我们可以看到,创建session需要executor这个对象,那Executor这个接口是干嘛的呢?executor就是执行器,用于预处理语句,然后再调用底层的statementHandler去执行sql语句。executor下有两个实现类BaseExecutor、CatchingExecutor,在创建session中,主要使用BaseExecutor的子类对象,BaseExecutor有四个子类:BatchExecutor、SimpleExecutor、RuseExecutor、ClosedExecutor,而一级缓存的初始化工作是在BaseExecutor这个抽象父类中进行的,看一下BaseExecutor的构造方法:

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

在构造方法中localCache变量就是我们的主角:一级缓存,Mybatis的session的一级缓存实际上就是由PerpetualCache这个类来维护的,那PerpetualCache何许人也呢,我们来看看他的真面目:

public class PerpetualCache implements Cache {

  private String id;

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

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

  public String getId() {
    return id;
  }

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

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

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

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

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

  public ReadWriteLock getReadWriteLock() {
    return null;
  }

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

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

}

是不是有点惊讶,就这点代码,内部其实就是一个HashMap来简单实现的,回想一下,常说session的一级缓存是线程不安全的,看到这里就有点恍然了,HashMap可不就是线程不安全的吗,类中的方法也基本上是对hashmap的操作,id属性其实一个标识。。比如上面的代码中new PerpetualCache("LocalCache"),仅仅就是告诉你这是本地的一级缓存。好,既然涉及到了hashmap,那么就不得不想到如何保证键即key的唯一性,也由于查询的一些东西存在key中,结果存在value里,也进而要问,对啊,你一级缓存不就是为了让我重复查的时候直接从缓存里取呗,那你怎么判断两次查询是完全相同的查询?带着疑问,我们继续看BaseExecutor,由于是只有在查询时才会去缓存里找,所以去找关于查询的代码:

扫描二维码关注公众号,回复: 920662 查看本文章
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

我们看到有这样一句话:CacheKey key = createCacheKey(..);顾名思义,这就是创建缓存那个map的key的方法啊,再点进去看:

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 (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
      ParameterMapping parameterMapping = parameterMappings.get(i);
      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);
      }
    }
    return cacheKey;
  }  


我们看到定义了一个CacheKey对象,并调用update方法,再点进去看下CacheKey中的update方法:

public void update(Object object) {
    if (object != null && object.getClass().isArray()) {
      int length = Array.getLength(object);
      for (int i = 0; i < length; i++) {
        Object element = Array.get(object, i);
        doUpdate(element);
      }
    } else {
      doUpdate(object);
    }
  }

  private void doUpdate(Object object) {
    int baseHashCode = object == null ? 1 : object.hashCode();

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

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

在这个类中,还有两个属性,就是hashcode和updateList,hashcode就是这个key的hashcode,而updateList则保存影响hashcode计算的条件,从上面可以看到有ms.getId()、getOffSet()、getLimit()、getSql()、parameter,即mybatis的mapper对应的statement的id、分页的范围、sql语句、设置的参数,即判断两次查询是相同的查询的条件就是以上四个条件,通过这四个条件来生成key,对应查询的结果,在查询的时候通过list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;来获取。

     以上基本上就是mybatis的session一级缓存的基本实现原理,当然除了存取缓存之外,mybatis还提供了如clearLocalCache()这样的接口方法来手动清除缓存。在了解了上面的原理之后,其实mybatis的一级缓存还是有不少不完善的地方,比如这个在数据库数据通过别的途径发生更改时,缓存不能做到更新,所以数据的操作最好只能在一个session中,还有前面说的session线程不安全的问题,不过这个在spring整合时得到了解决,spring注入了一个线程安全的session,这个以后有时间再仔细看看源码再写篇博客来讨论。

猜你喜欢

转载自blog.csdn.net/wetsion/article/details/78398755
今日推荐