MyBatis体系结构

MyBatis体系结构

MyBatis的工作流程

MyBatis的主要工作流程

解析配置文件

首先在MyBatis启动的时候我们要去解析配置文件,包括全局配置文件和映射器配置文件,这里面包含了怎么控制MyBatis的行为,和要对数据库下达的指令,也就是SQL信息,mybatis会把它们解析成一个Configuration对象。

提供操作接口

接下来就是操作数据库的接口,它在应用程序和数据库中间,代表我们跟数据库之间的一次连接,这个就是SqlSession对象,要获得一个会话,必须有一个会话工厂SqlSessionFactory。SqlSessionFactory里面又必须包含我们的所有的配置信息,所以我们会通过一个Builder来创建工厂类。

MyBatis是对JDBC的封装,也就是意味着底层一定会出现JDBC的一些核心对象,比如执行SQL的Statement,结果集ResultSet。在Mybatis里面,SqlSession只是提供给应用的一个接口,还不是SQL的真正的执行对象。

执行SQL操作

SqlSession持有了一个Executor对象,用来封装对数据库的操作。

在执行器Executor执行query或者update操作的时候mybatis创建一系列的对象,来处理参数、执行SQL、处理结果集,这里mybatis把它简化成一个对象:StatementHandler,可以把它理解为对Statement的封装。

下面就是MyBatis主要的工作流程,如图

image-20210523171606068

MyBatis架构分层与模块划分

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

按照功能职责的不同,所有的package可以分成不同的工作层次。

image-20210523171844325

接口层

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

核心处理层

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

核心处理层主要做了这几件事:

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

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

3执行SQL语句;

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

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

基础支持层

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

MyBatis缓存详解

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

缓存体系结构

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

PerpetualCache这个对象一定会创建,所以这个叫做基础缓存。但是缓存又可以有很多额外的功能,比如回收策略、日志记录、定时刷新等等,如果需要的话,就可以给基础缓存加上这些功能,如果不需要,就不加。

除了基础缓存之外,MyBatis也定义了很多的装饰器,同样实现了Cache接口,通过这些装饰器可以额外实现很多的功能。

一级缓存

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

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

如果要在同一个会话里面共享一级缓存,最好的办法是在SqlSession里面创建的,作为SqlSession的一个属性,跟SqlSession共存亡,这样就不需要为SqISession编号、再根据SqlSession的编号去查找对应的缓存了。

DefaultSqlSession里面只有两个对象属性:Configuration和Executor。

Configuration是全局的,不属于SqlSession,所以缓存只可能放在Executor里面维护实际上它是在基本执行器SimpleExecutor/ReuseExecutor/BatchExecutor的父类BaseExecutor的构造函数中持有了PerpetualCache。

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

一级缓存什么时候会被清空呢?

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

只有更新会清空缓存吗?查询会清空缓存吗?如果要清空呢?

一级缓存是在BaseExecutor中的update() 方法中调用clearLocalCache() 清空的(无条件)

如果是query会判断(只有select标签的fushCache=true才清空)

一级缓存的工作范围是一个会话。如果跨会话,会出现什么问题?

其他会话更新了数据,导致当前会话读取到的是过时的数据(一级缓存不能跨会话共享)

image-20210523184158780

一级缓存的不足

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

二级缓存

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

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

作为一个作用范围更广的缓存,它肯定是在SqlSession的外层,否则不可能被多个SqlSession共享。

而一级缓存是在SqlSession内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。

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

但是,二级缓存是不一定开启的。也就是说,开启了二级缓存,就启用这个对象,如果没有开启,就不用这个对象,我们应该怎么做呢?

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

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

image-20210523184221661

开启二级缓存的方法

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

1
<setting name="cacheEnabled" value="true"/>

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

二级缓存的总开关是默认开启的。但是每个Mapper的二级缓存开关是默认关闭的,一个Mapper要用到二级缓存,还要单独打开它自己的开关。

第二步:在Mapper.xml中配置<cache>标签

1
2
3
4
5
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!-- 最多缓存对象个数,默认1024 -->
eviction="LRU" <!-- 回收策 -->
flushInterval="120000" <!-- 自动刷新时间ms,未配置时只有调用时刷新 -->
readOnly="false" /> <!-- 默认是false(安全),改为true可读写时,对象必须支持序列化 -->

cache属性详解:

属性 含义 取值
type 缓存实现类 需要实现Cache接口,默认是PerpetualCache,可以使用第三方缓存
size 最多缓存对象个数 默认1024
eviction 回收策略(缓存淘汰算法) LRU-最近最少使用的:移除最长时间不被使用的对象(默认);
FIF0-先进先出:按对象进入缓存的顺序来移除它们;
SOFT-软引用:移除基于垃圾回收器状态和软引用规则的对象;
WEAK-弱引用:更积极的移除基于垃圾回收器状态和软引用规则的对象。
readOnly 是否只读 true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
false:读写缓存;会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是false。
改为false可读写时,对象必须支持序列化。
blocking 启用阻塞缓存 通过在get/put方式中加锁,保证只有一个线程操作缓存,基于Java重入锁实现

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

如果二级缓存拿到结果了,就直接返回(最外层的判断),否则再到一级缓存,最后到数据库。

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

只要cacheEnabled=true基本执行器就会被装饰。有没有配置<cache>,决定了在启动的时候会不会创建这个mapper的Cache对象,最终会影响到CachingExecutor query方法里面的判断,也就是说,此时会装饰,但是没有cache对象,依然不会走二级缓存流程。

如果一个Mapper需要开启二级缓存,但是这个里面的某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?

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

1
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">

事务不提交,二级缓存是不生效的

因为二级缓存使用TransactionalCacheManager (TCM)来管理,最后又调用了 TransactionalCache的get0bject()、putObject()和commit()方法,TransactionalCache 里面又持有了真正的Cache对象,比如是经过层层装饰的PerpetualCache。

在putObject的时候,只是添加到了entriesToAddOnCommit里面,只有它的commit() 方法被调用的时候才会调用flushPendingEntries() 真正写入缓存。它就是在DefaultSqISession调用commit() 的时候被调用的。

什么时候开启二级缓存?

一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?

1、因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询,否则缓存就失去了意义。

2、如果多个namespace中有针对于同一个表的操作,比如Blog表,如果在namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。

所以,推荐在一个Mapper里面只操作单表的情况使用。

如果要让多个namespace共享一个三级缓存,应该怎么做?

跨namespace的缓存共享的问题,可以使用<cache-ref>来解决

1
<cache-ref namespace="com.crud.dao.DepartmentMapper"/>

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。

注意:在这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了。

第三方缓存做二级缓存

MyBatis自带的二级缓存之外,我们也可以通过实现Cache接口来自定义二级缓存。

MyBatis官方提供了一些第三方缓存集成方式,比如ehcache和redis:

https://github.com/mybatis/redis-cache

pom文件引入依赖:

1
2
3
4
5
<dependency>X
<groupld>org.mybatis.caches</groupId>
<artifactld>mybatis-redis</artifactld>
<version>1.0.0-beta2</version>
</dependency>

Mapper.xml配置,type使用RedisCache:

1
2
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

redis.properties配置

1
2
3
4
5
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0
打赏

请我喝杯咖啡吧~

支付宝
微信