本文选自 Doocs 开源社区旗下“源码猎人 ”项目,作者 AmyliaY。 项目将会持续更新,欢迎 Star 关注。 项目地址:https://github.com/doocs/source-code-hunter
MyBatis 中的缓存分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是 Cache 接口的实现。在这篇文章里,我们就来分析 Cache 接口以及多个实现类的具体实现。
1 Cache 组件
MyBatis 中缓存模块相关的代码位于 org.apache.ibatis.cache 包 下,其中 Cache 接口 是缓存模块中最核心的接口,它定义了所有缓存的基本行为。
public interface Cache { /** * 获取当前缓存的 Id */ String getId (); /** * 存入缓存的 key 和 value , key 一般为 CacheKey 对象 */ void putObject ( Object key , Object value ); /** * 根据 key 获取缓存值 */ Object getObject ( Object key ); /** * 删除指定的缓存项 */ Object removeObject ( Object key ); /** * 清空缓存 */ void clear (); /** * 获取缓存的大小 */ int getSize (); /** * !!!!!!!!!!!!!!!!!!!!!!!!!! * 获取读写锁,可以看到,这个接口方法提供了默认的实现!! * 这是 Java8 的新特性!!只是平时开发时很少用到!!! * !!!!!!!!!!!!!!!!!!!!!!!!!! */ default ReadWriteLock getReadWriteLock () { return null ; } }
如下图所示,Cache 接口 的实现类有很多,但大部分都是装饰器,只有 PerpetualCache 提供了 Cache 接口 的基本实现。
1.1 PerpetualCache
PerpetualCache(Perpetual:永恒的,持续的)在缓存模块中扮演着被装饰的角色,其实现比较简单,底层使用 HashMap 记录缓存项,也是通过该 HashMap 对象 的方法实现的 Cache 接口 中定义的相应方法。
public class PerpetualCache implements Cache { // Cache对象 的唯一标识 private final String id ; // 其所有的缓存功能实现,都是基于 JDK 的 HashMap 提供的方法 private Map < Object , Object > cache = new HashMap <>(); public PerpetualCache ( String id ) { this . id = id ; } @Override public String getId () { return id ; } @Override public int getSize () { return cache . size (); } @Override public void putObject ( Object key , Object value ) { cache . put ( key , value ); } @Override public Object getObject ( Object key ) { return cache . get ( key ); } @Override public Object removeObject ( Object key ) { return cache . remove ( key ); } @Override public void clear () { cache . clear (); } /** * 其重写了 Object 中的 equals () 和 hashCode ()方法,两者都只关心 id 字段 */ @Override public boolean equals ( Object o ) { if ( getId () == null ) { throw new CacheException ( "Cache instances require an ID." ); } if ( this == o ) { return true ; } if (!( o instanceof Cache )) { return false ; } Cache otherCache = ( Cache ) o ; return getId (). equals ( otherCache . getId ()); } @Override public int hashCode () { if ( getId () == null ) { throw new CacheException ( "Cache instances require an ID." ); } return getId (). hashCode (); } }
下面来看一下 cache.decorators 包 下提供的装饰器,它们都直接实现了 Cache 接口,扮演着装饰器的角色。这些装饰器会在 PerpetualCache 的基础上提供一些额外的功能,通过多个组合后满足一个特定的需求。
1.2 BlockingCache
BlockingCache 是阻塞版本的缓存装饰器,它会保证只有一个线程到数据库中查找指定 key 对应的数据。
public class BlockingCache implements Cache { // 阻塞超时时长 private long timeout ; // 持有的被装饰者 private final Cache delegate ; // 每个 key 都有其对应的 ReentrantLock锁对象 private final ConcurrentHashMap < Object , ReentrantLock > locks ; // 初始化 持有的持有的被装饰者 和 锁集合 public BlockingCache ( Cache delegate ) { this . delegate = delegate ; this . locks = new ConcurrentHashMap <>(); } }
假设 线程 A 在 BlockingCache 中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应的锁,这样,线程 A 在后续查找 keyA 时,其它线程会被阻塞。
// 根据 key 获取锁对象,然后上锁 private void acquireLock ( Object key ) { // 获取 key 对应的锁对象 Lock lock = getLockForKey ( key ); // 获取锁,带超时时长 if ( timeout > 0 ) { try { boolean acquired = lock . tryLock ( timeout , TimeUnit . MILLISECONDS ); if (! acquired ) { // 超时,则抛出异常 throw new CacheException ( "Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate . getId ()); } } catch ( InterruptedException e ) { // 如果获取锁失败,则阻塞一段时间 throw new CacheException ( "Got interrupted while trying to acquire lock for key " + key , e ); } } else { // 上锁 lock . lock (); } } private ReentrantLock getLockForKey ( Object key ) { // Java8 新特性,Map系列类 中新增的方法 // V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) // 表示,若 key 对应的 value 为空,则将第二个参数的返回值存入该 Map集合 并返回 return locks . computeIfAbsent ( key , k -> new ReentrantLock ()); }
假设 线程 A 从数据库中查找到 keyA 对应的结果对象后,将结果对象放入到 BlockingCache 中,此时 线程 A 会释放 keyA 对应的锁,唤醒阻塞在该锁上的线程。其它线程即可从 BlockingCache 中获取 keyA 对应的数据,而不是再次访问数据库。
@Override public void putObject ( Object key , Object value ) { try { // 存入 key 和其对应的缓存项 delegate . putObject ( key , value ); } finally { // 最后释放锁 releaseLock ( key ); } } private void releaseLock ( Object key ) { ReentrantLock lock = locks . get ( key ); // 锁是否被当前线程持有 if ( lock . isHeldByCurrentThread ()) { // 是,则释放锁 lock . unlock (); } }
1.3 FifoCache 和 LruCache
在很多场景中,为了控制缓存的大小,系统需要按照一定的规则清理缓存。FifoCache 是先入先出版本的装饰器,当向缓存添加数据时,如果缓存项的个数已经达到上限,则会将缓存中最老(即最早进入缓存)的缓存项删除。
public class FifoCache implements Cache { // 被装饰对象 private final Cache delegate ; // 用一个 FIFO 的队列记录 key 的顺序,其具体实现为 LinkedList private final Deque < Object > keyList ; // 决定了缓存的容量上限 private int size ; // 国际惯例,通过构造方法初始化自己的属性,缓存容量上限默认为 1024个 public FifoCache ( Cache delegate ) { this . delegate = delegate ; this . keyList = new LinkedList <>(); this . size = 1024 ; } @Override public String getId () { return delegate . getId (); } @Override public int getSize () { return delegate . getSize (); } public void setSize ( int size ) { this . size = size ; } @Override public void putObject ( Object key , Object value ) { // 存储缓存项之前,先在 keyList 中注册 cycleKeyList ( key ); // 存储缓存项 delegate . putObject ( key , value ); } private void cycleKeyList ( Object key ) { // 在 keyList队列 中注册要添加的 key keyList . addLast ( key ); // 如果注册这个 key 会超出容积上限,则把最老的一个缓存项清除掉 if ( keyList . size () > size ) { Object oldestKey = keyList . removeFirst (); delegate . removeObject ( oldestKey ); } } @Override public Object getObject ( Object key ) { return delegate . getObject ( key ); } @Override public Object removeObject ( Object key ) { return delegate . removeObject ( key ); } // 除了清理缓存项,还要清理 key 的注册列表 @Override public void clear () { delegate . clear (); keyList . clear (); } }
LruCache 是按照"近期最少使用算法"(Least Recently Used, LRU)进行缓存清理的装饰器,在需要清理缓存时,它会清除最近最少使用的缓存项。
public class LruCache implements Cache { // 被装饰者 private final Cache delegate ; // 这里使用的是 LinkedHashMap,它继承了 HashMap,但它的元素是有序的 private Map < Object , Object > keyMap ; // 最近最少被使用的缓存项的 key private Object eldestKey ; // 国际惯例,构造方法中进行属性初始化 public LruCache ( Cache delegate ) { this . delegate = delegate ; // 这里初始化了 keyMap,并定义了 eldestKey 的取值规则 setSize ( 1024 ); } public void setSize ( final int size ) { // 初始化 keyMap,同时指定该 Map 的初始容积及加载因子,第三个参数true 表示 该LinkedHashMap // 记录的顺序是 accessOrder,即,LinkedHashMap.get()方法 会改变其中元素的顺序 keyMap = new LinkedHashMap < Object , Object >( size , . 75F , true ) { private static final long serialVersionUID = 4267176411845948333L ; // 当调用 LinkedHashMap.put()方法 时,该方法会被调用 @Override protected boolean removeEldestEntry ( Map . Entry < Object , Object > eldest ) { boolean tooBig = size () > size ; if ( tooBig ) { // 当已达到缓存上限,更新 eldestKey字段,后面将其删除 eldestKey = eldest . getKey (); } return tooBig ; } }; } // 存储缓存项 @Override public void putObject ( Object key , Object value ) { delegate . putObject ( key , value ); // 记录缓存项的 key,超出容量则清除最久未使用的缓存项 cycleKeyList ( key ); } private void cycleKeyList ( Object key ) { keyMap . put ( key , key ); // eldestKey 不为空,则表示已经达到缓存上限 if ( eldestKey != null ) { // 清除最久未使用的缓存 delegate . removeObject ( eldestKey ); // 制空 eldestKey = null ; } } @Override public Object getObject ( Object key ) { // 访问 key元素 会改变该元素在 LinkedHashMap 中的顺序 keyMap . get ( key ); //touch return delegate . getObject ( key ); } @Override public String getId () { return delegate . getId (); } @Override public int getSize () { return delegate . getSize (); } @Override public Object removeObject ( Object key ) { return delegate . removeObject ( key ); } @Override public void clear () { delegate . clear (); keyMap . clear (); } }
1.4 SoftCache 和 WeakCache
在分析 SoftCache 和 WeakCache 实现之前,我们再温习一下 Java 提供的 4 种引用类型,强引用 StrongReference、软引用 SoftReference、弱引用 WeakReference 和虚引用 PhantomReference。
• 强引用 平时用的最多的,如 Object obj = new Object(),新建的 Object 对象 就是被强引用的。如果一个对象被强引用,即使是 JVM 内存空间不足,要抛出 OutOfMemoryError 异常,GC 也绝不会回收该对象。• 软引用 仅次于强引用的一种引用,它使用类 SoftReference 来表示。当 JVM 内存不足时,GC 会回收那些只被软引用指向的对象,从而避免内存溢出。软引用适合引用那些可以通过其他方式恢复的对象,例如, 数据库缓存中的对象就可以从数据库中恢复,所以软引用可以用来实现缓存,下面要介绍的 SoftCache 就是通过软引用实现的。 另外,由于在程序使用软引用之前的某个时刻,其所指向的对象可能己经被 GC 回收掉了,所以通过 Reference.get()方法 来获取软引用所指向的对象时,总是要通过检查该方法返回值是否为 null,来判断被软引用的对象是否还存活。• 弱引用 弱引用使用 WeakReference 表示,它不会阻止所引用的对象被 GC 回收。在 JVM 进行垃圾回收时,如果指向一个对象的所有引用都是弱引用,那么该对象会被回收。所以,只被弱引用所指向的对象,其生存周期是 两次 GC 之间 的这段时间,而只被软引用所指向的对象可以经历多次 GC,直到出现内存紧张的情况才被回收。• 虚引用 最弱的一种引用类型,由类 PhantomReference 表示。虚引用可以用来实现比较精细的内存使用控制,但很少使用。• 引用队列(ReferenceQueue ) 很多场景下,我们的程序需要在一个对象被 GC 时得到通知,引用队列就是用于收集这些信息的队列。在创建 SoftReference 对象 时,可以为其关联一个引用队列,当 SoftReference 所引用的对象被 GC 时, JVM 就会将该 SoftReference 对象 添加到与之关联的引用队列中。当需要检测这些通知信息时,就可以从引用队列中获取这些 SoftReference 对象。不仅是 SoftReference,弱引用和虚引用都可以关联相应的队列。
现在来看一下 SoftCache 的具体实现。
public class SoftCache implements Cache { // 这里使用了 LinkedList 作为容器,在 SoftCache 中,最近使用的一部分缓存项不会被 GC // 这是通过将其 value 添加到 hardLinksToAvoidGarbageCollection集合 实现的(即,有强引用指向其value) private final Deque < Object > hardLinksToAvoidGarbageCollection ; // 引用队列,用于记录已经被 GC 的缓存项所对应的 SoftEntry对象 private final ReferenceQueue < Object > queueOfGarbageCollectedEntries ; // 持有的被装饰者 private final Cache delegate ; // 强连接的个数,默认为 256 private int numberOfHardLinks ; // 构造方法进行属性的初始化 public SoftCache ( Cache delegate ) { this . delegate = delegate ; this . numberOfHardLinks = 256 ; this . hardLinksToAvoidGarbageCollection = new LinkedList <>(); this . queueOfGarbageCollectedEntries = new ReferenceQueue <>(); } private static class SoftEntry extends SoftReference < Object > { private final Object key ; SoftEntry ( Object key , Object value , ReferenceQueue < Object > garbageCollectionQueue ) { // 指向 value 的引用是软引用,并且关联了 引用队列 super ( value , garbageCollectionQueue ); // 强引用 this . key = key ; } } @Override public void putObject ( Object key , Object value ) { // 清除已经被 GC 的缓存项 removeGarbageCollectedItems (); // 添加缓存 delegate . putObject ( key , new SoftEntry ( key , value , queueOfGarbageCollectedEntries )); } private void removeGarbageCollectedItems () { SoftEntry sv ; // 遍历 queueOfGarbageCollectedEntries集合,清除已经被 GC 的缓存项 value while (( sv = ( SoftEntry ) queueOfGarbageCollectedEntries . poll ()) != null ) { delegate . removeObject ( sv . key ); } } @Override public Object getObject ( Object key ) { Object result = null ; @SuppressWarnings ( "unchecked" ) // assumed delegate cache is totally managed by this cache // 用一个软引用指向 key 对应的缓存项 SoftReference < Object > softReference = ( SoftReference < Object >) delegate . getObject ( key ); // 检测缓存中是否有对应的缓存项 if ( softReference != null ) { // 获取 softReference 引用的 value result = softReference . get (); // 如果 softReference 引用的对象已经被 GC,则从缓存中清除对应的缓存项 if ( result == null ) { delegate . removeObject ( key ); } else { synchronized ( hardLinksToAvoidGarbageCollection ) { // 将缓存项的 value 添加到 hardLinksToAvoidGarbageCollection集合 中保存 hardLinksToAvoidGarbageCollection . addFirst ( result ); // 如果 hardLinksToAvoidGarbageCollection 的容积已经超过 numberOfHardLinks // 则将最老的缓存项从 hardLinksToAvoidGarbageCollection 中清除,FIFO if ( hardLinksToAvoidGarbageCollection . size () > numberOfHardLinks ) { hardLinksToAvoidGarbageCollection . removeLast (); } } } } return result ; } @Override public Object removeObject ( Object key ) { // 清除指定的缓存项之前,也会先清理被 GC 的缓存项 removeGarbageCollectedItems (); return delegate . removeObject ( key ); } @Override public void clear () { synchronized ( hardLinksToAvoidGarbageCollection ) { // 清理强引用集合 hardLinksToAvoidGarbageCollection . clear (); } // 清理被 GC 的缓存项 removeGarbageCollectedItems (); // 清理最底层的缓存项 delegate . clear (); } @Override public String getId () { return delegate . getId (); } @Override public int getSize () { removeGarbageCollectedItems (); return delegate . getSize (); } public void setSize ( int size ) { this . numberOfHardLinks = size ; } }
WeakCache 的实现与 SoftCache 基本类似,唯一的区别在于其中使用 WeakEntry(继承了 WeakReference)封装真正的 value 对象,其他实现完全一样。
另外,还有 ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache 等。ScheduledCache 是周期性清理缓存的装饰器,它的 clearInterval 字段 记录了两次缓存清理之间的时间间隔,默认是一小时,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 对象。不使用 SerializedCache 装饰器 进行装饰的话,每次从缓存中获取同一 key 对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程,以及缓存中的对象。而使用 SerializedCache 每次从缓存中获取数据时,都会通过反序列化得到一个全新的对象。SerializedCache 使用的序列化方式是 Java 原生序列化。
2 CacheKey
在 Cache 中唯一确定一个缓存项,需要使用缓存项的 key 进行比较,MyBatis 中因为涉及 动态 SQL 等多方面因素, 其缓存项的 key 不能仅仅通过一个 String 表示,所以 MyBatis 提供了 CacheKey 类 来表示缓存项的 key,在一个 CacheKey 对象 中可以封装多个影响缓存项的因素。CacheKey 中可以添加多个对象,由这些对象共同确定两个 CacheKey 对象 是否相同。
public class CacheKey implements Cloneable , Serializable { private static final long serialVersionUID = 1146682552656046210L ; public static final CacheKey NULL_CACHE_KEY = new NullCacheKey (); private static final int DEFAULT_MULTIPLYER = 37 ; private static final int DEFAULT_HASHCODE = 17 ; // 参与计算hashcode,默认值DEFAULT_MULTIPLYER = 37 private final int multiplier ; // 当前CacheKey对象的hashcode,默认值DEFAULT_HASHCODE = 17 private int hashcode ; // 校验和 private long checksum ; private int count ; // 由该集合中的所有元素 共同决定两个CacheKey对象是否相同,一般会使用一下四个元素 // MappedStatement的id、查询结果集的范围参数(RowBounds的offset和limit) // SQL语句(其中可能包含占位符"?")、SQL语句中占位符的实际参数 private List < Object > updateList ; // 构造方法初始化属性 public CacheKey () { this . hashcode = DEFAULT_HASHCODE ; this . multiplier = DEFAULT_MULTIPLYER ; this . count = 0 ; this . updateList = new ArrayList <>(); } public CacheKey ( Object [] objects ) { this (); updateAll ( objects ); } public void update ( Object object ) { int baseHashCode = object == null ? 1 : ArrayUtil . hashCode ( object ); // 重新计算count、checksum和hashcode的值 count ++; checksum += baseHashCode ; baseHashCode *= count ; hashcode = multiplier * hashcode + baseHashCode ; // 将object添加到updateList集合 updateList . add ( object ); } public int getUpdateCount () { return updateList . size (); } public void updateAll ( Object [] objects ) { for ( Object o : objects ) { update ( o ); } } /** * CacheKey 重写了 equals () 和 hashCode ()方法,这两个方法使用上面介绍 * 的 count 、 checksum 、 hashcode 、 updateList 比较两个 CacheKey 对象 是否相同 */ @Override public boolean equals ( Object object ) { // 如果为同一对象,直接返回 true if ( this == object ) { return true ; } // 如果 object 都不是 CacheKey类型,直接返回 false if (!( object instanceof CacheKey )) { return false ; } // 类型转换一下 final CacheKey cacheKey = ( CacheKey ) object ; // 依次比较 hashcode、checksum、count,如果不等,直接返回 false if ( hashcode != cacheKey . hashcode ) { return false ; } if ( checksum != cacheKey . checksum ) { return false ; } if ( count != cacheKey . count ) { return false ; } // 比较 updateList 中的元素是否相同,不同直接返回 false for ( int i = 0 ; i < updateList . size (); i ++) { Object thisObject = updateList . get ( i ); Object thatObject = cacheKey . updateList . get ( i ); if (! ArrayUtil . equals ( thisObject , thatObject )) { return false ; } } return true ; } @Override public int hashCode () { return hashcode ; } @Override public String toString () { StringJoiner returnValue = new StringJoiner ( ":" ); returnValue . add ( String . valueOf ( hashcode )); returnValue . add ( String . valueOf ( checksum )); updateList . stream (). map ( ArrayUtil :: toString ). forEach ( returnValue :: add ); return returnValue . toString (); } @Override public CacheKey clone () throws CloneNotSupportedException { CacheKey clonedCacheKey = ( CacheKey ) super . clone (); clonedCacheKey . updateList = new ArrayList <>( updateList ); return clonedCacheKey ; } }
全文完!
希望本文对大家有所帮助。如果感觉本文有帮助,有劳转发或点一下“在看”!让更多人收获知识!
长按识别下图二维码,关注公众号「Doocs 开源社区 」,第一时间跟你们分享好玩、实用的技术文章与业内最新资讯。