深入理解 Mybatis - Executor

承接上篇博客,本文探究MyBatis中的Executor,如下图: 是Executor体系图

executor体系图

本片博客的目的就是探究如上图中从顶级接口Executor中拓展出来的各个子执行器的功能,以及进一步了解Mybatis的一级缓存和二级缓存

预览:

  • BaseExecutor :实现了Executor的全部方法,包括对缓存,事务,连接提供了一系列的模板方法,这些模板方法中留出来了四个抽象的方法等待子类去实现如下
protected abstract int doUpdate(MappedStatement ms,Object parameter)
 throws SQLException;

protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
 throws SQLException;

protected abstract <E> List<E> doQuery(MappedStatement ms,Object parameter,RowBounds rowBounds,ResultHandler resultHandler,BoundSql boundSql)
 throws SQLException;

protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms,BoundSql boundSql)
 throws SQLException;
  • SimpleExecutor: 特点是每次执行完毕后都会将创建出来的statement关闭掉,他也是默认的执行器类型
  • ReuseExecutor: 在它在本地维护了一个容器,用来存放针对每条sql创建出来的statement,下次执行相同的sql时,会先检查容器中是否存在相同的sql,如果存在就使用现成的,不再重复获取
  • BatchExecutor: 特点是进行批量修改,她会将修改操作记录在本地,等待程序触发提交事务,或者是触发下一次查询时,批量执行修改

创建执行器

当我们通过SqlSessionFactory创建一个SqlSession时,执行openSessionFromDataBase()方法时,会通过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;
  }

通过这个函数,可以找到上面列举出来的所有的 执行器,MyBatis默认创建的执行器的类型的是SimpleExecutor,而且MyBatis默认开启着对mapper的缓存(这其实就是Mybatis的二级缓存,但是,不论是注解版,还是xml版,都需要添加额外的配置才能使添加这个额外配置的mapper享受二级缓存,二级缓存被这个CachingExecutor维护着)

BaseExecutor 的模板方法

在BaseExecutor的模本方法之前,其实省略了很多步骤,我们上一篇博文中有详细的叙述,感兴趣可以去看看,下面我就简述一下: 程序员使用获取到了mapper的代理对象,调用对象的findAll(),另外获取到的sqlSession的实现也是默认的实现DefaultSqlSession,这个sqlSession通过Executor尝试去执行方法,哪个Executor呢? 就是我们当前要说的CachingExecutor,调用它的query(),这个方法是个模板方法,因为CachingExecutor只知道在什么时间改做什么,但是具体怎么做,谁取做取决于它的实现类

如下是BaseExecutorquery()方法

  @Override
  public <E> List<E> query(MappedStatement ms,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,rowBounds,resultHandler,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;
  }

BaseExecutor维护的一级缓存

从上面的代码中,其实我们就跟传说中的Mybatis的一级缓存无限接近了,上面代码中的逻辑很清楚,就是先检查是否存在一级缓存,如果存在的话,就不再去创建statement查询数据库了

那问题来了,什么是这个一级缓存呢? **一级缓存就是上面代码中的localCache,如下图: **

一级缓存

再详细一点就看下面这张图:

一级缓存

嗯! 原来传说中的一级缓存叫localCache,它的封装类叫PerpetualCache 里面维护了一个String 类型的id,和一个hashMap 取名字也很讲究,perpetual意味永不间断,事实上确实如此,一级缓存默认存在,也关不了(至少我真的不知道),但是在与Spring整合时,Spring把这个缓存给关了,这并不奇怪,因为spring 直接干掉了这个sqlSession

一级缓存什么时候被填充的值呢?填充值的操作在一个叫做queryFromDataBase()的方法里面,我截图如下:

填充一级缓存

其中的key=1814536652:3224182340:com.changwu.dao.IUserDao.findAll:0:2147483647:select * from user:mysql

其实看到这里,平时听到的为什么大家会说一级缓存是属于SqlSession的啊,诸如此类的话就是从这个看源码的过程中的出来的结果,如果你觉的印象不深刻,我就接着补刀,每次和数据库打交道都的先创建sqlSession,创建sqlSession的方法会在创建出DefaultSqlSession之前,先为它创建一个Executor,而我们说的一级缓存就是这个Executor的属性

何时清空一级缓存

清空一级缓存的方法就是BaseExecutorupdate()方法

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

SimpleExecutor

SimpleExecutor是MyBatis提供的默认的执行器,他里面封装了MyBatis对JDBC的操作,但是虽然他叫XXXExecutor,但是真正去CRUD的还真不是SimpleExecutor,先看一下它是如何重写BaseExecutordoQuery()方法的

详细的过程在这篇博文中我就不往外贴代码了,因为我在上一篇博文中有这块源码的详细追踪

  @Override
 public <E> List<E> doQuery(MappedStatement ms,BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper,ms,boundSql);
      stmt = prepareStatement(handler,ms.getStatementLog());
      return handler.query(stmt,resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

创建StatementHandler

  public StatementHandler newStatementHandler(Executor executor,MappedStatement mappedStatement,Object parameterObject,BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor,mappedStatement,parameterObject,boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

虽然表面上看上面的代码,感觉它只会创建一个叫RoutingStatementHandler的handler,但是其实上这里面有个秘密,根据MappedStatement 的不同,实际上他会创建三种不同类型的处理器,如下:

  public RoutingStatementHandler(Executor executor,MappedStatement ms,BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        // 早期的普通查询,极其容易被sql注入,不安全
        delegate = new SimpleStatementHandler(executor,boundSql);
        break;
      case PREPARED:
       //  处理预编译类型的sql语句
        delegate = new PreparedStatementHandler(executor,boundSql);
        break;
      case CALLABLE:
       // 处理存储过程语句
        delegate = new CallableStatementHandler(executor,boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

创建PreParedStatement

点击进入上篇博文,查看如何创建PreparedStatement

执行查询

点击进入上篇博文,里面有记录如何执行查询

关闭连接

关于SimpleExecutor如何关闭statement,在上面一开始介绍SimpleExecutor时,我其实就贴出来了,下面再这个叫做closeStatement()的函数详情贴出来

  protected void closeStatement(Statement statement) {
    if (statement != null) {
      try {
        statement.close();
      } catch (SQLException e) {
        // ignore
      }
    }
  }

ReuseExecutor

这个ReuseExecutor相对于SimpleExecutor来说,不同点就是它先来的对Statement的复用,换句话说,某条Sql对应的Statement创建出来后被放在容器中保存起来,再有使用这个statement的地方就是容器中拿就行了

他是怎么实现的呢? 看看下面的代码就知道了

public class ReuseExecutor extends BaseExecutor {
    private final Map<String,Statement> statementMap = new HashMap();

    public ReuseExecutor(Configuration configuration,Transaction transaction) {
        super(configuration,transaction);
    }

嗯! 所谓的容器,不过是一个叫statementMap的HashMap而已

下一个问题: 这个容器什么时候派上用场呢? 看看下面的代码也就知道了--this.hasStatementFor(sql)

    private Statement prepareStatement(StatementHandler handler,Log statementLog) throws SQLException {
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();
        Statement stmt;
        if (this.hasStatementFor(sql)) {
            stmt = this.getStatement(sql);
            this.applyTransactionTimeout(stmt);
        } else {
            Connection connection = this.getConnection(statementLog);
            stmt = handler.prepare(connection,this.transaction.getTimeout());
            this.putStatement(sql,stmt);
        }

        handler.parameterize(stmt);
        return stmt;
    }

最后一点: 当MyBatis知道发生了事务的提交,回滚等操作时,ReuseExecutor会批量关闭容器中的Statement

BatchExecutor

这个执行器相对于SimpleExecutor的特点是,它的update()方法是批量执行的

执行器提交或回滚事务时会调用 doFlushStatements,从而批量执行提交的 sql 语句并最终批量关闭 statement 对象。

CachingExecutor与二级缓存

首先来说,这个CachingExecutor是什么? 那就得看一下的属性,如下:

public class CachingExecutor implements Executor {
  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

让我们回想一下他的创建时机,没错就是在每次创建一个新的SqlSession时创建出来的,源码如下,这就出现了一个惊天的大问号!!!,一级缓存和二级缓存为啥就一个属于SqlSession级别,另一个却被所有的SqlSession共享了? 这不是开玩笑呢? 我当时确实也是真的蒙,为啥他俩都是随时用随时new,包括上面代码中的TransactionalCacheManager也是随时用随时new,凭什么它维护的二级缓存就这么牛? SqlSession挂掉后一级缓存也跟着挂掉,凭什么二级缓存还在呢?

  public Executor newExecutor(Transaction transaction,transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

先说一下,我是看到哪行代码后意识到二级缓存是这么特殊的,如下:大家也看到了,下面代码中的tcm.getObject(cache,key);,是我们上面新创建出来的TransactionalCacheManager,然后通过这个空白的对象的getObject()竟然就将缓存中的对象给获取出来了,(我当时忽略了入参位置的cache,当然现在看,满眼都是这个cache)

  public <E> List<E> query(MappedStatement ms,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.query(ms,boundSql);
          tcm.putObject(cache,list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms,boundSql);
  }

我当时出现这个问题完全是我忽略了一部分前面解析配置文件部分的源码,下面我带大家看看这部分源码是怎么执行的

一开始MyBatis会创建一个XMLConfigBuilder用这个builder去解析配置文件(因为我们环境是单一的MyBatis,并没有和其他框架整,这个builder就是用来解析配置文件的)

我们关注什么呢? 我们关注的是这个builder解析<mapper>标签的,源码入下:

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      ...
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));

关注这个方法中的configuration.addMapper(mapperInterface);方法,如下: 这里面存在一个对象叫做,MapperRegistry,这个对象叫做mapper的注册器,其实我觉得这是个需要记住的对象,因为它出现的频率还是挺多的,它干什么工作呢? 顾名思义,解析mapper呗 我的当前是基于注解搭建的环境,于是它这个MapperRegistry为我的mapper生成的对象就叫MapperAnnotationBuilder见名知意,这是个基于注解的构建器

 public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

所以说我们就得去看看这个解析注解版本mapper的builder,到底是如何解析我提供的mapper的,源码如下:

  public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this,method));
        }
      }
    }

方法千千万,但是我关注的是它的 parseCache();方法,为什么我知道来这里呢? (我靠!,我找了老半天...)

接下来就进入了一个高潮,相信你看到下面的代码也会激动,为什么激动呢? 因为我们发现了Mybatis处理@CacheNamespace注解的细节信息

private void parseCache() {
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
      Properties props = convertToProperties(cacheDomain.properties());
      assistant.useNewCache(cacheDomain.implementation(),cacheDomain.eviction(),flushInterval,size,cacheDomain.readWrite(),cacheDomain.blocking(),props);
    }
  }

再往下跟进这个 assistant.useNewCache()方法,就会发现,MyBatis将创建出来的一个Cache对象,这个Cache的实现类叫BlockingCache

创建出来的对象给谁了?

  • Configuration对象自己留了一份 (放在了 caches = new StrictMap<>("Caches collection");中)
  • 当前类MapperBuilderAssistant也保留一了一份
  • 最主要的是MappedStatement对象中也保留了一份mappedStatement.cache

说了这么多了,附上一张图,用来纪念创建这个Cache的成员

创建二级缓存Cache体系

小结

其实上面创建这个Cache对象才是二级缓存者,前面说的那个CachingExecutor中的TransactionalCacheManager不过是拥有从这个Cache中获取数据的能力而已

我有调试他是如何从Cache中获取出缓存,事实证明,二级缓存中存放的不是对象,而是被序列化后存储的数据,需要反序列化出来

下图是Mybatis反序列化数据到新创建的对象中的截图

反序列化

下图是TransactionalCacheManager是如何从Cache中获取数据的调用栈的截图

从caching中获取数据调用栈

二级缓存与一级缓存的互斥性

第一点: 通过以上代码的调用顺序也能看出,二级缓存在一级缓存之前优先被执行,也就是说二级缓存不存在,则查询一级缓存,一级缓存再不存在,就查询DB

第二点: 就是说,对于二级缓存来说,无论我们有没有开启事务的自动提交功能,都必须手动commit()二级缓存才能生效,否则二级缓存是没有任何效果的

第三点: CachingExecutor 提交事务时的源码如下:

  @Override
  public void commit(boolean required) throws SQLException {
    // 代理执行器提交
    delegate.commit(required);
    // 事务缓存管理器提交
    tcm.commit();
  }

这就意味着,TransactionalCacheManager和BaseExecutor的实现类的事务都会被提交

为什么说二级缓存和以及缓存互斥呢?可以看看BaseExecutor的源码中commit()如下: 怎么样? 够互斥吧,一个不commit()就不生效,commit()完事把一级缓存干掉了

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

到这里本文又行将结束了,总体的节奏还是挺欢快挺带劲的,我是bloger-赐我白日梦,如果有错误欢迎指出,也欢迎您点赞支持...

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


1.pom.xml引入依赖 &lt;dependency&gt; &lt;groupId&gt;com.github.pagehelper&lt;/groupId&gt; &lt;artifactId&gt;pagehelper&lt;/artifactId&gt; &lt;version&gt;5
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt; &lt;!DOCTYPE configuration PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot; &qu
准备工作 ① 创建数据库&amp;数据表 ## 创建数据库 CREATE DATABASE `dbtest1`; ## 创建数据表 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(20) DEF
MyBatis逆向工程是指根据数据库表结构自动生成对应的实体类、Mapper接口以及SQL映射文件的过程。这个过程可以通过MyBatis提供的逆向工程工具来完成,极大地方便了开发人员,避免了重复的代码编写,提高了开发效率。 创建逆向工程的步骤 1、添加依赖&amp;插件 &lt;!-- 控制Mave
MyBatis获取参数值的两种方式:${}和#{} ${}的本质就是字符串拼接,#{}的本质就是占位符赋值。 ${}使用字符串拼接的方式拼接sql,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号;但是#{}使用占位符赋值的方式拼接sql,此时为字符串类型或日期类型的字段进行赋值时,可以自
resultMap作用是处理数据表中字段与java实体类中属性的映射关系。 准备工作 ① 创建数据库&amp;数据表 CREATE DATABASE `dbtest1`; CREATE TABLE `t_emp` ( `emp_id` int NOT NULL AUTO_INCREMENT, `em
EHCache缓存针对于MyBatis的二级缓存。 MyBatis默认二级缓存是SqlSessionFactory级别的。 添加依赖 &lt;!-- MyBatis-EHCache整合包 --&gt; &lt;dependency&gt; &lt;groupId&gt;org.mybatis.cac
MyBatis 提供了一级缓存和二级缓存的支持,用于提高数据库查询的性能,减少不必要的数据库访问。 一级缓存(SqlSession 级别的缓存) 一级缓存是 MyBatis 中最细粒度的缓存,也称为本地缓存。它存在于每个 SqlSession 的生命周期中,当 SqlSession 被关闭或清空时,
动态SQL是 MyBatis 中非常强大且灵活的功能,允许你根据不同的条件构建SQL查询。 这主要通过 &lt;if&gt;、&lt;choose&gt;、&lt;when&gt;、&lt;otherwise&gt;、&lt;foreach&gt;等标签实现。 查询场景 /** * 根据条件查询员工
本教程操作系统:windows10系统、DELL G3电脑。 MyBatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。在 MyBatis 中,配置数据库连接是非常重要的第一步。下面将详细介绍如何配置 MyBatis 的
今天小编给大家分享的是MyBatis批量查询、插入、更新、删除如何实现,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。
今天小编给大家分享的是Mybatis操作多数据源实现的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获...
本篇文章和大家了解一下mybatis集成到spring的方式有哪些。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。1 前言1.1 集成spring前使用mybat...
今天小编给大家分享的是mybatis-plus分页查询的3种方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获...
本篇内容主要讲解“mybatis之BaseTypeHandler怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“mybatis...
这篇文章主要介绍了mybatisforeach怎么传两个参数的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇mybatisforeach怎...
这篇“MyBatis映射文件中parameterType与resultType怎么使用”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的...
这篇文章主要介绍“MyBatis怎么获取自动生成的键值”,在日常操作中,相信很多人在MyBatis怎么获取自动生成的键值问题上存在疑惑,小编查阅了各式资料,整理出
这篇文章主要讲解了“怎么去掉IntelliJIDEA中mybatis对应的xml文件警告”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入...
这篇文章主要介绍“MybatisPlus使用@TableId主键id自增长无效如何解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这...