在之前的文章里面介绍了MyBatis启动过程的调用分析,MyBatis启动流程源码分析,这篇文章介绍了MyBatis当中调用所涉及到的类。这篇文章主要是从源码来分析下MyBatis当中如何实现一级缓存,二级缓存的。
关于一级缓存和二级缓存, 一级缓存是在一个SqlSession会话当中,每次执行完查询之后,会把数据缓存到session当中,如果第二次进行查询的话,就会直接从session的缓存当中获取数据,而不会去查询数据库。二级缓存是一种全局作用域的缓存,在整个应用开启运行状态期间,都是可以使用的。
一级缓存
一级缓存是MyBatis提供的,对于一级缓存默认是开启的SESSION级别的,可以在Setting文件当中进行配置,也可以设置成STATEMENT, 默认是SESSION,对于STATEMENT是这样的,在我们每次执行SQL之前都会将sql进行预编译,然后再去执行,如果我们设置的缓存级别是STATEMENT的,可以理解为缓存只对当前sql有效,session当中的缓存每次查询之后就会被清空。
<setting name="localCacheScope" value="SESSION"/>
在我们每次执行sql时,都会调用sqlSession当中的executor, 而在Executor内部维护了一个localCache。一级缓存就是从localCache当中获取数据的。 Executor当中保存的是,PrepetualCache,该类继承自Cache类,并且内部维护了一个HashMap,用来保存每次执行的查询结果。
由于一级缓存是存在于每个Session内部的,因此,如果我们创建了不同的session,那么不同的session使用不同的缓存,加入在session1当中getById,在session当中再次执行getById,session2会直接从数据库当中查询。
接下来我们来看一级缓存的代码实现,当我们执行selectList查询操作时,这里SqlSession使用的是DefaultSqlSession调用selectList方法,只截取部分代码
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
SqlSession将具体的查询调用Executor,我们设置的默认的executor,采用的SimpleExecutor, 这里会先调用BaseExecutor当中的query方法,我们来看下实现
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);
}
在BaseExecutor当中会根据当前查询sql的MappedStatement, 参数等信息生成一个缓存key,然后再调用自身当中的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(); // issue #601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}
1、首先会判断queryStack是否为0,如果是0的话,并且当前的sql设置的缓存失效,那我们就会把session当中的缓存map清空
2、从localCache当中根据sql的cacheKey查询是否存在,如果查询到的结果集为null,去数据库当中查询
3、在数据库当中查询之后,会将数据库当中查询的数据保存在localCache。
我们看下数据库查询逻辑
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;
}
1、先将key,和一个枚举类型放在localCache当中
2、doQuery是调用的SimpleExecutor当中的doQuery,进行查询,最终调用StatementHandler执行的查询操作
3、移除之前放的key,和枚举类型
4、将查询结果放入到localCache当中。
如果我们在一级缓存当中执行update,insert,delete操作都会将localCache清空,也就是,会使一级缓存失效。Session当中的localCache和Session的生命周期是一致的,如果现在SessionA进行一次查询,SessionB进行一次查询,分别都将数据保存在了各自的session当中,这个时候,如果sessionA更新数据,然后再使用sessionA进行查询,会直接从数据库当中查询,而SessionB自始至终都只执行过一次查询,我们再执行查询时,并不会从数据库当中查询,而是从sessionB的缓存当中获取,就会造成数据脏读。当我们在使用分布式架构时,不要把一级缓存设置成SESSION.
二级缓存
如何开启二级缓存,在配置文件下添加,这个是全局性配置
<setting name="cacheEnabled" value="true"/>
也可以在需要开启缓存的特定查询下设置
<cache/>
cache标签当中可以设置一些缓存参数
eviction 定义回收策略,FIFO,LRU
flushInterval 多久进行刷新
size:最多缓存多少个对象
readOly:设置为只读
blocking:如果缓存当中没有,是否会一直阻塞,直到缓存当中有数据
关于二级缓存的正确打开姿势:
每个命名空间内的session共用一个缓存,二级缓存是在缓存提交或者关闭后才会生效,换句话说,如果一个session查询到的记录,另外一个session也要查询相同的记录,那么需要第一个session提交或者关闭后,才会将查询到的数据保存到缓存,这样第二个session才可以查询到。
二级缓存的工作机制,如果查询一个数据,会先从当前命名空间下的二级缓存当中查询,如果查询不到,就在当前session所处的一级缓存当中查询,如果还是查询不到,就会直接查询数据库。
如果执行了update,insert, delete等修改操作的话,就会使缓存失效,再次查询时就会从数据库当中查询
如果不同的命名空间下,都有相同的查询sql,当我们使用不同的session进行查询时,即使已经提交了session,这两个不同命名空间下的查询,第二个查询也是需要从数据库当中查询,当然可以通过<cache-ref >来阴柔命名空间,这样这两个命名空间就共用了一个缓存。
如果我们将所有的命名空间都进行共用,这样的话,任何一个命名空间下的修改操作,都会造成缓存失效,这样缓存的存在也没有太大的意义。
接下来进入到源码分析阶段,上篇文章当中我们在分析Executor时,就看到如果开启了缓存,不管你配置是哪种类型的executor,都会将配置的executor嵌套到CachingExecutor当中
CachingExecutor当中在执行query类查询时,会从MappedStatement当中获取到cache对象,MappedStatement当中的cache获取到的就是Configuration当中的cache,也就是在解析配置文件时候,创建出来cache对象,我们来看下创建的Cache对象
在MapperBuilderAssistant类当中
这个过程就是将我们设置的一些缓存参数赋值进来,并创建一个Cache对象,将cache保存在configuration当中,并且设置当前缓存。
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) { // issue #352, do not apply decorators to custom caches
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
}
return cache;
}
1、设置默认的缓存实现接口,设置实现接口是PerpetualCache, 装饰类是LruCache
2、根据当前命名空间,即id,还有PerpetualCache创建一个Cache类的对象。通过反射获取到Constructor对象,然后调用newInstance。
3、将我们之前设置好的参数赋值进去
4、遍历装饰类接口,将装饰类都包装在PerpetualCache外面,并初始化装饰类
5、再次设置装饰类的属性参数
6、这一步是基于我们之前配置的是否定时刷新,是否为只读,来添加装饰类缓存,同时会在最外面添加上LoggingCache和SynchronizedCache
总结下来创建缓存的过程就是在Cache的实现类PerpetualCache的外面一层层嵌套cache的实现类。
那么当我们在调用Cache的时候,就是从最外层包装Cache类,一直到最内层的过程。其中红色的部分,如果没有配置只读和定时刷新毫秒数的就就没有。
SynchronizedCache---->LoggingCache----->SerializedCache------>ScheduledCache-------->LruCache-------->PerpetualCache
以上是Cache的构建过程,接下来我们看下在查询时候,如果使用的。
在CachingExecutor内部当中,持有了一个TransactionCacheManager对象,在TransactionCacheManager对象内部持有一个HashMap,key是Cache对象,value是transactionalCache。
当我们查询时,如果配置了cache,并且使用了缓存,那么就会通过transactionCacheManager获取到Cache对象,如果通过cache查询出来的TranscationCache为null,那么会把根据当前cache对象,也就是SynchronizedCache作为参数,创建一个TranscationCache,然后放入到TranscationCacheManager的hashMap当中。
我们在查询的时候,会从SynchronizedCache(保证线程安全)当中开始,然后到delegate的cache,即loggingCache, 每个装饰cache除了包装上自己的逻辑之外,都会调用代理的cache进行查找,最终调用PerpetualCache,这个cache当中保存了一个hashMap,也就是我们每次查询出来的结果存放的地方。
接下来我们来看下为什么在每个session提交或者关闭之后,其他的session在进行查询时才会从缓存当中获取。
接着上面的CachingExecutor当中的query查询,
如果在TranscationCacheManager当中查询不到结果的话,就会从数据库当中查询,然后将查询到的结果,放在TranscationCacheManager当中,我们开看下这个步骤
直接进入到TranscationalCache当中
@Override
public void putObject(Object key, Object object) {
entriesToRemoveOnCommit.remove(key);
entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
}
这个TranscationalCache当中保存了需要移除的map,以及需要新增加到缓存当中的map,并没有将查询到的结果放入到我们上面所说的缓存当中。
我们来看下在DefaultSqlSession当中,当执行commit操作时,会触发哪些动作。
接下来到CachingExecutor当中
这里会调用到TranscationCacheManager当中的commit方法
获取到TransactionalCaches当中的每个TransactionalCache并将其提交
接下来,这个地方就是我们之前放入到TransactionalCache当中的,在这里对entry当中的元素进行提交。
在这里才真正把我们之前从数据库当中查询到地数据放入到缓存当中,之前只是将查询到的数据暂时保存起来。
总结:二级缓存确实比一级缓存来说更加细粒度一些,但是也有不足,如果在不同的命名空间下,针对于同一张表的操作,也是会造成脏读。在分布式事务下,每个应用当中如果都有自己的二级缓存,那么势必会造成数据的脏读,因此建议还是讲缓存的数据放在redis中。
最后补充一点关于预编译问题:
在mysql当中可以通过对url增加开启预编译的配置,预编译可以防止sql注入,
"jdbc:mysql://127.0.0.1:3306/demo?useServerPrepStmts=true&cachePrepStmts=true";
在我们每次使用完PrepareStatement之后,将其关闭,就会将之前预编译的sql缓存起来,这样当同样的sql再次执行,只是参数不同时,便不会再进行预编译,这样执行可以提升sql执行的效率。