一、二级缓存原理分析
1.二级缓存的定义
二级缓存也称作是应用级缓存,与一级缓存不同的是它的作用范围是整个应用,而且可以跨线程使用,所以二级缓存有更高的命中率,适合缓存一些修改比较少的数据。
2.二级缓存扩展性需求
二级缓存的生命周期是整个应用,所以必须限制二级缓存的容量,在这里mybatis使用的是溢出淘汰机制。而一级缓存是会话级的生命周期非常短暂是没有必要实现这些功能的。相比较之下,二级缓存机制更加完善。
3.二级缓存的结构
二级缓存在结构设计上采用装饰器+责任链模式
3.1二级缓存是如何组装这些装饰器的呢?
这里我们先介绍一下CacheBuilder类顾名思义这是一个缓存构建类。该类就是二级缓存的构建类里面定义了一些上图装饰器类型的属性,以及构建组合这些装饰器的行为。
源码分析:
4.SynchronizedCache线程同步缓存区
实现线程同步功能,与序列化缓存区共同保证二级缓存线程安全。若blocking=false关闭则SynchronizedCache位于责任链的最前端,否则就位于BlockingCache后面而BlockingCache位于责任链的最前端,从而保证了整条责任链是线程同步的。
源码分析:只是对于操作缓存的方法进行了线程同步功能
5.LoggingCache统计命中率以及打印日志
统计二级缓存命中率并输出打印,由以下源码可知:日志中出现了“Cache Hit Ratio”便表示命中了二级缓存。
源码分析:
6.ScheduledCache过期清理缓存区
@CacheNamespace(flushInterval=100L)设置过期清理时间默认1个小时,若设置flushInterval为0代表永远不进行清除。
源码分析:操作缓存时都会进行检查缓存是否过期
7.LruCache(最近最少使用)防溢出缓存区
内部使用链表(增删比较快)实现最近最少使用防溢出机制。
源码分析:
8.FifoCache(先进先出)防溢出缓存区
源码分析:内部使用队列存储key实现先进先出防溢出机制
9.二级缓存的使用(命中条件)
1)会话提交后
2)sql语句、参数相同
3)相同的statementID
4)RowBounds相同
注意:设置为自动提交事务并不会命中二级缓存。
10.二级缓存的配置
11.二级缓存为什么要提交之后才能命中缓存?
会话一与会话二原本是两条隔离的事务,但由于二级缓存的存在导致彼此可见会发生脏读。若会话二的修改直接填充到二级缓存,会话一查询时缓存中存在即直接返回数据,此时会话二回滚会话一读到的数据就是脏数据。为了解决这一问题mybatis二级缓存机制就引入了事务管理器(暂存区),所有变动的数据都会暂时存放到事务管理器的暂存区中,只有执行提交动作后才会真正的将数据从暂存区中填充到二级缓存中。
1)会话:事务缓存管理器:暂存区=1:1:N
2)暂存区:缓存区=1:1(一个暂存区对应唯一一个缓存区)
3)会话关闭,事务缓存管理器也会关闭,暂存区也会被清空
4)一个事务缓存管理器管理多个暂存区
5)有多少个暂存区取决于你访问了多少个Mapper文件(缓存的key是Mapper文件全路径ID)
12.二级缓存执行流程
1)查询是实时查询缓存区的。
2)所有对二级缓存的实时变动都是通过暂存区来实现的。
3)暂存区清理完会进行标识,但此时二级缓存中数据并未清理,只有执行commit后才会真正清理二级缓存中的数据。
4)查询会实时查询缓存区,若暂存区清理标识为true就算从缓存区中查询到数据也会返回一个null,重新查询数据库(暂存区清理标识为true也会返回null是为了防止脏读,一旦提交清空掉二级缓存中的数据此时读取到的就是脏数据,因此返回null重新查询数据库得到的才是正确数据)。
源码分析:
a.若开启二级缓存进行查询方法的时候会走到类CachingExecutor中的query方法
b.根据上一步中的tcm.getObject(cache, key)方法查询二级缓存
二、动态sql原理分析
1.动态sql介绍
我们在使用mybatis的时候,会在xml中编写sql语句。比如这段动态sql代码:
mybatis底层是如何构造这段sql的?这方面的知识网上资料不多,于是就写了这么一篇文章。下面带着这个疑问,我们一步一步分析。
2.介绍MyBatis中一些关于动态SQL的接口和类
2.1SqlNode接口
简单理解就是xml中的每个标签,比如上述sql的update,trim,if标签:
public interface SqlNode {
boolean apply(DynamicContext context);
}
2.2SqlSource Sql源接口
代表从xml文件或注解映射的sql内容,主要就是用于创建BoundSql,有实现类DynamicSqlSource(动态Sql源),StaticSqlSource(静态Sql源)等:
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
2.3BoundSql类
封装mybatis最终产生sql的类,包括sql语句,参数,参数源数据等参数:
2.4XNode
一个Dom API中的Node接口的扩展类:
2.5BaseBuilder接口及其实现类
这些Builder的作用就是用于构造sql:
下面我们简单分析下其中4个Builder:
1) XMLConfigBuilder
解析mybatis中configLocation属性中的全局xml文件,内部会使用XMLMapperBuilder解析各个xml文件。
2) XMLMapperBuilder
遍历mybatis中mapperLocations属性中的xml文件中每个节点的Builder,比如user.xml,内部会使用XMLStatementBuilder处理xml中的每个节点。
3) XMLStatementBuilder
解析xml文件中各个节点,比如select,insert,update,delete节点,内部会使用XMLScriptBuilder处理节点的sql部分,遍历产生的数据会丢到Configuration的mappedStatements中。
4) XMLScriptBuilder
解析xml中各个节点sql部分的Builder。
2.6LanguageDriver接口及其实现类
该接口主要的作用就是构造sql:
简单分析下XMLLanguageDriver(处理xml中的sql,RawLanguageDriver处理静态sql):
XMLLanguageDriver内部会使用XMLScriptBuilder解析xml中的sql部分。
ok, 大部分比较重要的类我们都已经介绍了,下面源码分析走起。
3.源码分析
SqlSessionFactory方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件。
部分源码如下:
由于XMLConfigBuilder内部也是使用XMLMapperBuilder,我们就看看XMLMapperBuilder的解析细节。
我们关注一下,增删改查节点的解析。
XMLStatementBuilder的解析:
默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。
XMLLanguageDriver创建SqlSource:
XMLScriptBuilder解析sql:
得到SqlSource之后,会放到Configuration中,有了SqlSource,就能拿BoundSql了,BoundSql可以得到最终的sql。
4.实例分析
我以以下xml的解析大概说下parseDynamicTags的解析过程:
在看这段解析之前,请先了解dom相关的知识,xml dom知识, dom博文。
parseDynamicTags方法的返回值是一个List,也就是一个Sql节点集合。SqlNode本文一开始已经介绍,分析完解析过程之后会说一下各个SqlNode类型的作用。
a.首先根据update节点(Node)得到所有的子节点,分别是3个子节点
(1)文本节点 \n UPDATE users
(2)trim子节点 ...
(3)文本节点 \n where id = #{id}
b.遍历各个子节点
(1) 如果节点类型是文本或者CDATA,构造一个TextSqlNode或StaticTextSqlNode
(2) 如果节点类型是元素,说明该update节点是个动态sql,然后会使用NodeHandler处理各个类型的子节点。这里的NodeHandler是XMLScriptBuilder的一个内部接口,其实现类包括TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler等。看类名也就明白了这个Handler的作用,比如我们分析的trim节点,对应的是TrimHandler;if节点,对应的是IfHandler...
这里子节点trim被TrimHandler处理,TrimHandler内部也使用parseDynamicTags方法解析节点
c.遇到子节点是元素的话,重复以上步骤
trim子节点内部有7个子节点,分别是文本节点、if节点、是文本节点、if节点、是文本节点、if节点、文本节点。文本节点跟之前一样处理,if节点使用IfHandler处理。
遍历步骤如上所示,下面我们看下几个Handler的实现细节。
IfHandler处理方法也是使用parseDynamicTags方法,然后加上if标签必要的属性。
TrimHandler处理方法也是使用parseDynamicTags方法,然后加上trim标签必要的属性。
以上update方法最终通过parseDynamicTags方法得到的SqlNode集合如下:
trim节点:
由于这个update方法是个动态节点,因此构造出了DynamicSqlSource。
DynamicSqlSource内部就可以构造sql了:
DynamicSqlSource内部的SqlNode属性是一个MixedSqlNode。
然后我们看看各个SqlNode实现类的apply方法,下面分析一下两个SqlNode实现类的apply方法实现:
MixedSqlNode:
MixedSqlNode会遍历调用内部各个sqlNode的apply方法。
StaticTextSqlNode:
直接append sql文本。
IfSqlNode:
这里的evaluator是一个ExpressionEvaluator类型的实例,内部使用了OGNL处理表达式逻辑。
TrimSqlNode:
TrimSqlNode的apply方法也是调用属性contents(一般都是MixedSqlNode)的apply方法,按照实例也就是7个SqlNode,都是StaticTextSqlNode和IfSqlNode。 最后会使用FilteredDynamicContext过滤掉prefix和suffix。
5.总结
大致讲解了一下mybatis对动态sql语句的解析过程,其实回过头来看看不算复杂,还算蛮简单的。 之前接触mybaits的时候遇到刚才分析的那一段动态sql的时候总是很费解。
想搞明白这个trim节点的prefixOverrides到底是什么意思(从字面上理解就是前缀覆盖),而且官方文档上也没这方面知识的说明。我将这段xml改成如下:
(第二段第一个if节点多了个逗号) 结果我发现这2段xml解析的结果是一样的,非常迫切地想知道这到底是为什么,然后这也促使了我去看源码的决心,最终还是看下来了。
文章有点长,而且讲的也不是非常直观,希望对有些人有帮助。