前言
昨天晚上在研究mysql不同级别日志的时候,发现了很多脏读情况,一些明明已经被测试用例修改过的数据在读取后还是原值,不用说,肯定是缓存在作祟。百度了一些资料,发现Mybatis和Hibernate一样,也是分级缓存,于是想着借此机会研究一下Mybatis自带的缓存机制,看了一会儿源码,把自己的一些浅见在这里记录一下,希望能给看到的人一点启示。如果这篇源码导读有什么地方逻辑不是很清楚的,欢迎各位看官积极指正,通过大家的建议不断完善自己的表达思维也是很重要的。
SqlSession
为什么会首先提到这个,因为博主网上百度了一阵子后,发现Mybatis的所谓一级缓存,实际上就是一个Session级别的缓存,且了解到的测试一级缓存的代码如下:
String resource = "spring-mybatis.xml";
// 通过Mybatis包中的Resources对象很轻松的获取到配置文件
Reader reader = Resources.getResourceAsReader(resource);
// 通过SqlSessionFactoryBuilder创建
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 获得session实例
SqlSession session = sqlSessionFactory.openSession();
//测试实例
AuthorMapper authorMapper = session.getMapper(AuthorMapper.class);
看到这里,我们不难发现,SqlSession需要通过系统读取spring-mybatis.xml中配置的sqlSessionFactory来创建。于是我找到了jar包内关于sqlSessionFactory的源码。顺藤摸瓜,找到了SqlSessionFactory接口的一个实现类DefaultSqlSessionFactory。
红框中的方法即是openSession的具体实现,通过连接和通过数数据源两种方式获取Session。
// 获得session实例
SqlSession session = sqlSessionFactory.openSession();
我们来看一下具体实现的代码。
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,
autoCommit);
return new DefaultSqlSession(configuration, executor);
} 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();
}
}
从方法的具体实现代码中,我们可以发现SqlSession具体做了下面几件事情:
1) 从配置中获取Environment;
2) 从Environment中取得DataSource;
3) 从Environment中取得TransactionFactory;
4) 从DataSource里获取数据库连接对象Connection;
5) 通过DataSource创建事务对象Transaction;
6) 创建Executor对象
7) 创建sqlsession对象。
一个个查看这些创建的对象,我发现SqlSession关于缓存的所有操作都是由一个Executor接口的实现类BaseExecutor完成的。
找到了目标,接下来就是好好研究它实现的机制。
BaseExecutor
查看BaseExecutor的成员,我惊喜地发现了本地缓存localCache的身影。
protected PerpetualCache localCache;
点击PerpetualCache 查看缓存的具体实现,代码如下:
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public PerpetualCache(String id) {
this.id = id;
}
我们可以发现Mybatis的缓存实际上也是一个HashMap(为什么要说也。。。)
好了,不说cache对象的问题了,回到Executor,SqlSession把具体的查询职责全权委托给了Executor。按照配置条件一步一步往下找,循着判断的配置,我们可以发现如果开启了一级缓存的话(默认开启,这个后面会再提到),首先会进入BaseExecutor的query方法。代码如下所示:
@SuppressWarnings("unchecked")
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();
}
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}
上面这段代码有两个值得关注的关键点。
第一点是try包裹的那部分代码,我们可以看到请求会先去localCache里找,如果在localCache中未命中的话,才会进入queryFromDatabase方法,连接数据库获取对象,同时在queryFromDatabase中将新生成的cache键值key写入localCache。
localCache.putObject(key, EXECUTION_PLACEHOLDER);
这一点来说,和Hibernate很像,通过一个记录K(sql),V(statement)键值对的HashMap管理一级缓存。
第二点是下面这一段
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
通过这段代码我们可以看到,如果一级缓存的级别是STATEMENT,那么就会清理本地缓存。这就呼应了我前面说到的一点,为什么我说一级缓存是默认开启的呢,请看Configuration类中的一个成员变量:
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
我们发现configuration.getLocalCacheScope()的值默认是SESSION,所以结合上面的if条件,只有这个值为STATEMENT时,我们在查询的时候才会清理缓存,不会读出脏数据。我在文章开头提到的问题也就迎刃而解了,那就是让LocalCacheScope ==STATEMENT。
点开LocalCacheScope,我们发现这是一个枚举类。
public enum LocalCacheScope {
SESSION,STATEMENT
}
果然不出所料,STATEMENT就是选项之一,那么问题来了,这个值得选项应该在哪里配置呢?
还记得configuration是从哪里读取的吗?打开你的spring-mybatis.xml也就是配置sqlSessionFactory的地方。
<!-- 配置sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
<property name="dataSource" ref="dataSource" />
<!-- 文件映射器,指定类文件 -->
<property name="configLocation" value="classpath:mybatis/configuration.xml"/>
<property name="mapperLocations" value="classpath:com/lhx/x/sqlmap/*.xml" />
</bean>
找到配置configLocation的property,在这个引入的配置文件configuration.xml下的settings标签内加入如下代码:
<setting name="localCacheScope" value="STATEMENT"/>
好了,再去试试SqlSession下的查询还会出现脏读数据吗?
总结
1.Mybatis的缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念,同时只是使用了默认的hashmap,也没有做容量上的限定。
2.Mybatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,有操作数据库写的话,会引起脏数据,建议是把一级缓存的默认级别设定为Statement,即不使用一级缓存。
参考文章
十分感谢!
https://my.oschina.net/kailuncen/blog/1334771
深入浅出MyBatis-Sqlsessionhttp://blog.csdn.net/hupanfeng/article/details/9238127
Mybatis缓存特性的使用及源码分析,避坑指南~