Java教程

MyBatis(二)缓存

本文主要是介绍MyBatis(二)缓存,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

    • 缓存
      • 一级缓存命中条件
      • 一级缓存源码解析
        • 一级缓存结构
      • 一级缓存总结
        • 一级缓存失效情况
          • Spring集成MyBatis一级缓存失效?
    • Mapper
    • 二级缓存
      • 二级缓存的定义
      • 二级缓存的结构
          • 溢出淘汰策略
          • 过期清理策略
          • 线程安全
          • 序列化
          • Cache接口
          • 二级缓存命中条件
          • 二级缓存的配置
          • 二级缓存架构
          • 二级缓存执行流程
          • 二级缓存源码
            • TransactionalCache
            • CachingExecutor的query方法
            • TransactionalCache的getObject方法
            • TransactionalCache的putObject方法
            • TransactionalCache的commit方法

缓存

MyBatis共有两级缓存

  • 一级缓存:在Base Executor实现
  • 二级缓存:在Caching Executor实现

一级缓存命中条件

一级缓存不能打开,存储结构为Key-Value形式,底层为一个HashMap,当一级缓存命中时,Sql只会被编译处理一次

作用:当执行完一句查SQL的时候,再调用时,产生的两个对象是一致的,而且SQL只被编译和使用一次,因为第二次调用的时候,取得对象是从一级缓存中取得

命中相关条件

  • 运行时参数相关
  • 操作与配置相关

命中条件

  1. Sql和参数相同
  2. statementID要一样
    1. 方法名不一样,接口名不一样,也就是方法的全限定名称不一样都会导致StatementID不一样
    2. 不同的会话,执行相同的Sql的StateMentID会不一样(一级缓存被称为Session缓存)
    3. 使用不同的RowBounds进行分页,也会导致StatementID不一样
  3. 查询后清空缓存也不会命中缓存
    1. 方法加上注解flushCache=true
    2. 调用session的clearCache方法
  4. 当主动去调用mapper进行增删改操作的时候(这个增删改的判定是根据注解和配置形式来决定的,而不是sql决定,比如@Delete(select * …)也会被视作删除操作从而清空缓存),也会自动清空一次缓存(否则会产生数据一致性问题)
  5. 关闭一级缓存,修改localCacheScope改成STATEMENT(但此时不是真正关闭,嵌套查询依然会走缓存)
  6. 子查询不会清空缓存,因为子查询是要依赖一级缓存的

所以总的来说

运行时参数

  • sql和参数相同
  • 相同的statementID,即方法的全限定名称一样
  • sqlSession一样(会话级缓存)
  • RowBounds分页也一样

操作与配置

  • 不清空缓存(提交、回滚操作都会清空缓存,还有手动清空)
  • 不配置flushCache=true
  • 不执行update语句,即不执行增删改语句
  • 启用一级缓存,也就是缓存的作用域不改为STATEMENT

一级缓存源码解析

首先认识MyBatis执行SQl的一套流程

  1. 动态代理Mapper
  2. 创建SqlSession
  3. SqlSession中调用Executor
  4. Executor调用StatementHandler去处理SQL
  5. 在数据库执行SQL

而一级缓存(LocalCache)的调用,就在Executor中,具体来说是在BaseExecutor中,(BaseExecutor执行query方法时,会先查看是否存在一级缓存,不存在会执行doQuery方法(这个方法的具体实现是在调用的BaseExecutor实例中,将SQL放入数据库里面执行后,然后填充缓存),存在则使用PerpeturalCache,一级缓存的实现是在PerpetualCache中的,不需要继续走数据库执行SQL

一级缓存结构

一级缓存具体的结构其实是一个HashMap

  • Key:由SQL、Session、分页条件、参数等一系列东西组成的
  • Value:就是缓存的查出来的结果

一级缓存总结

  1. 与会话相关
  2. 参数条件相关
  3. 提交、修改会清空缓存

在这里插入图片描述

一级缓存失效情况

Spring集成MyBatis一级缓存失效?

这是因为Spring集成MyBatis会导致每次执行SQL都是一次会话(没有手动去配置事务),无论SQL是否一样,也就是违反了第一条,规定到底是每一个执行的SQL使用的执行器不一样,每次都会新建一个执行器Executor,从而导致会话是不可能同一的

怎么让其恢复,不失效?

使用手动开启事务,让其在一个会话里面

Mapper

在Spring里面,Mapper里面只有一个SQL,是怎样去执行的呢?

使用的是动态代理(进行拦截操作),从IOC容器里面获取的Mapper其实已经装配了SqlSessionTemplate,而其又动态代理装配了SqlSessionInterceptor、最终SqlSessionInterceptor调用SqlSessionFactory(SqlSessionFactory其实就是MyBatis)构造真正的SqlSession会话,使用会话执行Sql

这一套过程,从SqlSessionFactory开始才是关于MyBatis的,而前面的Mapper被SqiSessionTemplate动态代理的,此时创建了Mapper的实例才可以进行被调用里面的方法,而SqlSessionTemplate又被SqlSessionInterceptor动态代理了

在这里插入图片描述

二级缓存

一级缓存为会话级缓存,而二级缓存为应用级缓存

二级缓存有什么用?与一级缓存有什么不同?

二级缓存的定义

二级缓存为应用级缓存,针对不是一个会话了,而是整个项目而言,对于整个项目,每一次请求进来都是一个新线程,所以二级缓存是可以跨线程使用的,因此,二级缓存会拥有更高的命中率(因为一级缓存只要会话关闭了,就清空了,且不可以跨线程使用),适合缓存一些修改比较少的数据

二级缓存的结构

前面提到过,一级缓存的结构是一个HashMap去存储KeyValue的形式,但由于一级缓存是会话级别的,清空重建的频率比较大,所以不会出现缓存撑爆的现象,而二级缓存是应用级缓存,整个应用的缓存是很大的,容易出现缓存空间不足的现象,所以需要考虑使用什么容器去存储二级缓存,甚至还要考虑使用什么算法来淘汰旧的缓存从而可以使用新的缓存

溢出淘汰策略
  • FIFO:先进先出,即先淘汰出年龄大的数据
  • LRU:最近最少使用,即先淘汰出使用最少的数据(MyBatis默认的溢出淘汰策略就是LRU)
过期清理策略

假如,缓存的数据年龄太大,数据库都已经更新了,二级缓存还继续存着,这是没有意义的,所以需要使用过期清理策略,给缓存设置一个过期时间,到时间就要进行删除

线程安全

二级缓存是可以跨线程访问的,所以要保证线程安全!

序列化

也是因为二级缓存是可以跨线程使用的,假如两个线程同时去获取同一个缓存,那么这个缓存就不能是同个对象,否则会出现线程安全问题,所以在取出缓存后必须经过序列化去转化为不同的对象,才能给线程去使用

Cache接口

MyBatis设置二级缓存,实现的就是Cache接口,也就是说MyBatis只提供了Cache接口来让外界控制和访问二级缓存

现在问题就来了,只有一个Cache接口,那就是说对应上面的那些一系列需求(溢出淘汰、过期清理、序列化等)都是通过实现该接口的实现类去做的,按照平常的写法,我们可能会对应某个需求在实现类去写一个私有方法,让后在重写接口的方法上进行顺序调用,但这有一个不好的地方就是,这样的设计并不适合扩展,假如要去扩展一个新功能,实现类里面就要进行大改动

所以,MyBatis对于上诉需求的实现,采用两个设计模式,装饰器+责任链模式

在这里插入图片描述

每一个功能模块都实现了Cache接口(责任链模式的前提,规范化模块的责任,比如说,一系列模块都有自己的putObject的责任),当获取了SynchronizedCache去调用方法时,会先执行自己的逻辑,然后调用LoggingCache(装饰者模式,给LoggingCache加一层装饰),LoggingCache同样做自己的处理,然后调用LRUCache,继续往下,直到最后的PerpetualCache,相当于就是在进行责任传递,完成了自己的责任后交给下一个人去实现(使用接口规范属于同一种责任),并且使用装饰者模式进行对最上层调用进行解耦!即使后面增加了新节点,只需要在最底层进行添加调用即可让最上层的Cache包括了下面所有Cache的功能,完全屏蔽了底层的复杂性(责任链+装饰者的优点)!

二级缓存命中条件
  • 会话必须提交(手动提交、自动提交是不行的,因为二级缓存是跨线程使用的,假如事务没有提交被其他线程访问到,会出现脏读)
  • SQL语句、参数要相同
  • 分页条件相同
  • 相同的StatementID,也就是同一个Mapper里面的方法

与一级缓存的不同之处在于

  • 一级缓存还必须限制在同一个会话
  • 一级缓存不需要会话提交,反而会话提交后,一级缓存还会消失(新建的会话不能访问之前的一级缓存)
二级缓存的配置

在这里插入图片描述

这里要注意,当开启了缓存之后,使用注解开启的缓存,在Mapper配置文件里面的SQL是用不到的,还必须在Mapper配置里面加上引用缓存空间,即(即配置文件和注解是不能相互关联的!),而缓存空间则是Mapper接口的全限定名,当然,我们也可以引用其他Mapper的缓存空间,好处就是可以共同管理,当一个Mapper的缓存空间进行清空,另外一个缓存空间也会受影响

二级缓存架构

二级缓存因为支持跨线程访问,所以实现的复杂性会比一级缓存要难

二级缓存的架构分为三个部分

  • 会话
  • 事务缓存管理器
  • 缓存区

每个会话都有自己的唯一事务缓存管理器(存放进SqlSession里面的CachingExecutor里面),在事务缓存管理器会有对应的暂存区(暂存区决定于会话使用了多少个Mapper,一个Mapper就是一个暂存区),而对应的暂存区指向了对应的缓存区(对应的操作就是给Mapper指向了缓存空间,而且注意,只有会话提交后,事务管理器才会将暂存区里面的查询的结果转移到缓存区,会话结束了,事务缓存管理器也会注销掉,而缓存区则会保留
在这里插入图片描述

二级缓存执行流程

下面是MyBatis执行SqlSession时候的流程,查询时会先走二级缓存CachingExecutor,再走一级缓存BaseExecutor,一级缓存中没有,BaseExecutor就会进行查询数据库,然后将数据填充到事务管理器的暂存区,提交后,暂存区的数据转移进缓存区
在这里插入图片描述
下面看看二级缓存执行具体执行流程
在这里插入图片描述

  • 查询时,如果二级缓存找不到,后面经过一级缓存去找的时候,会将数据存进事务缓存管理器的暂存区,如果二级缓存找到,直接从二级缓存中取
  • 修改时,会先将会话的事务缓存管理器的缓存区给清理(修改的操作采用标记清空法,表明缓冲区被清空了,也就是说同一个会话的SQL, update语句后面的select经过自己的事务缓存管理器时得到的数据为Null, 此时交由Executor去查,再重新放入二级缓存,避免了)
  • 无论查询、修改操作,都只有在提交事务时,才会进行更新二级缓存
二级缓存源码

二级缓存的CRUD都是经过CachingExecutor去做的

在这里插入图片描述

  • delegate:装饰者模式,被装饰的Executor
  • tcm:事务缓存管理器,通过事务缓存管理器
TransactionalCache

在这里插入图片描述
事务管理器,用来存储暂存区,可以看到是使用HashMap来进行存储的!key为二级缓存,而value则为事务缓存

事务缓存其实是一个二级缓存的装饰对象!

在这里插入图片描述
从源码上可以看到,TransactionalCache装饰了Cache(二级缓存),前面提到过,事务没有提交的数据是没有进二级缓存的,那么保存在哪里呢?其实就是保存在事务缓存里面的entriesToAddOnCommit,也是一个HashMap对象,所以真正的暂存区其实是entriesToAddOnCommit

CachingExecutor的query方法

下面是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();
      //判断二级缓存是否为空,则责任链是不是为空
    if (cache != null) {
        //判断需不需要清空二级缓存
      flushCacheIfRequired(ms);
        //判断缓存是否开启
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
          //从缓存管理器里面去获取数据(通过缓存管理器走二级缓存)
        List<E> list = (List<E>) tcm.getObject(cache, key);
          //如果缓存管理器没有数据
        if (list == null) {
           //调用delegate,也就是BaseExecutor去查询,也就是下面会走一级缓存或者查询数据库
          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);
  }

在这里插入图片描述

TransactionalCache的getObject方法

走二级缓存的实现主要就是在TransactionalCache的getObject方法里面,而且这段代码也是很简洁的

@Override
  public Object getObject(Object key) {
    // issue #116
     //这一步就是调用二级缓存去获取
    Object object = delegate.getObject(key);
      //如果二级缓存中没有
    if (object == null) {
        //将未命中的缓存记录在entriesMissedInCache中
        //防止缓存穿透(数据库、缓存都没有数据)
        //entriesMissedInCache之所可以防止缓存穿透
        //因为其里面的键值对也会被刷新进二级缓存中
      entriesMissedInCache.add(key);
    }
    // issue #146
      // clearOnCommit为true时,表示这个事务缓存的二级缓存需要被清空!
      //也就是当前事务可能执行了修改动作,要对这个事务缓存的二级缓存进行清空
    if (clearOnCommit) {
        //因为缓存区要被清空,所以返回空!
        //而且这里返回空只针对当前事务!
        //因为对于其他事务来说,当前事务未提交所以是不可见的,所以不必对其他事务关闭二级缓存
        //但对于当前自己事务来说,自己的修改是可见的,所以要关闭二级缓存
        //让当前事务可以查数据库知道自己执行的动作(只有DB记录着当前事务的动作)
      return null;
    } else {
        //如果二级缓存不清空,返回二级缓存中的值
        //即使是空也返回
      return object;
    }
  }
TransactionalCache的putObject方法

putObject方法其实就是将二级缓存中没有,将Executor查询的结果放进去暂存区中,等事务提交后再将缓存区中的东西刷新进二级缓存中

public void putObject(Object key, Object object) {
    //从源码上看到,只是将key value存放进了entriesToAddOnCommit中
    entriesToAddOnCommit.put(key, object);
  }
TransactionalCache的commit方法

commit也就是相当于事务提交了,那么就要将entriesMissedInCache和entriesToAddOnCommit的数据刷新进二级缓存中去了

  public void commit() {
      //如果二级缓存需要清空
    if (clearOnCommit) {
        //清空二级缓存
      delegate.clear();
    }
      //刷新entriesMissedInCache和entriesToAddOnCommit的数据
    flushPendingEntries();
      //重置事务缓存
      //其实就是将clearOnCommit、entriesMissedInCache和entriesToAddOnCommit清空重置
    reset();
  }
private void flushPendingEntries() {
    //遍历暂存区,将暂存区所有数据存进二级缓存
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    //遍历未命中的数据,也将其放进二级缓存
    //不过要排除一开始未命中但可能后续存进暂存区的数据
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
          //存进去为null(可能再取的过程中,如果二级缓存中存在但为空,可能返回特殊值,来防止缓存穿透)
        delegate.putObject(entry, null);
      }
    }
  }
  private void reset() {
        //进行清空重置而已
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }
这篇关于MyBatis(二)缓存的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!