mybatis一级缓存详解

mybatis 缓存分为一级缓存,二级缓存和自定义缓存。本文重点讲解一级缓存

一:前言

在介绍缓存之前,先了解下 mybatis 的几个核心概念:

* SqlSession:代表和数据库的一次会话,向用户提供了操作数据库的方法

* MapperedStatement:代表要往数据库发送的要执行的指令,可以理解为 sql 的抽象表示

* Executor:用来和数据库交互的执行器,接收 MapperedStatement 作为参数

二:一级缓存

1. 一级缓存的介绍:

mybatis 一级缓存有两种:一种是 SESSION 级别的,针对同一个会话 SqlSession 中,执行多次条件完全相同的同一个 sql,那么会共享这一缓存,默认是 SESSION 级别的缓存;一种是 STATEMENT 级别的,缓存只针对当前执行的这一 statement 有效。

对于一级缓存的流程,看下图:

整个流程是这样的:

* 针对某个查询的 statement,生成唯一的 key

* 在 Local Cache 中根据 key 查询数据是否存在

* 如果存在,则命中,跳过数据库查询,继续往下走

* 如果没命中:

        * 去数据库中查询,得到查询结果

        * 将 key 和查询结果放到 Local Cache 中

        * 将查询结果返回

* 判断是否是 STATEMENT 级别缓存,如果是,则清除缓存

接下来针对一级缓存的几种情况,来进行验证。

情况 1:SESSION 级别缓存,同一个 Mapper 代理对象执行条件相同的同一个查询 sql

SqlSession sqlSession = getSqlSessionFactory().openSession();
        GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
        goodsMapper.selectGoodsById("1");
        goodsMapper.selectGoodsById("1");

结果:

Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3dd44d5e]
==>  Preparing: select * from goods where id = ? 
==> Parameters: 1(String)
<==    Columns: id, name, detail, remark
<==        Row: 1, title1, null, null
<==      Total: 1

总结:只向数据库进行了一次查询,第二次用了缓存

情况 2:SESSION 级别缓存,同一个 Mapper 代理对象执行条件不同的同一个查询 sql

    public void selectGoodsTest(){
        SqlSession sqlSession = getSqlSessionFactory().openSession();
        GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
        goodsMapper.selectGoodsById("1");
        goodsMapper.selectGoodsById("2");
    }

结果:

==>  Preparing: select * from goods where id = ? 
==> Parameters: 1(String)
<==    Columns: id, name, detail, remark
<==        Row: 1, title1, null, null
<==      Total: 1
==> Preparing: select * from goods where id = ? ==> Parameters: 2(String) <== Columns: id, name, detail, remark <== Row: 2, title2, null, null <== Total: 1

总结:因为查询条件不同,所以是两个不同的 statement,生成了两个不同 key,缓存中是没有的

情况 3:SESSION 级别缓存,针对同一个 Mapper 接口生成两个代理对象,然后执行查询条件完全相同的同一条 sql

    public void selectGoodsTest(){
        SqlSession sqlSession = getSqlSessionFactory().openSession();
        GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
        GoodsDao goodsMapper2 = sqlSession.getMapper(GoodsDao.class);
        goodsMapper.selectGoodsById("1");
        goodsMapper2.selectGoodsById("1");}

结果:

==> Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1

总结:这种情况满足:同一个 SqlSession 会话,查询条件完全相同的同一条 sql。所以,第二次查询是从缓存中查找的。

情况 4:SESSION 级别缓存,在同一次会话中,对数据库进行了修改操作,一级缓存是否是失效。

    // 这里对 id=2 的数据进行了 upate 操作,发现 id=1 的一级缓存也被清除,因为它们是在同一个 SqlSession 中
@Test
public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(); GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class); Goods goods = new Goods(); goods.setId("2"); goods.setName("篮球"); goodsMapper.selectGoodsById("1"); goodsMapper.updateGoodsById(goods); goodsMapper.selectGoodsById("1");}

结果:

==>  Preparing: select * from goods where id = ? 
==> Parameters: 1(String)
<==    Columns: id, name, detail, remark
<==        Row: 1, title1, null, null
<==      Total: 1
==> Preparing: update goods set name = ? where id = ? ==> Parameters: 篮球 (String), 2(String) <== Updates: 1
==> Preparing: select * from goods where id = ? ==> Parameters: 1(String) <== Columns: id, name, detail, remark <== Row: 1, title1, null, null <== Total: 1

总结:在同一个 SqlSession 会话中,如果对数据库进行了修改操作,那么该会话中的缓存都会被清除。但是,并不会影响其它会话中的缓存。

情况 5:SESSION 级别缓存,开启两个 SqlSession,在 SqlSession1 中查询操作,在 SqlSession2 中执行修改操作,那么 SqlSession1 中的一级缓存是否仍然有效?

@Test
    public void selectGoodsTest(){
        SqlSession sqlSession = getSqlSessionFactory().openSession();
        GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
        SqlSession sqlSession2 = getSqlSessionFactory().openSession();
        GoodsDao goodsMapper2 = sqlSession2.getMapper(GoodsDao.class);
        Goods goods = new Goods();
        goods.setId("1");
        goods.setName("篮球");
        Goods goods1 = goodsMapper.selectGoodsById("1");
        System.out.println("name="+goods1.getName());
        System.out.println("******************************************************");
        goodsMapper2.updateGoodsById(goods);
        Goods goodsResult = goodsMapper.selectGoodsById("1");
        System.out.println("******************************************************");
        System.out.println("name="+goodsResult.getName());}

结果:

==>  Preparing: select * from goods where id = ? 
==> Parameters: 1(String)
<==    Columns: id, name, detail, remark
<==        Row: 1, title1, null, null
<==      Total: 1
name=title1
******************************************************
Opening JDBC Connection
Created connection 644010817.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2662d341]
==>  Preparing: update goods set name = ? where id = ? 
==> Parameters: 篮球 (String), 1(String)
<==    Updates: 1
******************************************************
name=title1

总结:在 SqlSession2 中对 id=1 的数据做了修改,但是在 SqlSession1 中的最后一次查询中,仍然是从一级缓存中取得数据,说明了一级缓存只在 SqlSession 内部共享,SqlSession 对数据库的修改操作不影响其它 SqlSession 中的一级缓存。

情况 6:SqlSession 的缓存级别设置为 STATEMENT,即在配置文件中添加如下代码:

<settings>
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

执行代码:

    @Test
    public void selectGoodsTest(){
        SqlSession sqlSession = getSqlSessionFactory().openSession();
        GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
        goodsMapper.selectGoodsById("1");
        System.out.println("****************************************");
        goodsMapper.selectGoodsById("1");}

结果:

==>  Preparing: select * from goods where id = ? 
==> Parameters: 1(String)
<==    Columns: id, name, detail, remark
<==        Row: 1, title1, null, null
<==      Total: 1
****************************************
==>  Preparing: select * from goods where id = ? 
==> Parameters: 1(String)
<==    Columns: id, name, detail, remark
<==        Row: 1, title1, null, null
<==      Total: 1

总结:STATEMENT 级别的缓存,只针对当前执行的这一 statement 有效

 2. 一级缓存是如何被存取的?

我们知道,当与数据库建立一次连接,就会创建一个 SqlSession 对象,默认是 DefaultSqlSession 这个实现,这个对象给用户提供了操作数据库的各种方法,与此同时,也会创建一个 Executor 执行器,缓存信息就是维护在 Executor 中,Executor 有一个抽象子类 BaseExecutor,这个类中有个属性 PerpetualCache 类,这个类就是真正用于维护一级缓存的地方。通过看源码,可以知道如何根据 cacheKey,取出和存放缓存的。

在查询数据库前,先从缓存中查找,进入 BaseExecutor 类的 query 方法:

// 这是 BaseExecutor 的一个属性,用于存放一级缓存
protected PerpetualCache localCache;
@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++;
// 根据 CacheKey 作为 key,查询 HashMap 中的 value 值,也就是缓存,这就是取出缓存的过程 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(); } deferredLoads.clear(); // issue #601 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache(); // issue #482 } } return list; }

当从数据库中查询到数据后,需要把数据存放到缓存中的,然后再返回数据,这个就是存放缓存的过程,进入 queryFromDatabase 方法:

 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; }

3.CacheKey 是如何确定唯一的?

我们知道,如果两次查询完全相同,那么第二次查询就从缓存中取数据,换句话说,怎么判断两次查询是不是相同的?是否相同是根据 CacheKey 来判断的,那么看下 CacheKey 的生成过程,就知道影响 CacheKey 是否相同的元素有哪些了。

进入 BaseExecutor 类的 createCacheKey 方法:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) throw new ExecutorException("Executor was closed.");
    CacheKey cacheKey = new CacheKey();
// statement id cacheKey.update(ms.getId());
// rowBounds.offset cacheKey.update(rowBounds.getOffset()); // rowBounds.limit
cacheKey.update(rowBounds.getLimit()); // sql 语句
cacheKey.update(boundSql.getSql()); List
<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); }
// 传递的每一个参数 cacheKey.update(value); } }
return cacheKey; }

所以影响 Cachekey 是否相同的因素有:statementId,offset,limit,sql 语句,参数

接下来进入 cacheKey.update 方法,看它如何处理以上这五个元素的:

  private void doUpdate(Object object) {
// 获取对象的 HashCode
int baseHashCode = object == null ? 1 : object.hashCode(); // 计数器 +1 count++; checksum += baseHashCode;
// baseHashCode 扩大 count 倍 baseHashCode
*= count; // 对 HashCode 进一步做处理 hashcode = multiplier * hashcode + baseHashCode; // 把以上五个元素存放到集合中 updateList.add(object); }

CahceKey 的属性和构造方法:

private int multiplier;
private int hashcode;
private long checksum; 

  private int count;

  private List<Object> updateList;

public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<Object>();}

CacheKey 中最重要的一个方法来了,如何判断两个 CacheKey 是否相等?

public boolean equals(Object object) {
    if (this == object)
      return true;
    if (!(object instanceof CacheKey))
      return false;
</span><span style="color: rgba(0, 0, 255, 1)">final</span> CacheKey cacheKey =<span style="color: rgba(0, 0, 0, 1)"> (CacheKey) object;
<span style="color: rgba(0, 128, 0, 1)">// 判断HashCode是否相等
</span></span><span style="color: rgba(0, 0, 255, 1)">if</span> (hashcode !=<span style="color: rgba(0, 0, 0, 1)"> cacheKey.hashcode)
  </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;<br>    <span style="color: rgba(0, 128, 0, 1)">// 判断checksum是否相等
</span></span><span style="color: rgba(0, 0, 255, 1)">if</span> (checksum !=<span style="color: rgba(0, 0, 0, 1)"> cacheKey.checksum)
  </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;<br>   <span style="color: rgba(0, 128, 0, 1)"> // 判断count是否相等
</span></span><span style="color: rgba(0, 0, 255, 1)">if</span> (count !=<span style="color: rgba(0, 0, 0, 1)"> cacheKey.count)
  </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
<span style="color: rgba(0, 128, 0, 1)">// 逐一判断以上五个元素是否相等
</span></span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = 0; i &lt; updateList.size(); i++<span style="color: rgba(0, 0, 0, 1)">) {
  Object thisObject </span>=<span style="color: rgba(0, 0, 0, 1)"> updateList.get(i);
  Object thatObject </span>=<span style="color: rgba(0, 0, 0, 1)"> cacheKey.updateList.get(i);
  </span><span style="color: rgba(0, 0, 255, 1)">if</span> (thisObject == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (thatObject != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">)
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
  } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">thisObject.equals(thatObject))
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
  }
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;

}
// 只有以上所有的判断都相等时,两个 CacheKey 才相等

4. 一级缓存的生命周期是多长?

开始:mybatis 建立一次数据库会话时,就会生成一系列对象:SqlSession--->Executor--->PerpetualCache, 也就开启了对一级缓存的维护。

结束: 

         * 会话结束,会释放掉以上生成的一系列对象,缓存也就不可用了。

         * 调用 sqlSession.close 方法,会释放掉 PerpetualCache 对象,一级缓存不可用

         * 调用 sqlSession.clearCache 方法,会清空 PerpetualCache 对象中的缓存数据,该对象可用,一级缓存不可用

         * 调用 sqlSession 的 update,insert,delete 方法,会清空 PerpetualCache 对象中的缓存数据,该对象可用,一级缓存不可用

 

 

参考资料:https://blog.csdn.net/luanlouis/article/details/41280959