Mybatis缓存源码详解
示例代码地址:https://gitee.com/cq-laozhou/mybaits-source-code-analyzer
Mybatis内部使用2级缓存便于加速数据的查询,降低数据库的查询压力。
一级缓存 LocalCache
在Mybatis的执行流程中,首先去查找二级缓存,如果二级缓存中没有,再去一级缓存找,如果还没有,最后从数据库查询。
一级缓存示例
一级缓存逻辑相对简单,首先以一个简单的示例,来看看一级缓存带来的效果:
public void testLocalCacheSuccess(){
SqlSession sqlSession = getSqlSession();
SysUserMapper sysUserMapper = sqlSession.getMapper(SysUserMapper.class);
SysUser sysUser = sysUserMapper.selectById(1L);
SysUser sysUser1 = sysUserMapper.selectById(1L);
//在相同的sqlSession生命周期中,同样的查询方法,同样的参数,则命中1级缓存,返回相同的对象。
System.out.println(sysUser == sysUser1);
}
输出结果为true,同时只往DB发送了一次查询语句,因此一级缓存生效。
一级缓存默认是开启的,并且不能关闭,在同一次查询会话中如果出现相同的语句及参数,就会从缓存中取出不在走数据库查询。1级缓存只能作用于查询会话中 所以也叫做会话缓存。
不过想要一级缓存命中,需要满足下面的一系列的条件:
- 必须是相同的SqlSession,即相同的会话
- 必须是相同的Mapper类型(并不一定是相同的Mapper实例)
- 必须是相同的方法和参数(这个好理解,查的东西是一样的才能用缓存嘛)
- 查询语句的中间不能有写操作的语句,比如插入、更新、删除(这些写操作为让一级缓存全部清空)
具体案例见:
CacheTest的testLocalCacheWithDiffSqlSession、testLocalCacheWithDiffMapper、testLocalCacheWithDiffMethod、testLocalCacheWithDiffParameter、testLocalCacheWithWriteStatement等测试方法。
一级缓存实现源码
接下来,通过源码来分析下一级缓存的实现:
在前面的例子中,当执行sysUserMapper.selectById(1L);
语句后,最终会调用到 DefaultSqlSession.selectList方法上来,至于是如何调过来的,后面解析整个执行流程中再分析,目前单独看下一级缓存的实现:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//从configuration中获取MappedStatement。configuration封装了mybaits配置信息,而每条sql语句封装在MappedStatement中。
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();
}
}
在构建DefaultSqlSession实例时,executor为CachingExecutor,并使用装饰者模式,内部默认包装了SimpleExecutor(至于怎么构建的,同理在执行流程中分析),如下图所示:
在CachingExecutor.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) {
//从缓存中没有获取到结果,则调用内部的被装饰的Executor执行查询。
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将查询结果放入临时缓存中(TransactionalCache.entriesToAddOnCommit)最终session在commit的时候在放入二级缓存。
tcm.putObject(cache, key, list); // issue #578 and #116
}
//返回查询结果。
return list;
}
}
//没有获取到二级缓存对象,直接调用内部的被装饰的Executor执行查询,相当于使用二级缓存。
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
调用内部的Executor执行查询,会调用SimpleExecutor.query方法,这儿又使用了模板方法模式,在父类BaseExecutor中封装了query的整体执行流程, 而在子类SimpleExecutor中,封装了某些步骤的具体算法。先分析下BaseExecutor.query方法:
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//执行前的一些准备工作
...
List<E> list;
...
//从一级缓存localCache中获取结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//获取到了,进行一些其他特殊的处理
....
} else {
//没获取到,从DB中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
//一些其他处理
...
//返回结果
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中放置一个占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//这儿调用子类(即SimpleExecutor)的doQuery方法,执行正在的数据库查询。
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//将localCache中的占位符移除
localCache.removeObject(key);
}
//往localCache中放入查询结果
localCache.putObject(key, list);
....
return list;
}
SimpleExecutor.doQuery方法在执行流程中详解,现在只需要知道,它就是真正执行sql语句获取结果的。
这个localCache是PerpetualCache这种类型的cache,其实现非常简单,内部用了一个map来装缓存数据的
public class PerpetualCache implements Cache {
private String id;
//缓存数据存放在这个map中。
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
//放入缓存
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
//从缓存中获取
@Override
public Object getObject(Object key) {
return cache.get(key);
}
//移除缓存
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
//清空缓存
@Override
public void clear() {
cache.clear();
}
现在我们可以回答下,要想享受一级缓存带来的福利,为什么要满足下面这些条件了?
-
为什么必须是相同的SqlSession,即相同的会话
这是因为localCache对象是封装在SimpleExecutor对象中,这个有被DefaultSqlSession引用,因此localCache的活动范围和生命周期与Sqlsession一致,不同的sqlsession拥有不同的localCache对象。
-
为什么必须是相同的Mapper类型?
-
为什么必须是相同的方法和参数?
这两个问题,涉及到缓存key,看下缓存key是如何生成的, 在BaseExecutor.createCacheKey方法中:
CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); //语句ID,包括mapper的namespace和方法名称 cacheKey.update(rowBounds.getOffset()); //分页的偏移量 cacheKey.update(rowBounds.getLimit()); //分页的限制 cacheKey.update(boundSql.getSql()); //sql语句 ... for (ParameterMapping parameterMapping : parameterMappings) { //value为参数值 cacheKey.update(value); //所有参数值 } } if (configuration.getEnvironment() != null) { //环境ID cacheKey.update(configuration.getEnvironment().getId()); //环境ID } return cacheKey;
可以看出,缓存key的构建由一系列的变量控制,其中有任何一个不一样,生成的key就不同,那么自然就不会命中缓存了。这种是必须要有相同的mapper(其实要相同的namespace),相同的方法,相同的参数,相同的sql语句,分页参数也要一样,如果有环境的话,环境也要一样。
-
查询语句的中间不能有写操作的语句,比如插入、更新、删除(这些写操作为让一级缓存全部清空)
为了回答这个问题,需要看下执行写操作时,会发生什么?首先,先明确一点,DefaultSqlSession暴露出来的insert、update、和delete方法,实际上内部都是调用的update方法,比如:
//插入方法 public int insert(String statement, Object parameter) { return update(statement, parameter); } //删除方法 public int delete(String statement) { return update(statement, null); }
而update方法滴调用时是Executor的update方法:
public int update(String statement, Object parameter) { try { dirty = true; MappedStatement ms = configuration.getMappedStatement(statement); return executor.update(ms, wrapCollection(parameter)); } catch (Exception e) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
先忽略二级缓存(CachingExecutor),那么直接分析BaseExecutor的update方法:
public int update(MappedStatement ms, Object parameter) throws SQLException { .... //清空一级缓存LocalCache clearLocalCache(); //在执行子类的(SimpleExecutor)的doUpdate方法,执行真正的数据库更新。 return doUpdate(ms, parameter); }
可以看出,update方法一进来,二话不说,直接将一级缓存全部清空,所以在两次查询中间,夹杂着写操作,那么后面的查询是永不了缓存的。
二级缓存 Cache
一级缓存让我们在同一个SqlSession中,对多次执行同一个查询时,不用重复查询数据库,提高了查询效率。而二级缓存可让我们在跨SqlSession中命中缓存,提高查询效率,降低数据库查询压力。
二级缓存示例
同样以一个示例来体验下二级缓存:
@Test
public void testL2Cache(){
SqlSession sqlSession = getSqlSession();
try {
SysRoleMapper mapper = sqlSession.getMapper(SysRoleMapper.class);
SysRole sysRole1 = mapper.selectById2(1); //第一次查询,结果放入一级缓存中
SysRole sysRole2 = mapper.selectById2(1); //同一个sqlsession的第二次查询,此时二级缓存还没有,会走到一级缓存中,并命中
assertEquals(sysRole1, sysRole2); //L1Cache.
}finally {
sqlSession.close(); //将一级缓存中的结果,放入到二级缓存中。
}
sqlSession = getSqlSession(); //开启另外一个sqlsession
try {
SysRoleMapper mapper = sqlSession.getMapper(SysRoleMapper.class);
SysRole sysRole3 = mapper.selectById2(1); //直接命中2级缓存
System.out.println(sysRole3.getRoleName());
SysRole sysRole4 = mapper.selectById2(1); //再次命中2级缓存
assertEquals(sysRole3, sysRole4);
}finally {
sqlSession.close();
}
}
注意,二级缓存默认是没有的,需要配置上才能使用,xml配置和注解配置是一致的,这儿以xml为例:
<!ELEMENT cache (property*)> -- 子元素用于配置缓存实现类所需要的属性
<!ATTLIST cache
type CDATA #IMPLIED -- 缓存具体实现类,默认为PerpetualCache
eviction CDATA #IMPLIED -- 缓存逐出策略,默认为Lru,即最近最少使用。
flushInterval CDATA #IMPLIED -- 缓存过期时间,默认为0,即不过期。
size CDATA #IMPLIED -- 缓存大小,默认1024。注意该配置不是指缓存的对象数量,而是查询次数。
readOnly CDATA #IMPLIED -- 是否只读。默认false,即缓存实例获取后,会不会去修改。mybatis会根据此配置来决定返回给你的对象是否是个新对象(通过反序列化给你一个全新的对象,你对这个对象的任何操作不会污染到缓存)
blocking CDATA #IMPLIED -- 是否阻塞,默认false。用于解决缓存击穿时,对数据库带来瞬时压力问题。
>
比如,示例的配置:
<cache eviction="FIFO" size="512" readOnly="true"/>
二级缓存实现源码
二级缓存的实现相对于一级缓存来说,有些许复杂,不过掌握了其核心思想和脉络后,也比较清晰。
在前面一级缓存的分析过程中,我们简单的看了下二级缓存的处理流程,位于CachingExecutor.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) {
//从缓存中没有获取到结果,则调用内部的被装饰的Executor执行查询。
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将查询结果放入临时缓存中(TransactionalCache.entriesToAddOnCommit)最终session在commit的时候在放入二级缓存。
tcm.putObject(cache, key, list); // issue #578 and #116
}
//返回查询结果。
return list;
}
}
//没有获取到二级缓存对象,直接调用内部的被装饰的Executor执行查询,相当于使用二级缓存。
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
对于二级缓存来说,关键的代码就是从二级缓存获取结果tcm.getObject(cache, key);
以及将结果保存到二级缓存中tcm.putObject(cache, key, list);
。
在进入关键的代码之前,先热下身,了解下二级缓存的范围和生命周期,在mybatis框架初始化时,会解析配置并生成Configuration对象,该对象是单例的,应用级的生命周期。其有一属性 为protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
,这个map会在初始化过程中被填充,以namespace为key,而value为一系列的装饰者装饰过后的配置的cache实现类实例。
核心代码为,MapperBuilderAssistant.useNewCache方法逻辑:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//构建一个新的Cache
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中,也就是上面说的那个map,key就是currentNamespace。
configuration.addCache(cache);
currentCache = cache;
return cache;
}
看下CacheBuilder.build方法:
public Cache build() {
//默认使用PerpetualCache。
setDefaultImplementations();
//反射实例化具体的实现
Cache cache = newBaseCacheInstance(implementation, id);
//反射设置cache的相关属性
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
//如果实现类是PerpetualCache,使用一系列的装饰者装饰。注意如果是自定义的cache实现,不会被装饰。
if (PerpetualCache.class.equals(cache.getClass())) {
//使用指定的装饰者装饰,比如示例中定义的eviction="FIFO"配置,这儿会使用fifocache来装饰
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
//使用标准的装饰者装饰
cache = setStandardDecorators(cache);
}
//返回装饰好的cache对象。
return cache;
}
那么标准的装饰者,又装饰些啥上去了? 看下CacheBuilder.setStandardDecorators方法:
private Cache setStandardDecorators(Cache cache) {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
//配置了clearInterval,则使用ScheduledCache装饰一波
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
//如果可读写(非只读),则使用SerializedCache装饰一波
if (readWrite) {
cache = new SerializedCache(cache);
}
//使用LoggingCache装饰一波
cache = new LoggingCache(cache);
//再使用SynchronizedCache装饰一波
cache = new SynchronizedCache(cache);
//如果blocking为true,则使用BlockingCache装饰。
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
..
}
二级缓存的这块实现算是它的精髓了,如果想了解装饰者设计模式,可参考 https://blog.csdn.net/gruelxsp/category_9399300.html 中的 装饰者模式 说明。
比如,对于示例的配置,包装出来的cache长这个样子:
好了,热身结束,回到 从二级缓存获取结果tcm.getObject(cache, key);
以及将结果保存到二级缓存中tcm.putObject(cache, key, list);
这两个操作上来。
首先,tcm是TransactionalCacheManager示例,它在实例化CachingExecutor时,会示例化出来
private TransactionalCacheManager tcm = new TransactionalCacheManager();
而TransactionalCacheManager实现非常简单,其主要作用管理当前这个执行器中会用到的二级缓存,并用TransactionalCache这个装饰一波,代码如下:
public class TransactionalCacheManager {
//内部使用TransactionalCache包装过的map。
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
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);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
//如果map中有了就直接用,没有的话就new一个TransactionalCache来装饰。
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
因此tcm.getObject(cache, key);
调动实际上会调用TransactionalCache的getObject方法:
@Override
public Object getObject(Object key) {
//直接从被代理的cahe中获取(也就是前面分析的configuration中的对应的cache)
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
而tcm.putObject(cache, key, list);
这个方法会调用到TransactionalCache的putObject方法:
//这儿紧紧是放到了TransactionalCache的entriesToAddOnCommit的map中,并不是真正的放到二级缓存中
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
那么,何时会放到真正的二级缓存中呢?
在SqlSession执行commit或者close方法是,会调用CachingExecutor的commit或者close方法
public void commit(boolean force) {
...
executor.commit(isCommitOrRollbackRequired(force));
...
}
public void close() {
...
executor.close(isCommitOrRollbackRequired(false));
...
}
CachingExecutor的commit或者close方法,会调用tcm.commit();
方法:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
最终会调用到 TransactionalCache 的 flushPendingEntries 方法,这儿才是真正的将加过放到二级缓存中。
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
...
}
为什么不是在前面就放到二级缓存中,而是要使用TransactionalCache来中间做一层临时存储,而在sqlsession关闭或者提交时才将临时存储中的结果放到二级缓存中?
这个问题的答案就可在回滚逻辑中看出点端倪,以前面同样的思路分析,sqlsession的rollback会调用CachingExecutor的rollback方法,然后调用tcm.rollback();
,最终会调用到TransactionalCache的方法:
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear(); //清空临时存储结果。
entriesMissedInCache.clear();
}
可以看到,在rollback这种情况下,是不会往二级缓存中存放查询结果的。这个也好理解,如果回滚的情况下也往二级缓存存放的话,会存在其他sqlsession查询时命中查询结果,而这个查询结果是应该被回滚的,造成脏读。
比如:
在sqlsession1中:执行一条更新语句—> 查询该条结果 --> 放入二级缓存 --> 回滚
在sqlsession2中: 查询同样的记录—> 命中缓存 --> 返回结果(而这个结果在数据库中是被回滚的。
最后,当执行update方法时,即所有的写操作时,和一级缓存一样,会清空对应namespace的二级缓存,源码见CachingExecutor.update方法:
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
flushCacheIfRequired方法逻辑:
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) { //isFlushCacheRequired,非select方法默认为true。
tcm.clear(cache); //清空二级缓存。
}
}
到此,完整分析了Mybatis的缓存实现,整体还是容易理解,其精髓是多处使用了装饰者模式,让每个类的职责更加清晰,实现也更容易。
当然,本文没有详细分析具体的cache和这些装饰者的具体实现,这些可以留给你自己去分析下。