Mybatis源码分析之缓存

Mybatis源码分析之缓存

Mybatis缓存从使用到原理,剖析源码。

Mybatis缓存介绍

我们开始源码分析前先简单介绍下Mybatis缓存吧。

Mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能。Mybatis提供了两级缓存,一级缓存和二级缓存。

一级缓存

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

  @Test
  public void selectOne() throws IOException {
    String resource = "org/apache/ibatis/demo/user/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    try (SqlSession session = sqlSessionFactory.openSession()) {
      UserMapper mapper = session.getMapper(UserMapper.class);
      System.out.println(mapper.selectOne(1,"aaa"));
      System.out.println(mapper.selectOne(1,"aaa"));
    }
  }

执行结果 调用2次查询,但实际只执行了一次数据库

在这里插入图片描述

  • 说明
    • 可以看到2个结果实际是同一个对象

二级缓存

二级缓存是全局缓存,其作用域为 Mapper(Namespace),需要配置手才能生效。

多个SqlSession去操作同一个Mapper中的SQL语句,则这些SqlSession可以共享二级缓存,即二级缓存是跨SqlSession的

  • 配置步骤
  1. 在 mybatis-config.xml 的配置文件中进行显示配置,开启二级缓存(全局缓存)
  2. 在 Mapper.xml 文件中添加cache标签
  • 修改mybatis-config.xml
<settings>
    <!--显示的开启全局缓存 默认是true-->
    <setting name="cacheEnabled" value="true"/>
</settings>
  • 在mapper.xml中开启二缓存
<mapper >
    <!--在当前 Mapper.xml文件开启二级缓存-->
    <cache/>
</mapper>

也可以自定义

<mapper >
    <!--在当前 Mapper.xml文件开启二级缓存-->
    <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
</mapper>

eviction:清除策略为FIFO缓存,先进先出原则,默认的清除策略是 LRU
flushInterval:属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量
size:最多可以存储结果对象或列表的引用数
readOnly:只读属性,可以被设置为 true 或 false,如果为true时用户修改对象属性会影响到缓存中的值。 默认为false。

  • 演示
  @Test
  public void selectOne2() throws IOException {
    String resource = "org/apache/ibatis/demo/user/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    try (SqlSession session = sqlSessionFactory.openSession()) {
      UserMapper mapper = session.getMapper(UserMapper.class);
      System.out.println(mapper.selectOne(1,"aaa"));
    }
      //开启另一个sqlSession执行查询
    try (SqlSession session = sqlSessionFactory.openSession()) {
      UserMapper mapper = session.getMapper(UserMapper.class);
      System.out.println(mapper.selectOne(1,"aaa"));
    }
  }

在这里插入图片描述

从图上可以看出来二级缓存已经生效了,在未开启事务的情况下,查询2次只执行了一次数据库。

  • 说明

可以看到2个结果是不同一个对象

有效时间:当前Mapper全局有效,每次执行insert\update\delete会刷新(此配置可取消)。也会根据cache设置情况而定。

Mybatis缓存源码

Mybatis的缓存相关的代码均在org.apache.ibatis.cache包中,我们来看这个包中有什么?

在这里插入图片描述

  • Cache.java

    这个包中Cache是一个接口,在Mybatis中所有的缓存必须实现它。

  • CacheKey.java

    缓存塞值和取值用到的key。

  • decorators包

    缓存装饰器,里面提供了很多缓存策略。我们在二级缓存分析的时候会告诉你它的作用。

  • impl包

    缓存的默认实现,里面只有一个实现PerpetualCache.java。

一级缓存和二级缓存的优先级?

如果一级缓存、二级缓存都开启的情况下,是会先执行一级缓存还是二级缓存?

针对这个问题我去百度了下,居然有很多人说的是错的。因此,我特地将这个问题抛出来。

这里我先告诉你我的答案:首先会先查二级缓存,然后再查一级缓存!下面根据我理解画的图:

在这里插入图片描述

我们来看下Mybatis的调用过程,(可以看下之前的文章Mybatis源码分析(一)中对过程源码的分析)。

org.apache.ibatis.session.SqlSessionFactoryBuilder#build()
    //解析mybatis.xml,得到Configuration对象
 org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
    //得到sqlSession
  org.apache.ibatis.session.SqlSessionFactory#openSession()
    //执行查询
   org.apache.ibatis.session.defaults.DefaultSqlSession#selectList()
    //二级缓存查询
    org.apache.ibatis.executor.CachingExecutor#query()
    //一级缓存查询
     org.apache.ibatis.executor.BaseExecutor#query()
      org.apache.ibatis.executor.BaseExecutor#queryFromDatabase()
       org.apache.ibatis.executor.SimpleExecutor#doQuery()
        org.apache.ibatis.executor.statement.SimpleStatementHandler#query()
         java.sql.Statement#execute()
          org.apache.ibatis.executor.resultset.ResultSetHandler#handleResultSets()

上述的执行过程,我注释了缓存的查询顺序,我们看分别看下这2个缓存具体的执行。

一级缓存源码

沿着上面整理的执行方法,我们逐一看下来到org.apache.ibatis.executor.BaseExecutor#query()能看出端倪来。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
  List<E> list;
  try {
      //试图从缓存中取值
    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);
    }
  } 
...
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      clearLocalCache();
    }
  return list;
}

以上去掉了无关代码。

这段代码我们发现,mybatis在执行任何查询时,首先会从localCache.getObject(key)获取了结果list,如果list为空时才会查询数据库,如果取到了就直接返回了。localCache这就是我们要找的一级缓存

一级缓存的实现原理?
protected PerpetualCache localCache;

一级缓存的实现类就是PerpetualCache,在Mybatis中如果想实现缓存则必须要实现Cache接口。不管是此处的PerpetualCache还是后面需要讲的二级缓存的其他缓存策略。

在这里插入图片描述

我们来看下PerpetualCache的源码:

public class PerpetualCache implements Cache {
  private final String id;
  private final Map<Object, Object> cache = new HashMap<>();
  ....
}

里面的代码很简单,这里可以看出来一级缓存就是利用HashMap来实现数据的存储和获取!

HashMap不是线程安全的,所以一级缓存也不是线程安全的,但一级缓存只在同一个sqlSession中,因此一般情况下不会遇到线程问题。

既然是HashMap,那存储和取值必须要有一个key才能拿到值。

这个缓存key的组成是什么呢?

仍然在我们查询步骤执行类的主线上找下

org.apache.ibatis.executor.BaseExecutor#query()

看到如下代码

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    ...
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    ...
  }

继续跟进方法

    org.apache.ibatis.executor.BaseExecutor#createCacheKey()
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    ..
    CacheKey cacheKey = new CacheKey();
    //sqlId
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    ...
    for (ParameterMapping parameterMapping : parameterMappings) {
        ...
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
        ...
            cacheKey.update(value);
        
    }
  ...
        cacheKey.update(configuration.getEnvironment().getId());
  ...
    return cacheKey;
}

本地缓存PerpetualCache localCache的 key是CacheKey类,这个类中有自己的一套逻辑,排除这些逻辑我们只看和我们相关的组成。

在这里插入图片描述

上面代码以及调试过程截图我们可以看到key的组成是:

SqlId(NameSpace+Id)+分页起始值(默认0)+分页数量(默认int最大值)+sql语句+全部参数值+配置环境

一级缓存的生命周期?

生命周期就是从创建到销毁的过程。

  • 缓存的初始化

    从上面你可以知道,一级缓存是protected PerpetualCache localCache;它是BaseExecutor的属性,接着我们可以很快看到这个缓存对象是如何完成初始化的

    org.apache.ibatis.executor.BaseExecutor#BaseExecutor()
    
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
     ...
      this.localCache = new PerpetualCache("LocalCache");
      ...
    }
    

    我们通过构造方法可以看出来,在执行器BaseExecutor被创建的时候同时完成了缓存的初始化。

  • 缓存的对象插入

    在查询本地缓存没有数据后,会执行方法查询数据库

    org.apache.ibatis.executor.BaseExecutor#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);
        ...
      }
    

    在进入方法以后立马对key塞了一个缓冲占位EXECUTION_PLACEHOLDER,这是防止执行过程期间多次查询造成的影响。查询到结果以后将值作了替换。到此,缓存值就已经存储进来了。

  • 缓存的销毁

    还记得刚开始的方法org.apache.ibatis.executor.BaseExecutor#query()

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
     if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
       // issue #482
       clearLocalCache();
     }
    ...
    }
    @Override
    public void clearLocalCache() {
     if (!closed) {
         localCache.clear();
         localOutputParameterCache.clear();
     }
    }
    

    这里判断了,如果配置缓存作用域localCacheScope是STATEMENT则会每次清空缓存。

    localCacheScope缓存的作用域有SESSIONSTATEMENT2个配置选项,在中可以配置,默认情况下是SESSION

    除此以外,我们再看下调用到clearLocalCache()方法的地方,就是清除缓存的

  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
     ...
      clearLocalCache();
     ...
  }
  @Override
  public void commit(boolean required) throws SQLException {
     ...
      clearLocalCache();
     ...
  }
   @Override
  public void rollback(boolean required) throws SQLException {
      ...
      clearLocalCache();
      ...     
  }
  @Override
public void close(boolean forceRollback) {
    ...
  rollback(forceRollback);
    ...
    localCache = null;
      
  }

由上述的方法可以看出来,缓存在执行update()、commit()、rollback()、close()都会销毁

除此以外,就是在sqlSession被回收时会被销毁。因为缓存是跟随PerpetualCache localCache这个对象,而localCacheBaseExecutor–> DefaultSqlSession 的属性,也就是缓存的作用域是SqlSession,随着sqlSession消失而消失。

二级缓存源码

之前我们讲到,Mybatis中的CachingExecutor的带缓存的执行器,他与SimpleExecutor/BatchExecutor/ReuseExecutor不同,他是一个执行器的装饰类。在这个执行器中我们看下他都做了什么?

二级缓存默认开启还是关闭?

网上很多帖子都说二级缓存默认是关闭,但事实非如此

我们来看下源码:

org.apache.ibatis.session.SqlSessionFactoryBuilder#build()
    //解析mybatis.xml
 org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
    //解析其中的<settings>标签
  org.apache.ibatis.builder.xml.XMLConfigBuilder#settingsElement()
private void settingsElement(Properties props) {
  ...
  configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
  ...
}

这里可以看出来,如果没有配置cacheEnabled则默认为true会使用缓存执行器,但是为什么我们很多人说二级缓存是关闭的?接着往下看

二级缓存的实现原理?

CachingExecutor负责实现二级缓存的调用取值和塞值,见如下代码

org.apache.ibatis.executor.CachingExecutor#query()
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  //此处,如果设置<cache>标签,则拿到的cache对象为空。
  Cache cache = ms.getCache();
    //如果cache对象不为空则试图从缓存中拿到值
  if (cache != null) {
    ...
      //从二级缓存中取值
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        //查询数据库以后将值塞入缓存
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

CachingExecutor在执行SimpleExecutor/BatchExecutor/ReuseExecutor的查询前会先调用二级缓存取值。

那么

  • MappedStatement.cache 对象是什么时候初始化的?

我们得回到生成Configuration的地方,之前说过,在parse()方法中对xml进行了解析,并且针对xml所引入的mapper.xml也做了解析。具体如下:

org.apache.ibatis.session.SqlSessionFactoryBuilder#build()
 org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
  org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration()
    org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement()
    //以下代码开始解析mapper.xml中的内容
     org.apache.ibatis.builder.xml.XMLMapperBuilder#parse()
      org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement()
      //解析<cache>标签
       org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement()
private void cacheElement(XNode context) {
  if (context != null) {
     //缓存实现类,默认是PERPETUAL,也就是PerpetaulCache.java
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    //缓存清除策略,默认LRU
      //LRU – 最近最少使用:移除最长时间不被使用的对象。
	  //FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
	  //SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
	  // WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    //缓存刷新间隔(毫秒),默认无刷新时间
    Long flushInterval = context.getLongAttribute("flushInterval");
    //缓存大小 默认1024
    Integer size = context.getIntAttribute("size");
    //这个值比较有意思,曾经和ehcache融合时遇到过坑。缓存对象是否只读,如果为true时缓存值会随着外部修改而改变,默认为否。
    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);
  }
}

如果UserMapper.xml中没有配置标签,就不会解析标签,也就不会生成Cache对象,自然不会生效。因此网上很多人会误以为没开启。其实二级缓存默认是开启的,但没有生效,我们需要添加来让当前生成缓存对象实现缓存。

上面的代码是解析标签的,从这里我们可以看出来 有哪些属性了。其中需要特别说明的是type,type默认值是PERPETUAL就是对应的PerpetualCache.java。也就是说,和一级缓存一样,二缓存默认的实现类也是PerpetualCache类,原理就是利用HashMap来实现缓存的

关于这段代码,根据注释你就可以读懂大部分属性了,但我们这里要讲的不仅是属性的含义,而是他们是如何巧妙的实现这么多叠加功能的?

我们继续跟进代码builderAssistant.useNewCache()

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

这里用到了建造者模式创建了Cache对象,但他是如何实现的我们继续往下看:

org.apache.ibatis.mapping.CacheBuilder#build()
    org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators()
private Cache setStandardDecorators(Cache cache) {
  try {
    MetaObject metaCache = SystemMetaObject.forObject(cache);
    if (size != null && metaCache.hasSetter("size")) {
      metaCache.setValue("size", size);
    }
    if (clearInterval != null) {
      cache = new ScheduledCache(cache);
      ((ScheduledCache) cache).setClearInterval(clearInterval);
    }
    if (readWrite) {
      cache = new SerializedCache(cache);
    }
    cache = new LoggingCache(cache);
    cache = new SynchronizedCache(cache);
    if (blocking) {
      cache = new BlockingCache(cache);
    }
    return cache;
  } catch (Exception e) {
    throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
  }
}

原来如此,这里是通过装饰模式来将每个类功能附加上来,又能保留原始功能。我们上面看到的cache目录下的org.apache.ibatis.cache.decorators包中的类就是设计了不同的功能!这个模式我们之前在源码分析的时候也遇到过,CacheExecutor类也是用的这个装饰模式。

我整理了下全部的代码装饰类调用顺序(说明:只针对默认缓存类)

//缓存基础实现 org.apache.ibatis.mapping.CacheBuilder#setDefaultImplementations()
implementation = PerpetualCache.class;
//缓存LRU清除策略 org.apache.ibatis.mapping.CacheBuilder#setDefaultImplementations()
decorators.add(LruCache.class);
//配置flushInterval过期时间会执行 org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators()
cache = new ScheduledCache(cache);
//配置readOnly会执行
cache = new SerializedCache(cache);
//必执行
cache = new LoggingCache(cache);
//必执行
cache = new SynchronizedCache(cache);
//配置blocking会执行
cache = new BlockingCache(cache);

到此,我们终于搞明白了二级缓存是如何实现的了。

二级缓存的生命周期?
  • 缓存的初始化

    上一节已经讲了Cache对象是怎么完成初始化的了,在进行Configuration初始化时就完成了缓存的初始化。

    但这里我要讲的不只是这个。我想讲的是Cachemapper.xml的关系。还记得上面提到org.apache.ibatis.executor.CachingExecutor#query()方法中的Cache cache = ms.getCache();吗?这里说明了Cache对象是存在于MappedStatement中的,而每个mapper.xml是对应的一个MappedStatement吗?答应是否定的。

    //开始xml解析
    org.apache.ibatis.builder.xml.XMLConfigBuilder#parse()
        //解析<configuration>标签
     org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration()
        //解析<mappers>标签
      org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement()
        //找到对象的mapper.xml文件进行解析,此类中已经完成cache对象的创建(上一节已经讲了)
       org.apache.ibatis.builder.xml.XMLMapperBuilder#parse()
        //解析全部的<select|insert|update|delete>标签
        org.apache.ibatis.builder.xml.XMLMapperBuilder#parsePendingStatements()
        //解析其中一个 <select|insert|update|delete>标签
         org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode()
       	//构建statement对象并加入Configuration,Cache
       		 org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement()
    
    public MappedStatement addMappedStatement(String id,...) {
        //id=org.apache.ibatis.demo.user.UserMapper.selectOne
    	...
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            ...
            //这里的currentCache是在解析当前mapper.xml中的<cache>时就已经创建好了的。
            .cache(currentCache);
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);
        return statement;
      }
    

    这里可以看到,每一个<select|insert|update|delete>都会对应创建一个MappedStatement对象,同一个mapper.xml文件的多个MappedStatement都会使用同一个Cache对象。

  • 二级缓存的对象插入

    在上面已经说过了,不再赘述。

    org.apache.ibatis.executor.CachingExecutor#query()方法,在“二级缓存的实现原理”

  • 二级缓存的销毁

    二级缓存的销毁还挺多的,因为其存在清除策略,这里我们就不讲了。我们讲下没有达到清除策略的情况下是怎么销毁的。

    二级缓存对象一般是应用启动时创建的存储在Configuration中,并不会像一级缓存那样随着sqlSession而销毁。只有在以下几种情况会清除update()\close()\commit()\rollback(),以及标签配置了属性flushCache=true在执行query()配置每次清除。

    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
      flushCacheIfRequired(ms);
      return delegate.update(ms, parameterObject);
    }
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
      Cache cache = ms.getCache();
      if (cache != null) {
        flushCacheIfRequired(ms);
        ...
      }
      return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    @Override
    public void commit(boolean required) throws SQLException {
      ..
          //tcm管理缓存
      tcm.commit();
    }
    @Override
    public void rollback(boolean required) throws SQLException {
     ...
          tcm.rollback();
      ..
    }
    @Override
    public void close(boolean forceRollback) {
      try {
        // issues #499, #524 and #573
        if (forceRollback) {
          tcm.rollback();
        } else {
          tcm.commit();
        }
      } finally {
        delegate.close(forceRollback);
      }
    }
    
二级缓存是线程安全的吗?

默认的二级缓存和一级缓存实现原理是一样的,均采用HashMap。而一级缓存是缓存是不安全的,那二级缓存是不是线程安全的?

我想大部分人跟我一样,从理论上推断:既然二级缓存是全局的,一定是一个线程安全的,否则是不能全局使用的!但是为什么会是线程安全的呢?为了证实这个事情,我立马运行了一次程序,断点在org.apache.ibatis.executor.CachingExecutor#query(),看下缓存的实现类

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache();
  //断点处,查看cache对象
  ....
}

在这里插入图片描述

其中SerializedCache引起了我的注意,进入类

@Override
public synchronized int getSize() {
  return delegate.getSize();
}
@Override
public synchronized void putObject(Object key, Object object) {
  delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
  return delegate.getObject(key);
}
@Override
public synchronized Object removeObject(Object key) {
  return delegate.removeObject(key);
}
@Override
public synchronized void clear() {
  delegate.clear();
}

原来如此,通过synchronized 保证了HashMap的线程安全。

SerializedCache这个装饰器是在创建缓存时实例的org.apache.ibatis.mapping.CacheBuilder#setStandardDecorators()

二级缓存自定义缓存原理是什么?

我们知道,如果想实现自定义缓存,则需要在标签中添加type属性,指向缓存处理类,且此类必须满足2个条件:1)实现Cache接口,2)添加带String参数的构造方法。

例如我的

<mapper namespace="org.apache.ibatis.demo.user.UserMapper">
...
  <cache type="com.demo.cache.RedisCache"/>
</mapper>

上述提到过解析Mapper.xml文件的的代码

org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement()

这里会取的type属性作为实例类,

private void cacheElement(XNode context) {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    ...
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

在实例化缓存类时,调用了

org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache()
 org.apache.ibatis.mapping.CacheBuilder#build()
  org.apache.ibatis.mapping.CacheBuilder#newBaseCacheInstance()
   org.apache.ibatis.mapping.CacheBuilder#getBaseCacheConstructor()
private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
    Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
    try {
      return cacheConstructor.newInstance(id);
    } catch (Exception e) {
      throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: " + e, e);
    }
}
private Constructor<? extends Cache> getBaseCacheConstructor(Class<? extends Cache> cacheClass) {
  try {
    return cacheClass.getConstructor(String.class);
  } catch (Exception e) {
    throw new CacheException("Invalid base cache implementation (" + cacheClass + ").  "
      + "Base cache implementations must have a constructor that takes a String id as a parameter.  Cause: " + e, e);
  }
}

通过这段代码,我们可以看到,在实例化时利用反射`Constructor.newInstance()完成了缓存类有参数构造方法实现。

完成实例化以后的调用流程和默认的类差不多。

org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache()
 org.apache.ibatis.mapping.CacheBuilder#build()
public Cache build() {
  setDefaultImplementations();
  Cache cache = newBaseCacheInstance(implementation, id);
  ...
  if (PerpetualCache.class.equals(cache.getClass())) {
    ...
  } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
    cache = new LoggingCache(cache);
  }
  return cache;
}

通过上面的代码,可以知道自定义缓存的实现类,还会被LoggingCache装饰。我们实现Cache接口中的各个方法以后,CachingExecutor会完成调用。

缓存的坑

在spring整合mybatis以后我们大部分情况下都没有使用到Mybatis缓存,尤其是二级缓存。我们来看下Mybatis缓存存在哪些坑?

一级缓存问题

  1. 缓存数据被修改

一级缓存的优点非常明显减少了查询连接数据库,但同时也有缺点。就是如果你不小心对数据进行了修改,会造成第二次查询时的数据不准确。
在这里插入图片描述
在这里插入图片描述
可以很清楚的看到,明明同样的查询id=1,但结果却不一样。

  1. 脏数据问题

这里抛一个之前与同事讨论过的问题,看大家是怎么想的。

想象一下:SqlSession1 执行query1()和query2(),按照一级缓存的规则query2()是不会查询数据库的。假如此时SqlSession2()对数据作update(),此时query2()的结果就不对了。这个到底算不算脏数据问题?

说法1:因为sqlSession1.query2()未感知到sqlSession2的update()更新后的数据,因此读到的数据是不对,称为脏数据。

说法2:sqlSession1.query2()就应该是不能感知到sqlSession2.update()更新后的数据,否则就造成了不可重复读的问题。(数据库为了要解决不可重复读问题将事务隔离级别调整为RR)

你赞成上面的哪种说法呢?

二级缓存问题

  1. 脏数据问题

    二级缓存另外一个更严重的问题-脏数据!因为二级缓存是全局缓存,但只对同一个namespace有效,也就是说不同的namespace相互是感知不到表变化的,同样带来脏数据问题,而且范围更广

    更加危险的情况,假如多表关联查询时,任意一个表在其他mapper.xml中被修改时,都会导致查询结果错误

    这种情况也不是没有解决办法 ,多个namespace缓存不同步的情况可通过<cache-ref>可以实现几个namespace共用一个缓存。但此时又会引起另一个问题,就是不管是任意一个表做修改都会引起整个缓存的频繁刷新,失去了缓存的意义。

    如果上面的问题,还不足以说明问题,那么接下来看看更严重的问题。

    当二级缓存在分布式服务器的上,同时存在服务器1和2对数据进行查询时,查询的顺序如下图:
    在这里插入图片描述
    服务器在执行第1次查询时数据缓存了,此时服务器2对数据进行修改,此时当服务器1以同样的查询条件命中缓存时,查询的数据是修改前的数据。显然这不是我们想要的。

  2. 开销大

    因为是表级缓存,对查询结果进行全部缓存,如果表内容较多时对系统的开销比较大

总体来说,Mybatis二级缓存带来的好处比坏处更让人糟心,因此不建议使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值