MyBatis基础支持层位于 Mybatis 整体架构的最底层,支撑着 Mybatis 的核心处理层,是整个框架的基石。基础支持层中封装了多个较为通用的、独立的模块,不仅仅为 Mybatis 提供基础支撑,也可以在合适的场景中直接复用。

这篇文章介绍MyBatis的缓存模块
MyBatis作为 一个 强大的持久层 框 架,缓 存是其必不可少的功能之一。MyBatis中的缓 存 是两 层 结 构 的,分为 一级 缓 存、二级 缓 存,但在本质 上是相同的,它 们 使用的都是Cache接 口的实 现 。
在 MyBatis缓 存模块 中涉及了装 饰 器模式的相关 知识 。
装饰器模式
在实践生产中,新需求在软件的整个生命过程中总是不断出现的。当有新需求出现时,就需要为某些组件添加新的功能来满足这些需求。
添加新功能的方式有很多,我们可以直接修改己有组件的代码并添加相应的新功能,这显然会破坏己有组件的稳定性,修改完成后,整个组件需要重新进行测试,才能上线使用。这种方式显然违反了“开放-封闭”原则。
另一种方式是使用继承方式,我们可以创建子类并在子类中添加新功能实现扩展。*这种方法是静态的,用户不能控制增加行为的方式和时机。而且有些情况下继承是不可行的。
例如己有组件是被
final关键字修饰的类。另外,如果待添加的新功能存在多种组合,使用继承方式可能会导致大量子类的出现。
例如,有4个待添加的新功能,系统需要动态使用任意多个功能的组合,则需要添加15个子类才能满足全部需求。
装饰器模式能够帮助我们解决上述问题,装饰器可以动态地为对象添加功能,它是基于组合的方式实现该功能的。
在实践中,我们应该尽量使用组合的方式来扩展系统的功能,而非使用继承的方式。设计模式中常见的一句话:组合优于继承。
装饰器模式的类图,以及其中的核心角色:

Component(组件)
组件接口定义了全部组件实现类以及所有装饰器实现的行为。
ConcreteComponent (具体 组 件实 现 类 )
具体组件实现类实现了
Component接口。通常情况下,具体组件实现类就是被装饰器装饰的原始对象,该类提供了
Component接口中定义的最基本的功能,其他高级功能或后续添加的新功能,都是通过装饰器的方式添加到该类的对象之上的。Decorator(装饰器)
所有装饰器的父类,它是一个实现了
Component接口的抽象类,并在其中封装了一个Component对象,也就是被装饰的对象。而这个被装饰的对象只要是
Component类型即可,这就实现了装饰器的组合和复用。如下图,装饰器**C(ConcreteDecoratorl类型)修饰了装饰器B(ConcreteDecorator2类型)**并为其添加功能
W,而装饰器B(ConcreteDecorator2类型)又修饰了组件A(ConcreteComponent类型)并为其添加功能V。其中,组件对象A提供的是最基本的功能,装饰器B和装饰器C会为组件对象A添加新的功能。

ConcreteDecorator
具体的装饰器实现类,该实现类要向被装饰对象添加某些功能。如上图,装饰器B、C就是该角色,被装饰的对象只要是
Component类型即可。
在JavaIO包中,大量应用了装饰器模式,我们在使用JavaIO包读取文件时,经常会看到如下代码:BufferedlnputStream bis =
new BufferedlnputStream( new FilelnputStream(new File("D:/test.txt")));
FilelnputStream并没有缓冲功能,每次调用其read()方法时都会向操作系统发起相应的系统调用,当读取大量数据时,就会导致操作系统在用户态和内核态之间频繁切换,性能较低。
BufferedlnputStream是提供了缓冲功能的装饰器,每次调用其read()方法时,会预先从文件中获取一部分数据并缓存到BufferedlnputStream的缓冲区中,后面连续的几次读取可以直接从缓冲区中获取数据,直到缓冲区数据耗尽才会重新从文件中读取数据,这样就可以减少用户态和内核态的切换,提高了读取的性能。
在MyBatis的缓存模块中,使用了装饰器模式的变体,其中将Decorator接口和Component接口合并为一个Component接口

使用装饰器模式的优点:
- 相较于继承来说,装饰器模式的灵活性更强,可扩展性也强。正如前面所说,继承方式会导致大量子类的情况。而装饰者模式可以将复杂的功能切分成一个个独立的装饰器,通过多个独立装饰器的动态组合,创建不同功能的组件,从而满足多种不同需求。
- 当有新功能需要添加时,只需要添加新的装饰器实现类,然后通过组合方式添加这个新装饰器即可,无须修改己有类的代码,符合“开放-封闭”原则。
但是,随着添加的新需求越来越多,可能会创建出嵌套多层装饰器的对象,这增加了系统的复杂性,也增加了理解的难度和定位错误的难度。
Cache接口及其实现
MyBatis的缓存模块在org.apache.ibatis.cache包下,其中Cache接口是缓存模块的中最核心的接口,它定义了所有缓存的基本行为。
Cache
public interface Cache { |
Cache接口的实现类有多个,大部分都是装饰器,只有PerpetualCache提供了Cache接口的基本实现。

PerpetualCache
**PerpetualCache在缓存模块中扮演着ConcreteComponent的角色**,其实现比较简单,底层使用HashMap记录缓存项,也是通过该HashMap对象的方法实现的Cache接口中定义的相应方法。
public class PerpetualCache implements Cache { |
下面来介绍org.apache.ibatis.cache.decorators包下提供的装饰器,它们都直接实现了Cache接口,扮演着ConcreteDecorator的角色。
这些装饰器会在PerpetualCache的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求,后面介绍二级缓存时,会见到这些装饰器是如何完成动态组合的。
BlockingCache
BlockingCache是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定key对应的数据。
// 阻塞超时时间 |
假设线程A在BlockingCache中未查找到keyA对应的缓存项时,线程A会获取keyA对应的锁,这样后续线程在查找keyA时会发生阻塞

BlockingCache.getObject(Object key)
public Object getObject(Object key) { |
acquireLock(Object key)
private void acquireLock(Object key) { |
再来看一下getLockForKey方法
private ReentrantLock getLockForKey(Object key) { |
假设线程A从数据库中查找到keyA对应的结果对象后,将结果对象放入到BlockingCache中,此时线程A会释放keyA对应的锁,唤醒阻塞在该锁上的线程。
其他线程即可从BlockingCache中获取keyA对应的数据,而不是再次访问数据库。

BlockingCache.putObject()
|
BlockingCache.releaseLock(Object key)
private void releaseLock(Object key) { |
FifoCache&LruCache
在很多场景中,为了控制缓存的大小,系统需要按照一定的规则清理缓存。
FifoCache是先入先出版本的装饰器,**当向缓存添加数据时,如果缓存项的个数己经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。**
FifoCache中字段的含义:
// 被装饰的底层Cache对象 |
FifoCache.getObject()和removeObject()方法的实现都是直接调用底层Cache对象的对应方法。
在FifoCache.putObject()方法中会完成缓存项个数的检测以及缓存的清理操作。
public void putObject(Object key, Object value) { |
LruCache是按照近期最少使用算法**(LeastRecentlyUsed,LRU)**进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。
// 被装饰的底层Cache对象 |
LruCache的构造函数中默认设置的缓存大小是1024,我们可以通过其setSize()方法重新设置缓存大小
public void setSize(final int size) { |
LruCache.getObject()方法除了返回缓存项,还会调用keyMap.get()方法修改key的顺序,表示指定的key最近被使用。
LruCache.putObject()方法除了添加缓存项,还会将eldestKey字段指定的缓存项清除掉。
|
SoftCache&WeakCache
先了解一下Java提供的4种引用类型。
SoftCache中各个字段的含义:
// ReferenceQueue,引用队列,用于记录已经被GC回收的缓存项所对应的SoftEntry对象 |
SoftCache中缓存项的value是SoftEntry对象,SoftEntry继承了SoftReference,其中指向key的引用是强引用,而指向value的引用是软引用。
private static class SoftEntry extends SoftReference<Object> { |
SoftCache.putObject()方法除了向缓存中添加缓存项,还会清除己经被GC回收的缓存项,其具体实现如下:
public void putObject(Object key, Object value) { |
SoftCache.getObject()方法除了从缓存中查找对应的value,处理被GC回收的value对应的缓存项,还会更新hardLinksToAvoidGarbageCollection集合
public Object getObject(Object key) { |
SoftCache.removeObject()方法在清除缓存项之前,也会调用removeGarbageCollectedItems()方法清理被GC回收的缓存项。
SoftCache.clear()方法首先清理hardLinksToAvoidGarbageCollection集合,然后清理被GC回收的缓存项,最后清理底层delegate缓存中的缓存项。
public void clear() { |
WeakCache的实现与SoftCache基本类似,唯一的区别在于其中使用WeakEntry(继承自WeakReference)封装真正的value对象,其他实现完全一样。
others
ScheduledCache是周期性清理缓存的装饰器,它的clearlnterval字段记录了两次缓存清理之间的时间间隔,默认是一小时,lastClear字段记录了最近一次清理的时间戳。
ScheduledCache的getObject()、putObject()、removeObject()等核心方法在执行时,都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。
LoggingCache在Cache的基础上提供了日志功能,它通过hit字段和request字段记录了Cache的命中次数和访问次数。
在LoggingCache.getObject()方法中会统计命中次数和访问次数这两个指标,并按照指定的日志输出方式输出命中率。
SynchronizedCache通过在每个方法上添加synchronized关键字,为Cache添加了同步功能,有点类似于JDK中Collections中的SynchronizedCollection内部类的实现。
SerializedCache提供了将value对象序列化的功能。
SerializedCache在添加缓存项时,会将value对应的Java对象进行序列化,并将序列化后的byte[]数组作为value存入缓存。
SerializedCache在获取缓存项时,会将缓存项中的byte[]数组反序列化成Java对象。
使用前面介绍的Cache装饰器实现进行装饰之后,每次从缓存中获取同一key对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程以及缓存中的对象;而SerializedCache每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。**SerializedCache使用的序列化方式是Java原生序列化。**
CacheKey
在Cache中唯一确定一个缓存项需要使用缓存项的key,MyBatis中因为涉及动态SQL等多方面因素,其缓存项的key不能仅仅通过一个String表示,所以MyBatis提供了CacheKey类来表示缓存项的key,在一个CacheKey对象中可以封装多个影响缓存项的因素。
CacheKey中可以添加多个对象,由这些对象共同确定两个CacheKey对象是否相同。
// 参与计算hashcode,默认值37 |
在向CacheKey.updateList集合中添加对 象时 ,使用的是CacheKey.update()方法:
public void update(Object object) { |
参考
《MyBatis技术内幕》
部分图片来源——《MyBatis技术内幕》
