mybatis二级缓存
二级缓存的使用
mybatis 二级缓存不默认开启,需要手动配置,因为二级缓存是mapper级别的,所以不同的mapper可以使用不同的缓存策略。
二级缓存开启需要三个步骤
-
config.xml中需要配置,这个配置默认为true所以可以省略:
<settings> <!-- 开启二级缓存 这个配置默认为true 可省略--> <setting name="cacheEnabled" value="true" /> </settings>
上述配置对应的源码解析和使用:
像核心配置文件中的配置肯定是在mybatis第一步操作,只有解析完配置文件才能后续的操作,而且mybatis解析完都会把配置存到Configuration对象中,这里对缓存配置的代码如下图: XMLConfigbuilder:
根据代码显示默认配置就是true,所以这里的配置是可以省略的。
那在哪里用到了这个配置呢?
SqlSessionFactory在的openSession时候,会实例化一个Executor对象,如下图:
在上图代码中当cacheEnabled为true,就会实例化一个CachingExecutor对象(这里用到了装饰器的设计模式,对于设计模式这里不做赘述了),这个CachingExecutor就是对缓存使用的执行器。
-
Mapper.xml中:
#默认使用mybatis提供的二级缓存实现 <cache/> <!-- 使用redis实现来作为二级缓存的 实现--> <!-- <cache type="org.mybatis.caches.redis.RedisCache"></cache>- ->
下文会细致解析如何解析并使用这个cache标签的。
- 定制化二级缓存
这个可以根据需求针对某一条sql语句来配置二级缓存。
<select id="findOne" resultType="user" parameterType="int" useCache="true" flushCache="true">
select *
from user where id = #{id}
</select>
userCache: 这个字段表示可以个个性化给sql配置来启用二级缓存,默认查询是开启的,增删改默认不开启。
flushCache:这个字段表示是否在每次查询前都要刷新二级缓存,查询默认不开启,增删改开启。
下面说一下关于这两个属性的源码解析和使用:
与第一步的思路一样,肯定是在解析配置文件的时候,把这两个属性加载到了内中,在解析的标签代码中,也就是在创建MapperStatement中(因为我们知道一个select|update|delete|insert就是一个MapperStatement对象),把这两个属性初始化了: XMLStatementBuilder:
那么在何时使用的这个属性呢?
通过第一步我们的解析发现,在执行sql的过程中,它会走CachingExecutor对象,所以自然我们看一下这个类的query方法:
从上图中可以看出,在执行sql前会判断是否需要刷新缓存或者使用缓存。
综上可以看出对于mybatis二级缓存的使用,只需要在对应Mapper.xml文件中设置你要想用缓存类型(就是使用第三方缓存还是mybatis内置缓存)即可,其他的配置都是默认的,当然如果有需要定制化配置。
mybatis如何对Mapper.xml中的cache标签进行解析的?
mybatis在创建SqlSessionFactoryBuilder的时候就开始了对配置文件的解析工作,SqlSessionFactoryBuilder的build方法:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//通过对配置文件的字节流进行解析,调用其parse方法进行解析。
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
紧接着会调用到XMLConfigBuilder类中的parse方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//开始解析configuration标签下的所有配置
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//properties标签的解析
propertiesElement(root.evalNode("properties"));
//setting标签的解析
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
//typeAliases 别名标签的解析
typeAliasesElement(root.evalNode("typeAliases"));
//插件的解析
pluginElement(root.evalNode("plugins"));
//objectFactory 的解析
objectFactoryElement(root.evalNode("objectFactory"));
//objectWrapperFactory的解析
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
//mappers标签的解析,重点看这里
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
//该方法是针对mappers标签的解析过程。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果是以package方式配置的话的解析方式
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
//如果不是以package 方式的话,就是以resource、url、class方式配置的
//<mapper resource=""/>
//<mapper url=""/>
//<mapper class=""/>
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
//例如是以resource方式配置的话,使用到了XMLMapperBuilder这个类,调用其parse方法进行解析
ErrorContext.instance().resource(resource);
//同样是获取资源的字节流数据
InputStream inputStream = Resources.getResourceAsStream(resource);
//将字节流传递给解析类
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
下面我们看下XMLMapperBuilder这个类对mapper的解析过程:
//parse方法解析
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
//解析mapper下的所有标签
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
//校验其命名空间是否为空
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref 标签
cacheRefElement(context.evalNode("cache-ref"));
//解析cache标签,这里就是解析cache缓存标签的核心方法。
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
//解析cache标签
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//获取type值,默认perpetual为缓存的数据结构,即PerpetualCache。
String type = context.getStringAttribute("type", "PERPETUAL");
//根据type值获取对应的缓存类的Class对象。
//默认为PerpetualCache实现。
//TypeAliasRegistry 类型别名注册表在初始化的时候也就是无参构造y以及Configuration无参构造的时候默认初始化注册表。
//注册表就是一个map的数据结构: Map<String, Class<?>> TYPE_ALIASES
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);
}
}
//最后调用MapperBuilderAssistant类的这个方法创建一个缓存对象。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//new 一个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中添加缓存对象。
//configuration中会以map形式保存这个缓存对象
// caches.put(cache.getId(), cache); 以cache的id为key值,这个id 就是mapper.xml中的namespace.
configuration.addCache(cache);
//当前帮助类中也保留一份二级缓存对象的引用。
currentCache = cache;
return cache;
}
以上就是二级缓存标签的解析过程,最终二级缓存对象会保存在configuration类中,以及MapperBuilderAssistant帮助类中也会有一个缓存对象的引用,最终这个帮助类MapperBuilderAssistant的对象会在mapper解析的最后一步也就是select|update|delete|insert这些sql语句的标签解析完成后,一并将cache等信息封装到MappedStatement中,再将MappedStatement添加到cconfiguration中。
那么cache标签是在何时使用的呢?
我们知道在第一步的解析中,当默认cacheEnabled是true的话,Executor初始化的是CacheingExecutor,所以在其query方法中体现了cache标签的使用:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//这里的ms.getCache(),就是在MapperStatement对象中取出缓存对象。
//这里是缓存对象的get方法,当然set方法在上述Mapper对<cache>标签的解析中已经说的很详细了。
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);
}
mybatis如何实现的二级缓存?
我们先从SqlSession的接口开始一步步从源码看下去。
首先DefaultSqlSession的selectList方法:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据statementId,在configuration中获取到MapperStatement对象。
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();
}
}
从上面代码可以看出SqlSession接收到请求后,将请求传递给了Executor接口,Executor是mybatis四大执行器之一,定义了一系列操作数据库的方法,其有两个实现类, 以下是其简单的类图:
其中BaseExcutor是抽象类,其实现类为SimpleExcutor。 当我们开启二级缓存的第一步时也就是在核心配置文件中添加如下配置时(虽然可以不用配置,默认配置,上文已经详细介绍过了,这里就一笔带过了):
<settings>
<!-- 开启二级缓存 这个配置默认为true 可省略-->
<setting name="cacheEnabled" value="true" />
</settings>
这时候在创建SqlSession的时候就把Excuotr创建好了, 代码在SqlSessionFactory的openSession中:
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
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);
//这里调用newExecutor方法,拿到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();
}
}
//该方法返回Execuotr对象。
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);
}
//这里有一个判断,如果cacheEnabled为true,则返回一个装饰类CachingExecutor,
//所以核心在这里,为什么SqlSession中的Executor接口时CachingExecutor类对象了,而不是SimpleExecutor对象,除非在配置文件中,将cacheEnabled的配置设置为false。
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
因此我们继续看CachingExecutor类中的query方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取BoundSql对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
//创建Cachekey对象,即缓存中的key值。
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对象ms中获取二级缓存cache。
Cache cache = ms.getCache();
//如果缓存不为空
if (cache != null) {
//判断是否配置flushCache标签,是否在查询前刷新缓存。
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) {
//获取不到就调用SimpleExecutor的query方法,即再去一级缓存中去取,拿不到在查询数据库。
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);
}
这里有一个tcm变量:
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
TransactionalCacheManager 这个类是一个缓存事务管理器,在上面的代码中,调用了其getObject和putObject方法。分别来看下这两个方法.
getObject()方法:
//定义了一个map,
//key值时 Cache对象,就是在解析<cache/> 标签时候创建的Cache对象,
//value值是一个TransactionalCache对象。
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
//该方法内部调用了getTransactionalCache 这个方法。
public Object getObject(Cache cache, CacheKey key) {
//这里调用的是返回的TransactionalCache 对象的getObject()方法。
return getTransactionalCache(cache).getObject(key);
}
//该方法从transactionalCaches这个map中拿出
//TransactionalCache对象,如果没有则new一个新的放到map中。
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
上面分析到最后调用的是TransactionalCache对象的getObject方法。 TransactionalCache的getObject()方法:
@Override
public Object getObject(Object key) {
// issue #116
//这里调用的是delegate的getObject方法。
//也就是说mybatis二级缓存是从delegate的getObject方法中取出的。
//这个delegate是TransactionalCache内部的维护的一个Cache类型的一个 缓存成员变量,实际上TransactionalCache就是一个装饰类,因为mybatis机制用到了装饰器模式。
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
以上就是总结分析了从二级缓存中数据的过程。 下面再来看看TransactionalCacheManager中的putObject()方法:
public void putObject(Cache cache, CacheKey key, Object value) {
//这里同样的道理,与getObject()方法调用的一样的方法getTransactionalCache
//所以说这里putObject调用的也就是TransactionalCache的putObject()方法了。
getTransactionalCache(cache).putObject(key, value);
}
紧接着看一下TransactionalCache 的putObject方法:
从下面的代码看可以看出TransactionalCache中的putObject方法最终将数据存入到了一个map集合中。
private final Map<Object, Object> entriesToAddOnCommit;
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
那么问题来了,二级缓存取数据是从delegate中取,存数据是存到TransactionalCache的entriesToAddOnCommit这个map中去,这明显是不对的。
那这是为什么呢?我们在操作数据库的过程中,能够发现如果我们在进行一次数据库查询操作之后,如果commit的话,会发现第二次同样的查询还是会发送数据库语句取查询数据库,这就是问题所在,因为mybatis为了防止脏读,会先将数据存放到map中,当事务提交或者session关闭会将数据提交到二级缓存delegate中去。
下面我们来看一下TransactionalCache 的commit方法:
//commit方法调用了flushPendingEntries这个方法。
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
//该方法是将entriesToAddOnCommit 这个map中暂缓的数据提交到delegate中。实现二级缓存的刷新。
private void flushPendingEntries() {
//遍历entriesToAddOnCommit这map提交到delegate中。
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);
}
}
}
以上是个人对mybatis二级缓存的学习心得,如有模糊之处,忘不吝赐教!