接上次聊到了mybatis数据源组件后,今天我们来看看大家平时可能都用了的缓存组件。其实缓存组件在我们项目中要谨慎地选择使用,用不好不仅不会带来性能上的提升,反而会出现数据的错乱问题,为什么呢?那莫过于从mybatis缓存组件源码中来找到答案了。

整体设计架构

mybatis源码解析 - 核心基础组件之缓存_第1张图片

缓存的概况

1.首先mybatis的缓存分为一级缓存和二级缓存,一级缓存默认是开启的(你可以再mapper.xml中select标签加入flushCache="true"将它关闭);一级缓存的生命周期是SqlSession级别,它是线程安全的方式;同一个会话查询时,mybatis会把执行的方法和参数通过算法生成key, 将键值和查询结果存入map对象中。如果查询的方法和参数完全一致,那么算法会生成相同key, 这时若缓存中存在则直接返回缓存中的结果。

2.二级缓存需要开启的点有两个地方:第一个地方: 在mybatis-config.xml文件中的setting标签中设置, cacheEnabled="true"(mybatis全局开关,默认开启); 第二地方:在需要开启缓存的mapper接口的xml文件中加入标签,当然里面有一些属性你可以通过它们扩展cache的一些能力;

映射语句文件中的所有 select 语句将会被缓存。

eviction: 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。

flushInterval: 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。

size: 缓存会存储列表集合或对象(无论查询方法返回什么)的 512个引用。

readOnly: 缓存会被视为是 read/write(可读/可写)的缓存;

3.不管是一级缓存还是二级缓存,当有INSERT,UPDATE,DELETE语句执行时,都会自动刷新相关缓存;

4.二级缓存的生命周期为应用程序级,二级缓存的缓存单位时namespace,不同namespace之间允许共享同一缓存;

我们今天分析mybatis缓存组件要解决以下三个问题:

1.二级缓存的整体架构、关系以及设计思路;

2. 这种设计的好处 ?

3. 分析一些比较有代表性的部件源码;

4. 缓存能力在整个mybatis中的集成;

先来看看mybatis缓存组件的核心设计结构

核心设计结构

mybatis源码解析 - 核心基础组件之缓存_第2张图片

以上设计结构中:

PerpetualCache: 为缓存提供默认实现(基础的二级缓存能力) ;

其它均为Cache的各种装饰器

SynchronizedCache: 同步装饰器,赋予Cache同步、线程安全的二级缓存的能力;

SerializedCache:序列化装饰器,赋予Cache数据二进制流序列化能力;

LoggingCache:缓存日志装饰器,赋予Cache日志打印能力;

LruCache:清空缓存装饰器,赋予Cache清理淘汰策略(默认LRU, 最近最少使用清空策略);

以上四个装饰器是mybatis二级缓存默认自带的各种能力配置。我们通过修改标签的各个属性,可修改定义我们的缓存需要具备的能力和特性。这里我们重点来分析一个非常有意思的缓存装饰器:BlockingCache 阻塞式装饰器,在分析这个装饰器前,先来说说组件涉及到的装饰器设计模式。

装饰器设计模式

装饰器设计模式作用是:允许动态向现有对象添加新功能;是一种代替继承的技术。当前对象无需通过继承扩展父类的功能,相比于继承它更加灵活且避免了子类的快速增加。

mybatis源码解析 - 核心基础组件之缓存_第3张图片

Component: 组件作为装饰器的核心接口,它定义了装饰器需要实现的行为;

ConcreteComponent:组件实现类,实现了Component所需的行为的基本实现,后面各个具体的装饰器都基于该对象进行扩展;

Decorator: 装饰器的基类(需要时可附加此类),抽象多个装饰器在实现过程装的一些公共行为和基础特性;

ConcreteDecorator:具体的装饰器类,它继承于装饰器基类,实现某一个特性的装饰器行为;

装饰器设计模式的优点

类的继承和代理,从本质上讲只能算一种静态的设计。在设计时要具体地知道对象或类需要扩展哪个特性,较少的类没问题,但当

1.基础类需要扩展的特性很多;

2.扩展的这些特性设计人员并清楚怎样装配,而需要开放给使用它的用户个性化的组合;

遇到以上两个需求的时候,继承和代理就显得力不从心,要么需要增加繁杂的子类;要么需要修改原有增强功能的业务代码,这些都违背了单一职责和开闭原则。

所以mybatis在二级缓存的增强特性需要用户的个性化配置、添加和切换的设计目标下,选择了装饰器模式。

OK这里都明白后,我们接下来分析一下BlockingCache阻塞式缓存特性。

BlockingCache阻塞式缓存

直接上核心的实现源码,我们来看看它的实现细节

mybatis源码解析 - 核心基础组件之缓存_第4张图片

mybatis源码解析 - 核心基础组件之缓存_第5张图片

mybatis源码解析 - 核心基础组件之缓存_第6张图片

从以上源码我们不难发现:此装饰器在提供线程安全的缓存读写的同时,又考虑到了拿锁的开销。所以采用了粒度较小的分片锁机制,只针对特定的缓存key加锁。这样做至少有两个好处:

1.提高了缓存读写的效率,减小了阻塞的粒度;

2.在高并发时,这种对key加锁的机制可以有效解决缓存击穿的问题;

OK,这里get到了后,下面我们就来继续分享一下mybatis缓存的小点CacheKe类

缓存键CacheKey

mybatis源码解析 - 核心基础组件之缓存_第7张图片

由上截图,缓存的接口API我们可以得知,mybatis存取缓存的key是一个Object类型,这个Object实际传入的参数就是CacheKey对象

mybatis源码解析 - 核心基础组件之缓存_第8张图片

那Cachekey对象中会添加设置那些参数呢?mybatis缓存的key跟以下因素有关:

1.存储在mappedStatement对象中的id = namespace+id(命令id);

2.查询使用的sql语句;

3.查询传递的sql实际参数值;

4.指定查询结果集的范围;

口说无凭,我们看看源码(位置:BaseExecutor-->createCacheKey()方法)

mybatis源码解析 - 核心基础组件之缓存_第9张图片

OK如果都get到了以上的点,那我们来看看缓存技术在mybatis中如何被集成进去的

缓存的集成

先看看MapperBuilderAssistant-->useNewCache()方法

mybatis源码解析 - 核心基础组件之缓存_第10张图片

上图CacheBuilder构建Cache对象时,采用了链式的编程风格。我们来看它源码实现

mybatis源码解析 - 核心基础组件之缓存_第11张图片

很明显,这里采用的是建造者设计模式对CacheBuilder的各个部分进行的分步构建,最后调用build构造出Cache对象。此模式在mybatis源码中使用也比较多。

mybatis源码解析 - 核心基础组件之缓存_第12张图片

Builder: 抽象建造者接口,是定义建造的行为的基础接口;

ConcreteBuilder: 具体建造者实现类,抽象建造者行为的实现类;

Product: 建造者模式,最终生产出的产品;

Director: 使用建造者的场景类或入口;

建造者模式的本质:对内将一个复杂对象的创建过程解耦;对外屏蔽对象创建细节的复杂度。

适合场景:对象本身比较复杂或者它的创建有多个步骤(部分)组成,经过一系列分步构建,最终组合成完整的对象的场景。

类比Cache组件,各个角色为

image.png

它这里并没有定义基础的建造者接口。

mybatis源码解析 - 核心基础组件之缓存_第13张图片

mybatis源码解析 - 核心基础组件之缓存_第14张图片

一级缓存调用入口: BaseExecutor-->query()方法

mybatis源码解析 - 核心基础组件之缓存_第15张图片

二级缓存调用入口: CachingExecutor---->query()方法

mybatis源码解析 - 核心基础组件之缓存_第16张图片

以上截图代码已经把答案说得很详细了,相信不用我再多说。

总结

以上就是mybatis缓存组件设计和集成的整个过程。更多细节的东西,大家如果有兴趣可以多读一下mybatis源码。通过分析源码你会得到更多的收获!如果有其它问题欢迎在文章下方留言,请继续关注!