文章目录
1、mybatis二级缓存
1.1、介绍
在Mybatis中如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。
1.2、缓存源码分析
关系图
我们先来看下org.apache.ibatis.executor.CachingExecutor#query函数,这个是CachingExecutor对外的查询函数
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);
}
这边一开始获取BoundSql对象,然后创建了一个CacheKey对象,这个对象是用来在cache中唯一确定一个缓存项需要使用缓存项的key。
1.2.1、CacheKey类分析
这边我们先来思考下因为一般情况下标识一个key只需要一个String类型的对象即可,这边为什么需要创建一个复杂的类对象来CacheKey进行标识?原因是为了防止这个key值发生冲突,我们希望根据每一次查询,如果查询的是同一条sql语句,则可以返回缓存。
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}
首先来看下CacheKey的构造函数,可以看到他主要有四个属性,我们一般在hashmap中判断两个key是否相等是用了hashcode和equals,首先判断两个key的hashcode是否相等,如果相等再用equal去判断各个属性是否一致,如果都相等才能判定两个key是相同的。
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);
}
结合更新时的参数改变来看我们可以知道
- hashcode的默认值是17,他就是key的hashcode
- multiplier默认值是37,是一个乘法因子,他是获取更新后hashcode的一部分,hashcode = multiplier * hashcode + baseHashCode;作用也是为了防止冲突
- count标识更新的次数
- checksum表示所有更新对象baseHashCode的和,这个在构造函数里没有进行初始化,但也是CacheKey的参数
- updateList是一个list表,存放了更新的对象信息
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;
}
这边一开始判断这个key的对象是否为Object类型,java中只要是对象都是Object类型,也就是判断这个是否为一个java对象。然后判断这个key的类型是否是CacheKey类型。接着依次判断hashcode,checksum,count是否一致,最后再一次判断更新的对象是否一致。这样顺序是为了提高效率,先判断一些较小的数据是否相等,可以有效的提高判断效率。接下来再看下是如何创建CacheKey对象的,在这个函数里我们可以看到如果两个CacheKey相同必须要哪些数据相同。
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();
// 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;
}
上面可以看到调用cacheKey.update函数的地方就是影响一个CacheKey值的地方,说明判断的条件有
- Statement ID是否相等
- 返回结果的范围,具体对应的数值就是分页数据里的偏移量和返回结果的条数
- Sql语句是否相等
- 所有的入参的值,附加值,类型是否相等
1.2.2、CachingExecutor中调用TransactionalCache类分析
在了解CacheKey的用法后,在回到CachingExecutor类的query方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
/*获取在初始化时生成的cache值*/
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
/*判断二级缓存是否开启,我们对查询结果是否需要进行处理,在二级缓存开启后并且不需要对查询结果进行处理时可以直接返回缓存值*/
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
/*TransactionalCacheManager类中有cache和TransactionalCache的映射表,可以将一个cache和一个TransactionalCache对应起来*/
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
一开始会读取MappedStatement对象中的Cache,这个缓存是在初始化的时候配置的,这边小编读取的cache的信息如下
本质上是装饰器模式的使用,以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。
- SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
- LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
- SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
- LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
- ScheduledCache:Cache有期限,到期后会清除,类中有clearInterval和lastClear参数,在putObject,removeObject等一些基础操作中会先去判断现在的时间减去lastClear大于等于clearInterval,如果大于等于clearInterval,那么先会清除缓存,再进行原本的基础操作。
- PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
在读取到cache对象后判断其是否为空,在非空的情况下再判断是否配置中是否需要清除缓存,接着再判断二级缓存是否开启,是利用ms.isUseCache()来获取到是否开启的信息, 并且我们没有对查询结果做处理,也就是resultHandler == null,如果我们需要对查询结果处理,那就不能直接去缓存中的数据。然后利用TransactionalCacheManager存有Map<Cache, TransactionalCache>对象,我们可以根据一个cache找到其对应的TransactionalCache对象,TransactionalCacheManager中还有getObject可以根据CacheKey的值来找到TransactionalCache中存储缓存的value,这个CacheKey就是对应的TransactionalCache中存储缓存的key
如果缓存中没有找到,那么先是从数据库中获取过来,然后将值放入缓存中,那么下一次再查找的时候就可以直接从缓存中返回了。
1.3、二级缓存的隔离性分析
我们从上面可以看到获取对应的TransactionalCache需要从MappedStatement来获取相应的cache对象,而MappedStatement是唯一对应于每一个sql语句的,所以无论在哪一个sqlsession中,只要能够获取到MappedStatement对象,就可以通过CachingExecutor执行器来获取相应的缓存,这个缓存随着sql语句的改变会进行刷新。二级缓存可以使不同sqlsession之间互相读取缓存数据。
1.4、TransactionalCacheManager的事务性
我们在测试中可以发现,一个sqlsession更新数据后,另一个sqlsession去读取到的还是更新前的数据,原因就是第一个sqlsession没有进行commit。那么为什么没有进行commit我们就会读取不到缓存呢?
tcm.putObject(cache, key, list);
我们首先从这句语句分析,这个语句是CachingExecutor的query函数里的,tcm是TransactionalCacheManager,他调用putObject就是将从数据库中读取到的数据放入到缓存中,我们来看下他具体是如何操作的。
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
这个是根据cache获取到对应的TransactionalCache对象后调用TransactionalCache的putObject函数
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
/*TransactionalCache类的一部分参数*/
private final Cache delegate; //这个是TransactionalCache中最终存放缓存的地方
private boolean clearOnCommit;
private final Map<Object, Object> entriesToAddOnCommit; //这个是还没有commit前数据存放的地方
private final Set<Object> entriesMissedInCache;
可以看到这边TransactionalCache的putObject函数将数据不是直接放入到Cache对象中,而是放在了entriesToAddOnCommit这个参数对象中
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
而在TransactionalCache的getObject函数中可以看到获取缓存是从delegate中获取的,所以正常时获取不到的,那如何才能将数据从entriesToAddOnCommit移动到delegate对象中呢?那就是需要调用commit函数
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
在TransactionalCacheManager调用commit会将其内的Map<Cache, TransactionalCache>每一个TransactionalCache对象都调用commit函数,那么我们来看下TransactionalCache类的commit函数
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
可以看到在TransactionalCache的commit函数中会调用flushPendingEntries函数,这个函数中会将entriesToAddOnCommit的数据移动到delegate中。
2、mybatis一级缓存
2.1、缓存源码分析
上面介绍了mybatis的二级缓存,二级缓存可以在不同的sqlsession中互相使用缓存信息。在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,如果符合二级缓存的启动条件下首先会去查询二级缓存,如果二级缓存查询不到,然后会在查询数据库之前优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。
一级缓存的处理方式也二级缓存几乎一致,他是在BaseExecutor执行器中进行的,而SimpleExecutor,ReuseExecutor,BatchExecutor继承了BaseExecutor,自然也可以调用BaseExecutor中的函数
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的query函数中也是先创建CacheKey对象来生成key,这和二级缓存是一致的
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 {
/*查询数据库数据然后localCache参数中*/
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;
}
这边通过queryFromDatabase来查询数据库
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 {
/*doQuery函数在simple,resue和batch的执行器中都有各自的实现*/
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;
}
doQuery函数在BaseExecutor的子类中有各自的实现,会去数据库里获取结果,在这里会将获取到的结果放入到localCache这个PerpetualCache类中,PerpetualCache类非常简单,他只是简单的对一个HashMap进行了一点封装。并且在一级缓存中不需要再像二级缓存一样需要commit后才能获取到数据。这样在第二次获取时就可以去BaseExecutor的localCache中查找是否有相应的缓存。
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
在update函数里,doUpdate在BaseExecutor的子类中也有各自的实现,在执行真正的更新操作(调用doUpdate函数)之前,会先更新本地缓存,那么我们在更新操作过后就不能再获取到所有之前缓存的数据了。
2.2、一级缓存的隔离性分析
我们知道执行器是在构造sqlSession的过程中生成的。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
......
}
一个SqlSession对象中只有一个Executor的实现类,因为所有的执行器都是继承于BaseExecutor的,所以自然也会有BaseExecutor的数据,而BaseExecutor里存放了一级缓存localCache对象。所以如果换一个sqlSession对象,他其中的Executor的实现类自然也不是同一个了,自然缓存数据在不同SqlSession互相是读取不到的,所以一级缓存是只能在同一个sqlSession中才能共享的。
不同的sqlsession对象读取到的本地一级缓存都是不同的,所以不同的sqlsession对象不能读取到对象的一级缓存数据。