MyBatis的缓存机制

cache缓存

缓存是一般的ORM框架都会提供的功能,目的就是提升查询xiaolv和较少数据库的压力。在MyBatis中有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

MyBatis的缓存体系结构

MyBatis缓存相关的类都在cache包里,其中有一个接口Cache,他只有一个默认的实现类PerpetualCache,其内部使用HashMap实现数据存储的。
PerpetualCache叫做基础缓存,因为它一定会被创建,除了基础缓存之外,MyBatis也定义了很多的装饰器,同样实现了Cache接口,通过这些装饰器可以额外实现很多的功能。
类图:


继承关系

在通常情况下,我们在debug源码的时候会发现基础缓存会被装饰四五层,当然不管怎么对它装饰,最底层使用的还是基础的实现类(PerpetualCache)
所有的缓存实现类总体上分为三类:基本缓存、淘汰算法缓存、装饰器缓存


image.png

一级缓存

一级缓存也叫本地缓存(Local Cache),MyBatis的一级缓存是在会话(SqlSession)层面实现缓存的,MyBatis的一级缓存是默认开启的,不需要任何的配置,(可以通过在配置文件中设置 localCacheScope设置为STATEMENT关闭一级缓存)

      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }

在MyBatis执行的流程中,涉及到这么多的对象,那么我们想缓存PerpetualCache应该放到哪个对象里面去维护呢?
因为本地缓存是在同一个会话中共享的,所以我们可以想到它应该是放在了qlSession对象里的,这样的话,当同一个会话查询缓存的时候就不需要说再去别的地方匹配相应的编号获取了。
DefaultSqlSession是SqlSession的默认实现类,它里面有两个对象属性,分别是Configuration和Executor,然而Configuration是全局唯一的,不完全是属于SqlSession,所以缓存只能放到Executor里面维护,而实际上在基础执行器SimpleExecutor、ReuseExecutor、BatchExecutor的父类BaseExecutor的构造方法中持有了PerpetualCache。

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue deferredLoads;
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack;
  private boolean closed;

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }
...

在同一个会话中,多次执行同一个SQL语句,执行器会直接从内存取到缓存的结果,不会再去访问数据库;但是在不同的会话里执行相同的SQL语句是不会使用到一级缓存的。


image.png

代码验证

接下来验证一下,MyBatis的一级缓存到底是不是只能在一个会话中共享,以及跨会话操作相同的SQL语句会不会使用到一级缓存。
注意:演示一级缓存需要先关闭二级缓存,在配置文件中将localCacheScope设置为SESSION。

  • 
    
  • 
    

判断缓存是否命中:当再次发送SQL到数据库执行(控制台打印了SQL语句)说明没有命中缓存,如果直接打印对象,说明是从缓存中取到了结果。

  1. 在同一个会话中共享
 @Test
    public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            Blog blog = mapper0.selectBlogById(1);
            System.out.println(blog);

            System.out.println("第二次查询,相同会话,获取到缓存了吗?");
            System.out.println(mapper1.selectBlogById(1));

        } finally {
            session1.close();
        }
    }

结果:可以看出第二次查询没有打印SQL语句,直接得到了结果,说明是从缓存中拿到了数据

==>  Preparing: select * from blog where bid = ? 
==> Parameters: 1(Integer)
<==    Columns: bid, name, author_id
<==        Row: 1, 今天你学习了吗, 1001
<==      Total: 1
Blog{bid=1, name='今天你学习了吗', authorId='1001'}
第二次查询,相同会话,获取到缓存了吗?
Blog{bid=1, name='今天你学习了吗', authorId='1001'}
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4f83df68]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@4f83df68]
Returned connection 1334042472 to pool.
  1. 不同会话中是否共享
    @Test
    public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            Blog blog = mapper0.selectBlogById(1);
            System.out.println(blog);

            System.out.println("第二次查询,相同会话,获取到缓存了吗?");
            System.out.println(mapper1.selectBlogById(1));

            System.out.println("第三次查询,不同会话,获取到缓存了吗?");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));

        } finally {
            session1.close();
        }
    }

结果:第三次查询是不一样的SqlSession,在控制台可以看出,第三次查询打印了SQL语句,说明它是从数据库中读取数据的,没有用到缓存,说明了,在不同的会话(SqlSession)中执行相同的SQL语句缓存是不会跨会话共享的。

==>  Preparing: select * from blog where bid = ? 
==> Parameters: 1(Integer)
<==    Columns: bid, name, author_id
<==        Row: 1, 今天你学习了吗, 1001
<==      Total: 1
Blog{bid=1, name='今天你学习了吗', authorId='1001'}
第二次查询,相同会话,获取到缓存了吗?
Blog{bid=1, name='今天你学习了吗', authorId='1001'}
第三次查询,不同会话,获取到缓存了吗?
Opening JDBC Connection
Sun Aug 09 23:25:24 GMT+08:00 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Created connection 1984975621.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@76505305]
==>  Preparing: select * from blog where bid = ? 
==> Parameters: 1(Integer)
<==    Columns: bid, name, author_id
<==        Row: 1, 今天你学习了吗, 1001
<==      Total: 1
Blog{bid=1, name='今天你学习了吗', authorId='1001'}

源码分析一级缓存的存入与取出

因为一级缓存是在BaseExecutor中进行管理的,所以我们可以在这个对象中寻找答案,在SqlSession中最终会调用BaseExecutor中的query方法,下面是在SQL语句执行过程中对缓存操作的代码,下面简单的分析一下流程:首先会判断我们在配置中是否设置了fushCache为true,为true时表示即时执行的是查询语句也会对缓存进行清空操作clearLocalCache(),然后会先从一级缓存中去查询获取数据,如果查询到数据则直接包装结果,如果没有则直接查询数据库,调用queryFromDatabase方法,list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值作为缓存Cache的key值存储查询结果,

  public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 异常体系之 ErrorContext
    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()) {
      // flushCache="true"时,即使是查询,也清空一级缓存
      clearLocalCache();
    }
    List list;
    try {
      // 防止递归查询重复处理缓存
      queryStack++;
      // 查询一级缓存
      // ResultHandler 和 ResultSetHandler的区别
      list = resultHandler == null ? (List) 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();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List list;
    // 先占位
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 三种 Executor 的区别,看doUpdate
      // 默认Simple
      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;
  }

一级缓存什么时候会被清空呢?

在同一个会话中,update操作(包括delete)会导致一级缓存被清空,还有我们上面提到的在配置文件中或者在映射文件中设置了fushCache为true。源码中当在同一个会话中调用update操作会无条件的情况缓存。

  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

一级缓存的不足:

使用一级缓存的时候,因为它是不能跨会话共享的,在不同会话之间相同的数据可能有不一样的缓存,在别的会话更新数据的时候,自己会话中的缓存不会被清空,这就会造成查到过时的数据。(二级缓存可以解决)

二级缓存

  • 二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace级别的,可以被多个SqlSession共享,只要是在同一个接口中,它的生命周期与应用同步。
  • 二级缓存作为一个作业范围更广的缓存,而且被多个SqlSession共享,所以它应该是在SqlSession外层的,当查询语句执行的时候会先到二级缓存中查找,没有才会到会话中的一级缓存去取,没有则查询数据库。
  • 那么这个二级缓存是在哪个对象里来维护的呢,因为二级缓存是可以动态开启和关闭的,所以在代码中怎么做到开启则使用对象,关闭则不使用呢,这时候就用到装饰器模式了,而MyBatis就是使用一个装饰器类CachingExecutor来维护二级缓存的,如果开启二级缓存,MyBatis在创建Executor对象的时候会对Executor进行装饰。
  • 工作原理:CachingExecutor会对查询请求先做判断二级缓存中是否有缓存的对象,有则直接返回,没有就委派给真正的Executor实现类去执行查询。
image.png

二级缓存的开启方式

首先,在MyBatis-config.xml文件中配置(可以不做配置,因为默认事true)

  •  
    

只要我们没有将cacheEnabled设置为false,MyBatis都会用CachingExecutor去装饰基本的执行器Executor。
注意虽然二级缓存的开关是默认开启的,但是对于每一个Mapper的二级缓存则是默认关闭的,而我们需要使用二级缓存的话,需要在映射文件中配置。
在Mapper.xml中配置标签:


cache属性详解:

  • type 缓存实现类 需要实现Cache接口,默认事PerptualCache,可以使用第三方缓存。
  • size 最多缓存对象个数 默认事1024
  • eviction: 缓存的回收策略,默认是LRU
    LRU - 最近最少使用的:移除最长时间不被使用的对象
    FIFO - 先进先出策略:按对象进入缓存的顺序来移除它们
    SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象
    WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
  • flushInterval:缓存的刷新间隔,默认是不刷新的
  • readOnly:缓存的只读设置,默认是false
    true:只读 mybatis认为只会对缓存中的数据进行读取操作,不会有修改操作为了加快数据的读取,直接将缓存中对象的引用交给用户
    false:不只读 mybatis认为不仅会有读取数据,还会有修改操作。
    会通过序列化和反序列化的技术克隆一份新的数据交给用户
  • blocking: 启用阻塞缓存 ,给get/put方式加锁
    在Mapper.xml中配置了,二级缓存就生效了,select()查询会被缓存,但是update、delete、insert操作依旧会刷新缓存。
    再次说明,如果cacheEnabled=true,而Mapper.xml没有配置标签,二级缓存还是相当于开启的,只是没有起到作用,
    在源码中CachingExecutor的query()方法中有这么一个判断 if (cache != null)这个cache是否为空,而这个cache对象是在前期扫描Mapper.xml映射文件获取的,没有配置则就为空,也就是说不会走到if方法里,也就不会使用二级缓存。
  public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    // cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
    // 由  标签决定
    if (cache != null) {
      // flushCache="true" 清空一级二级缓存 >>
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // 获取二级缓存
        // 缓存通过 TransactionalCacheManager、TransactionalCache 管理
        @SuppressWarnings("unchecked")
        List list = (List) 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;
      }
    }
    // 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如果一个Mapper需要开启二级缓存,但是这里面的某些查询对数据的实时性要求很高,不想走缓存,那么我们可以在单个Statement ID上配置关闭二级缓存

  •  
    

二级缓存开启选择

一级缓存是默认打开的,二级缓存需要我们配置才会开启,那么在什么情况下才有必要开启二级缓存呢?

  1. 因为所有的增删改都会刷新二级缓存,所以适合在以查询为主的应用中,比如,历史交易,历史订单等查询。
  2. 如果多个namespace中有针对同一个表的操作,如果在一个namespace中刷新了缓存,另一个namespace中就没有刷新,这样会出现读到脏数据的情况,所以,推荐在一个Mapper里面只操作单表的情况使用。
    跨namespace的缓存共享设置
  • 
    

cache-ref 代表引用别的命名空间的Cache配置,表示两个命名空间使用同一个Cache,在关联的表比较少,或按照业务可以对表进行分组的时候使用。
注意,这两个Mapper中只要有增删改等操作都会刷新缓存,缓存的意义就不大了。

第三方做二级缓存

除了MyBatis自带的二级缓存外,我们也可以通过实现Cache接口来定义二级缓存。
MyBatis官方提供了一些第三方缓存集成方式,如:ehcache和redis
pom中引入依赖:


            org.mybatis.caches
            mybatis-redis
            1.0.0-beta2
        

Mapper.xml配置,type使用RedisCache


然后在配置redis的连接配置

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

这就集成Redis做二级缓存了,在分布式中可以使用独立的缓存服务器,可以不使用MyBatis自带的二级缓存。

你可能感兴趣的:(MyBatis的缓存机制)