Mybatis源码 - 详解缓存机制 一二级缓存配置方式 源码解析

目录

前言

在Mybatis中,存在缓存机制。分为一级缓存和二级缓存,一级缓存是SqlSession级别的,二级缓存则是namespace级别、即跨SqlSession级别的。一级缓存默认开启,二级缓存开启则需要进行配置。今天通过本文章分享一下Mybatis的缓存机制,希望可以帮到大家,谢谢~

一级缓存

效果演示

在Mybatis中,一级缓存默认开启。下面可以进行一个简单测试。

java 复制代码
public interface UserMapper {
    User findUserById(int id);
    void updateUser(User user);
}
xml 复制代码
<mapper namespace="com.sxb.mapper.UserMapper">
    <select id="findUserById" parameterType="int" resultType="com.sxb.pojo.User">
        select id,username from user where id = #{id}
    </select>
    <update id="updateUser" parameterType="com.sxb.pojo.User">  
        update user set username = #{username} where id = #{id}  
    </update>
</mapper>

下面这个测试方法中,同一个SqlSession下,相同的查询条件,查出来的User对象将会是同一个对象,user1==user2 会是true。

也就是说,第二次相同条件的查询,没有去查询数据库,而是命中了缓存。

如果两次查询中间,进行一次更新操作,则一级缓存会被清除。

java 复制代码
    public void firstLevelCacheTest() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        User user1 = userMapper.findUserById(1);
        User user2 = userMapper.findUserById(1);

        User user = new User();  
        user.setId(1);  
        user.setUsername("zhangsan");  
        userMapper.updateUser(user);  

        User user3 = userMapper.findUserById(1);  

        System.out.println(user1 == user2);//true
        System.out.println(user2 == user3);//false
    }

源码解析

Mybaits中的一级缓存,类型为PerpetualCache,这个类很简单,内部封装了一个Map集合cache,key为CacheKey对象,值为查询出来的结果。

java 复制代码
public class PerpetualCache implements Cache {

  private final String id;

  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }
}

一级缓存的应用位置,在执行器(Executor)这个组件中,整个查询的流程,包括涉及到一级缓存的使用,以前的文章中介绍的比较清楚了,可以参考:Mybatis源码 - SqlSession.selectOne(selectList)执行流程解析 一二级缓存 参数设置 结果集封装

那么,更新操作时,一级缓存的清除是在什么位置呢?当然也是在SqlSession中,在Mybatis中,只要涉及到更新的方法,无论是增、删、改,其实最后执行到SqlSession中,都是走的update方法,可以看一下方法源码:

java 复制代码
public class DefaultSqlSession implements SqlSession {
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
}

第6行:委派给了执行器进行更新操作。看一下执行器中的源码

java 复制代码
  @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);
  }

可以看到,调用了clearLocalCache方法,方法中,进行了一级缓存的清除。

java 复制代码
  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

注意:在xml编写sql语句时,更新操作最好不要因为懒使用<select>标签,因为这样的话,会被框架识别为查询操作,执行完毕之后,不会去清除一级缓存,可能会造成,查不到最新数据的情况。

二级缓存

开启配置

想要开启二级缓存,需要三步

  1. 核心配置文件中,配置<settings>标签,将cacheEnabled的值设置为true。(默认为true)

    xml 复制代码
    <configuration>
        <!--开启二级缓存(本身默认开启)-->
        <settings>
            <setting name="cacheEnabled" value="true"/>
        </settings>
        
        <!--省略其它配置项...-->
    </configuration>
  2. 在想要开启二级缓存的mapper.xml中配置<cache></cache>标签。

    xml 复制代码
    <mapper namespace="com.sxb.mapper.UserMapper">
        <!--开启二级缓存-->
        <cache></cache>
    
        <!--省略sql语句...-->
    </mapper>
  3. mapper.xml中sql语句标签上,配置useCache的值为true。(默认为true)

    xml 复制代码
    <mapper namespace="com.sxb.mapper.UserMapper">
        <!--开启二级缓存-->
        <cache></cache>
    
        <select id="findUserById" parameterType="int" resultType="com.xxx.pojo.User" useCache="true">
            select id,username from user where id = #{id}
        </select>
    </mapper>

完成以上步骤之后,就针对某个Mapper,开启了二级缓存。多个SqlSession会共享缓存。

效果演示

java 复制代码
public void secondLevelCacheTest() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    //这次查询会去查询数据库
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    User user1 = userMapper1.findUserById(1);

    //只有调用了sqlSession的commit或close方法,才会令对二级缓存的操作生效
    sqlSession1.commit();

    //这次查询不会去查询数据库,而是会命中缓存
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    User user2 = userMapper2.findUserById(1);

    System.out.println(user1 == user2);//尽管命中了缓存,还是false
}

以上测试方法中,有几个需要注意的点:

  1. 针对二级缓存的操作,不管是查询完数据库,将结果设置到二级缓存;还是执行了update操作,清空二级缓存。都需要执行sqlSession的commit或close方法,才会生效。
  2. 最后对于两个对象的比较是false,是因为二级缓存的实现方式,是把数据持久化到磁盘,读取时再从磁盘读取,所以这里是false。

这两点,我们将在下面的源码解析中进行讲解。

源码解析

<settings>标签解析

对于核心配置文件中,<configuration>标签中的<settings>标签的解析工作,是由XmlConfigBuilderparse方法处理的。在这个方法中,调用了本类中的parseConfiguration方法,解析了每个子标签,其中就包括<settings>标签。

java 复制代码
  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

settingsElement方法中,进行了非常多配置项的赋值。

java 复制代码
  private void settingsElement(Properties props) {
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    //这里将cacheEnabled的值,设置到了configuration中,默认值为true
    configuration.setCacheEnabled(
        booleanValueOf(props.getProperty("cacheEnabled"), true));
    //省略其它项的设置...
}

<cache>标签解析

<cache>标签是映射配置文件中的标签,是<mapper>的子标签,所以它的解析是交由XMLMapperBuilder中的parse方法来处理的。这个方法中,会调用本类方法configurationElement来处理<mapper>标签下的所有子标签,其中就包括了<cache>标签。

java 复制代码
  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);
    }
  }

第9行:调用了cacheElement方法,解析了<cache>标签。

java 复制代码
  private void cacheElement(XNode context) {
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
  • 第3-11行:获取封装了<cache>标签中的所有属性值,第3行中,缓存的类型为PERPETUAL,底层数据结构为PerpetualCache,与一级缓存一致。
  • 第12行:使用上一步封装好的各个属性,调用MapperBuilderAssistant对象的useNewCache方法,创建了Cache对象。
java 复制代码
  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;
  }
  • 创建Cache对象之后,这里会将Cache对象封装到configuration对象中,同时也会将cache的值赋值给本类中的成员变量currentCache
  • 后续在解析select|insert|update|delete四类标签、构建MappedStatement对象时,会调用MapperBuilderAssistant对象的addMappedStatement方法,这里会把currentCache属性值赋值到 MappedStatement 对象的cache属性中,所以说,一个Mapper下的多个sql,共享同一个Cache对象。

缓存 设置&获取

Mybatis中,对于数据库的操作,最后会委派到执行器来执行,而因为Mybatis默认开启一级缓存,所以在创建执行器时会进行一层包装,创建出CachingExecutor对象。

SqlSession中的查询方法,无论是selectList还是selectOne,都会执行到执行器对象的query方法,下面就直接从执行器的query方法看起,看一下二级缓存是怎么使用的:

java 复制代码
  @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);
  }
  • 第4行:调用了createCacheKey方法创建了CacheKey对象,这个是缓存中的key,一二级缓存中都会使用。
  • 第5行:调用了重载的query方法,执行查询操作。看一下源码:
java 复制代码
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //获取mapper.xml中解析<cache>标签后封装的对象
    Cache cache = ms.getCache();
    //如果不为空,说明开启了二级缓存。进到查询二级缓存的逻辑中
    if (cache != null) {
      //判断如果sql标签上,标注了`flushCache=true`,就清除二级缓存。
      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);
  }
  • 第3行:从MappedStatement对象中获取了Cache对象,这里就是在解析mapper.xml映射配置文件时,解析出来的<cache>标签。
  • 第4行:对Cache对象进行判空,如果不为null,说明mapper.xml中配置了二级缓存,这里就走到查询二级缓存的逻辑中。
  • 第7行:判断如果sql标签上,标注了flushCache=true,就清除二级缓存。
  • 第12行:调用了tcm对象的getObject方法,传入了Cache对象和CacheKey对象,查询了二级缓存。
  • 第17行:调用了tcm对象的putObject方法,将查询出来的结果,添加到了二级缓存(此处只是存到了一个Map集合,并未真的放到二级缓存。只有SqlSession在调用commit或close方法时,才会生效)。

tcm对象就是TransactionalCacheManager,事务缓存管理器。里面封装了对二级缓存的操作方法,这里看一下这个类的结构。

TransactionalCacheManager

java 复制代码
public class TransactionalCacheManager {

  //封装了每一个mapper对应的TransactionalCache对象
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  //查询二级缓存,这里是从真正的二级缓存中查询
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

  //设置二级缓存:只会将值设置到entriesToAddOnCommit中,不会真正的设置到二级缓存中
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

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

  /**
   * 1、将entriesMissedInCache的元素从delegate中删除
   * 2、清空两个Map集合,设置clearOnCommit为false
   */
  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

}
  • 首先,这个类中,维护了一个Map集合:transactionalCaches,key 为解析 xml文件 中的 <cache> 标签后封装的 cache 对象,value 是一个TransactionalCache对象。这里就可以解释为什么PerpetualCache中重写了equals和hashcode方法,因为这里被用作了HashMap中的key。

  • 可以看到,类中其他的方法,其实都是获取到Map集合中的TransactionalCache对象,然后再调用它的方法,我们看一下这个类的结构:

TransactionalCache

java 复制代码
public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  private final Cache delegate;
  private boolean clearOnCommit;
  private final Map<Object, Object> entriesToAddOnCommit;
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

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

  @Override
  public Object removeObject(Object key) {
    return null;
  }

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

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

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

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

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
            + "Consider upgrading your cache adapter to the latest version. Cause: " + e);
      }
    }
  }

}
  • 成员变量解释

    • Cache delegate:真正的二级缓存存放对象。数据结构默认为PerpetualCache
    • boolean clearOnCommit:是否提交事务的时候清除二级缓存
    • Map<Object, Object> entriesToAddOnCommit:未提交事务时的缓存数据,提交事务才会生效,回滚事务时会清空。
    • Set<Object> entriesMissedInCache:未命中的缓存key的集合,commit时也存入了二级缓存。可以理解为是对null值也做了缓存。
  • commit方法:

    1. 如果之前调用过clear方法,会设置一个标识clearOnCommit为true,本方法中会判断这个标识,为true就会清空二级缓存
    2. 遍历entriesToAddOnCommit集合,将里面的值赋值到delegate,真正的二级缓存。
    3. 遍历entriesMissedInCache集合,将未命中的缓存key存储到到delegate,value为null。
    4. 清空两个Map集合,设置clearOnCommit为false
  • rollback方法:

    1. entriesMissedInCache的元素从delegate中删除
    2. 清空两个Map集合,设置clearOnCommit为false

这也就解释了,为什么只有调用了commit方法,二级缓存才会生效。原因就在于,在设置二级缓存时,首先是设置到了entriesToAddOnCommitMap集合中,只有提交事务,这些数据才会生效,如果回滚事务,这些数据会被清空。

到这里,整个解析流程就结束了。感谢你的阅读,如果有不对的地方,欢迎在评论区指正!谢谢~

相关推荐
从种子到参天大树36 分钟前
SpringBoot源码阅读系列(一):启动流程概述
后端
m0_748254881 小时前
Spring Boot实现多数据源连接和切换
spring boot·后端·oracle
庄周de蝴蝶1 小时前
一次 MySQL IF 函数的误用导致的生产小事故
后端·mysql
韩数2 小时前
Nping: 支持图表实时展示的多地址并发终端命令行 Ping
后端·rust·github
18号房客2 小时前
云原生后端开发(一)
后端·云原生
胡尔摩斯.3 小时前
SpringMVC
java·开发语言·后端·spring·代理模式
Bony-4 小时前
Go语言高并发实战案例分析
开发语言·后端·golang
ac-er88884 小时前
Golang并发机制以及它所使⽤的CSP并发模型
开发语言·后端·golang
Pandaconda4 小时前
【Golang 面试题】每日 3 题(六)
开发语言·笔记·后端·面试·职场和发展·golang·go
海绵波波1074 小时前
flask后端开发(2):URL与视图
后端·python·flask