Mybatis二级缓存

  • 一级缓存是sqlsession级别的,不同sqlsession之间的缓存互不影响,而二级缓存实现了不同sqlsession之间的缓存共享。
  • 二级缓存是mapper级别的,同一个mapper的namespace相同,所以同一个mapper(或者namespace)下的查询共享一个二级缓存,不同mapper之间的二级缓存互不影响。
  • 如果配置了二级缓存,首先从二级缓存中查询,如果未找到再从一级缓存中查询。

二级缓存的配置方式

  1. mybatis的配置文件中开启二级缓存
1
<setting name="cacheEnabled" value="true"/>
  1. 在Mapper.xml中设置
1
2
3
4
5
6
7
<mapper namespace="mapper.StudentMapper">
<!-- 开启二级缓存 -->
<cache/>
<select id="getStudentById">
SELECT * FROM student WHERE id = #{id}
</select>
</mapper>
  1. 假如某个语句不想使用缓存,每次都需要从数据库查询最新的数据,那么通过useCache设置即可.
1
2
3
4
5
6
7
8
<mapper namespace="mapper.StudentMapper">
<cache/>
<!-- 配置useCache为false,禁用二级缓存 -->
<select id="getStudentById" useCache="false">
SELECT * FROM student WHERE id = #{id}
</select>

</mapper>
  1. 刷新缓存,同一个namespace下,insert、update、delete操作默认会刷新缓存,当然也可以通过设置flushCache=false不刷新缓存.
1
2
3
4
5
6
7
8
9
<mapper namespace="mapper.StudentMapper">
<cache/>

<!-- flushCache为false每次执行语句将不刷新缓存 -->
<update id="updateStudent" flushCache="false">
UPDATE student SET name = #{name} WHERE id = #{id}
</update>

</mapper>

Executor的生成

二级缓存使用CachingExecutor实现,看一下Executor的生成过程。

1.SqlSessionFactory

从SqlSessionFactoryBuilder说起,通过它的build方法构建SqlSessionFactory时返回一个DefaultSqlSessionFactory:

1
2
3
4
public SqlSessionFactory build(Configuration config) {
//创建一个DefaultSqlSessionFactory返回,它实现了SqlSessionFactory接口
return new DefaultSqlSessionFactory(config);
}

然后进入DefaultSqlSessionFactory,在它的openSeesion方法中又调用了openSessionFromDataSource方法,在此方法中,通过调用configuration的newExecutor方法创建了Executor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);//生成执行器
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
2. Configuration

Configuration的newExecutor方法中,根据Executor类型判断创建哪种执行器,默认是使用SimpleExecutor,如果开启了二级缓存,使用CachingExecutor对执行器进行装饰,完成Executor的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

缓存的使用过程

1. DefaultSqlSessionFactory

回到DefaultSqlSessionFactory的penSessionFromDataSource方法,该方法返回了一个DefaultSqlSession,那么进入DefaultSqlSession,以selectList方法为例,调用了Executor的query方法,由于开启二级缓存使用的是CachingExecutor,那么进入CachingExecutor查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
//调用了执行器的query方法
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();
}
}
2. CachingExecutor

CachingExecutor包含了两个成员变量分别为Executor类型的delegate和TransactionalCacheManager类型的tcm。
从CachingExecutor的构造函数中也可以看到,CachingExecutor使用的是装饰者模式,对基本的Executor实现类进行装饰。

1
2
3
4
5
6
7
8
9
10
public class CachingExecutor implements Executor {

private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);//装饰者模式
}
}

CachingExecutor的query方法中,由于CachingExecutor使用了装饰者模式对Executor实现者(delegate变量)进行了装饰,如果未开启二级缓存,将调用被装饰者delegate的query方法进行查询,接下来流程就与一级缓存的流程一致。如果开启了二级缓存,首先从缓存中获取数据,如果未获取到,同样调用delegate的query方法走一级缓存的流程,然后将结果放入到二级缓存中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建缓存key,一级缓存中提过如何创建CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//从MappedStatement获取缓存
Cache cache = ms.getCache();
//如果缓存不为空
if (cache != null) {
//是否需要刷新缓存
flushCacheIfRequired(ms);
//是否开启了useCache
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);//从缓存取数据
if (list == null) {
//如果为空,调用Executor中的query方法,走一级缓存的流程
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // 将数据放入缓存
}
return list;
}
}
//二级缓存为空,调用Executor中的query方法,走一级缓存的流程
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
3. Cache

首先,在query方法有一个从MappedStatement获取Cache对象的过程,MappedStatement是在MyBatis启动的时候存入Configuration中的,因此Mybatis的二级缓存的生命周期与应用的生命周期一致,应用不结束,二级缓存就会一直存在。

1
2
//从MappedStatement获取缓存
Cache cache = ms.getCache();

Cache只是一个接口,它有多个实现类,并且使用了装饰者模式进行层层包装,在构建Cache的过程中会提到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Cache {

String getId();

void putObject(Object key, Object value);

Object getObject(Object key);

Object removeObject(Object key);

void clear();

int getSize();

ReadWriteLock getReadWriteLock();

}
4. TransactionalCacheManager

从tcm缓存中获取数据的过程:

1
List<E> list = (List<E>) tcm.getObject(cache, key);//从缓存取数据

TransactionalCacheManager中使用了HashMap存储结构,key为Cache对象,value为TransactionalCache:

1
2
3
4
public class TransactionalCacheManager {

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
}

key:Cache对象,是一个接口,它会根据Mapper文件中配置的创建对应的实例。

比如使用Mybatis内置的cache:

1
<cache eviction="FIFO" flushInterval="10000" readOnly="true" />

value:TransactionalCache事物缓存,它也是Cache的一个实现类。

调用TransactionalCacheManager的getObject或者putObject方法时都会首先调用getTransactionalCache方法获取当前的Cache对应的TransactionalCache对象,如果没有将会实例化一个TransactionalCache对象,因为TransactionalCache是Cache的实现类,因此可以使用TransactionalCache继续装饰当前的Cache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TransactionalCacheManager {

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}

public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}

private TransactionalCache getTransactionalCache(Cache cache) {
//根据Cache获取TransactionalCache对象
TransactionalCache txCache = transactionalCaches.get(cache);
//如果获取为空
if (txCache == null) {
//创建一个TransactionalCache对象,并装饰当前的Cache对象的实现者
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
5. TransactionalCache

存取流程:

  • 当调用tcm.getObject时,实际上调用的就是TransactionalCache类的getObject()方法,由于TransactionalCache装饰了当前的Cache对象,索引调用getObject()职责会一层一层传递给PerpetualCache,然后获取缓存数据。TransactionalCache类还有一个clearOnCommit成员变量,表示使用事务提交时是否清空缓存,默认为false。

  • tcm.putObject()方法用于将数据放入缓存,实际上也是调用的TransactionalCache的putObject()方法,然后将数据放入到entriesToAddOnCommit待提交缓存数据中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class TransactionalCache implements Cache {

private static final Log log = LogFactory.getLog(TransactionalCache.class);

private final Cache delegate;
private boolean clearOnCommit;//事务提交时是否清空缓存
private final Map<Object, Object> entriesToAddOnCommit;//待提交的缓存数据
private final Set<Object> entriesMissedInCache;//统计命中率使用

public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false; //默认提交清除缓存为false
this.entriesToAddOnCommit = new HashMap<Object, Object>();
this.entriesMissedInCache = new HashSet<Object>();
}

/**
* 获取缓存
* @param key The key
* @return
*/
@Override
public Object getObject(Object key) {
// getObject方法中,会一路传递到PerpetualCache,然后获取数据
Object object = delegate.getObject(key);
//如果未获取到数据
if (object == null) {
//将数据放入集合,统计命中率使用
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}

/**
* 添加缓存数据
* @param key Can be any object but usually it is a {@link CacheKey}
* @param object
*/
@Override
public void putObject(Object key, Object object) {
//首先将数据放入到待提交缓存数据中
entriesToAddOnCommit.put(key, object);
}

//省去了其他方法

}

将数据放入缓存时首先将缓存放入到了待提交缓存中,所以如果不使用commit提交事务的时候,两次相同的查询,第二次并不会拿到第一次的缓存数据,因为第一次的数据被放在待提交的缓存数据中,相当于没有提交缓存数据,因此查询的时候也就取不到。

接下来就来看看DefaultSqlSession的commit方法,它调用了Executor实现类的commit方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void commit() {
commit(false);
}

@Override
public void commit(boolean force) {
try {
//调用了Executor实现类的commit方法
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

CachingExecutor的commit方法:

1
2
3
4
5
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();//调用了TransactionalCacheManager的commit方法
}

TransactionalCacheManager的commit方法:

1
2
3
4
5
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();//调用了TransactionalCache的commit方法
}
}

回到TransactionalCache类,查看它的commit方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();//清空待提交缓存数据
}

public void commit() {
//如果事务提交需要清空缓存
if (clearOnCommit) {
delegate.clear();//交给装饰的Cache对象清空缓存
}
//刷新缓存数据
flushPendingEntries();
//重置
reset();
}

private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();//清空待提交的缓存数据
entriesMissedInCache.clear();
}

private void flushPendingEntries() {
//遍历待提交的缓存数据
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
//委托给装饰的Cache类,将待提交的缓存数据加入缓存,会交给 PerpetualCache对象
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}

TransactionalCache的commit方法中,如果clearOnCommit为true,那么将会清空缓存数据,如果为false,本次需要提交的缓存数据将会委托给TransactionalCache装饰的Cache类,调用putObject方法,将数据加入到缓存中。


Cache的构建过程

1.XmlMapperBuilder

XmlMapperBuilder中的cacheElement方法首先或获取缓存的配置信息,然后根据配置信息调用MapperBuilderAssistant的useNewCache方法创建缓存对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}
2.MapperBuilderAssistant

MapperBuilderAssistant中的useNewCache方法中使用建造者模式创建了Cache对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
3.CacheBuilder

CacheBuilder的build方法中,首先调用了setDefaultImplementations()将PerpetualCache作为默认的实现类,完成Cache的实例化。接着会判断当前创建的实例是否是PerpetualCache的实例,如果是调用setStandardDecorators方法对PerpetualCache进行装饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public Cache build() {
setDefaultImplementations();//设置默认的Cache实现类
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;
}

private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;//使用PerpetualCache作为默认的实现类
if (decorators.isEmpty()) {
decorators.add(LruCache.class);//如果装饰器为空,添加LruCache装饰器,对Cache对象进行装饰
}
}
}

在setStandardDecorators中可以看到装饰链:

BlockingCache -> SynchronizedCache -> LoggingCache -> SerializedCache -> ScheduledCache-> LruCache -> PerpetualCache。

其中LruCache是在setDefaultImplementations()中设置的。

通过装饰链可以明白为什么在TransactionalCache添加或者获取缓存的时候最终会从PerpetualCache调用。
一级缓存也是使用PerpetualCache实现,二级缓存实际上就是通过装饰者模式对PerpetualCache进行层层包装,所以从二级缓存中未获取到数据时,还可以走一级缓存的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
//如果clearInterval不为空,使用cheduledCache装饰
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
//使用SerializedCache装饰
cache = new SerializedCache(cache);
}
//使用LoggingCache装饰
cache = new LoggingCache(cache);
//使用SynchronizedCache装饰
cache = new SynchronizedCache(cache);
if (blocking) {
//使用BlockingCache装饰
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}

参考:

凯伦:聊聊MyBatis缓存机制

五月的仓颉:【MyBatis源码解析】MyBatis一二级缓存

Double-Eggs:【Mybatis框架】查询缓存(二级缓存)

零度anngle:MyBatis缓存策略之二级缓存

jtlgb:mybatis 缓存(cache)的使用