MyBatis的缓存一级缓存机制

MyBatis做为持久层的框架,跟数据库的交互式是最多的,在互联网的这种经常面临的高并发情况下,缓存的中要性不言而喻,比如大家常见的Redis、Memcached等,同样MyBatis也提供了缓存的的机制,MyBatis提供了两种缓存机制,一级缓存和二级缓存,接下来我们就来先来看下一级缓存的是怎么实现的

一级缓存

一级缓存是MyBatis中的默认提供的缓存的,也就是说,我们在使用Mybatis的时候本身就在使用,他是默认开启的, 一级缓存是SqlSession级别的缓存,只有在一个SqlSession内的查询才能共享缓存的数据,当我们关闭sqlSession的时候或者执行增删改查的操作的时候,缓存就会被清空

接下来我们来验证下,上代码

   /**
     * 测试一级缓存
     */
    @Test
    public void testCacheOne() throws Exception {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory builder = new SqlSessionFactoryBuilder().build(inputStream);
        // 第一次获取sqlsession对象
        SqlSession sqlSession = builder.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        System.out.println("第一次查询....");
        User user = mapper.selectUserById(1);
        System.out.println(user);
        // 修改数据
//        user.setName("哈哈");
//        user.setAge(12);
//        int i = mapper.updateUserByUserId(user);
//        sqlSession.commit();

        // 第二次查询
        System.out.println("第一次查询....");
        UserMapper mapper1 = sqlSession.getMapper(UserMapper.class);
        User user1 = mapper1.selectUserById(1);
        System.out.println(user1);
        sqlSession.close();
    }

验证一级缓存

看下运行结果图

第一次查询....
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 嘿嘿, 21
<==      Total: 1
User(id=1, name=嘿嘿, age=21)
第一次查询....
User(id=1, name=嘿嘿, age=21)

我们看到在第一次查询的时候,发送了sql去数据库中去查询,在第二次查询的时候没有发送任何sql,并且返回了同样的结果,这就说明在第二次查询的时候,并没有去数据库查询而是去缓存中拿了

清空一级缓存

接着我们在看下清空缓存的测试

第一次查询....
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 嘿嘿, 21
<==      Total: 1
User(id=1, name=嘿嘿, age=21)
==>  Preparing: update t_user set name=?,age=? where id=? 
==> Parameters: 哈哈(String), 12(Integer), 1(Integer)
<==    Updates: 1
第二次查询....
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 哈哈, 12
<==      Total: 1
User(id=1, name=哈哈, age=12)

这里可以看出来,在我们执行了update更新语句的时候,MyBatis清空了一级缓存,第二次查询的时候,再一次发送了Sql去数据库中查询

知其然,知其所以然

到这里案例已经帮我证明的了一级缓存的存在,以及使用的过程中是如何清空的,但凡事都要问个究竟那接下来我们就一起探寻源码,看看一级缓存是咱么实现的,又是怎么清除的

咱么按照第二次的测试,先去查询,在执行update操作,然后再去查询的步骤,来探寻源码的执行

在这里我直接找到缓存的执行的逻辑,不在去看其他的执行细节,因为之前源码分析的环节,已经分析过详细的执行流程

执行查询

第一次

在执行openSession的时候,会创建Executor对象,根据执行器的类型ExecutorType选择的是SIMPLE类型,

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
        // 创建简单执行器
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        // 创建缓存执行器
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

然后创建的SimpleExecutor对象,在这个对象创建的过程中,是调用的父类BaseExecutor对象,在父类实例化的过程中,创建PerpetualCache缓存对象

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

紧接着又创建了CachingExecutor对象,进到这个对象中,这里是处理二级缓存的地方装饰器对象,在执行的查询的时候,MyBatis会先去二级缓存中去查找

Cache是从MappedStatement中获取到的,而MappedStatement又和每一个insert、delete、update、select绑定并在MyBatis启动的时候存入Configuration中:

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    Cache cache = ms.getCache();
        if(cache != null) {
            this.flushCacheIfRequired(ms);
            if(ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, parameterObject, boundSql);
                List list = (List)this.tcm.getObject(cache, key);
                if(list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);
                }
                return list;
            }
        }
    //先读取二级缓存,再去查询
        return this.delegate.query(ms, 
        parameterObject, 
        rowBounds, 
        resultHandler, 
        key, 
        boundSql);
    }

接下来的查询从一级缓存中查找,测试的是第一次查询,一级缓存中是没有数据

  public  List query(MappedStatement ms, 
  Object parameter, 
  RowBounds rowBounds, 
  ResultHandler resultHandler, 
  CacheKey key, BoundSql boundSql) throws SQLException {
    // 删除不必要代码。。。。
    List list;
    try {
      queryStack++;
        // 去本地缓存中查询,这次是第一次查询,缓存中没有返回空
      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 {
        // 去数据库查询
      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;
  }

更新数据

刚才的测试结果中,我看到更新数据会清空缓存这里我们主要看下在执行更新的时候,是怎么清空缓存的

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

在执行更新操作前 ,会先清空一级缓存,所以到这里我们就能才到,在第二次执行的时候,缓存中已经没有数据了,所以会在此执行和第一次一样的逻辑,继续从数据库中查找,所以在控制台我们看到会再一次的发送sql语句。其他的删除、新增等操作对对缓存的影响,也是同样的逻辑(因为新增和删除底层都是执行的update()方法),大家可以试着自己去看下

所以,这里第二查询其实和第一查询是一致的。这里就不在源码分析了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注