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集合中,只有提交事务,这些数据才会生效,如果回滚事务,这些数据会被清空。

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

相关推荐
tan180°4 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
DuelCode5 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社25 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术5 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理5 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
ai小鬼头6 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github
简佐义的博客6 小时前
破解非模式物种GO/KEGG注释难题
开发语言·数据库·后端·oracle·golang
Code blocks7 小时前
使用Jenkins完成springboot项目快速更新
java·运维·spring boot·后端·jenkins
荔枝吻7 小时前
【沉浸式解决问题】idea开发中mapper类中突然找不到对应实体类
java·intellij-idea·mybatis