mybati缓存了解


title: “mybati缓存了解”
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
draft: false
author: “ggball”
tags: [“mybatis”]
categories: [“java”]
description: “mybati缓存了解”

mybatis的缓存

首先来看下mybatis对缓存的规范,规范嘛就是定义的接口啦。

缓存接口

​ Cache接口 定义了缓存的方法

public interface Cache {

  /**获取缓存的id
   * @return The identifier of this cache
   */
  String getId();

  /**添加缓存
   * @param key Can be any object but usually it is a {@link CacheKey}
   * @param value The result of a select.
   */
  void putObject(Object key, Object value);

  /**根据缓存键获取缓存
   * @param key The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**移除缓存
   * As of 3.3.0 this method is only called during a rollback 
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that 
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null 
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be 
   * available instead of hitting the database.
   *
   * 
   * @param key The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance
   */  
  void clear();

  /**
   * Optional. This method is not called by the core.
   * 
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();
  
  /** 
   * Optional. As of 3.2.6 this method is no longer called by the core.
   *  
   * Any locking needed by the cache must be provided internally by the cache provider.
   * 
   * @return A ReadWriteLock 
   */
  ReadWriteLock getReadWriteLock();

}

实现

mybati缓存了解_第1张图片

mybatis实现了多种缓存,比如perpetualCache 是Cache接口的默认实现,通过hashMap来操作缓存,logginCache,在具有缓存的功能下,添加了打印日志的功能。

  • BlockingCache:阻塞版本的缓存装饰器,能够保证同一时间只有一个线程到缓存中查找指定的Key对应的数据。

  • FifoCache:先入先出缓存装饰器,FifoCache内部有一个维护具有长度限制的Key键值链表(LinkedList实例)和一个被装饰的缓存对象,Key值链表主要是维护Key的FIFO顺序,而缓存存储和获取则交给被装饰的缓存对象来完成。

  • LoggingCache:为缓存增加日志输出功能,记录缓存的请求次数和命中次数,通过日志输出缓存命中率。LruCache:最近最少使用的缓存装饰器,当缓存容量满了之后,使用LRU算法淘汰最近最少使用的Key和Value。

  • LruCache中通过重写LinkedHashMap类的removeEldestEntry()方法获取最近最少使用的Key值,将Key值保存在LruCache类的eldestKey属性中,然后在缓存中添加对象时,淘汰eldestKey对应的Value值。具体实现细节读者可参考LruCache类的源码。

  • ScheduledCache:自动刷新缓存装饰器,当操作缓存对象时,如果当前时间与上次清空缓存的时间间隔大于指定的时间间隔,则清空缓存。清空缓存的动作由getObject()、putObject()、removeObject()等方法触发。

  • SerializedCache:序列化缓存装饰器,向缓存中添加对象时,对添加的对象进行序列化处理,从缓存中取出对象时,进行反序列化处理。

  • SoftCache:软引用缓存装饰器,SoftCache内部维护了一个缓存对象的强引用队列和软引用队列,缓存以软引用的方式添加到缓存中,并将软引用添加到队列中,获取缓存对象时,如果对象已经被回收,则移除Key,如果未被回收,则将对象添加到强引用队列中,避免被回收,如果强引用队列已经满了,则移除最早入队列的对象的引用。

  • SynchronizedCache:线程安全缓存装饰器,SynchronizedCache的实现比较简单,为了保证线程安全,对操作缓存的方法使用synchronized关键字修饰。

  • TransactionalCache:事务缓存装饰器,该缓存与其他缓存的不同之处在于,TransactionalCache增加了两个方法,即commit()和rollback()。当写入缓存时,只有调用commit()方法后,缓存对象才会真正添加到TransactionalCache对象中,如果调用了rollback()方法,写入操作将被回滚。WeakCache:弱引用缓存装饰器,功能和SoftCache类似,只是使用不同的引用类型。

mybatis一级缓存

概念:

会话(session)级别的缓存称为一级缓存,默认开启的。

为什么使用一级缓存?

mybatis毕竟是查询数据库的一个半orm框架,查询数据库势必要消耗服务器的性能,为了减少服务器的性能,使用了缓存。将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

实现原理自己的概括

当程序与数据库建立了一次会话,中间开始查询数据,每次查询会根据mapper的id、命名空间、sql等等创建缓存key,先去查询本地缓存是否有值,如果有值,则获取解析值,返回,如果没有值,则去查询数据库,再把结果缓存到本地缓存。

一级缓存实现原理

mybati缓存了解_第2张图片

首先来看下缓存实例是存在哪里的,在BaseExecutor中有本地缓存localCache,所以继承BaseExecutor的执行器都有localCache,包括但不限于

SimpleExecutor、BatchExecutor,ReuseExecutor。

接下来大概介绍查询流程,具体介绍用到一级缓存的地方

  @Test
    public  void testMybatisCache () throws IOException {
        // 获取配置文件输入流
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        // 通过SqlSessionFactoryBuilder的build()方法创建SqlSessionFactory实例
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 调用openSession()方法创建SqlSession实例
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 获取UserMapper代理对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        // 执行Mapper方法,获取执行结果
        List<UserEntity> userList = userMapper.listAllUser();

        UserMapper userMapper1 = sqlSession.getMapper(UserMapper.class);
        // 执行Mapper方法,获取执行结果
        List<UserEntity> userList1 = userMapper.listAllUser();

        System.out.println(JSON.toJSONString(userList));
    }

  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // 获取Mybatis主配置文件配置的环境信息
      final Environment environment = configuration.getEnvironment();
      // 创建事务管理器工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 创建事务管理器
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 根据Mybatis主配置文件中指定的Executor类型创建对应的Executor实例
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建DefaultSqlSession实例
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  1. 从mybatis配置文件获取配置信息输入流,然后利用SqlSessionFactoryBuilder的build的方法创建SqlSession工厂
  2. openSession方法创建默认SqlSession(获取mybatis环境信息,创建事务管理器,创建执行器,构造默认SqlSession)
  3. 调用sqlSession的getMapper方法,利用动态代理(实现InvocationHadler)创建代理对象
  4. 调用Mapper的方法,实际上就是调用代理对象的invoke方法,而且调用查询方法(不管是默认还是自己写的),最后都会调用sqlSession的select相关方法。

根据上面的流程可以知道,SqlSession,Executor,localcache之间的关系

mybati缓存了解_第3张图片

接下来主要看SqlSession的select方法

  @Override
  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  
    @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取BoundSql对象,BoundSql是对动态SQL解析生成的SQL语句和参数映射信息的封装
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 创建CacheKey,用于缓存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 调用重载的query()方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

  @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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 从缓存中获取结果
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // 所有参数值
    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 metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
      }
    }
    // Environment Id
    if (configuration.getEnvironment() != null) {
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
    }
    return cacheKey;
  }
  1. 调用SqlSession的select方法,从configuration拿出mappedSatement(里面封装了mapper的属性),调用内置的执行器的query方法
  2. 执行器的query方法,先获取BoundSql对象,创建cacheKey(利用MapperId,偏移量,SQL语句…),调用重载的query
  3. 先根据cacheKey去执行器的localCache查询是否有值,如果没有再调用queryFromDatabase查询数据库,缓存结果到localCache。注意:LocalCacheScope=SATATEMENT时,每次查询都会清空缓存。

注意:在分布式环境下,务必将MyBatis的localCacheScope属性设置为STATEMENT,避免其他应用节点执行SQL更新语句后,本节点缓存得不到刷新而导致的数据一致性问题。

在MyBatis中,关于缓存设置的参数一共有2个:localCacheScope,cacheEnabled。





mybatis二级缓存

概念

二级缓存是全局的缓存,即使不同会话之间也能共享二级缓存,默认是不开启的;

二级缓存实现原理

首先说下如何开启他,在mybatis配置文件添加 和在对应的mapper.xml添加cache实例

mybati缓存了解_第4张图片

mybati缓存了解_第5张图片

其次看下二级缓存是如何生效的

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    // 根据executor类型创建对象的Executor对象
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    // 如果cacheEnabled属性为ture,这使用CachingExecutor对上面创建的Executor进行装饰
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 执行拦截器链的拦截逻辑
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

  @Override
  public Transaction getTransaction() {
    return delegate.getTransaction();
  }

  @Override
  public void close(boolean forceRollback) {
    try {
      //issues #499, #524 and #573
      if (forceRollback) { 
        tcm.rollback();
      } else {
        tcm.commit();
      }
    } finally {
      delegate.close(forceRollback);
    }
  }


  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 调用createCacheKey()方法创建缓存Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.queryCursor(ms, parameter, rowBounds);
  }

  @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);
        // 从MappedStatement对象对应的二级缓存中获取数据
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 如果缓存数据不存在,则从数据库中查询数据
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 將数据存放到MappedStatement对象对应的二级缓存中
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

}

public class TransactionalCacheManager {
  // 通过HashMap对象维护二级缓存对应的TransactionalCache实例
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

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

  public Object getObject(Cache cache, CacheKey key) {
    // 获取二级缓存对应的TransactionalCache对象,然后根据缓存Key获取缓存对象
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

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

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

  private TransactionalCache getTransactionalCache(Cache cache) {
    // 获取二级缓存对应的TransactionalCache对象
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      // 如果获取不到则创建,然后添加到Map中
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}


  1. 还是在sqlSessionFactory.openSession(); 时,会创建执行器,当cacheEnabled属性为ture,会创建CachingExecutor缓存执行器。

  2. 看下CachingExecutor的结构,它包含了一个委托执行器(使用了委托模式),用来真正执行的sql,而自己主要的作用是放在了建立和使用二级缓存

  3. 当执行sql,最后会进入到执行器,如果执行器是CachingExecutor时,会调用他的query方法,进入方法后,首先会从MappedStatement拿出二级缓存实例(你以为这就是二级缓存?不,你错了),然后判断是否要刷新缓存,再根据二级缓存实例从缓存管理器(CacheExecutor维护了TransactionalCacheManager缓存管理器,缓存管理器里面维护了二级缓存实例和TransactionalCache的关系)中得到TransactionalCache,再利用cacheKey获取TransactionalCache中对应的二级缓存,如果缓存不存在,则使用委托执行器去数据库查询数据,再缓存结果,如果存在,则直接返回。

    再回过头看,当时的MappedStatement是如何get二级缓存实例的;

      private void configurationElement(XNode context) {
        try {
          // 获取命名空间
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
          }
          // 设置当前正在解析的Mapper配置的命名空间
          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"));
    

createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
createTime: 2021-12-08T12:19:57+08:00
updateTime: 2021-12-08T12:19:57+08:00
} catch (Exception e) {
throw new BuilderException(“Error parsing Mapper XML. The XML location is '” + resource + "'. Cause: " + e, e);
}
}

 private void cacheElement(XNode context) throws Exception {
   if (context != null) {
     String type = context.getStringAttribute("type", "PERPETUAL");
     Class typeClass = typeAliasRegistry.resolveAlias(type);
     String eviction = context.getStringAttribute("eviction", "LRU");
     Class 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);
   }
 }

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

如上面的代码所示,在获取标签的所有属性信息后,调用MapperBuilderAssistant对象的userNewCache()方法创建二级缓存实例,然后通过MapperBuilderAssistant的currentCache属性保存二级缓存对象的引用。在调用MapperBuilderAssistant对象的addMappedStatement()方法创建MappedStatement对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement对象中,所以这就是需要配置的原因。

流程

![image-20211116235954488](https://img-blog.csdnimg.cn/img_convert/695760986f3bbfb9701b238c790e5892.png)



### mybatis二级缓存解决了什么问题

解决了一级缓存在不同session存在脏读的问题,但是分布式二级缓存也存在脏读。



![image-20211117000030143](https://img-blog.csdnimg.cn/img_convert/ae605add0674bdcb75bfb19684740c78.png)



### 总结

MyBatis一级缓存是SqlSession级别的缓存,默认就是开启的,而且无法关闭;二级缓存需要在MyBatis主配置文件中通过设置cacheEnabled参数值来开启。

你可能感兴趣的:(kafka,分布式,后端)