mybatis的缓存

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

缓存接口

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

 1public interface Cache {
 2
 3  /**获取缓存的id
 4   * @return The identifier of this cache
 5   */
 6  String getId();
 7
 8  /**添加缓存
 9   * @param key Can be any object but usually it is a {@link CacheKey}
10   * @param value The result of a select.
11   */
12  void putObject(Object key, Object value);
13
14  /**根据缓存键获取缓存
15   * @param key The key
16   * @return The object stored in the cache.
17   */
18  Object getObject(Object key);
19
20  /**移除缓存
21   * As of 3.3.0 this method is only called during a rollback 
22   * for any previous value that was missing in the cache.
23   * This lets any blocking cache to release the lock that 
24   * may have previously put on the key.
25   * A blocking cache puts a lock when a value is null 
26   * and releases it when the value is back again.
27   * This way other threads will wait for the value to be 
28   * available instead of hitting the database.
29   *
30   * 
31   * @param key The key
32   * @return Not used
33   */
34  Object removeObject(Object key);
35
36  /**
37   * Clears this cache instance
38   */  
39  void clear();
40
41  /**
42   * Optional. This method is not called by the core.
43   * 
44   * @return The number of elements stored in the cache (not its capacity).
45   */
46  int getSize();
47  
48  /** 
49   * Optional. As of 3.2.6 this method is no longer called by the core.
50   *  
51   * Any locking needed by the cache must be provided internally by the cache provider.
52   * 
53   * @return A ReadWriteLock 
54   */
55  ReadWriteLock getReadWriteLock();
56
57}

实现

image-20211116203147375

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,先去查询本地缓存是否有值,如果有值,则获取解析值,返回,如果没有值,则去查询数据库,再把结果缓存到本地缓存。

一级缓存实现原理

image-20211116205926071

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

SimpleExecutor、BatchExecutor,ReuseExecutor。

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

 1  @Test
 2    public  void testMybatisCache () throws IOException {
 3        // 获取配置文件输入流
 4        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
 5        // 通过SqlSessionFactoryBuilder的build()方法创建SqlSessionFactory实例
 6        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
 7        // 调用openSession()方法创建SqlSession实例
 8        SqlSession sqlSession = sqlSessionFactory.openSession();
 9        // 获取UserMapper代理对象
10        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
11        // 执行Mapper方法,获取执行结果
12        List<UserEntity> userList = userMapper.listAllUser();
13
14        UserMapper userMapper1 = sqlSession.getMapper(UserMapper.class);
15        // 执行Mapper方法,获取执行结果
16        List<UserEntity> userList1 = userMapper.listAllUser();
17
18        System.out.println(JSON.toJSONString(userList));
19    }
20
21  @Override
22  public SqlSession openSession() {
23    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
24  }
25
26  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
27    Transaction tx = null;
28    try {
29      // 获取Mybatis主配置文件配置的环境信息
30      final Environment environment = configuration.getEnvironment();
31      // 创建事务管理器工厂
32      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
33      // 创建事务管理器
34      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
35      // 根据Mybatis主配置文件中指定的Executor类型创建对应的Executor实例
36      final Executor executor = configuration.newExecutor(tx, execType);
37      // 创建DefaultSqlSession实例
38      return new DefaultSqlSession(configuration, executor, autoCommit);
39    } catch (Exception e) {
40      closeTransaction(tx); // may have fetched a connection so lets call close()
41      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
42    } finally {
43      ErrorContext.instance().reset();
44    }
45  }
  1. 从mybatis配置文件获取配置信息输入流,然后利用SqlSessionFactoryBuilder的build的方法创建SqlSession工厂
  2. openSession方法创建默认SqlSession(获取mybatis环境信息,创建事务管理器,创建执行器,构造默认SqlSession)
  3. 调用sqlSession的getMapper方法,利用动态代理(实现InvocationHadler)创建代理对象
  4. 调用Mapper的方法,实际上就是调用代理对象的invoke方法,而且调用查询方法(不管是默认还是自己写的),最后都会调用sqlSession的select相关方法。

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

image-20211116211707062

接下来主要看SqlSession的select方法

  1  @Override
  2  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  3    try {
  4      MappedStatement ms = configuration.getMappedStatement(statement);
  5      executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  6    } catch (Exception e) {
  7      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  8    } finally {
  9      ErrorContext.instance().reset();
 10    }
 11  }
 12  
 13    @Override
 14  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
 15    // 获取BoundSql对象,BoundSql是对动态SQL解析生成的SQL语句和参数映射信息的封装
 16    BoundSql boundSql = ms.getBoundSql(parameter);
 17    // 创建CacheKey,用于缓存Key
 18    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
 19    // 调用重载的query()方法
 20    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 21 }
 22
 23  @Override
 24  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 25    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
 26    if (closed) {
 27      throw new ExecutorException("Executor was closed.");
 28    }
 29    if (queryStack == 0 && ms.isFlushCacheRequired()) {
 30      clearLocalCache();
 31    }
 32    List<E> list;
 33    try {
 34      queryStack++;
 35      // 从缓存中获取结果
 36      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
 37      if (list != null) {
 38        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
 39      } else {
 40        // 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
 41        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
 42      }
 43    } finally {
 44      queryStack--;
 45    }
 46    if (queryStack == 0) {
 47      for (DeferredLoad deferredLoad : deferredLoads) {
 48        deferredLoad.load();
 49      }
 50      // issue #601
 51      deferredLoads.clear();
 52      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
 53        // issue #482
 54        clearLocalCache();
 55      }
 56    }
 57    return list;
 58  }
 59
 60  @Override
 61  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
 62    if (closed) {
 63      throw new ExecutorException("Executor was closed.");
 64    }
 65    CacheKey cacheKey = new CacheKey();
 66createTime: 2021-12-08T12:19:57+08:00
 67updateTime: 2021-12-08T12:19:57+08:00
 68createTime: 2021-12-08T12:19:57+08:00
 69updateTime: 2021-12-08T12:19:57+08:00
 70createTime: 2021-12-08T12:19:57+08:00
 71updateTime: 2021-12-08T12:19:57+08:00
 72createTime: 2021-12-08T12:19:57+08:00
 73updateTime: 2021-12-08T12:19:57+08:00
 74    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
 75    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
 76    // 所有参数值
 77    for (ParameterMapping parameterMapping : parameterMappings) {
 78      if (parameterMapping.getMode() != ParameterMode.OUT) {
 79        Object value;
 80        String propertyName = parameterMapping.getProperty();
 81        if (boundSql.hasAdditionalParameter(propertyName)) {
 82          value = boundSql.getAdditionalParameter(propertyName);
 83        } else if (parameterObject == null) {
 84          value = null;
 85        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
 86          value = parameterObject;
 87        } else {
 88          MetaObject metaObject = configuration.newMetaObject(parameterObject);
 89          value = metaObject.getValue(propertyName);
 90        }
 91createTime: 2021-12-08T12:19:57+08:00
 92updateTime: 2021-12-08T12:19:57+08:00
 93      }
 94    }
 95    // Environment Id
 96    if (configuration.getEnvironment() != null) {
 97createTime: 2021-12-08T12:19:57+08:00
 98updateTime: 2021-12-08T12:19:57+08:00
 99    }
100    return cacheKey;
101  }
  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。

1<!-- 二级缓存开关 有效值: true|false,默认值为true -->
2<settingname="cacheEnabled"value="true"/>
3<!-- 是否清除一级缓存 SESSION不清除,STATEMENT清除  有效值:SESSION|STATEMENT,默认值为SESSION -->
4<settingname="localCacheScope"value="SESSION"/>

mybatis二级缓存

概念

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

二级缓存实现原理

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

image-20211116213954482

image-20211116213939859

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

  1  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  2    executorType = executorType == null ? defaultExecutorType : executorType;
  3    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  4    Executor executor;
  5    // 根据executor类型创建对象的Executor对象
  6    if (ExecutorType.BATCH == executorType) {
  7      executor = new BatchExecutor(this, transaction);
  8    } else if (ExecutorType.REUSE == executorType) {
  9      executor = new ReuseExecutor(this, transaction);
 10    } else {
 11      executor = new SimpleExecutor(this, transaction);
 12    }
 13    // 如果cacheEnabled属性为ture,这使用CachingExecutor对上面创建的Executor进行装饰
 14    if (cacheEnabled) {
 15      executor = new CachingExecutor(executor);
 16    }
 17    // 执行拦截器链的拦截逻辑
 18    executor = (Executor) interceptorChain.pluginAll(executor);
 19    return executor;
 20  }
 21
 22public class CachingExecutor implements Executor {
 23
 24  private final Executor delegate;
 25  private final TransactionalCacheManager tcm = new TransactionalCacheManager();
 26
 27  public CachingExecutor(Executor delegate) {
 28    this.delegate = delegate;
 29    delegate.setExecutorWrapper(this);
 30  }
 31
 32  @Override
 33  public Transaction getTransaction() {
 34    return delegate.getTransaction();
 35  }
 36
 37  @Override
 38  public void close(boolean forceRollback) {
 39    try {
 40      //issues #499, #524 and #573
 41      if (forceRollback) { 
 42        tcm.rollback();
 43      } else {
 44        tcm.commit();
 45      }
 46    } finally {
 47      delegate.close(forceRollback);
 48    }
 49  }
 50
 51
 52  @Override
 53  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
 54    BoundSql boundSql = ms.getBoundSql(parameterObject);
 55    // 调用createCacheKey()方法创建缓存Key
 56    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
 57    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
 58  }
 59
 60  @Override
 61  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
 62    flushCacheIfRequired(ms);
 63    return delegate.queryCursor(ms, parameter, rowBounds);
 64  }
 65
 66  @Override
 67  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
 68      throws SQLException {
 69    // 获取MappedStatement对象中维护的二级缓存对象
 70    Cache cache = ms.getCache();
 71    if (cache != null) {
 72      // 判断是否需要刷新二级缓存
 73      flushCacheIfRequired(ms);
 74      if (ms.isUseCache() && resultHandler == null) {
 75        ensureNoOutParams(ms, boundSql);
 76        // 从MappedStatement对象对应的二级缓存中获取数据
 77        @SuppressWarnings("unchecked")
 78        List<E> list = (List<E>) tcm.getObject(cache, key);
 79        if (list == null) {
 80          // 如果缓存数据不存在,则从数据库中查询数据
 81          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
 82          // 將数据存放到MappedStatement对象对应的二级缓存中
 83          tcm.putObject(cache, key, list); // issue #578 and #116
 84        }
 85        return list;
 86      }
 87    }
 88    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
 89  }
 90
 91}
 92
 93public class TransactionalCacheManager {
 94  // 通过HashMap对象维护二级缓存对应的TransactionalCache实例
 95  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
 96
 97  public void clear(Cache cache) {
 98    getTransactionalCache(cache).clear();
 99  }
100
101  public Object getObject(Cache cache, CacheKey key) {
102    // 获取二级缓存对应的TransactionalCache对象,然后根据缓存Key获取缓存对象
103    return getTransactionalCache(cache).getObject(key);
104  }
105  
106  public void putObject(Cache cache, CacheKey key, Object value) {
107    getTransactionalCache(cache).putObject(key, value);
108  }
109
110  public void commit() {
111    for (TransactionalCache txCache : transactionalCaches.values()) {
112      txCache.commit();
113    }
114  }
115
116  public void rollback() {
117    for (TransactionalCache txCache : transactionalCaches.values()) {
118      txCache.rollback();
119    }
120  }
121
122  private TransactionalCache getTransactionalCache(Cache cache) {
123    // 获取二级缓存对应的TransactionalCache对象
124    TransactionalCache txCache = transactionalCaches.get(cache);
125    if (txCache == null) {
126      // 如果获取不到则创建,然后添加到Map中
127      txCache = new TransactionalCache(cache);
128      transactionalCaches.put(cache, txCache);
129    }
130    return txCache;
131  }
132
133}
134
135
  1. 还是在sqlSessionFactory.openSession(); 时,会创建执行器,当cacheEnabled属性为ture,会创建CachingExecutor缓存执行器。

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

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

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

     1  private void configurationElement(XNode context) {
     2    try {
     3      // 获取命名空间
     4      String namespace = context.getStringAttribute("namespace");
     5      if (namespace == null || namespace.equals("")) {
     6        throw new BuilderException("Mapper's namespace cannot be empty");
     7      }
     8      // 设置当前正在解析的Mapper配置的命名空间
     9      builderAssistant.setCurrentNamespace(namespace);
    10      // 解析<cache-ref>标签
    11      cacheRefElement(context.evalNode("cache-ref"));
    12      // 解析<cache>标签
    13      cacheElement(context.evalNode("cache"));
    14      // 解析所有的<parameterMap>标签
    15      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    16      // 解析所有的<resultMap>标签
    17      resultMapElements(context.evalNodes("/mapper/resultMap"));
    18      // 解析所有的<sql>标签
    19      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);
}
}

 1 private void cacheElement(XNode context) throws Exception {
 2   if (context != null) {
 3     String type = context.getStringAttribute("type", "PERPETUAL");
 4     Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
 5     String eviction = context.getStringAttribute("eviction", "LRU");
 6     Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
 7     Long flushInterval = context.getLongAttribute("flushInterval");
 8     Integer size = context.getIntAttribute("size");
 9     boolean readWrite = !context.getBooleanAttribute("readOnly", false);
10     boolean blocking = context.getBooleanAttribute("blocking", false);
11     Properties props = context.getChildrenAsProperties();
12     builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
13   }
14 }
15
16   MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
17       .resource(resource)
18       .fetchSize(fetchSize)
19       .timeout(timeout)
20       .statementType(statementType)
21       .keyGenerator(keyGenerator)
22       .keyProperty(keyProperty)
23       .keyColumn(keyColumn)
24       .databaseId(databaseId)
25       .lang(lang)
26       .resultOrdered(resultOrdered)
27       .resultSets(resultSets)
28       .resultMaps(getStatementResultMaps(resultMap, resultType, id))
29       .resultSetType(resultSetType)
30       .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
31       .useCache(valueOrDefault(useCache, isSelect))
32       .cache(currentCache);
 1
 2如上面的代码所示,在获取<cache>标签的所有属性信息后,调用MapperBuilderAssistant对象的userNewCache()方法创建二级缓存实例,然后通过MapperBuilderAssistant的currentCache属性保存二级缓存对象的引用。在调用MapperBuilderAssistant对象的addMappedStatement()方法创建MappedStatement对象时会将当前命名空间对应的二级缓存对象的引用添加到MappedStatement对象中,所以这就是需要配置<cache>的原因。
 3
 4流程
 5
 6![image-20211116235954488](https://gitee.com/zxqzhuzhu/imgs/raw/master/picGo/image-20211116235954488.png)
 7
 8
 9
10### mybatis二级缓存解决了什么问题
11
12解决了一级缓存在不同session存在脏读的问题,但是分布式二级缓存也存在脏读。
13
14
15
16![image-20211117000030143](https://gitee.com/zxqzhuzhu/imgs/raw/master/picGo/image-20211117000030143.png)
17
18
19
20### 总结
21
22MyBatis一级缓存是SqlSession级别的缓存,默认就是开启的,而且无法关闭;二级缓存需要在MyBatis主配置文件中通过设置cacheEnabled参数值来开启。