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

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一级缓存和二级缓存

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

安卓手机好用的清单软件有哪些?

生活中每个人都有丢三落四的习惯&#xff0c;伴随着生活节奏的加快&#xff0c;人们常忘事的情况会更加频繁的出现&#xff0c;这时候很多人就开始选择手机上记录清单类的软件&#xff0c;安卓手机在手机市场中占有很大的分量&#xff0c;在安卓手机上好用的记录清单的软件有哪…

别熬夜了!人真的会变臭

身为当代社畜&#xff0c;你一定经历过如下瞬间——— 周一早高峰的地铁车厢&#xff0c;拥挤的人群里若有若无地飘荡出一股刺鼻臭味&#xff0c;即使戴着口罩也难以抵挡其穿透性&#xff1b; 深夜还灯火通明的办公室工位上&#xff0c;浑浊的空气裹挟着疲惫的身体&#xff0…

Shell 通配符与正则表达元字符

Author&#xff1a;rab 目录 前言一、通配符1.1 *1.2 ?1.3 []1.4 {} 二、正则表达元字符2.1 *2.2 .2.3 ^2.4 $2.5 []2.6 \2.7 \<\>2.8 \{\} 总结 前言 不管是学任何语言&#xff0c;几乎都会涉及到通配符与正则的使用。有时候对于 Linux 初学者来说&#xff0c;往往会将…

Rust使用iced构建UI时,如何在界面显示中文字符

注&#xff1a;此文适合于对rust有一些了解的朋友 iced是一个跨平台的GUI库&#xff0c;用于为rust语言程序构建UI界面。 iced的基本逻辑是&#xff1a; UI交互产生消息message&#xff0c;message传递给后台的update&#xff0c;在这个函数中编写逻辑&#xff0c;然后通过…

轻松记录收支明细,一键打印,财务无忧!

作为现代人&#xff0c;管理好个人财务是非常重要的。但是&#xff0c;如何记录收支明细并打印出来呢&#xff1f;今天&#xff0c;我们向您推荐一款财务软件&#xff0c;帮助您轻松解决这个问题。 首先第一步&#xff0c;我们要打开【晨曦记账本】&#xff0c;并登录账号。 第…

Delphi 12 Athens 发布了!

官方安装包 ☞ https://altd.embarcadero.com/download/radstudio/12.0/RADStudio_12_0_4915718.iso 安装辅助工具、控件可以戳这里 &#xff1a;Delphi 12 资源 RAD Stuido 12 Athens &#xff0c;这次更新的细节还是比较多的&#xff0c;但主要还是多端&#xff08;iOS、An…

BMS基础知识:BMS基本功能,铅酸和锂电池工作原理,电池系统的重要概念!

笔者有话说&#xff1a; 作为BMS从业者来讲&#xff0c;目前接触的BMS系统并不是很高大尚&#xff0c;但基础功能都是有的。 关于BMS的基本功能&#xff0c;工作原理&#xff0c;运行逻辑等&#xff0c;在此做一个梳理&#xff0c;讲一些最基础的扫盲知识&#xff0c;可以作为…

微服务 Spring Cloud 9,RPC框架,客户端和服务端如何建立网络连接?

目录 一、客户端和服务端如何建立网络连接&#xff1f;1、HTTP通信2、Socket通信 二、服务端如何处理请求&#xff1f;1、通常来说&#xff0c;有三种处理方式&#xff1a;2、不同的处理方式对应着不同的业务场景&#xff1a; 三、HTTP协议传输流程四、数据该如何序列化和反序列…

浅谈安科瑞智能照明系统在马来西亚国家石油公司项目的应用

摘要&#xff1a;随着社会经济的发展及网络技术、通信技术的提高&#xff0c;人们对照明设计提出了新的要求&#xff0c;它不仅要控制照明光源的发光时间、 亮度&#xff0c;而且与其它系统来配合不同的应用场合做出相应的灯光场景。本文介绍了马亚西亚石油公司智能照明项目的应…

2023.11.22 IDEA Spring Boot 项目热部署

目录 引言 操作步骤 1. 在 pom.xml 中添加热部署框架支持 2. Setting 开启项目自动编译 3. 以后创建的新项目进行同步配置 4. 重复 配置 步骤2 的内容 5. 开启运行中的热部署 引言 Spring Boot 的热部署是一种在项目正在运行的时候修改代码&#xff0c;却不需要重新启动…

无线电通信常见的12种天线

天线&#xff0c;作为无线通信系统的重要组成部分&#xff0c;有着不可替代的作用。 在我们的生活中&#xff0c;有很多类型的通信需求&#xff0c;例如长距离通信、短距离通信、卫星通信、微波通信、手机通信、点对点通信、点对面通信等等。 使用卫星电话进行通信 不同的需求&…

【高级程序设计】django与网页设计

django安装与配置 使用pip进行Django的安装: pip install djangoconda install django验证django安装成功 import django django.__version__ ## django.get_version()新建一个名称为blog的django项目(project), D:\>cd web-project D:\web-project>django-admi…

亚马逊防关联要注意什么?看这一篇,防关联有技巧!

亚马逊账号关联的问题&#xff0c;对于跨境电商来说都不陌生&#xff0c;店铺的安全问题往往和账号关联有关&#xff0c;一旦亚马逊账号被关联就很可能导致我们的店铺被封&#xff0c;对于被亚马逊封店的卖家都会有申诉机会&#xff0c;如果无法成功申诉&#xff0c;那将永久被…

政务大数据与资源平台建设解决方案:PPT全文75页,附下载

关键词&#xff1a;智慧政务解决方案&#xff0c;大数据解决方案&#xff0c;数据中心解决方案&#xff0c;数据治理解决方案 一、政务大数据与资源平台建设背景 1、政务大数据已成为智慧城市建设的必要基础 为响应国家不断加快5G基建、大数据、人工智能等新型基础设施建设布…

生命周期评估(LCA)与SimaPro碳足迹分析

SimaPro提供最新的科学方法和数据库以及丰富的数据&#xff0c;使您可以收集和评估产品和流程的环境绩效。通过这种方式&#xff0c;您可以将改变公司产品生命周期的想法提交给您的同事&#xff0c;以便阐明您的业务未来。 SimaPro软件的特点和功能&#xff1a; 完全控制产品生…

oracle数据库巡检常见脚本-系列三

简介 作为数据库管理员&#xff08;DBA&#xff09;&#xff0c;定期进行数据库的日常巡检是非常重要的。以下是一些原因&#xff1a; 保证系统的稳定性&#xff1a;通过定期巡检&#xff0c;DBA可以发现并及时解决可能导致系统不稳定的问题&#xff0c;如性能瓶颈、资源利用率…

时间复杂度 空间复杂度 ---java

目录 一. 算法效率 二.时间复杂度 2.1 时间复杂度的概念 2.2 大O的渐进表示法 2.3常见时间复杂度计算举例 三. 空间复杂度 一. 算法效率 算法效率分析分为两种&#xff1a;第一种是时间效率&#xff0c;第二种是空间效率 。 时间效率被称为时间复杂度&#xff0c;而空间…

基于 Flink CDC 打造企业级实时数据集成方案

本文整理自Flink数据通道的Flink负责人、Flink CDC开源社区的负责人、Apache Flink社区的PMC成员徐榜江在云栖大会开源大数据专场的分享。本篇内容主要分为四部分&#xff1a; CDC 数据实时集成的挑战Flink CDC 核心技术解读基于 Flink CDC 的企业级实时数据集成方案实时数据集…

算法复杂度分析

文章目录 有数据范围反推算法复杂度以及算法内容一般方法递归 有数据范围反推算法复杂度以及算法内容 c一秒可以算 1 0 7 10^7 107~ 1 0 8 10^8 108次 一般方法 看循环 有几层循环就可以初步分析O( n i n^i ni) 双指针算法除外O(n) 递归 公式法 根据公式的形式&#xff0…

痤疮分级实验笔记-ResNet

组织数据集 方式1&#xff1a;根据txt文件分类的数据集组织形式&#xff08;放弃&#xff09; 你可以使用Python来读取txt文件中的训练集图片信息&#xff0c;并将这些图片从原始文件夹复制到目标文件夹中。 当程序无法找到标签对应的图片或者目标文件夹中已经存在同名图片时…