<MyBatis缓存机制>一级缓存源码浅析

前言

昨天晚上在研究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-Sqlsession

http://blog.csdn.net/hupanfeng/article/details/9238127
Mybatis缓存特性的使用及源码分析,避坑指南~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章