mybatis专栏 https://blog.csdn.net/worn_xiao/category_6530299.html?spm=1001.2014.3001.5482
一 Mybatis查询缓存
mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能。
Mybaties缓存模型
一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
1.1 一级缓存:
如上图所示:第一次查询的时候,根据用户id查询用户信息,先查询缓存,在一级缓存没有查询到用户信息,从数据库查询,查询到用户信息之后,放入缓存进行存储。
如果中途有crud操作进行,就清空缓存,防止出现脏读的情况。
如上图所示:第二次以相同的id查询用户信息时,先查询缓存,如果缓存中已经有了,就直接返回,提高查询效率,如果缓存中没有的话,就查询数据库,走相同的过程。
测试一级缓存
public void testOneLevelCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
User user1 = mapper.findUserById(1);
System.out.println(user1);
// 第二次查询ID为1的用户
User user2 = mapper.findUserById(1);
System.out.println(user2);
sqlSession.close();
}
如图所示两次查询只输出了一条查询语句。
@Test
public void testOneLevelCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
User user1 = mapper.findUserById(1);
System.out.println(user1);
User user = new User();
user.setUsername("东哥1");
user.setAddress("清河宝盛西里");
//执行增删改操作,清空缓存
mapper.insertUser(user);
// 第二次查询ID为1的用户
User user2 = mapper.findUserById(1);
System.out.println(user2);
sqlSession.close();
}
如上图所示:由于插入操作的时候把缓存清空了,所以发送了两条SQL语句进行查询。
正式开发中:
将mybatis和spring进行整合开发,事务控制在service中。
一个service方法中包括很多mapper方法调用。
service{
//开始执行时,开启事务,创建SqlSession对象
//第一次调用mapper的方法findUserById(1)
//第二次调用mapper的方法findUserById(1),从一级缓存中取数据
//方法结束,sqlSession关闭
}
如果是执行两次service调用查询相同的用户信息,不走一级缓存,因为session方法结束,sqlSession就关闭,一级缓存就清空。
1.2 二级缓存:
第一次调用mapper下的sql去查询用户信息时,查询结果会存放在二级缓中。
第二次调用相同的namespace下的mapper映射文件去执行相同的sql时,会去对应的缓存中取结果。
如果调用相同的namespace下的mapper进行增删改的话就会清空二级缓存。
开启二级缓存。
1、 在核心配置文件SqlMapConfig.xml中加入以下内容(开启二级缓存总开关):
在settings标签中添加以下内容:
<!-- 开启二级缓存总开关 -->
<setting name="cacheEnabled" value="true"/>
2. 在UserMapper映射文件中,加入以下内容,开启二级缓存:
<!-- 开启本mapper下的namespace的二级缓存,默认使用的是mybatis提供的PerpetualCache -->
<cache></cache>
由于二级缓存的数据不一定都是存储到内存中,它的存储介质多种多样,所以需要给缓存的对象执行序列化。
如果该类存在父类,那么父类也要实现序列化。
@Test
public void testTwoLevelCache() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
// 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
User user1 = mapper1.findUserById(1);
System.out.println(user1);
// 关闭SqlSession1
sqlSession1.close();
// 第二次查询ID为1的用户
User user2 = mapper2.findUserById(1);
System.out.println(user2);
// 关闭SqlSession2
sqlSession2.close();
}
如上图所示,两个不同的mapper去取相同的命名空间,查询二级缓存,并且产生了缓存命中率。那么如何去理解什么是一级缓存什么是二级缓存呢,我们可以认为事物级的缓存,在同一个事务以内的缓存是一级缓存.跨事务的缓存是二级缓存.如下图所示是mybatis的缓存示意图
我们知道,MyBatis分为一级缓存和二级缓存。一级缓存是会话(session)级别的,二级缓存是应用(application)级别的。但是,MyBatis并不是简单地对整个Application就只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper级别的,即每一个Mapper都可以拥有一个Cache对象,具体如下:
(1)为每一个Mapper分配一个Cache缓存对象(使用<cache>节点配置或者 @CacheNamespace注解 );
(2)多个Mapper共用一个Cache缓存对象(使用<cache-ref>节点配置或者本文所提到的@CacheNamespaceRef注解);
好的,接下来我们说一下Mybaties的缓存原理,在说缓存原理之前。我建议大家先去看看我过去发的文章关于 Mybaties初始化,执行流程,插件原理等等的。主要还是看一下执行流程吧。
好的相信看过我写的执行流程文章的同学,看到executor都不陌生吧。那么我们来看看Executor的具体创建过程
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);
//创建一个Executor
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();
}
}
首先在我们openSession开启会话的时候,实际上是调用了openSessionFromConnnection这个方法,那么这个方法做了什么呢,实际上对于我么还说就是,主要构建了数据库的环境,封装了一层事务。然后创建了一个执行器。然后把这个执行器放到了DefaultSqlSession中。
那么我们来看一看2部分的代码。
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;
}
如上代码所示是创建执行器的代码,我们可以看到cacheEnabled这个开启二级缓存的参数。实际上它是配置在Mybaties-config.xml文件中的。决定是否开启二级缓存。那么开启二级缓存以后实际上就是在执行器的上层包装了一个缓存执行器。那好这个包装呢其实就是一个装饰者模式的实现过程。那么大家可以在BaseExecutor中找到一级缓存,PerpeCache实际上就是对Hashmap的封装。那么为什么一级缓存是SqlSession级别的缓存呢。因为每open一个SqlSession都要new 一个Executor.而每个Executor各自实例化是一个缓存类。
这个图呢,是我之前执行流程中的一个图,如图所示是执行器,包装了缓存执行器的过程。那其实按照我们对装饰者模式的理解留下来的。好的装饰者模式做了什么。
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, 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);
}
实际上它是在查询执行之前。包装了一层缓存的查询。那么好的。让我们来看看缓存查询的代码。可以看到这里呢有一个cache用来做判空用了。那么好的接下来看看实际的delegate做了什么
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);
}
Delegate就是对执行器的封装。实际就是被装饰的组合对象。也就是我们的三个主要执行器。这个是在开启二级缓存的时候,用来做二级缓存用的。如果不开启二级缓存呢。二级缓存就是我们说的mapper级别的缓存。可以跨越不同的会话。具体的没有回调输出参数等等的细节可以自己去看了。可以看到ms.getCache()拿到二级缓存。那么这个二级缓存是什么时候构建的呢。为什么所有的会话都走这个缓存呢。其实在Mybatis初始化构建Configuration的时候就已经实例化好了Cache存放到了Configuration和mapperstatemate中了,这个时候只是拿出来用而已。
好的接下来我们看看如果不走二级缓存。一级缓存的代码是什么。一级缓存也就是我们说的SqlSession级别的缓存。那么是在哪里呢。
接下来我们从delegate.query这里往下看
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;
}
这里从localCache中获取结果集,没有再查询数据库。那么是什么时候放进去的呢。
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 {
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查询到结果集。然后放入到localCache的缓存中。那么好了我们知道了实际上一级缓存就是localCache缓存。二级缓存就是基于localCache之上的自定义缓存。如果开启了二级缓存会先走二级缓存。再走一级缓存。好了接下看看一级缓存是什么吧。
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
//一级缓存本质上是一个本地缓存 内部实际是一个hashMap
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
嗯哼实际上它是一个PerpetualCache()的缓存。接下俩让我们看看这个缓存是什么缓存。
public class PerpetualCache implements Cache {
private final String id;
//一级缓存实际是一个本地的hashMap缓存
private Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
}
这回我们看到了。实际上它是一个有唯一表示符的hashmap缓存。好了一级缓存解密完成。
一级缓存的基本方法如上图所示。好了一级缓存很简单。就是一个基本的本地Hashmap级别的SqlSession级别的缓存。
一般事务回滚的时候呢,都会清理一级缓存。这个是清理缓存的方法。接下来让我们回到二级缓存的地方。
让我们来看看ms.getCache()这里是个什么缓存。
实际上它是在构造器模式,构造mapperStatement的时候就封装好了的缓存。
再追溯可以看到实际上是在Configuration初始化就根据名称空间装配好的缓存。也就是说mapper级别的缓存。
一路跟代码发现它是在Mybaties初始化的时候通过mapper文件创建封装好的.
那么我们点进去发现。它是通过CacheBuilder构建出来的一个缓存。可以看到这里可以设置一些缓存时间,缓存大小,是还可以指定原生的缓存装饰的缓存。那么可以看到实现的基本缓存就是PerpetualCache就是最基本的缓存。用LruCache做了装饰。
接下来我们看看二级缓存的和兴构建类。首先是一个基本的implementation的缓存。然后
是一个把最基本的缓存通过decorators这个装饰的缓存不断的进行装饰。最后调用setStardardDecorator(cache)做最后的装饰。来看看这个方法。
简直就是经典。那么问题来了MyBaties的二级缓存定时清除怎么实现的。一个简单的HashMap怎么设置过期时间呢。不可能的所以定时清除绝对不会是一个属性。雷士一个ScheduledCache的缓存装饰类。可以看到我们最终build出来放到StatementMentents中的缓存是一个经过重重定制包装好的缓存。实现了过期时间等等的缓存策略。
然后build好之后放回到MapperBuilderAssistant这个类中。好的那么接下来我们看看这个类做了什么。
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
封装了MappedStatement.Builder构建了一个MappedStatement有了这个包装好了的缓存。