Mybatis在开始使用一次数据库的时候,会创建一个新的SqlSession,简称一次会话。
在对数据库的一次会话中,有时候会反复快速地执行完全相同的查询语句,如果没一级缓存的话,每一次查询都会查询一次数据库,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。如果使用一级缓存的话,会将每次查询结果缓存起来,当下次查询的时候,会判断是否存在完全一样的查询,存在的话直接从缓存中将结果取出,返回给用户,不存在的话再去数据库中查,接着将查询结果缓存起来。
2. Mybatis一级缓存怎么存储的?
其实每次Mybatis的查询与更新都是Executor这个接口来完成, Executor的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,如下所示:
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++; //每次查询都会调用PerpetualCache的getObject()方法来验证先前是否有相同的查询 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(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; } //如果先前有相同的查询,会调用这个方法 private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) { if (ms.getStatementType() == StatementType.CALLABLE) { //调用PerpetualCache的getObject()的方法来获取相同查询的查询结果 final Object cachedParameter = localOutputParameterCache.getObject(key); if (cachedParameter != null && parameter != null) { final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter); final MetaObject metaParameter = configuration.newMetaObject(parameter); for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) { if (parameterMapping.getMode() != ParameterMode.IN) { final String parameterName = parameterMapping.getProperty(); final Object cachedValue = metaCachedParameter.getValue(parameterName); metaParameter.setValue(parameterName, cachedValue); } } } } }
所以Mybatis的一级缓存实际上是 PerpetualCache来实现,PerpetualCache其实也是靠HashMap来存储一级缓存,代码如下:
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); } //Mybatis的任何一个update操作(update()、delete()、insert()) ,都会 //调用这个方法,清空PerpetualCache对象存储的一级缓存数据 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(); } }3. 怎么判断查询是否相同?
PerpetualCache将每次查询的特征作为key,查询的结果作为value存储到Map中。所以看下key是怎么生成的,就知道怎么判断了。通过源码,发现key是通过BaseExecutor的createCacheKey的方法生成,代码如下:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); //statementId cacheKey.update(ms.getId()); //rowBounds的Offset() cacheKey.update(rowBounds.getOffset()); //rowBounds的Limit() cacheKey.update(rowBounds.getLimit()); //SQL语句 cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (ParameterMapping parameterMapping : parameterMappings) { 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); } } if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }从上面的代码中可以看出,如果下面条件一样,就可以判断为两个相同的查询,条件如下:
1、statementId ;2、RowBounds的offset、limit的结果集分页属性;3、SQL语句;4、传给JDBC的参数值
对了还有一点,添加一些无关的参数值(比如添加Student的age的参数,但查询条件没有age这个参数),还是相同的两个查询;只要statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就是两个相同的查询。