(0) MyBatis缓存
-
MyBatis缓存分类
-
一级缓存:一级缓存是SqlSession级别的缓存,在同一个会话session中对于相同的查询,会从缓存中返回结果而不是查询数据库;
-
二级缓存:二级缓存是Mapper级别的,定义在Mapper文件中标签并需要开启此标签;多个Mapper文件可以共用一个缓存,依赖标签配置;
-
(1)MyBatis一级缓存
1. 一级缓存的实现流程
- 缓存存在的意义是为了避免多次重复性的数据库查询IO操作,因此缓存执行流程的入口是查询操作;
@Override
public <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执行器中;
@Override
public <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,保证数据源相同;
-
@Override
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<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()方法尝试获取二级缓存,如果获取成功则直接返回结果,否则尝试获取一级缓存;
@Override
public <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")
@Override
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();
}
// 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来从缓存中取值;
-
如果没有缓存就从数据库中查询并将结果放入缓存中;
-
@Override
public <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操作,先从二级缓存中查询是否存在相应的缓存,如果不存在从一级缓存中查询是否存在相应的缓存,如果不存在则从数据中查询,并把查询结果放入一级缓存和二级缓存中;