【mybatis源码系列1】 二级缓存

本文详细解析mybatis二级缓存的实现机制,从SqlSessionFactory的创建到SqlSession的使用,探讨缓存在Executor中的位置,以及一级缓存与二级缓存的区别。同时提出关于缓存的一系列问题,如缓存存储位置、开启二级缓存的影响以及二级缓存的实际应用场景。
摘要由CSDN通过智能技术生成

面试mybatis必问二级缓存,都知道有二级缓存,那mybatis是怎么实现的,本系列文章以mybatis-3.5.6版本为例。啰里啰唆写了,想直接看缓存的跳到缓存部分。

一、mybatis流程

写mybatis代码的时候,逃不过这几步

// 1.获取配置文件
InputStream in =Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatis-config.xml");
//2.开启SqlSession工厂        
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(in);
//3.开启SqlSession        
SqlSession sqlSession = build.openSession();
//4.执行sql并返回 
// 4.1 直接用   
    sqlSession.selectOne("com.faith.mybatiscode.mapper.ProductConfigMapper.selectOne", 1);
// 4.2 用mapper代理
 ProductConfigMapper mapper = sqlSession.getMapper(ProductConfigMapper.class);
        List<ProductConfigVo> productConfigVos = mapper.selectProductConfigs(Arrays.asList(1));

各种写法大同小异,区别主要是两点,第3步的时候是用单例sqlSession还是多实例的,第4步直接用sqlSession进行操作还是用mapper代理的方式。

1.结构

还记得我们写jdbc代码的时候怎么写的吗?大概这么几步

 String url = "jdbc:mysql://127.0.0.1:3306/Supermarket?characterEncoding=utf-8";
        String username = "root";
        String password = "123";
        try{
            // 1.加载驱动程序
            Class.forName("com.mysql.jdbc.Driver");
            
            // 2.获得数据库链接
            Connection conn = DriverManager.getConnection(url, username, password);
            
            // 3.通过数据库的连接操作数据库,实现增删改查(使用Statement类)
            String name="张三";
            String sql="select * from product_config where id_no=?";
            PreparedStatement statement = conn.prepareStatement(sql);
            statement.setString(1, name);

            // 4.处理数据库的返回结果(使用ResultSet类)
            ResultSet rs = statement.executeQuery();
            while (rs.next()) {
                System.out.println(rs.getString("UserName") + " " + rs.getString("Password"));
            }

            // 5.关闭资源
            rs.close();
            statement.close();
            conn.close();					
        }catch(SQLException | ClassNotFoundException se){
            System.out.println("数据库连接失败!");
            se.printStackTrace() ;
        }

那mybatis是怎么帮我们做这些步骤的呢?先看这几个对象:

1.SqlSessionFactory

public interface SqlSessionFactory {

  SqlSession openSession();

  SqlSession openSession(boolean autoCommit);

  SqlSession openSession(Connection connection);

  SqlSession openSession(TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

  Configuration getConfiguration();

}

看SqlSessionFactory接口,做了两件事,获取SqlSession和Configuration,看他的默认实现类DefaultSqlSessionFactory持有一个Configuration对象,看这个Configuration里有什么

 要了命了,这么多变量,冷静下来看下,基本上包括我们在Mybatis-config.xml里配置的所有信息,其他的不认识,碰到再说。看 new SqlSessionFactoryBuilder().build(in) 做了什么,主要是

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

所以就是解析配置文件为Configuration对象。

openSession

 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

只是返回一个DefaultSession对象,注意这里openSession一次就new一个SqlSession,缓存分级就从这里区别开来了。从而我们知道每个SqlSession持有相同的Configuration对象,不同的Executor对象,即

 

sqlSession.selectOne
 @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

从代码我们可知,SqlSession是用到了Configuration对象组装好MappedStatement,然后用executor执行得到结果,对比jdbc,似乎有这种感觉。

1.configuration在解析xml的时候就组装好带占位符的sql语句了。在这里取出来给executor用。

2.executor管理了参数的拼装,即PreparedStatement的实例化和参数设置,获取连接,执行,返回ResultSet和解析ResultSet到封装的对象。

如果设这样的话,缓存一定在executor里了。

缓存

1.在mybatis-config.xml里配置<cache/>标签,表示开启二级缓存,一级缓存是默认开启的。看CachingExecutor里的代码逻辑。

2.二级

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

先从缓存里取,取不到再执行查询,这一级的cache是MappedStatement里的,这一步还没到事务呢,所以是跨事务的。实际上代码里的tcm就是一个TransactionalCacheManager,

真正的数据缓存是这个map的value,TransactionalCache,

主要关注delegate和entriesToAddOnCommit,delegate实际上和一级缓存一样,是一个

 无论一级缓存还是二级缓存,最终都是存进这个cache的Map对象里。

而entriesToAddOnCommit这个map,看名字就知道是一组entries,调用SqlSession.commit()了之后才会刷到PerpetualCache的cache里,这里头才是真正的缓存数据。其实SqlSession.close()也会刷。

3.一级

 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);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

 这个localCache就是上边提到的 PerpetualCache,所以只要是查询都会缓存。

问题

1.一级缓存二级缓存都存到一个Map里,而且key一样,所有如果没有配置外部的二级缓存,内存里只存了一份缓存吧?

2.网上说二级缓存不要轻易开启,容易导致内存撑爆,这是什么道理?就算开启了二级缓存,内存里只有一份缓存数据,怎么会因为开启二级缓存而导致内存占用变大呢?除非是一直SqlSession不commit(),entriesToAddOnCommit这个map把内存撑爆,或者是一次SqlSession查出来的数据太大,在commit之前就爆了(如果这中情况的话一级缓存也爆)。

3.就算配置了外部二级缓存,要想使用上二级缓存,第一次必须查数据库,意味着一级缓存里也被存进去了。所以一级缓存该占用的内存还是占用啊,那二级缓存的意义在哪里?难道是为了多应用的架构,让别的应用直接去外部的二级缓存里取,而不用查数据库?

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值