MyBatis源码学习(七)——从源码看MyBatis如何使用缓存

63 篇文章 11 订阅

目录

前言和总纲

一,创建SqlSession

二,创建CacheKey

三,执行sql

四,sqlSession的提交


 

先上总结:

1,创建sqlSession阶段。创建Executor执行器,其中有两个参数,localCache和localOutputParameterCache,localCache是一级缓存,localOutputParameterCache用于缓存存储过程的OUT和INOUT参数。Executor执行器首先会被CachingExecutor装饰,然后又被插件列表用拦截器封装,创建动态代理,最后Executor成为了一个Plugin的动态代理。

2,执行sql前创建CacheKey阶段。CacheKey的生成调用的是CachingExecutor的createCacheKey方法,使用的是CacheKey的update方法,CacheKey包含hashcode,multiplier,count,updateList四个参数。会有6个参数参与update操作:MappedStatement.id,rowBounds的offset,rowBounds的limit,sql语句字符串,参数列表的非OUT类型参数值,environment对象的id。CacheKey的hashcode计算方式:count++,对象哈希值乘以count,新哈希值等于原哈希值乘以37再加上第2步的结果。

3,执行sql阶段的二级缓存。先判断是否有二级缓存,有就直接取二级缓存。二级缓存管理器除了保存有HashMap外,还有entriesToAddOnCommit(HashMap)负责收集未提交的查询结果,entriesMissedInCache(Set)负责收集收集二级缓存未覆盖的key。查询出的结果会先放在entriesToAddOnCommit中。

4,执行sql阶段的一级缓存。先判断是否有一级缓存,如果没有就直接取一级缓存。如果没有一级缓存就查数据库,查到结果保存到一级缓存中,如果是存储过程,还要处理localOutputParameterCache。另外,insert、update、delete操作会清空一级缓存。configuration的一级缓存的作用域LocalCacheScope配置为STATEMENT时也会每次查询后清空一级缓存。

5,sqlSession的提交阶段。对于一级缓存,如果没开事务则清空。对于二级缓存,如果事务需要提交,则会把entriesToAddOnCommit中的值保存至二级缓存,然后把entriesMissedInCache中的值保存到二级缓存且值为null,然后把这二者清空。

 

前言和总纲

1,MyBatis为我们定义的每个Mapper接口创建了一个MapperProxy动态代理。

2,当我们调用Mapper接口中的方法时,实际上调用的是MapperProxy动态代理的invoke()方法。

3,MapperProxy动态代理根据调用的select,update,insert,delete等语句,调用不同的子方法,以最简单的单行查询为例,调用的是:sqlSession.selectOne()方法

注意,此处的sqlSession指的是SqlSessionTemplate实例。

4,对于SqlSessionTemplate的selectOne()方法来说,他的代码是这样的:

public <T> T selectOne(String statement, Object parameter) {
    return this.sqlSessionProxy.selectOne(statement, parameter);
}

这里的sqlSessionProxy由SqlSessionTemplate的内部类SqlSessionInterceptor提供:

private class SqlSessionInterceptor implements InvocationHandler {
    private SqlSessionInterceptor() {
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

        Object unwrapped;
        try {
            Object result = method.invoke(sqlSession, args);
            if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
                sqlSession.commit(true);
            }

            unwrapped = result;
        } catch (Throwable var11) {
            unwrapped = ExceptionUtil.unwrapThrowable(var11);
            if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
                sqlSession = null;
                Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
                if (translated != null) {
                    unwrapped = translated;
                }
            }

            throw (Throwable)unwrapped;
        } finally {
            if (sqlSession != null) {
                SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            }

        }

        return unwrapped;
    }
}

这段代码里面可以看到SqlSession的整个生命周期。

注意:这里的sqlSession是用来执行sql用的,对应MySQL的session会话,默认由DefaultSqlSession实现,和上面提到的SqlSessionTemplate不是一回事。前后用同名变量实在是让人有点眼晕。

 

下面关注以几个和缓存有关的重点,通过这几个点可以看到MyBatis是如何玩转缓存的:

1)创建SqlSession

SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

这段代码用来创建SqlSession,在这里面可以看到MyBatis缓存的代码结构。

2)执行sql

Object result = method.invoke(sqlSession, args);

这个方法负责执行sql,方法中会先查缓存,缓存不存在则查询数据库,然后会根据配置把查询结果放入缓存。同时要知道,MyBatis的缓存实际上以HashMap的形式存在,其中HashMap的key就是在正式开始查询前生成的。

3)SqlSession的提交

if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
    sqlSession.commit(true);
}

SqlSession提交时,对缓存进行了一系列后续的处理。

对于以上三点,下面分别介绍。

 

一,创建SqlSession

创建SqlSession的逻辑从这里开始:

SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

这段代码来自SqlSessionTemplate的内部类SqlSessionInterceptor,看一下SqlSessionUtils的getSqlSession()方法:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
    Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
    Assert.notNull(executorType, "No ExecutorType specified");
    SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
        return session;
    } else {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Creating a new SqlSession");
        }

        session = sessionFactory.openSession(executorType);
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
        return session;
    }
}

逻辑很简单,先通过Spring的事务管理器判断当前session是否已经处于在事务中,如果是则直接返回session,如果没有则创建一个session,调用的是这行代码:

session = sessionFactory.openSession(executorType);

这里的sessionFactory是DefaultSqlSessionFactory对象,他的openSession()方法是这样的:

@Override
public SqlSession openSession(ExecutorType execType) {
  return openSessionFromDataSource(execType, null, false);
}

继续,openSessionFromDataSource()方法:

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);
    final Executor executor = configuration.newExecutor(tx, execType);
    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();
  }
}

在这段代码中出现了一个重要的对象,Executor执行器,sql将由此对象执行,executor执行器由以下方法创建:

final Executor executor = configuration.newExecutor(tx, execType);

这里的configuration对象来自MyBatis的Configuration类,他的newExecutor()方法是这样的:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  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);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

可见,根据executorType种类的不同,executor有几种不同的实现类,默认情况下由SimpleExecutor类来实现。

需要注意的是,SimpleExecutor有个父类:BaseExecutor,SimpleExecutor的构造方法中调用了BaseExecutor的构造方法:

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;
}

注意到,这其中除了参数传入之外,还初始化了两个属性:

this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");

都是PerpetualCache类的对象,这个类是MyBatis专门处理一级缓存用的,但是BaseExecutor设置了两个PerpetualCache对象,其中localCache就是通常意义上的一级缓存,而localOutputParameterCache只有存储过程会用到,他缓存了存储过程的OUT和INOUT类型参数的输出结果。

PerpetualCache类中维护了一个HashMap:

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

专门用来保存一级缓存的信息。

 

生成了executor的实现类后,判断了一下cacheEnabled参数,cacheEnabled是二级缓存用的参数,用户可以在配置文件中使用cacheEnabled=true,或者使用注解@CacheNameSpace来进行配置。此参数默认就是true。

注意:cacheEnabled参数为true不代表一定会使用二级缓存,二级缓存的使用需要几个配置同时生效。

 

继续代码逻辑,当cacheEnabled参数为true时,executor执行器被替换成了一个CachingExecutor对象,原来的执行器对象作为参数传入,这是一种装饰者模式。CachingExecutor的构造是这样的:

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

可见,原来的SimpleExecutor对象成为了CachingExecutor的其中一个属性delegate,这种装饰者模式扩展了原有对象的功能。

 

再后面的代码,executor执行器又变了,被interceptorChain添加了插件列表,interceptorChain使用了责任链模式,保存了拦截器列表,实际上就是插件列表,每个插件其实都是一个拦截器。interceptorChain的pluginAll()方法是这样的:

public Object pluginAll(Object target) {
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

可以看到,如果插件列表中有插件,那么CachingExecutor就会被拦截器处理,调用拦截器的plugin()方法,CachingExecutor对象会变成一个动态代理Proxy。如果插件列表中什么插件都没有,返回的CachingExecutor就会是之前的CachingExecutor对象。

 

下面以MyBatis的分页插件PageHelper为例,看这个插件是如何影响CachingExecutor的。

PageHelper中的拦截器是PageInterceptor类,他的plugin()方法是这样的:

public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

其中target参数就是传入的CachingExecutor(也有可能是被之前拦截器处理过的动态代理),this参数就是这个拦截器本身,下面是Plugin.wrap()的代码:

public static Object wrap(Object target, Interceptor interceptor) {
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  Class<?> type = target.getClass();
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

逻辑大概就是:拿到class对象,接口列表,然后创建动态代理。

注意newProxyInstance()方法的第三个参数:new Plugin(target, interceptor, signatureMap),这个Plugin对象将成为动态代理的InvocationHandler,于是这个动态代理就成了一个Plugin代理。

Plugin的这个构造其实就是传入了这几个参数,没有其他逻辑了:

private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
  this.target = target;
  this.interceptor = interceptor;
  this.signatureMap = signatureMap;
}

我们看到,Plugin的target就是原始的CachingExecutor对象,这个后面会用到。

得到了Plugin代理后,这个代理将被赋值给executor执行器。于是executor执行器的最终形态可能是一个CachingExecutor对象,或是一个Plugin代理

 

执行器创建完成,我们回到openSessionFromDataSource()方法:

final Executor executor = configuration.newExecutor(tx, execType);

这段代码执行完成,下面是:

return new DefaultSqlSession(configuration, executor, autoCommit);

根据刚刚创建的执行器等参数构造了一个DefaultSqlSession,构造方法里也没别的逻辑,都是参数传入。

至此,创建SqlSession部分结束。

 

二,创建CacheKey

重新贴一下SqlSessionInterceptor的invoke()方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

    Object unwrapped;
    try {
        Object result = method.invoke(sqlSession, args);
        if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
            sqlSession.commit(true);
        }

        unwrapped = result;
    } catch (Throwable var11) {
        unwrapped = ExceptionUtil.unwrapThrowable(var11);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
            SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            sqlSession = null;
            Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
            if (translated != null) {
                unwrapped = translated;
            }
        }

        throw (Throwable)unwrapped;
    } finally {
        if (sqlSession != null) {
            SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }

    }

    return unwrapped;
}

在上一节中创建好了sqlSession,这一节将开始调用:

Object result = method.invoke(sqlSession, args);

来执行sql,在正式执行sql之前,会先创建缓存用的key。

此处method的invoke方法经过一系列反射的调用,代码会来到DefaultSqlSession的selectList()方法:

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    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();
  }
}

这里的DefaultSqlSession就是上一节创建好的sqlSession,这段代码的重点是下面这个query()方法:

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

从上一节中我们知道,这里的executor对象可能是CachingExecutor对象或者Plugin的动态代理,如果是CachingExecutor对象,此处将直接调用CachingExecutor的query()方法,如果是动态代理,则会调用Plugin的invoke()方法,方法最终还会调用CachingExecutor的query()方法。

 

本文中我们只关注代码逻辑中和缓存相关的部分,上面说的两种方式都有相同的一段逻辑,就是调用CachingExecutor的createCacheKey()方法获得key,然后调用query()方法查询。

下面分别看一下这两种情况。

第一种情况,没有拦截器,executor就是CachingExecutor对象。直接调用query()方法:

@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);
}

可以看到,先是调用了createCacheKey()方法拿到key,然后调用query()方法查询。

 

第二种情况,有拦截器,executor是Plugin的动态代理,Plugin的invoke()方法是这样的:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

其中的method就是query方法,从signatureMap中能拿到这个方法,所以代码会返回:

return interceptor.intercept(new Invocation(target, method, args));

这里的interceptor就是Plugin里面的PageInterceptor,分页插件的拦截器,其intercept()方法是这样的:

public Object intercept(Invocation invocation) throws Throwable {
    Object var16;
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement)args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds)args[2];
        ResultHandler resultHandler = (ResultHandler)args[3];
        Executor executor = (Executor)invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        if (args.length == 4) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            cacheKey = (CacheKey)args[4];
            boundSql = (BoundSql)args[5];
        }

        this.checkDialectExists();
        if (this.dialect instanceof Chain) {
            boundSql = ((Chain)this.dialect).doBoundSql(Type.ORIGINAL, boundSql, cacheKey);
        }

        List resultList;
        if (!this.dialect.skip(ms, parameter, rowBounds)) {
            if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql);
                if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                    Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    return var12;
                }
            }

            resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }

        var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if (this.dialect != null) {
            this.dialect.afterAll();
        }

    }

    return var16;
}

这其中提到了invocation的args数组,这个args数组是什么?再回顾一下这个动态代理执行的那行代码,那是DefaultSqlSession的selectList方法,注意executor.query方法执行时的参数列表:

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    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();
  }
}

args数组中的值就是ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER,四个参数组成的数组。

 

回到PageInterceptor的intercept()方法,注意到这段:

if (args.length == 4) {
    boundSql = ms.getBoundSql(parameter);
    cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
}

这个数组长度为4的场景就是符合当前场景的,在逻辑中构造了cacheKey,用的是executor的createCacheKey()方法,此处的executor是

Executor executor = (Executor)invocation.getTarget();

得到的,也就是Plugin构造时传入的CachingExecutor对象,于是代码又来到了CachingExecutor的createCacheKey()方法。

 

这样,不论excutor执行器是CachingExecutor对象还是Plugin动态代理,方法都调用了CachingExecutor的createCacheKey方法,这个方法代码是这样的:

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}

这里的delegate就是生成CachingExecutor时用的SimpleExecutor,他的createCacheKey方法在其父类BaseExecutor中:

@Override
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;
}

这个方法用于产生缓存用key,可以看到,方法创建了一个CacheKey对象,然后对cacheKey用几个参数进行了多次update操作,经过几次update后,得到的cacheKey对象将来会作为缓存的key。

 

下面看一下这个CacheKey和他的update()方法,CacheKey的构造方法是这样的:

public CacheKey() {
  this.hashcode = DEFAULT_HASHCODE;
  this.multiplier = DEFAULT_MULTIPLYER;
  this.count = 0;
  this.updateList = new ArrayList<Object>();
}

可以看到CacheKey包含了四个属性:

  • hashcode,CacheKey的哈希值,CacheKey在update操作时会修改此值,是判定CacheKey是否相同的重要依据。
  • multiplier,乘数,等于DEFAULT_MULTIPLYER变量,数值37,不会改变。
  • count,经历的update次数,应该是当做计数器用的。
  • updateList,每次update操作用到的对象列表。

另外CacheKey中还有个属性checksum,记录了每个参与update的对象的哈希值之和,也是校验CacheKey是否相同的依据之一。

 

然后就是CacheKey的update()方法:

public void update(Object object) {
  int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 

  count++;
  checksum += baseHashCode;
  baseHashCode *= count;

  hashcode = multiplier * hashcode + baseHashCode;

  updateList.add(object);
}

方法逻辑中,除了count加1,checksum+=对象哈希值,updateList添加对象之外,就是计算新的hashcode了,步骤分为以下几步:

  1. 计算对象哈希值。
  2. 对象哈希值乘以count。
  3. 新哈希值等于原哈希值乘以37再加上第2步的结果。

关于第2步乘以count的必要性,目测是为了使计算流程有序,相同的哈希值交换位置后能得到不同的结果,这一点后面会说。

以上便是每次update对hashcode的影响方式,根据createCacheKey()方法的代码,有以下对象参与了update:

  1. MappedStatement的id。这个字符串是调用的接口名加上方法名,比如com.test.TestMapper.getOrder。
  2. rowBounds的offset,分页配置的第一个参数,不写则为0。
  3. rowBounds的limit,分页配置的第二个参数,不写则为2147483647。
  4. sql语句字符串。如果是预编译模式那么这里的字符串就是预编译的sql语句,带问号的那种。
  5. 参数列表的非OUT类型参数。也就是IN和INOUT类型参数。
  6. configuration中的environment对象的id。

注意到处理参数列表的参数时,有这么一段代码:

MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);

metaObject是根据参数列表创建的一个工具类,保留了当前sql所有可接收的参数Map,其中key是参数名,value是给sql传的参数值。

然后调用getValue()方法,根据参数名获得传入的参数值。注意是参数值,和参数名没关系。

从这个逻辑中我们能得到两点结论:

1,只有sql需要的参数才会参与update操作,参数列表中传入的其他无关参数不会影响hashcode。

2,传入的参数值会参与update操作,和参数名无关。

 

查尔斯王子思考了一下,于是问题来了:

参数名不影响hashcode结果,如果有两次查询的查询条件如下:

第一次查询,字段A="abc" and 字段B="def",

第二次查询,字段A="def" and 字段B="abc",

那么参与update操作的对象都是"abc"和"def",最后会不会算出相同的hashcode,从而使第二次查询直接使用第一次查询的缓存?

目测这个问题就是计算hashcode的步骤中,对象hashcode要先乘以count的原因,只要保证两个查询的计算都先处理字段A再处理字段B(或反之),那么两个查询即使用的参数值相同,最终也能得到不同的hashcode。

假设这两个字段的hashcode分别要乘的count是5和6(因为MappedStatement.id要乘1,offset要乘2,limit要乘3,sql语句要乘4),那么两次查询时hashcode就会是这么算(只列出计算入参的部分):

第一次查询:

hashcode=hashcode*37+"abc".hashcode*5;

hashcode=hashcode*37+"def".hashcode*6;

第二次查询:

hashcode=hashcode*37+"def".hashcode*5;

hashcode=hashcode*37+"abc".hashcode*6;

于是两次查询得到不同的hashcode,第二次查询不会用第一次的缓存。

 

查尔斯王子又思考了一下,为什么不让字段名+字段值一起参与update呢?比如给"A=abc"这样,不就不用考虑顺序了么?

另外,我们看这个工具类的生成代码:

MetaObject metaObject = configuration.newMetaObject(parameterObject);

这段代码是写在参数列表的循环中的,但是这个对象本身和某个具体参数没关系,那为什么这行代码不写在循环外面,而是在循环内部一遍一遍的创建呢?只是为了快速给GC回收么?

 

在createCacheKey()方法的最后,cacheKey还用configuration中的environment对象的id进行了一次update操作,一般情况下这个environment对象的id就是"SqlSessionFactoryBean"。

至此,hashcode计算完成,cacheKey对象创建完成。

 

三,执行sql

cacheKey对象创建完成后,下面准备开始执行sql。不论是否使用了PageHelper插件,代码最终会执行到CachingExecutor的query()方法:

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  Cache cache = ms.getCache();
  if (cache != null) {
    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.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

方法最开始的:

Cache cache = ms.getCache();

先获得cache对象,cache不为空说明配置了二级缓存,后面的逻辑就是,如果有二级缓存,则考虑从二级缓存中取值,否则就查数据库。

于是我们看到:MyBatis实际上是先访问二级缓存再访问一级缓存的

 

下面看看从二级缓存中获取数据的逻辑,最开始调用了一个刷缓存的方法:

flushCacheIfRequired(ms);

方法的内容是这样的:

private void flushCacheIfRequired(MappedStatement ms) {
  Cache cache = ms.getCache();
  if (cache != null && ms.isFlushCacheRequired()) {      
    tcm.clear(cache);
  }
}

返回值取决于MappedStatement的isFlushCacheRequired()方法,这个方法的返回值来自MyBatis的xml配置文件,在方法节点中可以配置flushCache="true"参数,这个参数在<select>节点中默认是false,即不清空二级缓存,在<insert>、<update>、<delete>节点中默认是true,即数据更新操作会清空二级缓存。

方法中的tcm对象是CachingExecutor中的TransactionalCacheManager属性,这是CachingExecutor的二级缓存管理器。

 

继续看二级缓存的逻辑,在后面的代码中可以看到,并不是存在二级缓存就一定会用的,还有其他的条件,比如ms.isUseCache()==true。MappedStatement的isUseCache()方法的返回值来自MyBatis的xml配置文件,在<select>节点中可以配置useCache="true",不过这个参数默认就是true。

 

继续,如果<select>节点中配置了useCache="true",可以准备使用二级缓存了,不过在此之前还有一层判断:

ensureNoOutParams(ms, boundSql);

这是一个参数类别的判断:

private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
      if (parameterMapping.getMode() != ParameterMode.IN) {
        throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
      }
    }
  }
}

意思是开通了二级缓存的CALLABLE类型的sql中,参数类别必须是IN类型,OUT和INOUT不行,否则会抛异常。异常中说,缓存的存储过程不能支持OUT类型的参数,请把useCache配成false。

也就是说,只要参数中有IN类型就直接不让用二级缓存

 

再往后就是从二级缓存中拿结果了,也就是这段:

List<E> list = (List<E>) tcm.getObject(cache, key);

tcm是TransactionalCacheManager的对象,这是MyBatis的CachingExecutor的二级缓存管理器,缓存保存在其中的一个HashMap属性里:

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

这个Map的key就是之前生成的CachingKey,而他的value由TransactionalCache实现,这是某个具体的二级缓存对象。

tcm.getObject()方法体现了一些二级缓存的设计思路:

public Object getObject(Cache cache, CacheKey key) {
  return getTransactionalCache(cache).getObject(key);
}

getTransactionalCache()方法就是获得TransactionalCache对象,然后调用他的getObject()方法:

@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;
  }
}

注意到其中有一个entriesMissedInCache对象,是个Set类型,存储了二级缓存之前没能覆盖到的缓存key(注意,只是key),这个key会在TransactionalCache的commit()方法时保存到二级缓存中,但是保存的value是个null,作用不太清楚,有网友指出这是为了防止缓存穿透。

如果没有拿到缓存信息,则需要调用:

list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

进行查询,这个查询和没有使用二级缓存时的查询是一样的。

然后调用tcm.putObject()方法,注意,tcm.putObject()方法并不会把查询结果直接放入二级缓存,而是放入entriesToAddOnCommit这个Map,看看这个putObject()方法:

public void putObject(Cache cache, CacheKey key, Object value) {
  getTransactionalCache(cache).putObject(key, value);
}

getTransactionalCache()方法获得的是一个具体的二级缓存对象,由TransactionalCache类实现,然后调用了他的putObject()方法:

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

可见,查询结果保存在了entriesToAddOnCommit对象中,entriesToAddOnCommit是TransactionalCache中维护的一个HashMap,相当于二级缓存的缓存,这个HashMap中的内容只有在调用TransactionalCache的commit()方法时才会正式放入二级缓存。这种事务提交之后才写入二级缓存的操作能避免脏读。

 

二级缓存的使用到此结束。

 

然后就是没有开通二级缓存的情况,CachingExecutor的query方法调用的也是:

return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

和二级缓存一样的方法,这里的delegate是CachingExecutor封装的SimpleExecutor对象,他的query()方法在父类BaseExecutor中:

@SuppressWarnings("unchecked")
@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 {
      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;
}

重点关注一下try{}代码块中的内容,queryStack是计数器,用来给嵌套查询用的,queryStack=0的时候才算是最上层查询结束。

然后是从一级缓存中取查询结果,也就是这一段:

list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;

可见,一级缓存是从BaseExecutor的localCache对象中获取的,key就是之前生成的CacheKey对象。

 

继续看代码,如果一级缓存中保存了缓存信息,下面调用了这样一个方法:

handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

这个方法是专门给CALLABLE类型sql用的,代码如下;

private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    final Object cachedParameter = localOutputParameterCache.getObject(key);
    if (cachedParameter != null && parameter != null) {
      final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
      final MetaObject metaParameter = configuration.newMetaObject(parameter);
      for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
        if (parameterMapping.getMode() != ParameterMode.IN) {
          final String parameterName = parameterMapping.getProperty();
          final Object cachedValue = metaCachedParameter.getValue(parameterName);
          metaParameter.setValue(parameterName, cachedValue);
        }
      }
    }
  }
}

从方法名字就能看出来,这个方法是处理存储过程的输出参数(OUT和INOUT)用的。

方法的大概逻辑就是,轮询了sql中的参数列表,筛选出其中不是IN类型的参数(也就是OUT和INOUT类型),缓存了这些参数被存储过程影响后的结果。

 

以上便是一级缓存中有值的情况,下面看看一级缓存中没值的场景,也就是:

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

这段代码,从数据库中获取list,queryFromDatabase()方法代码如下:

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;
}

我们看到,一开始调用:

localCache.putObject(key, EXECUTION_PLACEHOLDER);

这个方法,在一级缓存中添加了对应key的一个占位符。没看明白这么做的目的。

后面就是调用doQuery()方法查询数据库,这个方法里没有和缓存相关的逻辑,就是在查数据库。

在finally{}代码块中,又把刚刚的占位符给删了……

接着又重新往一级缓存localCache中添加了正确的查询结果。

然后不能忘了CALLABLE要用的localOutputParameterCache缓存,不过这里缓存的不是查询结果,而是查询的参数,因为OUT和INOUT类型的参数值会被存储过程影响。

 

做完这一切,数据的查询就完成了,我们还可以看看query()方法中的其他逻辑,比如代码中有两个地方调用了clearLocalCache()这个方法,这个方法用于清空一级缓存,方法代码是:

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

很简单,把BaseExecutor中的两个PerpetualCache缓存全部清空。

 

第一个清空一级缓存的地方是:

if (queryStack == 0 && ms.isFlushCacheRequired()) {
  clearLocalCache();
}

也就是说,MappedStatement的isFlushCacheRequired()方法返回true时清空一级缓存,二级缓存的逻辑里面也是用这个参数来清空的。

这个方法的返回值来自MyBatis的xml配置文件,在方法节点中可以配置flushCache="true"参数,这个参数在<select>节点中默认是false,即不清空二级缓存,在<insert>、<update>、<delete>节点中默认是true。

第二个清空缓存的地方就是:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
  // issue #482
  clearLocalCache();
}

configuration的LocalCacheScope参数是一级缓存的作用域,作用域是LocalCacheScope.STATEMENT时表示每次查询都清空一级缓存,与之相对的是LocalCacheScope.SESSION。

 

sql的执行过程就到此为止。

 

四,sqlSession的提交

再一次回到SqlSessionInterceptor的invoke()方法:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

    Object unwrapped;
    try {
        Object result = method.invoke(sqlSession, args);
        if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
            sqlSession.commit(true);
        }

        unwrapped = result;
    } catch (Throwable var11) {
        unwrapped = ExceptionUtil.unwrapThrowable(var11);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
            SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            sqlSession = null;
            Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
            if (translated != null) {
                unwrapped = translated;
            }
        }

        throw (Throwable)unwrapped;
    } finally {
        if (sqlSession != null) {
            SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }

    }

    return unwrapped;
}

在上一节中已经调用method.invoke()方法查到了结果,这一节将调用session的commit()方法,也就是这一段:

if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
    sqlSession.commit(true);
}

if判断中的条件是当前是否开启了MyBatis事务,如果没开启事务则会调用sqlSession的commit()方法,参数传的是true。

对于commit()方法来说如果参数是true则会提交或回滚事务,但是这里的调用和事务没什么关系,貌似只是为了清空一级缓存。

此处sqlSession对象由DefaultSqlSession类实现,他的commit()方法代码如下:

@Override
public void commit(boolean force) {
  try {
    executor.commit(isCommitOrRollbackRequired(force));
    dirty = false;
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

其中调用了executor的commit()方法,传入的参数代表是否需要提交事务。

dirty参数貌似是判断是否是脏数据用的,commit()之后就不是脏数据了。

如果使用了插件,这个executor就是一个Plugin的动态代理,所以这里的executor.commit()会调用Plugin的invoke方法:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
      return interceptor.intercept(new Invocation(target, method, args));
    }
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

其中的signatureMap中是没有commit()这个方法的,所以会调用最后的method.invoke()方法,参数中的target就是生成Plugin代理时定义的CachingExecutor对象。

如果没用插件,executor就是CachingExecutor对象。

 

CachingExecutor的commit()方法是这样的:

@Override
public void commit(boolean required) throws SQLException {
  delegate.commit(required);
  tcm.commit();
}

分为两部分,一部分是delegate的commit(),用于处理一级缓存,另一部分是tcm的commit(),用于处理二级缓存

delegate对象就是生成CachingExecutor时传入的SimpleExecutor对象,他的commit()方法在父类BaseExecutor中:

@Override
public void commit(boolean required) throws SQLException {
  if (closed) {
    throw new ExecutorException("Cannot commit, transaction is already closed");
  }
  clearLocalCache();
  flushStatements();
  if (required) {
    transaction.commit();
  }
}

其中的clearLocalCache()上面看过了,清空一级缓存。

而flushStatements()这个方法没有做什么,这个方法返回的是一个空的List。

required如果是true则会提交事务,但是这里的调用和事务没什么关系。

于是我们发现:只要没开事务,一级缓存基本就用不上,每次查询之后都会给清除掉

 

然后是tcm的commit()方法,tcm是TransactionalCacheManager的对象,是二级缓存管理器,他的commit()方法是这样的:

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

其中调用的是TransactionalCache的commit()方法:

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

这个方法涉及二级缓存的一点设计机制,TransactionalCache中有clearOnCommit变量,如果在查询时clearOnCommit被设为true,那么调用commit()方法时将会清除二级缓存。

TransactionalCache中有entriesToAddOnCommit属性,是一个Map,相当于二级缓存的缓存,如果里面有值,那么调用commit()方法时将会把entriesToAddOnCommit的值放入二级缓存,也就是flushPendingEntries()方法的逻辑:

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);
    }
  }
}

这个方法还把之前保存的entriesMissedInCache这个Set存入了二级缓存,存的值是null。可能有防止缓存穿透的效果。

然后调用reset方法:

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

初始化clearOnCommit和entriesToAddOnCommit等参数。

于是commit()方法结束。

从上面的逻辑可以看到,Session提交时,一级缓存会被清除(无事务),而二级缓存会从entriesToAddOnCommit写入

 

本文完

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值