Mybatis二级缓存和一级缓存

为什么要使用缓存

当我们需要使用DB数据库的数据时,需要使用statement对象去操作DB数据库,如果同时又多个请求一样的statement的时候就需要去查询多次,如果使用了缓存,就不会出现这种情况,这样又多个相同的statement对象请求来的时候只会执行一次DB数据库,因为会先去缓存中获取数据,确认是否又缓存的数据,有就返回数据,如果没有才去执行DB数据库。

mybatis二级缓存的使用测试
1.同一个命名空间同一个sqlsession对象执行相同语句 只会操作一次数据库,第二次会从数据库拿

MybatisMapper mapper = sqlSessionFactory.openSession().getMapper(MybatisMapper.class);
System.out.println(mapper.selectById(“2”, “测试数据操作”));
System.out.println(mapper.selectById(“2”, “测试数据操作”));
控制台数据记录:
在这里插入图片描述

2.同一个命名空间不同sqlsession对象执行相同语句
SqlSession sqlSession = sqlSessionFactory.openSession();
      MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
      System.out.println(mapper.selectById("2", "测试数据操作"));
      sqlSession.commit();
      SqlSession sqlSession1 = sqlSessionFactory.openSession();
      MybatisMapper mapper1 = sqlSession1.getMapper(MybatisMapper.class);
      System.out.println(mapper1.selectById("2", "测试数据操作"));

控制台数据记录:
在这里插入图片描述

mybatis一级缓存的使用测试
1.同一个sqlsession对象执行同一个语句
SqlSession sqlSession = sqlSessionFactory.openSession();
      MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
      System.out.println(mapper.selectById("2", "测试数据操作"));
      System.out.println(mapper.selectById("2", "测试数据操作"));

控制台数据记录:
在这里插入图片描述

2.不同sqlsession对象执行同一个语句
     SqlSession sqlSession1 = sqlSessionFactory.openSession();
      SqlSession sqlSession = sqlSessionFactory.openSession();
      MybatisMapper mapper1 = sqlSession1.getMapper(MybatisMapper.class);
      MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
      System.out.println(mapper.selectById("2", "测试数据操作"));
      System.out.println(mapper1.selectById("2", "测试数据操作"));

控制台数据记录:
在这里插入图片描述

如果有事务的提交的话就会清楚缓存,这样同样的语句就会执行多次
SqlSession sqlSession = sqlSessionFactory.openSession();
      MybatisMapper mapper = sqlSession.getMapper(MybatisMapper.class);
      System.out.println(mapper.selectById("2", "测试数据操作"));
      sqlSession.commit();
      System.out.println(mapper.selectById("2", "测试数据操作"));

控制台数据记录:
在这里插入图片描述

mybatis缓存的实现机制

一般需要实现缓存的时候会在莫个作用域里面创建一个Map对象key-value的形式来保存数据,当然mybatis缓存的实现也不例外,分别是二级缓存使用Map<String, Cache> caches = new StrictMap<>(“Caches collection”)来存储二级缓存的数据,而一级缓存是使用PerpetualCache 类的Map<Object, Object> cache = new HashMap<>()属性来存储一级缓存的数据,从上面可以看出来不管是二级缓存还是一级缓存mybatis缓存都是使用的cache接口对象保存。

mybatis二级缓存的初始化

mybatis规定如果需要使用二级缓存,需要在mybatis的配置文件一般是xxxMapper.xml中配置,具体配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.mybatistestcustom.MybatisMapper">
 <!-- 这里是配置二级缓存方式1,具体参数设置可以参考官网的配置 -->
  <cache />
   <!-- 这里是配置二级缓存方式2,具体参数设置可以参考官网的配置 -->
   <cache-ref/>
  <sql id="common">
    select name from user where
  </sql>
    <resultMap id="dsa" type="org.apache.ibatis.mybatistestcustom.TestDomain">
      <result property="name" column="name"/>
    </resultMap>
    <select id="selectById" parameterType="map" resultMap="dsa">
        <include refid="common"/> id = ${id} and name = #{name}
    </select>
</mapper>
mybatis二级缓存的初始话需要先解析/Mapper的文件,在根据该文件去解析cache,cache-ref获得配置的缓存数据,然后在把获取的缓存数据保存到指定Map对象中
如果是通过<cache-ref/>获取的配置缓存数据如下:

1.根据命名空间去获取对应的缓存对象
2.把这个缓存对象设置成当前的缓存对象

public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      Cache cache = configuration.getCache(namespace);
      if (cache == null) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
  }
如果是通过<cache/>获取的配置缓存数据如下:

1.通过建筑类CacheBuilder(currentNamespace)传入当前的命名空间的名字来新建一个cache缓存对象
2.把获取的cache缓存对象保存到Configuration类的Map<String, Cache> caches = new StrictMap<>("Caches collection")属性当中,且把当前缓存对象设置成刚刚通过命名空间获取的cache缓存对象。

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

从上面两种获取二级缓存的方式中可以看出来,两种方式都是通过命名空间去获取的,所以二级缓存的作用域应该就是命名空间的作用域了。

把获取的二级缓存的数据保存到指定Map对象如下:

这个保存操作是在解析/Mapper的select|insert|update|delete的节点时候保存的。
1.先解析select|insert|update|delete 节点

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

2.通过XMLStatementBuilder建筑类的parseStatementNode()方法去解析该节点的所有属性设置,这里会有很多的属性的解析,我们这里不用去关系这些属性的解析,只需要留意下面代码最后一个方法builderAssistant.addMappedStatement() 这个方法即可

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

3.初始话二级缓存的操作就在builderAssistant.addMappedStatement() 这个方法中执行.cache(currentCache)设置的,具体代码如下,会把上面获取到的currentCache当前缓存对象设置到MapperStatementBuilder建筑类的二级缓存属性中,在后续需要使用二级缓存的时候就通过MapperStatement对象获取cache即可,到此二级缓存初始话完成。

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);
mybatis一级缓存的初始化

mybatis一级缓存根据二级缓存不一样,二级缓存如果需要使用的话需要在xxxMapper.xml文件中设置或者来配置二级缓存,而一级缓存mybatis默认是开启的,不需要手动注册,当时初始化也是要初始话的。

一级缓存默认是开启的
一级缓存的作用域的默认设置

1.这个setting的属性设置有很多设置操作,这里忽略了其他的设置操作,只显示了设置一级缓存作用域的设置。
2.可以看到默认的一级缓存作用域是session,如果想要一级缓存失效可以把这个一级缓存的作用域设置成STATEMENT
3.官方对这个作用域的设置解析如下:

localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。可选值 SESSION | STATEMENT 默认值是 SESSION

private void settingsElement(Properties props) {
    。。。。
    configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
    。。。。
  }
初始话一级缓存

1.首先需要获取opsession。

SqlSession sqlSession = sqlSessionFactory.openSession()

2.然后在使用DefaultSqlSessionFactory新建sqlSession对象的时候新建一个执行器newExecutor

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    。。。
      final Executor executor = configuration.newExecutor(tx, execType);
      。。。
       }

3.新建newExecutor的内部逻辑中的simpleExcetor初始话的时候会对父类构造函数BaseExcetor()初始化,会把一级缓存设置为 PerpetualCache类,对于一级缓存的操作都在PerpetualCache 类的Map<Object, Object> cache Map属性中操作。

protected BaseExecutor(Configuration configuration, Transaction transaction) {
    。。。
    this.localCache = new PerpetualCache("LocalCache");
    。。。
  }
mybatis使用二级缓存的代码

如果在xxxMapper.xml文件中配置了二级缓存,就会在opsession的时候获取完simpleExcetor后还会再对这个执行器包装一个CacheExcetor类,然后在使用CacheExcetor操作数据库,在操作数据库的时候就会去先查询一次二级缓存是否有需要的数据,接着再去查询一级缓存是否有需要的数据,最后两个缓存都没有数据就去DB查询数据

  1. 根据配置是否需要CacheExcetor装饰
    可以看到是根据cacheEnabled来判断是否需要装饰为CacheExcetor类,cacheEnabled这个属性默认就是true的,在Configuration类初始化的时候就初始话为true了。
    官方对这个cacheEnabled的配置解析为:

cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 可选值true | false 默认值true

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
   。。。
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    。。。
  }
  1. 使用二级缓存的代码,如果上面配置了二级缓存,那么下面这个executor.query就会去调用CacheExcetor的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    。。。
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      。。。
  }
  1. 在CacheExcetor执行器中的处理逻辑如下,可以看出首先会去MappedStatement对象中获取已经设置好的二级缓存对象,如果不为空,就执行二级缓存的逻辑,会去当前的二级缓存中获取需要的数据,如果获取不到就去执行DB数据库获取数据,最后会把数据保存到缓存当中。
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);
  }
mybatis使用一级缓存的代码

如果没有在xxxMapper.xml文件中配置二级缓存,就会在opsession的时候获取完simpleExcetor后不会再对这个执行器包装一个CacheExcetor类,然后在使用simpleExcetor操作数据库,在操作数据库的时候就会去先查询一次一级缓存是否有需要的数据,如果一级缓存都没有数据就去DB查询数据
1.使用一级缓存的代码,如果上面没有配置二级缓存,那么下面这个executor.query就会去调用CacheExcetor的方法

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    。。。
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      。。。
  }
  1. 在simpleExcetor执行器中的处理逻辑如下,可以看出首先会去localCache对象中获取已经设置好的一级缓存对象,如果不为空,就执行一级缓存的逻辑,会去当前的一级缓存中获取需要的数据,如果获取不到就去执行DB数据库获取数据,最后会把数据保存到缓存当中。
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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) 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;
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值