深入理解MyBatis(六)—MyBatis的缓存机制
来源:互联网 发布:灵犀一动知乎 编辑:程序博客网 时间:2024/06/06 11:08
深入理解MyBatis(六)—MyBatis的缓存机制
频繁的数据库查询操作是非常耗费性能的额,因为数据库查询底层依靠文件存储机制的IO操作,而IO操作的速度相比于内存作要慢几个量级,因此更好的解决方案是把相同的查询语句的结果存储在内存中,下次再查询时直接从内存中读取;
个人主页:tuzhenyu’s page
原文地址:深入理解MyBatis(六)—MyBatis的缓存机制
(0) MyBatis缓存
MyBatis缓存分类
一级缓存:一级缓存是SqlSession级别的缓存,在同一个会话session中对于相同的查询,会从缓存中返回结果而不是查询数据库;
二级缓存:二级缓存是Mapper级别的,定义在Mapper文件中标签并需要开启此标签;多个Mapper文件可以共用一个缓存,依赖标签配置;
(1)MyBatis一级缓存
1. 一级缓存的实现流程
- 缓存存在的意义是为了避免多次重复性的数据库查询IO操作,因此缓存执行流程的入口是查询操作;
@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); }}
- sqlSession将具体的sql操作委托给Executor执行器,缓存信息也被维护在Executor执行器中;
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
cacheKey的定义
- 一级缓存存储在BaseExecutor对象中的localCache属性中,localCache的实现类是perpetualCache,其底层是用HashMap存储缓存对象,CacheKey对象作为HashMap的key,缓存对象作为HashMap作为value;因此CacheKey对象的hashcode将决定存储位置;
public class CacheKey implements Cloneable, Serializable { private static final long serialVersionUID = 1146682552656046210L; public static final CacheKey NULL_CACHE_KEY = new NullCacheKey(); private static final int DEFAULT_MULTIPLYER = 37; private static final int DEFAULT_HASHCODE = 17; private int multiplier; private int hashcode; private long checksum; private int count; private List<Object> updateList; public CacheKey() { this.hashcode = DEFAULT_HASHCODE; this.multiplier = DEFAULT_MULTIPLYER; this.count = 0; this.updateList = new ArrayList<Object>(); } public CacheKey(Object[] objects) { this(); updateAll(objects); } public int getUpdateCount() { return updateList.size(); } public void update(Object object) { int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object); } public void updateAll(Object[] objects) { for (Object o : objects) { update(o); } } @Override public boolean equals(Object object) { if (this == object) { return true; } if (!(object instanceof CacheKey)) { return false; } final CacheKey cacheKey = (CacheKey) object; if (hashcode != cacheKey.hashcode) { return false; } if (checksum != cacheKey.checksum) { return false; } if (count != cacheKey.count) { return false; } for (int i = 0; i < updateList.size(); i++) { Object thisObject = updateList.get(i); Object thatObject = cacheKey.updateList.get(i); if (!ArrayUtil.equals(thisObject, thatObject)) { return false; } } return true; } @Override public int hashCode() { return hashcode; } @Override public String toString() { StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum); for (Object object : updateList) { returnValue.append(':').append(ArrayUtil.toString(object)); } return returnValue.toString(); } @Override public CacheKey clone() throws CloneNotSupportedException { CacheKey clonedCacheKey = (CacheKey) super.clone(); clonedCacheKey.updateList = new ArrayList<Object>(updateList); return clonedCacheKey; }}
调用createCacheKey()方法创建查询语句的唯一标示cacheKey,创建cacheKey主要依据以下几个条件:
MappedStatement的id也就是select标签所在mapper文件的namespace+select的id相同
MappedStatement中的boundSql中的sql语句相同
RowBounds的offset属性和limit()属性相同;
遍历输入参数列表必须满足每个参数相同;
获取Environment的id,保证数据源相同;
@Overridepublic 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 (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) { cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey;}
- 调用update()方法将唯一性判断条件加入cache对象中,并根据每个条件的hashcode更新cacheKey的hashcode的值
public void update(Object object) { int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); count++; checksum += baseHashCode; baseHashCode *= count; hashcode = multiplier * hashcode + baseHashCode; updateList.add(object);}
根据cacheKey获取缓存
- 调用query()方法尝试获取二级缓存,如果获取成功则直接返回结果,否则尝试获取一级缓存;
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
- 调用query()重写方法尝试获取一级缓存,如果失败则从数据库中查询
@SuppressWarnings("unchecked")@Overridepublic <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(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list;}
2. 一级缓存的生命周期
MyBatis创建数据库会话sqlSession时,会初始化Executor执行器,Executor对象初始化过程中会创建PerpetualCache对象作为一级缓存;
当会话结束也就是调用session.close()方法时,会释放Executor对象,同时也会释放PerpetualCache对象;一级缓存不可用;
数据库会话调用clearCache()方法,会清空PerpetualCache对象,对象仍可用;
sqlSession中执行了update操作(update,insert,delete)都会清空PerpetualCache对象;
3. 一级缓存的性能
MyBatis的一级缓存简单的采用HashMap来存储缓存对象,没有对HashMap的容量大小进行限制,如果一直使用同一个session进行查询操作,可能会出现OOM错误;MyBatis不对HashMap大小进行限制的原因是session存在的时间较短,同时只要进行update操作缓存就会被清空,另外可以通过clearCache()方法手动清空缓存;
一级缓存是一种粗粒度的缓存机制,没有过期机制同时一旦执行updata操作所有的缓存都将被清空;
MyBatis认为的完全相同的查询,不是指使用sqlSession查询时传递给算起来Session的所有参数值完完全全相同,你只要保证statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就可以了。
(2)MyBatis二级缓存
1. 二级缓存的实现流程
二级缓存的入口在上文提到的query()方法;二级缓存读取在CachingExecutor类中,一级缓存的读取在BaseExecutor中;二级缓存的存取优先级高于一级缓存;
尝试从MappedStatement中获取cache对象,只有使用标签或者标签标记使用缓存的Mapper.xml或Mapper接口才会有二级缓存,即cache对象不为空;
根据sql操作的flushCache属性来确定是否清空缓存;
根据sql操作的useCache属性来确定时候使用缓存;
根据上面生成的cacheKey来从缓存中取值;
如果没有缓存就从数据库中查询并将结果放入缓存中;
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, parameterObject, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}
二级缓存的初始化
根据Mapper.xml配置文件初始化缓存标签cache和cache-ref
type属性是设置为”PERPETUAL”是指缓存存储方式使用PerpetualCache类,底层由HashMap实现;
eviction属性设置为”LRU”是指缓存容量管理算法采用LRU算法即最近最少使用算法;
- eviction属性主要包括LRU最近最少使用算法,FIFO先进先出算法,Scheduled时间间隔清空算法
private void cacheElement(XNode context) throws Exception { if (context != null) { 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); }}
根据属性的设置创建缓存
判断缓存存储是否为PerpecutalCache,如果是则采用装饰器模式装饰cache,给PerpecutalCache加上LRU功能;如果缓存存储采用第三方存储或者自定义存储只将cache装饰为LoggingCache,未定义数据定期清除功能,淘汰过期数据功能;
调用setStandardDecorators进行cache的参数设置;
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); currentCache = cache; return cache;}
public Cache build() { setDefaultImplementations(); Cache cache = newBaseCacheInstance(implementation, id); setCacheProperties(cache); // issue #352, do not apply decorators to custom caches if (PerpetualCache.class.equals(cache.getClass())) { for (Class<? extends Cache> decorator : decorators) { cache = newCacheDecoratorInstance(decorator, cache); setCacheProperties(cache); } cache = setStandardDecorators(cache); } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) { cache = new LoggingCache(cache); } return cache;}
2. 二级缓存的启用的条件
缓存全局开关:在config.xml配置文件中,设定cacheEnabled属性的值为true;
select语句所在的Mapper.xml文件中,配置了或者标签;
该select语句的参数useCache=true;
3. 二级缓存存在的问题
二级缓存是以namespace为单位的,不同namespace下的操作互不影响;如果多个namespace同时操作一个表就会造成多个namepace下的缓存不一致从而出现脏数据;比如在一个namepace对一个表进行了update操作,而其他namespace没有刷新缓存就会造成脏数据;
多表联合查询语句,命名空间不是同一个的话,一旦表有update操作就会出现数据未更新的脏数据现象;
select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;
如果上述语句的命名空间在MapperA中,如果tableB出现了update操作,命名空间MapperB会清空缓存而命名空间MapperA不会清空;如果再从MapperA查询就会出现脏数据;
(3)MyBatis一级缓存和二级缓存区别
生命周期不同:一级缓存是Session级别的,一次会话结束就会被清空;二级缓存是Configuration级别的初始化时候创建;
开启机制不同:一级缓存是默认支持的缓存用户不能进行定制;二级缓存用户需要手动开启
存储机制不同:一级缓存存储在PerpetualCache中,而二级缓存存储默认存储在PerpetualCache中,也可以存储在第三方缓存和自定义缓存中;
(4)总结
MyBatis的缓存机制是为了进行减少消耗性能的数据库IO操作,先从二级缓存中查询是否存在相应的缓存,如果不存在从一级缓存中查询是否存在相应的缓存,如果不存在则从数据中查询,并把查询结果放入一级缓存和二级缓存中;
- 深入理解MyBatis(六)—MyBatis的缓存机制
- 《深入理解mybatis原理(六)》 MyBatis缓存机制的设计与实现如何细粒度地控制你的MyBatis二级缓存
- 深入理解mybatis原理(六) MyBatis缓存机制的设计与实现如何细粒度地控制你的MyBatis二级缓存
- 《深入理解mybatis原理(六)》 MyBatis缓存机制的设计与实现如何细粒度地控制你的MyBatis二级缓存
- 深入理解MyBatis(五)—MyBatis的插件机制
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理(五)》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 深入理解mybatis原理(五) MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- SSM 之《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- 《深入理解mybatis原理》 MyBatis缓存机制的设计与实现
- git分布式版本控制系统
- Refletion2017.9.10
- 53. Maximum SubArray(divide and conquer)
- C++ 标准库 vector类型
- Python OpenCV获取视频
- 深入理解MyBatis(六)—MyBatis的缓存机制
- HDU6170 Two strings(动态规划)
- Gradle学习系列之十——自定义Plugin
- C++对象切割
- java异常·超出打开游标最大值
- 推送
- 阶乘
- 1. Two Sum
- 数据库总结之字段语法