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

来源:互联网 发布:tcp网络编程java 编辑:程序博客网 时间:2024/06/14 19:00


最近由于受工作中业务需要和现有工程中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,由于是只有在查询时才会去缓存里找,所以去找关于查询的代码:

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,这个以后有时间再仔细看看源码再写篇博客来讨论。

阅读全文
0 0