Mybatis一级缓存和二级缓存原理剖析与源码详解

news2024/11/16 2:48:54

Mybatis一级缓存和二级缓存原理剖析与源码详解

在本篇文章中,将结合示例与源码,对MyBatis中的一级缓存二级缓存进行说明。

MyBatis版本:3.5.2

文章目录

  • Mybatis一级缓存和二级缓存原理剖析与源码详解
    • ⼀级缓存
      • 场景一
      • 场景二
      • ⼀级缓存原理探究与源码分析
      • createCacheKey方法源码解析
      • BaseExecutor.query()方法源码解析
    • 二级缓存
      • 场景一
      • 场景二
      • 场景三
      • 场景四
      • 场景五
      • 二级缓存源码分析

⼀级缓存

场景一

在⼀个sqlSession中,对User表根据id进⾏两次查询,查看他们发出sql语句的情况

public class MybatisCacheDemo {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = factory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        User u1 = userMapper.selectUserById(1);
        System.out.println(u1);
        //第⼆次查询,由于是同⼀个sqlSession,会在缓存中查询结果
        //如果有,则直接从缓存中取出来,不和数据库进⾏交互
        User u2 = userMapper.selectUserById(1);
        System.out.println(u2);

    }
}

执行得到的查询结果如下:

在这里插入图片描述

场景二

同样是对user表进⾏两次查询,只不过两次查询之间进⾏了⼀次update操作。

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = factory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        User u1 = userMapper.selectUserById(1);
        System.out.println(u1);

        //第⼆步进⾏了⼀次更新操作,sqlSession.commit()
        u1.setUsername("windy");
        userMapper.update(u1);
        sqlSession.commit();

        //第⼆次查询,由于是同⼀个sqlSession.commit(),会清空缓存信息
        //则此次查询也会发出sql语句
        User u2 = userMapper.selectUserById(1);
        System.out.println(u2);

    }

查看控制台打印情况:

在这里插入图片描述

总结

1、第⼀次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,如果没有,从数据库查询⽤户信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。

2、 如果中间sqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的 ⼀级缓存,这样做的⽬的是为了让缓存中存储的是最新的信息,避免脏读。

3、 第⼆次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,缓存中有,直接从缓存中获取⽤户信息

在这里插入图片描述

⼀级缓存原理探究与源码分析

⼀级缓存到底是什么?⼀级缓存什么时候被创建、⼀级缓存的⼯作流程是怎样的?相信你现在应该会有 这⼏个疑

问,那么我们本节就来研究⼀下⼀级缓存的本质。

我们知道,MyBatis中的SqlSession可以用于执行SQL语句,封装了对数据库的操作,包括增删改查、事务控制等,是MyBatis中最重要的核心对象之一。为此,我们进入该类中,看看能不能找到查询相关的执行逻辑,进而探究查询过程中使用缓存的代码逻辑。

public interface SqlSession extends Closeable {
  void select(String var1, Object var2, ResultHandler var3);
}

⬇️
public class DefaultSqlSession implements SqlSession {
    public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        try {
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }

    }
}

⬇️
public interface Executor {
      <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
}

⬇️
public abstract class BaseExecutor implements Executor {  
      public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
}  

就这样,经过层层递进,我们终于第一次看到了一个跟缓存相关的类:CacheKey,这里我们暂且不管这个类的实现和作用。将上面的查询调用链总结如下所示:

在这里插入图片描述

有了上面的调用链之后,我们就对整个执行mybatis查询的调用方法链有了一个初步的认知。接下来,就从上面BaseExecutorquery()方法中来一探究竟。

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }

createCacheKey方法源码解析

在上述query()方法中,先会在MappedStatement中获取绑定关联的SQL语句,然后生成CacheKey。这个CacheKey实际上就是一个缓存主键,用于唯一标识一个查询结果。

在这里插入图片描述

CacheKey中包含multiplierhashcodechecksumcountupdateList等属性用于判断CacheKey之间是否相等,具体判断逻辑可以查看equals方法如下所示:

@Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

可以看到,equals方法中,对上述hashcodechecksumcountupdateList属性进行判断,用于判断两个CacheKey是否属于同一个CacheKey。同时hashcodechecksumcountupdateList字段会在CacheKeyupdate() 方法中被更新,如下所示:

  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

主要逻辑就是在update()方法中,更新CacheKey的相关属性值,这些传入的参数object都与唯一确定CacheKey有关。

在了解了CacheKey类的相关原理后,我们回到上面BaseExecutorquery()方法中,看一下createCacheKey方法实现了什么功能:

  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 创建CacheKey对象
    CacheKey cacheKey = new CacheKey();
    
    // 基于MappedStatement的id更新CacheKey
    cacheKey.update(ms.getId());
    // 基于RowBounds的offset更新CacheKey
    // RowBounds 是用于分页查询的参数对象。offset 表示查询的起始位置或偏移量,它指定了从结果集的哪一行开始返回数据。
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    // 基于Sql语句更新CacheKey
    cacheKey.update(boundSql.getSql());
    // 获取绑定sql的参数元数据Mapping
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          // MetaObject提供了一些便捷的方法,封装了一些根据parameterObject类型,获取相应propertyName值的方法,而不需要直接使用反射操作。
          // MetaObject会根据传入的parameterObject类型,封装成不同的Wrapper
          // 例如: MapWrapper、CollectionWrapper、BeanWrapper
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          // 如果是MapWrapper,则getValue会调用Map的get方法
          // 如果是BeanWrapper,则getValue会调用Bean对象的get属性方法
          value = metaObject.getValue(propertyName);
        }
        // 基于查询参数值更新CacheKey
        cacheKey.update(value);
      }
    }
    // 基于Environment的id更新CacheKey
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

上面的代码非常清晰明确的展示了如何去确定唯一的CacheKey,依据就是MappedStatement id + RowBounds offset + RowBounds limit + SQL + Parameter值 + Environment id相等。

上面代码中,boundSql.getParameterMappings();就是获取从查询sql占位符中解析出的参数元数据信息。然后进行遍历,获取每个参数名,并从查询接口传入的参数中获取相应的参数值。Mybatis在执行Mapper接口方法时,会利用反射对调用方法时传入的参数进行解析,封装成parameterObject。(具体可以参考MapperMethod类的execute方法源码)。

比如下面的查询接口:

User selectUserById(@Param("id") int id);

User selectUserByEntity(User user);

对于第一个查询接口,parameterObject会被解析成一个Map类型,内容为: {"id":id值, "param1":id值}

而对于第二个查询接口,parameterObject仍然被保留为原始的User类。

因此,configuration.newMetaObject(parameterObject)方法就是根据传入的parameterObject参数,封装成MetaObject类对象,然后借助MetaObject中封装的objectWrapper属性,利用不同Wrapper的get方法获取propertyName相应的值。

至此,我们详细分析了createCacheKey方法内的实现原理。

BaseExecutor.query()方法源码解析

在上面获取到CacheKey后,会调用BaseExecutor中重载的query() 方法。源码如下所示:

  @Override
  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.");
    }
    // queryStack是BaseExecutor的成员变量
    // queryStack主要用于递归调用query()方法时防止一级缓存被清空
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 先从一级缓存localCache中根据CacheKey判断是否命中缓存,如果命中,则直接返回查询结果
      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();
      // 如果一级缓存作用范围是STATEMENT时,每次query执行完毕后都需要清空一级缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

这里就是mybatis执行查询时的核心query逻辑了。上面query()方法的执行逻辑还是比较好理解的,如上述代码注释所示。

localCache是一个PerpetualCache类型的对象,而PerpetualCache内部主要是基于一个Map(实际为HashMap)用于数据存储。

而查询数据库的queryFromDatabase源码如下所示:

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 插入CacheKey的占位符
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 根据入参执行查询sql语句
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    // 将查询结果放入localCache一级缓存
    localCache.putObject(key, list);
    // 如果ms的类型为CALLABLE(存储过程或可调用语句),则将参数对象(parameter)与缓存键(key)关联起来,存储在localOutputParameterCache缓存中。
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

上面的doQuery方法,实际上就是根据传入的参数,创建Statement对象,然后将参数值set到sql的相应占位符上去,最后执行ps.execute()执行查询结果并返回。

最后,我们再来看一下,为什么在最初的场景二中,执行了update方法后,就没有命中缓存而是重新查了一次数据库。

MyBatis中如果是执行操作,并且在禁用二级缓存的情况下,均会调用到BaseExecutorupdate() 方法,如下所示:

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

显而易见,上面的update代码中,执行了clearLocalCache()来清楚一级缓存。所以Mybatis中的一级缓存在执行了增、改和删操作后,会被清空立即失效。目的就在于,执行了更新操作后,如果不清除缓存,可能会导致脏读。

总结

至此,我们就基本分析完了Mybatis一级缓存的执行逻辑。总结一下,实际上就是当用户调用查询接口方法时,Mybatis会根据查询的MappedStatement id + RowBounds offset + RowBounds limit + SQL + Parameter值 + Environment id唯一创建一个CacheKey,来标识一个查询请求。接着,在执行查询SQL时,会首先根据该CacheKey从localCache缓存中判断是否命中,如果命中,则直接返回查询结果。否则,创建、初始化Statement对象,用入参替换占位符,执行查询SQL,并将查询结果放入localCache缓存中,以便下次查询时直接返回。

一级缓存的使用流程可以用下图进行概括:

在这里插入图片描述

二级缓存

⼆级缓存的原理和⼀级缓存原理⼀样,第⼀次查询,会将数据放⼊缓存中,然后第⼆次查询则会直接去 缓存中取。但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也就是说多个sqlSession可以共享⼀个mapper中的⼆级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中

和⼀级缓存默认开启不⼀样,⼆级缓存需要我们⼿动开启

首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:

    <settings>
        <setting name="cacheEnabled" value="true"/>
        <setting name="localCacheScope" value="STATEMENT"/>
    </settings>

上述配置文件中将一级缓存的作用范围设置为了STATEMENT,目的是为了在例子中屏蔽一级缓存对查询结果的干扰。

其次在UserMapper.xml⽂件中开启缓存:

    <cache eviction="LRU"
           type="org.apache.ibatis.cache.impl.PerpetualCache"
           flushInterval="600000"
           size="1024"
           readOnly="true"
           blocking="false"
    >

    </cache>

下面我们接着来看几个实用二级缓存的场景。

场景一

创建两个会话,会话1以相同SQL语句连续执行两次查询,会话2以相同SQL语句执行一次查询。代码如下所示:

public class MybatisCacheDemo2 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();

        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

        System.out.println(userMapper1.selectUserById(1));
        System.out.println(userMapper1.selectUserById(1));
        System.out.println(userMapper2.selectUserById(1));

    }
}

执行结果如下所示:

在这里插入图片描述

可以看到,Mybatis在开启二级缓存后,每次查询会先去二级缓存中查看是否命中查询结果(上图中打印出的Cache Hit Ratio…)。未命中时才会使用一级缓存以及直接去查询数据库。上面的截图显示,无论是同一会话的两次查询还是另一会话的一次查询,均是查询的数据库,这点跟我们上面的一级缓存场景有不同(在一级缓存中,同一会话的相同sql第二次查询结果是直接从缓存中获取到的)。

这是为什么呢?

实际上,在二级缓存中,将查询结果写入到二级缓存中时需要执行事务提交操作,上面的场景中并没有事务提交,因此二级缓存中是没有内容的,最终导致三次查询均是直接访问数据库。这里暂时先给出结论,直接源码性分析,我们留在下面统一来进行分析。

场景二

创建两个会话,会话1执行完查询操作后提交事物,接着会话1再执行相同的查询,然后会话2以相同sql语句执行一次查询。代码如下所示:

public class MybatisCacheDemo2 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();

        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

        System.out.println(userMapper1.selectUserById(1));
        sqlSession1.commit();
        System.out.println(userMapper1.selectUserById(1));
        System.out.println(userMapper2.selectUserById(1));

    }
}

执行结果如下所示:

在这里插入图片描述

可以看到,在会话1提交了事务之后,后两次的查询都是命中了二级缓存(三次查询命中二次,所以Cache Hit Ratio=0.666),验证了我们上面的结论。

场景三

创建两个会话,会话1执行一次查询并提交事务,然后会话2执行一次更新并提交事务,接着会话1再执行一次相同的查询。代码如下所示:

public class MybatisCacheDemo2 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        // 场景三
        // 将事务隔离级别设置为读已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);

        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

        System.out.println(userMapper1.selectUserById(1));
        sqlSession1.commit();
        User user = new User();
        user.setId(1);
        user.setUsername("lisi");
        userMapper2.update(user);
        sqlSession2.commit();
        System.out.println(userMapper1.selectUserById(1));
    }
}

执行结果如下所示:

在这里插入图片描述

执行结果表明,执行更新操作并提交事务后,会清空二级缓存,执行新增和删除操作也是同理。

场景四

创建两个会话,每个会话操作不同的数据表。会话1首先执行一次表1和表2的级联查询,然后提交事务。会话2针对表2执行一次更新操作以更新某个字段值并提交事务。最后,会话1再执行一次表1和表2的级联查询。代码如下所示:

public class MybatisCacheDemo2 {

    public static void main(String[] args) throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        // 场景四
        // 将事务隔离级别设置为读已提交
        SqlSession sqlSession1 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        OrderMapper orderMapper = sqlSession2.getMapper(OrderMapper.class);

        System.out.println(userMapper1.selectUserOrder(2));
        sqlSession1.commit();
        System.out.println();
        // 更新order表的total字段从20.0 -> 30.0
        orderMapper.update(3, 30.0);
        sqlSession2.commit();
        System.out.println(userMapper1.selectUserOrder(2));

    }
}

执行结果如下所示:

在这里插入图片描述

可以看到,在会话1第一次多表查询时,得到的用户id=2的order表total字段=20.0,并提交了事务。然后会话2执行更新操作将order表uid=2的total字段值更新为30.0并提交事务。接着,会话1再次执行了多表查询,得到的关联结果中,用户id=2的order表total字段值仍然为20.0,出现了脏读。

上述问题的原因就在于之前提到过的,二级缓存的作用范围是同一命名空间,这里的命令空间就是映射文件的na mespace,可以理解为每一个映射文件持有一份二级缓存。所有会话对该映射文件的所有操作,都会共享这个二级缓存。所以上面的场景中,会话2对Order表执行的更新操作并提交事务时,清空的是OrderMapper.xml这个映射文件持有的二级缓存。UserMapper.xml的二级缓存无法感知到Order表的数据发生了变化,仍然保留着原先的缓存数据。最终导致会话1第二次执行相同的多表查询时从二级缓存中命中了脏数据,发生了脏读。

场景五

在场景四的基础上,在OrderMapper.xml文件中添加下面的标签:

<cache-ref namespace="com.example.mybatis.data.dao.UserMapper"/>

再次执行上面场景四中的测试代码,得到结果如下所示:

在这里插入图片描述

这一次,会话1在第二次执行多表查询语句时,获取了Order表中更新的total字段值。

原理在于,在OrderMapper.xml文件中使用<cache-ref>标签引用命名空间为UserMapper.xml映射文件使用的二级缓存。因此相当于OrderMapper.xml映射文件与UserMapper.xml映射文件持有同一份二级缓存。因此,上面场景中,会话2执行的更新操作并提交事物后,会把UserMapper.xml映射文件持有的二级缓存清空,导致会话1的第二次多表查询无法在二级缓存中命中,从而去数据库中查询数据。

至此,介绍完上面二级缓存的几个测试场景后,我们对Mybatis的二级缓存机制进行一个总结:

  • Mybatis中的二级缓存默认不开启,可以在Mybatis配置文件的<settings>标签中添加<setting name="cacheEnabled" value="true"/>来打开二级缓存。
  • Mybatis的二级缓存作用范围是基于mapper的命名空间,这里的命令空间就是映射文件的namespace。也就是说多个sqlSession可以共享⼀个mapper中的⼆级缓存区域,并且如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中。
  • Mybatis中执行查询操作后,需要提交事务才能将查询结果缓存到二级缓存中。
  • Mybatis中执行增、删、改操作并提交事务后,会清空相应的二级缓存
  • Mybatis中需要在映射文件中添加<cache>标签来为映射文件配置二级缓存,也可以在映射文件中添加<cache-ref>标签来引用其他映射文件的二级缓存以达到多个映射文件持有同一份二级缓存的效果。

最后对<cache>标签和<cache-ref>标签进行说明

  • <cache>
    • eviction:缓存淘汰策略。LRU表示最近最少使用的优先被淘汰;FIFO表示先被缓存的会先被淘汰;SOFT表示基于软引用规则来淘汰; WEAK表示基于弱引用规则来淘汰。
    • flushInterval: 缓存刷新间隔,单位毫秒
    • type:缓存类型
    • size:最多缓存的对象格式
    • blocking: 缓存未命中时是否阻塞
    • readOnly:缓存中的对象是否只读。配置为true时,表示缓存对象只读,命中缓存时会直接将缓存对象返回,性能更快,但是线程不安全;配置为false时,表示缓存对象可读写,命中缓存时会讲缓存的对象克隆然后返回克隆的对象,性能更慢,但是线程安全。
  • <caceh-ref>
    • namespace: 其他映射文件的命名空间,设置之后则当前映射文件将和其他映射文件将持有同一份二级缓存。

二级缓存源码分析

介绍了上面那么多原理和结论,接下来我们去代码中一探究竟,来找到上面原理的支撑依据,在源码中加深我们的印象和理解。

首先,Mybatis中开启二级缓存之后,执行查询操作时,调用链如下所示:

在这里插入图片描述

与一级缓存中不同的是换了一个Executor执行类。

 @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

query()方法的执行逻辑与我们上面在分析一级缓存中的一致。在此不再赘述,直接来看重载的query()方法如下:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 取出MappedStatement中对应的二级缓存
    Cache cache = ms.getCache();
    if (cache != null) {
      // 清空二级缓存(如果需要的话)
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 从二级缓存中根据CacheKey命中查询结果
        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);
  }

上面的源码中可以看到,二级缓存在根据CacheKey命中查询结果时,并没有一个类似localCache的缓存,而是通过调用tcm.getObject(cache, key)来获取,这是与之前分析一级缓存时的不同。那么就要去思考为什么要这样去处理。

观察tcm可以发现它是一个TransactionalCacheManager类的对象,根据该类的名称:事务缓存管理器,也可以联系到该类实现的功能可能跟我们之前提及的二级缓存要提交事务这一特性有关。点进该类可以发现, 该类包含一个transactionalCaches属性,类型是Map<Cache, TransactionalCache>。该Map的键为Cache,值为TransactionCache,即一个二级缓存对应一个TransactionalCache。

继续看该类的getObject()方法,如下所示:

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

通过上面的代码可以知道,一个二级缓存对应一个TransactionalCache,且TransactionalCache中持有这个二级缓存的引用。当调用TransactionalCacheManager的getObject()方法时,TransactionalCacheManager会将调用请求转发给TransactionalCache。

接着看TransactionalCache的getObject()方法:

  @Override
  public Object getObject(Object key) {
    // issue #116
    // delegate是一个Cache类型对象
    // 在二级缓存中命中查询结果
    Object object = delegate.getObject(key);
    if (object == null) {
      // 未命中则将CacheKey添加到entriesMissedInCache中,用于统计命中率
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

到这里就可以知道了,在CachingExecutor中通过CacheKey命中二级缓存结果时,步骤如下:

  • CachingExecutor将请求发送给TransactionalCacheManager
  • TransactionalCacheManager将请求转发给二级缓存对应的TransactionalCache
  • 最后再由TransactionalCache根据其持有的二级缓存引用,将请求最终传递到二级缓存中。

在上述getObject()方法中,如果clearOnCommit为true的话,则getObject方法会一直返回null。那clearOnCommit参数是在什么地方被设置为true的呢?其实就是在CachingExecutor的flushCacheIfRequired()方法中。回到上面CachingExecutor的query()方法中,找到flushCacheIfRequired()这个方法,看一下它的代码:

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
      tcm.clear(cache);
    }
  }

调用TransactionalCacheManager的clear()方法时,最终会调用到TransactionalCache的clear()方法,代码如下:

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

可以看到,在TransactionalCache的clear()方法中,将其clearOnCommit属性设置为了true。并且我们会发现还执行了entriesToAddOnCommit.clear(),这个entriesToAddOnCommit属性是什么呢?

同样回到上面CachingExecutor的query()方法中,看到下面这行代码,也就是上面说的将查询结果放入二级缓存的过程是借助于TransactionalCacheManager来控制的。

tcm.putObject(cache, key, list);

继续看TransactionalCacheManager的putObject方法:

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

最终,会调用到TransactionalCache的putObject方法:

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

到这里就可以看到,TransactionalCacheManager在将查询结果放入二级缓存中时,会利用TransactionalCache先将结果缓存到entriesToAddOnCommit中。那么entriesToAddOnCommit中的值与二级缓存是如何关联上的呢?

我们在上面介绍过,查询结果要缓存到二级缓存中时需要事务提交,那么会不会是在会话执行commit操作时,将entriesToAddOnCommit中暂存的查询结果刷新到二级缓存中的呢?为此,我们首先进入DefaultSqlSession的commit()方法中来一探究竟:

  @Override
  public void commit() {
    commit(false);
  }
  
  @Override
  public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在DefaultSqlSession的commit()方法中会调用到CachingExecutor的commit()方法:

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

在CachingExecutor的commit()方法中,会调用TransactionalCacheManager的commit()方法

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

最终,会调用TransactionalCache的commit()方法,并在其中调用flushPendingEntries()方法和reset()方法。

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  private void flushPendingEntries() {
    // 将entriesToAddOnCommit中暂存的查询结果全部缓存到二级缓存中
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      // delegate是Cache类型对象,代表二级缓存
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

至此,一切都很明显了,当调用SqlSession的commit()方法时,会一路传递到TransactionalCache的commit()方法,最终调用TransactionalCache的flushPendingEntries()方法,将暂存在entriesToAddOnCommit中的查询结果转存入二级缓存中。同时,reset()方法会将刚刚提到的clearOnCommit属性值设置为false,从而使得后续调用该TransactionalCache对象持有的二级缓存来命中查询结果时,执行getObject不会一直返回null。

弄清楚上面的流程之后,我们也可以很清晰地知道为什么当执行增、删、改操作并提交事务后,二级缓存会被清空。原因就是,当调用update()方法时,会调用到CachingExecutor的flushCacheIfRequired方法。

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

已经知道flushCacheIfRequired()方法中如果所执行的方法对应的MappedStatement的flushCacheRequired字段为true的话(默认即为true),则最终会讲TransactionalCache中的clearOnCommit字段置为true。随后,在执行SqlSession的commit()事务提交时,会执行delegate.clear();将二级缓存清空。而加载映射文件时,解析CRUD标签为MappedStatement时有如下一行:

    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);

即如果没有在CRUD标签中显式的设置flushCache属性,则会给flushCache字段一个默认值,且默认值为非查询标签下默认为true。

至此,二级缓存的执行流程源码也已经分析的十分清晰了。

总结

  • Mybatis中二级缓存默认不开启,需要手动配置开启,开启后,查询二级缓存会执行CachingExecutor的query()方法。
  • 在二级缓存中,并不是直接将查询结果放入二级缓存中的,而是由TransactionalCacheManager将插入请求传递给二级缓存对应的TransactionalCache,而TransactionalCache中又持有该二级缓存的引用,所有最终由TransactionalCache将查询结果先缓存在entriesToAddOnCommit中。
  • 只有当会话进行事务提交,执行SqlSession的commit()方法后,才会将entriesToAddOnCommit中的结果刷新到二级缓存中。
  • 另外,当执行增、删、改操作的update()方法后,会将TransactionalCache的clearOnCommit属性值置为true,并在commit()提交事务后,执行delegate.clear();来清空二级缓存。

整体二级缓存的流程可以用下图进行概括:

在这里插入图片描述

参考文章:

一文搞懂MyBatis一级缓存和二级缓存

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1248288.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mac VScode 添加PHP debug

在VScode里面添加PHP Debug 插件,根据debug描述内容操作 1: 随意在index里面写个方法,然后用浏览器访问你的hello 方法,正常会进入下边的内容 class IndexController {public function index(){return 您好&#xff01;这是一个[api]示例应用;}public function hello() {phpin…

【ArcGIS Pro微课1000例】0036:栅格影像裁剪与提取(矢量范围裁剪dem高程数据)

本实验讲解在ArcGIS Pro中进行栅格影像裁剪与提取(矢量范围裁剪dem高程数据)的方法。DEM、DOM、DSM等栅格数据方法也可以实现。 文章目录 一、加载实验数据二、裁剪工具的使用1. 裁剪栅格2. 按掩膜提取一、加载实验数据 加载配套实验数据包中的0036.rar中的dem数据和矢量裁剪…

【21年扬大真题】编写程序,去除掉字符串中所有的星号。

【21年扬大真题】 编写程序&#xff0c;去除掉字符串中所有的星号。 int main() {int i 0;int j 0;char arr[30] {0};char brr[30] {0};printf("请输入一个字符串:");gets(arr);for (i 0;i < 30;i){if (arr[i] ! *) {brr[j] arr[i];j;}}int tmp j;for (i …

RK3399 板子烧录Armbian

本来不想写在CSDN这里的。帮有需要的同学了吧。 板子上面标记型号为&#xff1a; GC18-108-RK3399-V2.0TEAN E120339 94V-OML1没有HDMI接口&#xff08;我也是汗&#xff0c;买的时候注意到&#xff0c;坑了&#xff09;&#xff0c;配置信息。 CPU : RK3399RAMROM: 4G16G无…

高性能计算HPC所面临的问题

一、电力墙问题 能源动力领域关注高性能计算主要关注其能效和功耗等问题&#xff0c;也就是在高性能计算&#xff08;High-Performance Computing, HPC&#xff09;领域中&#xff0c;所谓的"电力墙"&#xff08;Power Wall&#xff09;&#xff0c;电力墙是一个描述…

ddns-go部署在linux虚拟机

ddns-go部署ubuntu1804 1.二进制部署 1.虚拟机部署 1.下载linux的x86二进制包 wget https://github.com/jeessy2/ddns-go/releases/download/v5.6.3/ddns-go_5.6.3_linux_x86_64.tar.gz2.解压 tar -xzf ddns-go_5.6.3_linux_x86_64.tar.gz3.拷贝执行文件到PATH下&#xff0c…

VMware虚拟机安装华为OpenEuler欧拉系统

首先去欧拉官方网站下载openEuler的安装镜像&#xff1a; openEuler下载 | 欧拉系统ISO镜像 | openEuler社区官网 我下载的是最新的23.03长期维护版本&#xff0c;架构选择x86_64。 创建新虚拟机&#xff1a;选择典型配置&#xff0c;点击下一步&#xff1a;选择下载的镜像文…

html table样式的设计 表格边框修饰

<!DOCTYPE html> <html> <head> <meta http-equiv"Content-Type" content"text/html; charsetutf-8" /> <title>今日小说排行榜</title> <style> table {border-collapse: collapse;border: 4px double red; /*…

Celonis推出流程智能图,希望建立首个世界级“流程智能维基百科”

近日&#xff0c;全球流程挖掘领域的领导者Celonis在其年度客户大会Celosphere上推出了流程智能领域的一项创新&#xff0c;即流程智能图Process Intelligence Graph™&#xff08;PI Graph&#xff09;。 PI Graph 是一个与具体系统无关的、丰富的业务数字孪生体&#xff0c;…

VBA_MF系列技术资料1-227

MF系列VBA技术资料 为了让广大学员在VBA编程中有切实可行的思路及有效的提高自己的编程技巧&#xff0c;我参考大量的资料&#xff0c;并结合自己的经验总结了这份MF系列VBA技术综合资料&#xff0c;而且开放源码&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-04属于定…

解决cad找不到vcruntime140.dll的方法,实测有效的5个的方法

最近&#xff0c;我在使用CAD软件时遇到了一个困扰我已久的问题&#xff1a;由于找不到vcruntime140.dll文件而导致CAD无法正常运行。经过一番努力和尝试&#xff0c;我终于找到了解决这个问题的方法。那么&#xff0c;如何解决vcruntime140.dll丢失的问题呢&#xff1f;本文将…

2023仿聚合搜索程序源码/轻量级搜狗泛站群程序源码/PHP整站源码+完美SEO优化+符合搜狗算法

源码简介&#xff1a; 2023仿聚合搜索/轻量级搜狗泛站群程序整站源码&#xff0c;作为PHP源码&#xff0c;可以完美SEO优化&#xff0c;符合搜狗搜索引擎算法。 轻量级的PHP搜狗泛站群程序源码&#xff0c;完美SEO优化符合搜狗搜索引擎算法&#xff0c;无需任何采集&#xff…

闲人闲谈PS之四十七——PS顾问能力评价参考标准

惯例闲话&#xff1a;逝者如斯夫&#xff0c;一晃2023年进入年尾&#xff0c;初步盘点下今年做的事情&#xff0c;还真不少&#xff0c;PLM项目、接口开发、扫码系统、数字彩虹图、专利申请…闲人发现&#xff0c;不经意间&#xff0c;SAP从自己的主营业务中占据的比重已经越来…

[pyqt5]PyQt5窗体背景图片拉伸填充

1. background-image效果 这里&#xff0c;我添加的是如下这个图片。 结果只显示了图片的部分&#xff08;天空&#xff09;&#xff0c;没有拉伸填充。 2. border-image效果 图片出现了拉伸填充整个widget&#xff0c;图中的button背景也是图片的背景。 如果想要按钮不受背景…

中低压MOSFET 2N7002W 60V 300mA 双N通道 SOT-323封装

2N7002W小电流双N通道MOSFET&#xff0c;电压60V电流300mA&#xff0c;采用SOT-323封装形式。超高密度电池设计&#xff0c;适用于极低的ros (on)&#xff0c;具有导通电阻和最大直流电流能力&#xff0c;ESD保护。可应用于笔记本中的电源管理&#xff0c;电池供电系统等产品应…

P9 C++类

目录 01 类是什么 02 如何创建类 03 方法 后话 本期我们要讲的是 C 中的类。 我们终于讲到了面向对象编程&#xff0c;这是一种非常流行的编程方式&#xff0c;面向对象编程实际上只是一种你可以采用的编写代码的方式&#xff0c;其他语言例如 C#、Java 这些主要是面向对象…

软件测试工程师必备之软技能:结构化思维

前言 今年是进入测试行业的第十年&#xff0c;回想在这十年职业生涯中&#xff0c;来来往往也接触过很多很多的人。在跟不同的人一起工作的过程中&#xff0c;我会经常产生一些困惑&#xff0c;比如&#xff1a; 面对同样复杂的测试任务&#xff0c;有些人可以在一天之内梳理…

Windows从源码构建tensorflow(离线编译)

由一开始的在线编译&#xff0c;到后面的离线编译&#xff0c;一路踩坑无数&#xff0c;历经整整6个半小时&#xff0c;终于编译成功&#xff01;在此记录一下参考过的文章&#xff0c;有时间整理一下踩坑记录。 一、环境配置 在tensorflow官网上有版本对应关系 win10 bazel …

App Inventor 2 文本转数字

App Inventor 2 是弱语言类型&#xff0c;文本和数字之间不用刻意去转换&#xff0c;之间赋值就可以了。文本赋值给数字变量如下&#xff1a; 运行结果&#xff1a;124 注意&#xff1a;数字变量初始化的时候要给一个数字的初始值&#xff0c;表明它是数字。 如果文本中含有非…

【MATLAB】全网入门快、免费获取、持续更新的科研绘图教程系列2

14 【MATLAB】科研绘图第十四期表示散点分布的双柱状双Y轴统计图 %% 表示散点分布的双柱状双Y轴统计图%% Made by Lwcah &#xff08;公众号&#xff1a;Lwcah&#xff09; %% 公众号&#xff1a;Lwcah %% 知乎、B站、小红书、抖音同名账号:Lwcah&#xff0c;感谢关注~ %% 更多…