通过源码分析MyBatis的缓存
Posted 懂一点架构
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过源码分析MyBatis的缓存相关的知识,希望对你有一定的参考价值。
按一般资料上的说法,Mybatis有两种缓存:一级缓存和二级缓存。一级缓存的作用域是SqlSession,而二级缓存是全局缓存,跨SqlSession。
一,一级缓存
testCase1:同个session进行两次相同查询:
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user2);
} finally {
sqlSession.close();
}
}
结果:MyBatis只进行1次数据库查询;
testCase2:同个session进行两次不同的查询:
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 2);
log.debug(user2);
} finally {
sqlSession.close();
}
}
结果:MyBatis进行两次数据库查询;
testCase3:不同session,进行相同查询:
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
User user2 = (User)sqlSession2.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user2);
} finally {
sqlSession.close();
sqlSession2.close();
}
}
结果:MyBatis进行了两次数据库查询;
testCase4:同个session,查询之后更新数据,再次查询相同的语句:
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
user.setAge(100);
sqlSession.update("org.format.mybatis.cache.UserMapper.update", user);
User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user2);
sqlSession.commit();
} finally {
sqlSession.close();
}
}
结果:更新操作之后缓存会被清除,MyBatis进行了两次数据库查询;
小结:在同个SqlSession中,查询语句相同的sql会被缓存,但是一旦执行新增或更新或删除操作,缓存就会被清除。
源码分析
看下DefaultSqlSession(SqlSession接口实现类,MyBatis默认使用这个类)的selectList源码(上面例子上使用的是selectOne方法,调用selectOne方法最终会执行selectList方法):
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
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();
}
}
Executor根据ExecutorType的不同而创建,最常用的是SimpleExecutor,本文的例子也是创建这个实现类。 一级缓存最重要的代码就是BaseExecutor的query方法:
说明:
1,BaseExecutor的属性localCache是个PerpetualCache类型的实例,PerpetualCache类是实现了MyBatis的Cache缓存接口的实现类之一,内部有个Map类型的属性用来存储缓存数据。 这个localCache的类型在BaseExecutor内部是写死的。 这个localCache就是一级缓存!
2,cacheKey是MappedStatement + parameterObject + rowBounds一起构造而成。
接下来我们看下为何执行新增或更新或删除操作,一级缓存就会被清除这个问题。
public int update(MappedStatement ms, Object parameter) throws SQLException {
clearLocalCache();
return doUpdate(ms, parameter);
}
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
二,二级缓存
二级缓存的作用域是全局的,二级缓存在SqlSession关闭或提交之后才会生效。
二级缓存跟一级缓存不同,一级缓存不需要配置任何东西,且默认打开。 二级缓存就需要配置一些东西。最简单的配置,在mapper文件上加上这句配置即可:<cache/>
其实二级缓存跟3个配置有关:
1,mybatis全局配置文件中的setting中的cacheEnabled需要为true(默认为true,不设置也行);
2,mapper配置文件中需要加入<cache>节点;
3,mapper配置文件中的select节点需要加上属性useCache需要为true(默认为true,不设置也行);
testCase1:不同SqlSession,查询相同语句,第一次查询之后commit SqlSession(或者close SqlSession):
public void testCache2() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
String sql = "org.format.mybatis.cache.UserMapper.getById";
User user = (User)sqlSession.selectOne(sql, 1);
log.debug(user);
// 注意,这里一定要提交。 不提交还是会查询两次数据库
sqlSession.commit();
User user2 = (User)sqlSession2.selectOne(sql, 1);
log.debug(user2);
} finally {
sqlSession.close();
sqlSession2.close();
}
}
结果:MyBatis仅进行了一次数据库查询;
testCase2:不同SqlSesson,查询相同语句。 第一次查询之后SqlSession不提交:
public void testCache2() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
String sql = "org.format.mybatis.cache.UserMapper.getById";
User user = (User)sqlSession.selectOne(sql, 1);
log.debug(user);
User user2 = (User)sqlSession2.selectOne(sql, 1);
log.debug(user2);
} finally {
sqlSession.close();
sqlSession2.close();
}
}
结果:MyBatis执行了两次数据库查询;
源码解读:
我们从mapper文件中加入的<cache/>中开始分析源码,接下来我们看下这个cache的解析,XMLMappedBuilder的解析方法如下:
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);
cacheRefElement(context.evalNode("cache-ref"));
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);
}
}
其中cacheElement(context.evalNode("cache"));就是解析cache节点的代码,其中最重要的功能就是构造一个Cache实例并加入到Configuration。
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;
}
接下来再构造MapperStatement对象时,就会把currentCache参数传给它。
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)
.resulSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
接下来我们回过头来看查询的源码,CachingExecutor的query方法:
小结:使用二级缓存之后:查询数据的话,先从二级缓存中拿数据,如果没有的话,去一级缓存中拿,一级缓存也没有的话再查询数据库。
为什么SqlSession commit或close之后,二级缓存才会生效呢?
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
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);
}
}
}
参考文献
通过源码分析MyBatis的缓存
http://www.cnblogs.com/fangjian0423/p/mybatis-cache.html
以上是关于通过源码分析MyBatis的缓存的主要内容,如果未能解决你的问题,请参考以下文章