最近由于受工作中业务需要和现有工程中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();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
public class PerpetualCache implements Cache {
private String id;
private Map
是不是有点惊讶,就这点代码,内部其实就是一个HashMap来简单实现的,回想一下,常说session的一级缓存是线程不安全的,看到这里就有点恍然了,HashMap可不就是线程不安全的吗,类中的方法也基本上是对hashmap的操作,id属性其实一个标识。。比如上面的代码中new PerpetualCache("LocalCache"),仅仅就是告诉你这是本地的一级缓存。好,既然涉及到了hashmap,那么就不得不想到如何保证键即key的唯一性,也由于查询的一些东西存在key中,结果存在value里,也进而要问,对啊,你一级缓存不就是为了让我重复查的时候直接从缓存里取呗,那你怎么判断两次查询是完全相同的查询?带着疑问,我们继续看BaseExecutor,由于是只有在查询时才会去缓存里找,所以去找关于查询的代码:
public List 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);
}
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 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
以上基本上就是mybatis的session一级缓存的基本实现原理,当然除了存取缓存之外,mybatis还提供了如clearLocalCache()这样的接口方法来手动清除缓存。在了解了上面的原理之后,其实mybatis的一级缓存还是有不少不完善的地方,比如这个在数据库数据通过别的途径发生更改时,缓存不能做到更新,所以数据的操作最好只能在一个session中,还有前面说的session线程不安全的问题,不过这个在spring整合时得到了解决,spring注入了一个线程安全的session,这个以后有时间再仔细看看源码再写篇博客来讨论。