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技术内幕》