Mybatis的一级缓存机制与源码解析

本文基于Mybatis-3.4.6。

简介

  • 解决完全相同的重复查询,减少查询数据库的次数,减少资源浪费。MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

  • 一级缓存是session级别的数据缓存。

  • mybatis中sqlsession只是一个对外接口类,用户通过它来和数据库交互。SqlSession实际上将用户的操作委托给Executor执行器来完成,包括各类对数据库的操作。MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

  • mybatis的缓存实现包括了三个对象,分别是SqlSession,Executor对象,PerpetualCache对象。

    • SqlSession接口的实现类DefaultSqlSession有一个Executor类型的成员变量。
    • Executor接口的抽象实现类BaseExecutor中有两个个PerpetualCache类型的成员变量,其中localCache就是缓存的存储对象
    • PerpetualCache的内部有一个HashMap类型的成员变量cache,该对象就是mybatis中,真正的一级缓存的存储对象
    • 综上,mybatis中的一次Session查询的所有查询结果,都将被放置BaseExector的localCache成员变量里,在Perpetualcache类型的localCache中的cache成员变量里

生命周期

  • 存放一级缓存的cache的初始化过程如下:
    • sqlSessionFactory.openSession(); 从DefaultSqlSessionFactory中获取SqlSession,相关源码如下:
        public SqlSession openSession() {
        		return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
        }
        private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
                Transaction tx = null;
                try {
                  final Environment environment = configuration.getEnvironment();
                  final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
                  tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
                  // 注意此处,Executor已经被生成,在SqlSession的生成过程中,作为参数传递到SqlSession中
                  // newExecutor()源码在下一部分
                  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();
                }
      }
      
    • new DefaultSqlSession(configuration, executor, autoCommit); 调用DefaultSqlSession的构造函数来新建该对象。源码如下:
        public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
                this.configuration = configuration;
                this.executor = executor;
                this.dirty = false;
                this.autoCommit = autoCommit;
        }
      
    • configuration.newExecutor(tx, execType) 这一段代码在Configuration中,源码如下:
        public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
                executorType = executorType == null ? defaultExecutorType : executorType;
                executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
                Executor executor;
                // 根据不同需要创建不同的Executor,但是这些Executor都继承自BaseExecutor
                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);
                }
                if (cacheEnabled) {
                  executor = new CachingExecutor(executor);
                }
                executor = (Executor) interceptorChain.pluginAll(executor);
                return executor;
        }
      

      在源码中,根据方法传入的参数,决定创建不同的Executor类型。但调用的构造函数无一例外,都是BaseExecutor的构造函数,以下代码分别是不同Executor的构造函数。源码如下:

        public BatchExecutor(Configuration configuration, Transaction transaction) {
        		super(configuration, transaction);
        }
        public ReuseExecutor(Configuration configuration, Transaction transaction) {
        		super(configuration, transaction);
        }
        public SimpleExecutor(Configuration configuration, Transaction transaction) {
        		super(configuration, transaction);
        }
        protected BaseExecutor(Configuration configuration, Transaction transaction) {
                this.transaction = transaction;
                this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
                // 一级缓存的初始化
                this.localCache = new PerpetualCache("LocalCache");
                this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
                this.closed = false;
                this.configuration = configuration;
                this.wrapper = this;
        }
      

      在BaseExecutor中,对自身的成员变量进行了初始化,其中备受关注的localCache也在其中。其仅仅完成了一个工作,就是给定id并将其传入到实例中。cache自己进行了初始化,被赋值为一个HashMap的实例对象。

  • 通过上面的源码分析,基本上能清晰的看到一级缓存的生命周期伴随着SqlSession的生命周期存在。因为它只是SqlSession的一个成员变量,因此不可能抛开SqlSession独立存活。一旦SQLSession被关闭并被释放,一级缓存也将烟消云散。

  • SqlSession调用close()方法,Executor中的PerpetualCache成员变量将被释放,一级缓存不可用。源码如下:
      public void close() {
              try {
                  // 释放executor
                  executor.close(isCommitOrRollbackRequired(false));
                  closeCursors();
                  dirty = false;
              } finally {
                  ErrorContext.instance().reset();
              }
      }
      private boolean isCommitOrRollbackRequired(boolean force) {
              return (!autoCommit && dirty) || force;
      }
      private void closeCursors() {
              if (cursorList != null && cursorList.size() != 0) {
                  for (Cursor<?> cursor : cursorList) {
                      try {
                          cursor.close();
                      } catch (IOException e) {
                          throw ExceptionFactory.wrapException("Error closing cursor.  Cause: " + e, e);
                      }
                  }
                  cursorList.clear();
              }
      }
    

    释放executor资源,主要是重置其中的成员变量的引用关系。BaseExector源码如下:

      public void close(boolean forceRollback) {
              try {
                  try {
                  	// 是否需要强制回滚
                      rollback(forceRollback);
                  } finally {
                  	// 如果事务不为null,就关闭事务
                      if (transaction != null) {
                          transaction.close();
                      }
                  }
              } catch (SQLException e) {
                  // Ignore.  There's nothing that can be done at this point.
                  log.warn("Unexpected exception on closing transaction.  Cause: " + e);
              } finally {
              	// 将成员变量的引用关系置为null,方便其对象被回收
                  transaction = null;
                  deferredLoads = null;
                  localCache = null;
                  localOutputParameterCache = null;
                  closed = true;
              }
      }
    
  • SqlSession调用clearCache(),会清空Executor中PerpetualCache成员变量中的数据,但SqlSession仍然可用。DefaultSqlSession源码如下:
      public void clearCache() {
          	executor.clearLocalCache();
      }
    

    BaseExecutor中的clearLocalCache()方法将清理其两个PerpetualCache类型的成员变量。源码如下:

      public void clearLocalCache() {
              if (!closed) {
                  localCache.clear();
                  localOutputParameterCache.clear();
              }
      }
    

    PerPetualCache中的clear()方法如下:

    扫描二维码关注公众号,回复: 9046014 查看本文章
      public void clear() {
          	cache.clear();
      }
    

    因为cache是HashMap类型的,因此这实际就是调用hashMap的clear方法。

  • 在DefaultSQLSession中,insert(),update(),delete()方法的实现,最终都依赖于一个update()方法。源码如下:
      public int insert(String statement) {
          return insert(statement, null);
      }
      public int insert(String statement, Object parameter) {
          return update(statement, parameter);
      }
      public int update(String statement) {
          return update(statement, null);
      }
      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);
          } fina 大专栏  Mybatis的一级缓存机制与源码解析lly {
            ErrorContext.instance().reset();
          }
      }
      public int delete(String statement) {
          return update(statement, null);
      }
      public int delete(String statement, Object parameter) {
          return update(statement, parameter);
      }
    

    仔细观察会发现insert,update,delete都是两组方法,但是无论是哪一种操作,最后都通用了update(statement, parameter);。在该方法中,使用Executor的update()方法区完成数据库更新操作,这再一次印证了前文说到的,SqlSession所有的数据操作都被委托给Executor完成。下面展示BaseExecutor的update方法:

    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.");
          }
          // 该方法在上文有展示源码,就是去清理Executor两个本地PerpetualCache类型的成员变量中的HashMap中的数据
          clearLocalCache();
          return doUpdate(ms, parameter);
    }
    
  • 从上一部分的源码我们能得出结论,mybatis中插入,修改,删除,都会对一级缓存进行清空,注意仅仅是清空数据,没有释放缓存对象!

工作原理

  • 一级缓存的工作原理如下:
    • mybatis中的所有查询操作,最终都会调用SqlSession中的selectList(String statement, Object parameter, RowBounds rowBounds)来完成,源码展示如下:
        public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
                try {
                    MappedStatement ms = configuration.getMappedStatement(statement);
                    // 此处再次印证前文所说的,SqlSession对于数据库的所有操作,都是委托给Executor去完成的
                    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
                } catch (Exception e) {
                    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
                } finally {
                    ErrorContext.instance().reset();
                }
        }
      
    • 注意其中的executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);该方法中隐藏了一级缓存的实现细节,源码如下:
        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);
        }
        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++;
                    // 根据key值从缓存中获取值
                    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
                    if (list != null) {
                        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                    } else {
                    	// 如果缓存中的结果为null,则去查询数据库。源码下面
                        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;
        }
        private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
        			CacheKey key, BoundSql boundSql) throws SQLException {
                List<E> list;
                // 将键值放入缓存
                localCache.putObject(key, EXECUTION_PLACEHOLDER);
                try {
                    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
                } finally {
                	// 移除缓存
                    localCache.removeObject(key);
                }
                // 将心的结果放入缓存
                localCache.putObject(key, list);
                if (ms.getStatementType() == StatementType.CALLABLE) {
                    localOutputParameterCache.putObject(key, parameter);
                }
        		return list;
          }
      

      通过以上源码,大致能看懂一级缓存的实现路径。在查询过程中,首先去读取缓存中,如果缓存中没有,就读取数据库,并将数据库结果放入到缓存。

    • Mybatis的SqlSession一级缓存的工作流程
      • 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
      • 判断从Cache中根据特定的key值取的数据数据是否为空;
      • 如果不为空就直接返回
      • 如果为空就去查询数据库,将结果放入到缓存中,返回结果
  • mybatis通过以下内容相同,则确定两次查询是相同的查询:
    • 传入的statementId相同
    • 查询时要求的结果集中的结果范围(结果的范围通过rowBounds.offset和rowBounds.limit表示)相同。
    • 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql())相同
    • 传递给java.sql.Statement要设置的参数值
  • 传入的statementId,对于MyBatis而言,你要使用它,必须需要一个statementId,它代表着你将执行什么样的Sql。MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页。条件一和条件二是Mybatis自身的一个判定。由于MyBatis底层还是依赖于JDBC实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC而言,也是完全一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBC的SQL语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。上面的第三第四个条件都符合的总结就是,调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。
          public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
                  if (closed) {
                    throw new ExecutorException("Executor was closed.");
                  }
                  CacheKey cacheKey = new CacheKey();
                  cacheKey.update(ms.getId());
                  cacheKey.update(rowBounds.getOffset());
                  cacheKey.update(rowBounds.getLimit());
                  cacheKey.update(boundSql.getSql());
                  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 metaObject = configuration.newMetaObject(parameterObject);
                              value = metaObject.getValue(propertyName);
                          }
                          cacheKey.update(value);
                      }
                  }
                  if (configuration.getEnvironment() != null) {
                      // issue #176
                      cacheKey.update(configuration.getEnvironment().getId());
                  }
                  return cacheKey;
          }
    
  • 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession查询的时候,要控制好SqlSession的生存时间,SqlSession的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空SqlSession中的缓存。对于只执行、并且频繁执行大范围的select操作的SqlSession对象,SqlSession对象的生存时间不应过长。

参考

猜你喜欢

转载自www.cnblogs.com/liuzhongrong/p/12289439.html
今日推荐