mybatis源码解析(二) 解读一、二级缓存

mybatis缓存

mybatis缓存有一级缓存和二级缓存,mybatis为什么会采用两种缓存机制呢?让我们带着问题进入今天的主题吧。

mybatis 一级缓存

mybatis缓存在执行相同的SQL和相同的参数时,不会去数据库中重复调用,只需要从缓存中取出来即可,只要保持在同一会话和其它相关的条件下,我们的多求请求实际只调用了一次,可以大大节省查询数据的开销。但在我们实际开发中,一级缓存使用的频率较低, 为什么呢?
一级缓存只存在于当前会话中, 如果脱离了当前会话将不可以使用,但我们常搭配spring来使用mybaits,但spring中每 条sq|语句处于单独的事务中,执行成功后默认就提交了事务并且关闭了会话。

小知识:我们之前有提到一个会话中有且只存在一 个事务, 所以spring在使用mybatis时默认每次都会重新开启一个新的会话,但如果我们给语言加上一 个事务@Transactional, 就可以使用事务了。

解读一级缓存

一级缓存的逻辑在BaseExecutor里面完成的,主要逻辑如下:

//源码位为BaseExecutor 140行
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.");
    }
    //flushCache 设置判断是不是需要清除缓存,这个参数可以设置
    //queryStack =0 时没有递归调用
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      //防止调用时会被清空,这样递归调用到上面的时候就不会再清局部缓存了
      queryStack++;
      //先根据cachekey从localCache去查,如果设置了结果处理器同样不会使用一级缓存
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //若在缓存列表中找到有对应的缓存,处理localOutputParameterCache
        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
        //缓存作用域如果是STATEMENT,由清除一级缓存和参数缓存
        clearLocalCache();
      }
    }
    return list;
  }
一级缓存的命中场景

运行时必要条件:

1.必须必在同一个会话
2.SQL语句、参数必须相同
3.相同的statementId
4.RowBounds 相同(即添加的分页属性)

    // 1.sql 和参数必须相同
    // 2.必须是相同的statementID
    // 3.sqlSession必须一样 (会话级缓存)
    // 4.RowBounds 返回行范围必须相同
    @Test
    public void test1(){
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        // com.toto.UserMapper.selectByid
        User user = mapper.selectByid(1);
        RowBounds rowbound=RowBounds.DEFAULT;
        //selectOne最终的底层调用的也是selectList,因为statementID、sqlSession、rowbound、sql和参数一样,所以一级缓存可用
        List user1 = sqlSession.selectList(
                "com.toto.UserMapper.selectByid", 1,rowbound);
        System.out.println(user == user1.get(0));
    }

输出结果:
Preparing: select * from db_user where id=?
Parameters: 1(Integer)
Columns: id, name, password, status, nickname, createTime
Row: 1, admin, admin, 1, 管理员大哥, 2016-08-07 14:07:10
Total: 1
Cache Hit Ratio [com.toto.UserMapper]: 0.0
true

操作与配置相关条件:

1.未提交、回滚当前事务,这相当于手动清空缓存
2.未配置flushCache = true,默认default,如果配置true上面代码就会删除缓存,下面有图例
3.未执行update
4.缓存作用域不是STATEMENT

    // 1.未手清空
    // 2.未调用 flushCache=true的查询
    // 3.未执行Update
    // 4.缓存作用域不是 STATEMENT-->
    @Test
    public void test2(){
        // 会话生命周期是很短暂的
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.selectByid3(1);
        sqlSession.commit();// 这会调用clearCache()
        sqlSession.rollback();// 这会调用clearCache()
//        mapper.setNickName(1,"管理员小弟");// 修改时也会 clearCache()
        User user1 = mapper.selectByid3(1);// 数据一致性问题
        System.out.println(user == user1);
    }

在这里插入图片描述

一级缓存源码解析

我先发我整理的图运行流程图(太丑勿喷,能看就行):
在这里插入图片描述
我跟着流程把关键代码走读一下(多图警告):
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Spring与Mybatis一级缓存不得不说的事

前面我有提到增加事务就能使用Mybatis的事务,这是为什么呢?
让我们带速读一下流程吧
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

    @Test
    public void testBySpring(){
        ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
        UserMapper mapper = context.getBean(UserMapper.class);
     //动态代理                             动态代理                       MyBatis
    // mapper ->SqlSessionTemplate --> SqlSessionInterceptor-->SqlSessionFactory

        DataSourceTransactionManager transactionManager =
                (DataSourceTransactionManager) context.getBean("txManager");
        // 手动开启事物
        TransactionStatus status = transactionManager
                .getTransaction(new DefaultTransactionDefinition());

        User user = mapper.selectByid(1); // 每次都会构造一个新会话 发起调用
// 每次都会构造一个新会话 发起调用,但开启事物后原会话没被关闭,仍使用同一个事务,即可使用一级缓存
        User user1 =mapper.selectByid(1);
        
        System.out.println(user == user1);

    }

总结

1.与会话密切相关,相同会话才能触发
2.参数条件相关,包括分页属性也是参数的条件,还有参数及其相关配置项
3.提交回滚,修改都会清空
MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码。所以参数条件很重要

Mybatis 二级缓存

应用级缓存,可以跨线程使用,一般应用于修改情况比较少的表做查询缓存,并且查询时优先于一级缓存,搭配使用时攻防兼备。

完整的解决方案

1.内存存储: 实现简单,速度比较快,但不能持久化,并且容量比较有限
2.存到硬盘:可以持久化,容量较大,但访问速度没内存快
3.分布式存储:分布式场景存到单硬盘/内存服务器存储缓存会造成一些通病问题,可以使用redis来进行缓存,这也是分布式场景的解决方法之一。

缓存溢出淘汰机制

当容量满的了时候我们就会对缓存进行清除,清除的算法的就是溢出淘汰机制
常见的算法有:

1.FIFO:先进先出算法
2.LRU:最近最少使用
3.WeakReference:弱引用
将缓存对象进行弱引用包装,当Java进行gc的时候, 不论当前的内存空间是否足够,这个对象都会被回收
4.SoftReference:软引用
与弱引用类似,不同在于只有当空间不足时GC才回收软引用对象

缓存线程的安全问题

我们要保证二级缓存被其它线程使用,为了保证线程写和读的安全,当拿到缓存数据后,可以对它进行修改而不能影响原缓存数据, 通常采取的是序列化缓存对象然后进行深拷贝操作。所以这要求我们要缓存的实体类必须序列化(实现Serializable) ,不然会出现不能序列化的BUG.

小知识:我们常使用的存储类型基本都实现了Srializable方法,例如: hashmap,arraylist,String …

Mybatis二级缓存解决方案

mybaits是怎么实现它的二级缓存的解决方案呢?

mybaits使用的是责任链的解决方案,首先它定义了缓存中的一些最基本的公用方法,全部定义到Cache的类里面, 这些功能包括设置缓存,获取缓存,清除缓存,获取缓存数量,然后我们上述的所有功能都会对应一个组件类,并基于装饰者模式责任链的模式,将各个组件进行串连。在执行缓存的基本功能时,其它的缓存通过责任链来依次往下传递。
那它的设计模式有哪些优点呢?

职责单一,各个节点只负责自己的逻辑, 不关心其它逻辑
扩展性强,可根据需要扩展节点,删除节点还可以调换顺序,保证了灵活性
松耦合,各节点之间不仅没有强制依赖其它节点,而是通过顶层的Cache接口进行间接依赖

在这里插入图片描述
详细介绍每个缓存对应的功能篇幅较长,后面挖坑再更,mybatis提供的一些cache在org.apache.ibatis.cache包下,其中一级缓存使用的impl下的,二级缓存使用的是decorators包下的。

二级缓存开启条件

二级缓存不像一级缓存一样会默认开启,我们可以通过@CacheNamespace或为指定Mappedstatement做声明。声明后该缓存为该Mapper所独有, 其它未设置的不能便用。但如果需要多个mapper用同一个缓存空间,我们可以通过@CacheNamespaceRef来进行引用到同一个缓存空间。
如果是xml,可以通过标签来添加对此mapper进行级缓存,同理使用可以引用到同一个缓存会话空间。
下面介绍一下我们使用缓存时可以配置的相关参数:

配置名配置描述
implementation指定缓存的存储实现类默认是用PerpetualCache的HashMap存储在内存当中
eviction指定缓存溢出淘汰实现类,默认LRU,清除最近最少使用
flushInterval设置缓存定时全部清空时间,默认不清空。清空策略是到时间整体清空而不是为某个值清空
size指定缓存容量,超出后会按eviction所指定的算法进行淘汰
readWithtue通过序列化复制来保证缓存对象是可读与的,默认true, false则多线程操作同个对象
blocking为每个key的访问添加了阻塞锁,可以有效防止缓存击穿
properties为上述组件,配置额外参数,key对应组件中的字段名

如:

@CacheNamespace(implementation = MybatisRedisCache.class,eviction = FifoCache.class)
 <cache type="com.toto.MybatisRedisCache">
        <property name="eviction" value="LRU" />
        <property name="size" value="1024" />
        <property name="readOnly" value="false" />
    </cache>

二级缓存命中条件

二级缓存和一级缓存命中差不多,但必须要提交会话,并且可以不同会话,下面列举了命中条件

1.会话提交
2.SQL语句和参数相同
3.相同的StatementId
4.RowBounds相同

为什么二级缓存一定需要提交事务呢,我画一张图来表示吧:
在这里插入图片描述
如果会话不提交,则会出现脏读,为了避免这种事情发生,所以必须先提交才能获取二级缓存,包括对缓存的清空,也必须是会话正常提交之后才生效。 那如果没提交,是不是存在一个临时存储区域呢?

二级缓存结构

为了实现会话提交之后才变更二级缓存,MyBatis为每个会话设立了若干个暂存区,当前会话对指定缓存空间的变更,都存放在对应的暂存区,当会话提交之后才会提交到每个暂存区对应的缓存空间。为了统一管理这些暂存区,每个会话都一个唯一的事物缓存管理器,所以这里暂存区也可叫做事物缓存。
如图:
在这里插入图片描述
我画了一张两个会话情景下事物缓存和暂存区的图:
在这里插入图片描述

二级缓存执行流程

原本会话是通过执行器来实现SQL调用,这里是基于装饰器模式使用CachingExecutor对SQL调用逻辑进行拦截,然后嵌入了二级缓存相关逻辑。
在这里插入图片描述

查询操作query

当会话调用query() 时,会基于查询语句、参数等数据组成缓存Key,然后尝试从二级缓存中读取数据。读到就直接返回,没有就调用被装饰的Executor去查询数据库,然后在填充至对应的暂存区。
在这里插入图片描述
源代码:

  //CachingExecutor 93 行
  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) { //如果设置了返回值处理Handler则不会触发缓存
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);//从缓存的tcm中获取缓存
        if (list == null) {
        // 向下通往BaseExecutor,先走一级缓存,最后取真实的执行器执行
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 查询成功后添加进二级缓存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
更新操作update

当执行update操作时,同样会基于查询的语句和参数组成缓存KEY,然后在执行update之前清空缓存。这里清空只针对暂存区,同时记录清空的标记,以便当会话提交之时,依据该标记去清空二级缓存空间。但如果在查询操作中配置了flushCache=true ,也会执行相同的操作。
在这里插入图片描述

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms); //是否有设置了清除缓存,update
    return delegate.update(ms, parameterObject);
  }
提交操作commit

当会话执行commit操作后,会将该会话下所有暂存区的变更,更新到对应二级缓存空间去。

  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

一级缓存和二级缓存的基本流程图

图(太晚了后面会补源码对应的图):暂无

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值