栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

二,MyBatis体系结构与工作原理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

二,MyBatis体系结构与工作原理

1.MyBatis应用分析与实践
2.MyBatis体系结构与工作原理
3.MyBatis插件原理及Spring集成
4.手写自己的MyBatis框架
本节目标:

1、 掌握 MyBatis 的工作流程

2、 掌握 MyBatis 的架构分层与模块划分

3、 掌握 MyBatis 缓存机制

4、 通过阅读 MyBatis 源码掌握 MyBatis 底层工作原理与设计思想

一,MyBatis 的工作流程分析

在上一篇《应用分析与实践》里面,我们学习了 MyBatis 的编程式使用的方法,我们再来回顾一下 MyBatis 的主要工作流程:

首先在 MyBatis 启动的时候我们要去解析配置文件,包括全局配置文件和映射器 配置文件,这里面包含了我们怎么控制 MyBatis 的行为,和我们要对数据库下达的指令, 也就是我们的 SQL 信息。我们会把它们解析成一个 Configuration 对象。
接下来就是我们操作数据库的接口,它在应用程序和数据库中间,代表我们跟数据库之间的一次连接:这个就是 SqlSession 对象。
我们要获得一个会话 , 必须有一个会话工厂 SqlSessionFactory 。 SqlSessionFactory 里面又必须包含我们的所有的配置信息,所以我们会通过一个 Builder 来创建工厂类。
我们知道,MyBatis 是对 JDBC 的封装,也就是意味着底层一定会出现 JDBC 的一 些核心对象,比如执行 SQL 的 Statement,结果集 ResultSet。在 Mybatis 里面, SqlSession 只是提供给应用的一个接口,还不是 SQL 的真正的执行对象。

我们上次提到了,SqlSession 持有了一个 Executor 对象,用来封装对数据库的操作。

在执行器 Executor 执行 query 或者 update 操作的时候我们创建一系列的对象, 来处理参数、执行 SQL、处理结果集,这里我们把它简化成一个对象:StatementHandler, 在阅读源码的时候我们再去了解还有什么其他的对象。

这个就是 MyBatis 主要的工作流程,如图:

二,MyBatis 架构分层与模块划分

在 MyBatis 的主要工作流程里面,不同的功能是由很多不同的类协作完成的,它们分布在MyBatis jar 包的不同的 package 里面。

我们来看一下 MyBatis 的 jar 包(基于 3.5.6),jar 包结构是这样的(22 个包):

大概有 300 多个类,这样看起来不够清楚,不知道什么类在什么环节工作,属于什么层次。跟 Spring 一样,MyBatis 按照功能职责的不同,所有的 package 可以分成不同的工作层次。我们可以把 MyBatis 的工作流程类比成餐厅的服务流程:

  • 第一个是跟客户打交道的服务员,它是用来接收程序的工作指令的,我们把它叫做接口层。
  • 第二个是后台的厨师,他们根据客户的点菜单,把原材料加工成成品,然后传到窗口。这一层是真正去操作数据的,我们把它叫做核心层。
  • 最后就是餐厅也需要有人做后勤(比如清洁、采购、财务),来支持厨师的工作和整个餐厅的运营。我们把它叫做基础层。

来看一下这张图,我们根据刚才的分层,和大体的执行流程,做了这么一个总结。 当然,从不同的角度来描述,架构图的划分有所区别,这张图画起来也有很多形式。我们先从总体上建立一个印象。每一层的主要对象和主要的功能我们也给大家分析一下。

接口层

首先接口层是我们打交道最多的。核心对象是 SqlSession,它是上层应用和 MyBatis 打交道的桥梁,SqlSession 上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。

核心处理层

接下来是核心处理层。既然叫核心处理层,也就是跟数据库操作相关的动作都是在 这一层完成的。核心处理层主要做了这几件事:

  1. 把接口中传入的参数解析并且映射成 JDBC 类型;

  2. 解析 xml 文件中的 SQL 语句,包括插入参数,和动态 SQL 的生成;

  3. 执行 SQL 语句;

  4. 处理结果集,并映射成 Java 对象。

插件也属于核心层,这是由它的工作方式和拦截的对象决定的。

基础支持层

最后一个就是基础支持层。基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml 解析、反射、IO、 事务等等这些功能。

这个就是 MyBatis 的主要工作流程和架构分层。接下来我们来学习一下基础层里面 的一个主要模块,缓存。我们一起来了解一下 MyBatis 一级缓存和二级缓存的区别,和 它们的工作方式,以及使用过程里面有什么注意事项。

三,MyBatis 缓存详解 cache 缓存

缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟 Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

缓存体系结构

MyBatis 跟缓存相关的类都在 cache 包里面,其中有一个 Cache 接口,只有一个默认的实现类 PerpetualCache,它是用 HashMap 实现的。

除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策 略、日志记录、定时刷新等等:

“装饰者模式(Decorator Pattern)是指在不改变原有对象的基础之上,将功能附加到对象上,提供了比继承更有弹 性的替代方案(扩展原有对象的功能)。”

但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认 PerpetualCache)。

所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存:

缓存实现类描述作用装饰条件
基本缓存缓存基本实现类默认是 PerpetualCache,也可以自定义比如 RedisCache、EhCache 等,具备基本功能的缓存类
LruCacheLRU 策略的缓存当缓存到达上限时候,删除最近最少使用的缓存 (Least Recently Use)eviction=“LRU”(默 认)
FifoCacheFIFO 策略的缓存当缓存到达上限时候,删除最先入队的缓存eviction=“FIFO”
SoftCache WeakCache带清理策略的缓存通过 JVM 的软引用和弱引用来实现缓存,当 JVM 内存不足时,会自动清理掉这些缓存,基于 SoftReference 和 WeakReferenceeviction=“SOFT” eviction=“WEAK”
LoggingCache带日志功能的缓存比如:输出缓存命中率基本
SynchronizedCache同步缓存基于 synchronized 关键字实现,解决并发问题基本
BlockingCache阻塞缓存通过在 get/put 方式中加锁,保证只有一个线程操作缓存,基于 Java 重入锁实现blocking=true
SerializedCache支持序列化的缓存将对象序列化以后存到缓存中,取出时反序列化readonly=false(默 认)
ScheduledCache定时调度的缓存在进行 get/put/remove/getSize 等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是 一小时),如果是则清空缓存–即每隔一段时间清 空一次缓存flushInterval 不为空
TransactionalCache事务缓存在二级缓存中使用,可一次存入多个缓存,移除多 个缓存在 TransactionalCach eManager 中用 Map 维护对应关系

思考:缓存对象在什么时候创建?什么情况下被装饰? 我们要弄清楚这个问题,就必须要知道 MyBatis 的一级缓存和二级缓存的工作位置和工作方式的区别。

一级缓存 一级缓存(本地缓存)介绍

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

首先我们必须去弄清楚一个问题,在 MyBatis 执行的流程里面,涉及到这么多的对象,那么缓存 PerpetualCache 应该放在哪个对象里面去维护?如果要在同一个会话里面共享一级缓存,这个对象肯定是在 SqlSession 里面创建的,作为SqlSession的一个属 性。

DefaultSqlSession里面只有两个属性,Configuration是全局的,所以缓存只可能 放在 Executor 里面维护——SimpleExecutor/ReuseExecutor/BatchExecutor的父类baseExecutor的构造函数中持有了 PerpetualCache。

在同一个会话里面,多次执行相同的 SQL 语句,会直接从内存取到缓存的结果,不会再发送 SQL 到数据库。但是不同的会话里面,即使执行的 SQL 一模一样(通过一个 Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。

接下来我们来验证一下,MyBatis 的一级缓存到底是不是只能在一个会话里面共享, 以及跨会话(不同 session)操作相同的数据会产生什么问。

一级缓存验证

(注意演示一级缓存需要先关闭二级缓存, localCacheScope 设置为 SESSION)

判断是否命中缓存:如果再次发送 SQL 到数据库执行,说明没有命中缓存;如果直接打印对象,说明是从内存缓存中取到了结果。

  1. 在同一个 session 中共享

        @Test
        public void testCache() throws IOException {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
            SqlSession session1 = sqlSessionFactory.openSession();
            SqlSession session2 = sqlSessionFactory.openSession();
            try {
                BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
                BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
                Blog blog = mapper0.selectBlogById(1);
                System.out.println(blog);
    
                System.out.println("第二次查询,相同会话,获取到缓存了吗?");
                System.out.println(mapper1.selectBlogById(1));
    
                System.out.println("第三次查询,不同会话,获取到缓存了吗?");
                BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
                System.out.println(mapper2.selectBlogById(1));
    
            } finally {
                session1.close();
            }
        }
    
  2. 不同 session 不能共享

    SqlSession session1 = sqlSessionFactory.openSession();
    BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
    System.out.println(mapper.selectBlog(1));
    

    PS:一级缓存在 baseExecutor 的 query()——queryFromDatabase()中存入。在 queryFromDatabase()之前会 get()。

  3. 同一个会话中,update(包括 delete)会导致一级缓存被清空

        @Test
        public void testCacheInvalid() throws IOException {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
            SqlSession session = sqlSessionFactory.openSession();
            try {
                BlogMapper mapper = session.getMapper(BlogMapper.class);
                System.out.println(mapper.selectBlogById(1));
    
                Blog blog = new Blog();
                blog.setBid(1);
                blog.setName("2021年7月24日14:39:58");
                mapper.updateByPrimaryKey(blog);
                session.commit();
    
                // 相同会话执行了更新操作,缓存是否被清空?
                System.out.println("在执行更新操作之后,是否命中缓存?");
                System.out.println(mapper.selectBlogById(1));
    
            } finally {
                session.close();
            }
        }
    

一级缓存是在 baseExecutor 中的 update()方法中调用 clearLocalCache()清空的(无条件),query 中会判断。如果跨会话,会出现什么问题?

  1. 其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)。

        @Test
        public void testDirtyRead() throws IOException {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
            SqlSession session1 = sqlSessionFactory.openSession();
            SqlSession session2 = sqlSessionFactory.openSession();
            try {
                BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
                System.out.println(mapper1.selectBlogById(1));
    
                // 会话2更新了数据,会话2的一级缓存更新
                Blog blog = new Blog();
                blog.setBid(1);
                blog.setName("after modified 112233445566");
                BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
                mapper2.updateByPrimaryKey(blog);
                session2.commit();
    
                // 其他会话更新了数据,本会话的一级缓存还在么?
                System.out.println("会话1查到最新的数据了吗?");
                System.out.println(mapper1.selectBlogById(1));
            } finally {
                session1.close();
                session2.close();
            }
        }
    
一级缓存的不足

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。

【思考】一级缓存怎么命中?CacheKey 怎么构成?

【思考】一级缓存是默认开启的,怎么关闭一级缓存?

二级缓存 二级缓存介绍

二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是 namespace 级别的,可以被多个 SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享), 生命周期和应用同步。

思考一个问题:如果开启了二级缓存,二级缓存应该是工作在一级缓存之前,还是 在一级缓存之后呢?二级缓存是在哪里维护的呢?

作为一个作用范围更广的缓存,它肯定是在 SqlSession 的外层,否则不可能被多个 SqlSession 共享。而一级缓存是在 SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。

第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的 baseExecutor 已经满足不了需求了,那我们应该在 baseExecutor 之外创建一个对象。

实际上 MyBatis 用了一个装饰器的类来维护,就是 CachingExecutor。如果启用了 二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。

CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器 Executor 实现类,比如 SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

一级缓存是默认开启的,那二级缓存怎么开启呢?

开启二级缓存的方法

第一步:在 mybatis-config.xml 中配置了(可以不配置,默认是 true):


只要没有显式地设置 cacheEnabled=false,都会用 CachingExecutor 装饰基本的执行器。

第二步:在 Mapper.xml 中配置标签。


eviction="LRU" 
flushInterval="120000" 
readonly="false"/> 
cache 属性详解:
属性含义取值
type缓存实现类需要实现 Cache 接口,默认是 PerpetualCache
size最多缓存对象个数默认 1024
eviction回收策略(缓存淘汰算法)LRU – 最近最少使用的:移除最长时间不被使用的对象(默认)。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval定时自动清空缓存间隔自动刷新时间,单位 ms,未配置时只有调用时刷新
readOnly是否只读true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象 不能被修改。这提供了很重要的性能优势。
false:读写缓存;会返回缓存对象的拷贝(通过序列化),不会共享。这 会慢一些,但是安全,因此默认是 false。 改为 false 可读写时,对象必须支持序列化。
blocking是否使用可重入锁实现缓存的并发控制true,会使用 BlockingCache 对 Cache 进行装饰 默认 false

Mapper.xml 配置了之后,select()会被缓存。update()、delete()、insert() 会刷新缓存。

思考:如果 cacheEnabled=true,Mapper.xml 没有配置标签,还有二级缓存吗? 还会出现 CachingExecutor 包装对象吗?

只要 cacheEnabled=true 基本执行器就会被装饰。有没有配置,决定了在启动的时候会不会创建这个 mapper 的 Cache 对象,最终会影响到 CachingExecutor query 方法里面的判断:

@Override
  public  List 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 list = (List) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?

我们可以在单个 Statement ID 上显式关闭二级缓存(默认是 true)