Mybatis的缓存
mybatis是一个查询数据库的封装框架,主要是封装提供灵活的增删改sql,开发中,service层能够通过mybatis组件查询和修改数据库中表的数据;作为查询工具,mybatis有使用缓存,这里讲一下mybatis的缓存相关源码。
缓存
在计算机里面,任何信息都有源头,缓存一般指源头信息读取后,放在内存或者其他读取较快的地方,下次读取相同信息不去源头查询而是直接从内存(或者能快速存取的硬件)读取。这样可以减少硬件使用,提高读取速度。
mybatis也是这样,查询数据库的数据之后,mybatis可以把查询结果缓存到内存,下次查询如果查询语句相同,并且查询相关的表的数据没被修改过,就可以直接返回缓存中的结果,而不用去查询数据库的语句,有效节省了时间。
关于mybatis中一级和二级缓存命名
缓存概念较早用于CPU读取数据,有一级和二级缓存,读取顺序是先一级缓存,再二级缓存。
按照这个概念,通过源码了解mybatis的Mapper中的缓存是一级缓存,SqlSession的中缓存是二级缓存。看到一些介绍mybatis缓存的相关文章命名反过来的,称SqlSession中的缓存称为一级缓存,对此有疑惑…
简单看一下mybatis缓存相关源码
Mapper中的缓存(一级)
mapper中的缓存,默认配置是开启,但需要在映射文件mapper.xml中添加<cache/>
标签
<mapper namespace="userMapper"> <cache/><!-- 添加cache标签表示此mapper使用缓存 --></mapper>
配置false可以关闭mapper中的缓存
mybatis: configuration: cache-enabled: false #默认值为true,表示开启
mapper缓存的解析
org.apache.ibatis.builder.xml.XMLMapperBuilder
private void configurationElement(XNode context) { try { //... cacheElement(context.evalNode("cache")); //解析mapper.xml中的cache标签 } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } }
private void cacheElement(XNode context) { if (context != null) { // if hava cache tag 如果有cache标签才执行下面的逻辑 String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);//建立mapper缓存 } }
org.apache.ibatis.builder.MapperBuilderAssistant.useNewCache()
:
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache);//mapper缓存赋值,如果cache标签为空,不会执行此方法,currentCache为空 currentCache = cache; return cache; }
在映射文件mapper中如果没有cache标签,解析时不会执行上面的useNewCache方法,cache为null,就不会使用mapper缓存(相当于失效)。
查询使用mapper缓存逻辑
org.apache.ibatis.executor.CachingExecutor
:
@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); //获取mapper缓存 if (cache != null) {//如果mapper缓存对象不为空 尝试在mapper缓存中获取(没有cache标签此对象就是空) flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); //从mapper缓存中获取数据 if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //如果为空,使用delegate查询(BaseExecutor) tcm.putObject(cache, key, list); // 查询结果保存到mapper缓存 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
上面代码中ms对象(MappedStatement )是在系统启动时创建的对象,cache也是,不与SqlSession绑定,所以SqlSession不同,mapper缓存依然可以使用,这区别于SqlSession中的缓存。
SqlSession中的缓存(二级)
通过查看源码可知,SqlSsession中是有缓存的,所以每次(新请求)会话SqlSession不同,缓存是空的;相同的SqlSession中的缓存才有效。
mybatis默认Sqlsession:org.apache.ibatis.session.defaults.DefaultSqlSession
构造方法中传入executor(查询执行对象)
public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) { this.configuration = configuration; this.executor = executor; this.dirty = false; this.autoCommit = autoCommit; }
executor中携带二级缓存成员变量:
protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<>(); this.localCache = new PerpetualCache("LocalCache"); //默认SqlSession中的缓存 this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }
查询使用SqlSession缓存逻辑
org.apache.ibatis.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());
List<E> list; try { queryStack++; //localCache SqlSession中的缓存 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; //先从SqlSession中的缓存中获取,key是通过sql语句生成 if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 如果缓存中没有 才从数据库查询 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } 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);//将SqlSession中的缓存清除 } localCache.putObject(key, list);//返回查询结果之前,放入SqlSession中的缓存 刷新 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
二级缓存和一级缓存不用想,数据库的数据被修改时要清空缓存,不然数据有误;至于怎么清空,是另一套逻辑,mapper中的cache标签可以配置一些参数,比如缓存定期清空。
一级二级缓存先后顺序
从概念上来将,先读取的就是一级缓存,后读取是二级缓存,那么Mapper中的缓存是一级缓存。
通过newExecutor源码可以知道这里使用了类似装饰者模式对executor进行了包装,在创建Executor的时候,逻辑如下
org.apache.ibatis.session.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); } if (cacheEnabled) {//如果使用缓存 executor = new CachingExecutor(executor);//创建CachingExecutor } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
//CachingExecutor构造 public CachingExecutor(Executor delegate) { this.delegate = delegate; //进行一层包装 delegate = BaseExecutor delegate.setExecutorWrapper(this); }
开启缓存,查询逻辑从SqlSession.selectList开始,先调用CachingExecutor对象
org.apache.ibatis.session.defaults.DefaultSqlSession.selectList()
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { //... //executor = CachingExecutor return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); }
1 CachingExecutor.query 中尝试读取mapper中的缓存(一级)
org.apache.ibatis.executor.CachingExecutor.query()
//1 Mapper cacheCache cache = ms.getCache();if (cache != null) { //... List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // delegate.query is BaseExecutor.query } return list; } }
2 BaseExecutor.query 中尝试读取SqlSession中的缓存(二级)
org.apache.ibatis.executor.BaseExecutor.query()
//2 SqlSession cachelist = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else { //3 DabaBase list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}
3 最后一级二级缓存都没有,就查询数据库