• java架构师学习路线-Mybatis一级缓存源码分析


    图灵学院  java架构师学习路线

    一. 为什么要有一级缓存

        每当我们使用Mybatis开启一次和数据库的会话, 就会创建一个SqlSession对象来表示这个会话。就在这一次会话中, 我们有可能反复执行完全相同的查询语句, 这些相同的查询语句在没有执行过更新的情况下返回的结果也是一致的。相信机智的你已经想到, 如果每次都去和数据库进行交互查询的话, 就会造成资源浪费。 所以, mybatis加入了一级缓存, 用来在一次会话中缓存查询结果。

        总结下一级缓存的存在起到的作用: 在同一个会话里面,多次执行相同的sql语句(statementId, 参数, rowbounds完全相同),会直接从内存取到缓存的结果,不会再发送sql到数据库与数据库交互。但是不同的会话里面,即使执行的sql一模一样,也不能使用到一级缓存。

    二. 一级缓存与会话的关系

    一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话层面(SqlSession)进行缓存的。默认开启,不需要任何的配置。

    首先我们先思考一个问题,在MyBatis 执行的流程里面,涉及到这么多的对象,那么缓存Cache 应该放在哪个对象里面去维护?

    先来进行一下推断, 我们已经知道一级缓存的作用范围是会话,那么这个对象肯定是在SqlSession 里面创建的,作为SqlSession 的一个属性存在。SqlSession本身是一个接口, 它的实现类DefaultSqlSession 里面只有两个属性---Configuration和Executor。Configuration 是全局的,与我们知道的一级缓存的作用范围不符, 所以缓存只可能放在Executor 里面维护---而事实也正是如此, SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中就持有了Cache。

    那到底是不是这样的呢....关门, 放源码! 

    (1)创建会话的源码部分:

    首先是调用DefauldSqlSessionFactory的openSession()方法, 即:开启会话

    openSession()方法中调用了openSessionFromDataSource()方法, openSessionFromDataSource()方法中先是调用 configuration.newExecutor(tx, execType)创建了执行器(executor), 然后调用DefaultSqlSession的构造方法, 并传入了创建好的执行器(executor), 这样就创建出了DefaultSqlSession对象并让其持有了executor属性。

    DefauldSqlSessionFactory:

    //创建会话的方法

    @Override

    public SqlSession openSession(ExecutorType execType) {

      return openSessionFromDataSource(execType, 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执行器

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

        // 注意: 看这里!!创建DefaultSqlSession, executor作为DefaultSqlSession构造方法的一个参数传入

        //DefaultSqlSession持有了executor

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

      }

    }

    (2)创建执行部分源码

    而创建执行器的时候, 会根据具体传入的执行器(executor)的类型, 来选择一个合适的执行器(executor)创建出来。但是不管最终选择哪个执行器, 他们都是BaseExecutor的子类 (缓存执行器除外, 涉及二级缓存相关, 这里暂且不提, 会专门写二级缓存的文章), 而我们的一级缓存, 正是BaseExecutor的一个属性, 而创建好的执行器作为BaseExecutor的子类也有着父类的属性。所以SqlSession对象持有了executor属性, 而executor持有了一级缓存。我们之前的一级缓存与会话的关系也得到了印证。

    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;

    }

    看下BaseExecutor的属性, 它持有了PerpetualCache, 也就是一级缓存。

    public abstract class BaseExecutor implements Executor:

      protected PerpetualCache localCache;

    既然PerpetualCache就是一级缓存了, 我们现在就来康康一级缓存到底是个啥吧, 没错, 最终这些东西都存在了一个HashMap里面。

    public class PerpetualCache implements Cache {

      private final String id;

      //一级缓存最终存入容器

      private Map

      public PerpetualCache(String id) {

        this.id = id;

      }

      @Override

      public String getId() {

        return id;

      }

      @Override

      public int getSize() {

        return cache.size();

      }

      @Override

      public void putObject(Object key, Object value) {

        cache.put(key, value);

      }

      @Override

      public Object getObject(Object key) {

        return cache.get(key);

      }

      @Override

      public Object removeObject(Object key) {

        return cache.remove(key);

      }

      @Override

      public void clear() {

        cache.clear();

      }

    }

    三. 一级缓存的生命周期

    当会话结束时,SqlSession对象及其内部的Executor对象还有Cache对象也一并释放掉。

    如果SqlSession调用了close()方法,会释放掉一级缓存Cache对象,一级缓存将不可用;

    如果SqlSession调用了clearCache(),会清空Cache对象中的数据,但是该对象仍可使用;

    SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空Cache对象的数据,但是该对象可以继续使用;

    四. 一级缓存的执行流程概要 

        缓存执行的大致思路与我们熟知的缓存思想一致。

    对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果

    判断从Cache中根据特定的key值取的数据是否为空,即是否命中;

    如果命中,则直接将缓存结果返回;

    如果没命中:

         去数据库中查询数据,得到查询结果;

    将key和查询到的结果分别作为key,value对存储到Cache中;

    将查询结果返回;

    具体是怎么实现的呢, 这里又要放源码了:

    (1)查询入口:

    可以看见, 查询最终是调用了DefaultSqlSession持有的属性executor的query()方法。

    DefaultSqlSession: 

    private final Configuration configuration;

    private final Executor executor;

    public

       try {

          //根据传入的statementId,获取MappedStatement对象

          MappedStatement ms = configuration.getMappedStatement(statement);

          //RowBounds是用来逻辑分页(按照条件将数据从数据库查询到内存中,在内存中进行分页)

          // wrapCollection(parameter)是用来装饰集合或者数组参数

          // 注意:看这里 !! 调用执行器的查询方法

          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()方法进行了一级缓存的逻辑, 会调用localCache.getObject(key)从缓存中获取数据, 如果获取不到, 又会调用queryFromDatabase()方法。见名知意, 这个方法就是用来来与数据库进行交互取数据。

    在queryFromDatabase()方法中, 调用doQuery()来执行查询, 再把得到的结果调用localCache.putObject(key, list)放入一级缓存。

    如果我们继续查看doQuery()方法, 就会发现这个方法是抽象的, 这里涉及到了一个常用的设计模式: 模板模式。真正的doQuery()方法的实现是在BaseExecutor的子类方法中去完成的, 完成从数据库中查询数据封装数据的部分, 暂且不提。

    模板模式(Template Pattern): 一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

    BaseExcecutor:

    protected PerpetualCache localCache;

    Override

    public

      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

      try {

        queryStack++;

        // 注意: 看这里!!从一级缓存中获取数据

        list = resultHandler == null ? (List

        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;

    }

    private

      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;

    }

    // 注意: 看这里!! 这是一个抽象的方法, 等着子类去实现

    protected abstract

        throws SQLException;

    五. 结构与总计(不涉及二级缓存)

    小结: sqlSession 持有 BaseExecutor , BaseExecutor持有了一级缓存, 查询时调用BaseExecutor的query()方法, 并在query()方法中完成了一级缓存的功能。

    缓存查到了就返回查询结果, 查询不到就调用queryFromDatabase()方法, 然后queryFromDatabase()方法中调用doQuery()方法从数据库中查询数据, 然后放入一级缓存, 其中doQuery()方法是抽象的 , 需要BaseExecutor的不同类型子类具体实现。

    整体结构图如下:

    java高级架构师教程,java架构师培训,图灵学院

    怎么样, 现在对mybatis一级缓存是如何实现的是不是有了大概的了解~

    尽管Java架构师学习路线已经分享给大家,但有多少人能认真的去践行,这个就难说了。互联网寒冬已经到来,作为程序员,更应在此时提高自己,有着更高远的追求。

    篇幅有限,如果需要更详细的java架构师学习路线资料可加博主qq:1993712276,或者去图灵官网查看

  • 相关阅读:
    vue项目接入百度地图
    angularJS 十六进制与字符串相互转换
    angular项目实现mqtt的订阅与发布 ngx-mqtt
    消息中间件MQTT
    Zigbee 与 WiFi 的区别
    angular6 路由拼接查询参数如 ?id=1 并获取url参数
    SpringBoot拦截器
    SpringBoot定时任务
    SpringBoot 各层之间的关系
    百度离线地图 —— 瓦片地图下载
  • 原文地址:https://www.cnblogs.com/tulingxueyuan/p/13604387.html
Copyright © 2020-2023  润新知