news 2026/9/20 23:43:56

MyBatis 缓存模块源码解析:Cache 接口、装饰器家族与 CacheKey 设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis 缓存模块源码解析:Cache 接口、装饰器家族与 CacheKey 设计

MyBatis 缓存模块源码解析:Cache 接口、装饰器家族与 CacheKey 设计

【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter

导读

本文基于 source-code-hunter 仓库对 MyBatis 基础支持层缓存模块的源码剖析,系统讲解org.apache.ibatis.cache包下的Cache接口、以PerpetualCache为核心的装饰器家族(BlockingCacheFifoCacheLruCacheSoftCacheWeakCache等)以及缓存键CacheKey的设计原理。读完本文,你将理解 MyBatis 一级缓存与二级缓存的底层存储与扩展机制,掌握如何通过组合装饰器定制缓存行为,并弄清 SQL 相同但参数不同的两条查询为何不会命中同一条缓存。

1 缓存模块的整体设计

MyBatis 中的缓存分为一级缓存、二级缓存,但在本质上是相同的,它们使用的都是Cache接口的实现。MyBatis 缓存模块的设计使用了装饰器模式Cache接口提供统一的缓存行为契约,PerpetualCache是唯一提供基本实现(被装饰者)的类,cache.decorators包下的各类装饰器在它的基础上附加阻塞、容量控制、回收策略、日志、同步、序列化等额外功能,多个装饰器组合起来满足一个特定的需求。

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(); /** * 获取读写锁,可以看到,这个接口方法提供了默认的实现 */ default ReadWriteLock getReadWriteLock() { return null; } }

注意getReadWriteLock()是 Java 8 引入的默认方法,平时开发中很少直接使用,但在 MyBatis 中它允许Cache实现类按需暴露读写锁(例如用于TransactionalCacheManager的加锁控制),同时不破坏已有实现。

如下图所示,Cache接口的实现类有很多,但大部分都是装饰器,只有PerpetualCache提供了Cache接口的基本实现:

2 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(); } @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(); } }

两个值得注意的细节:

  • id是 Cache 对象的唯一标识,重写的equals()hashCode()都只关心id字段,且当idnull时会抛出CacheException。这保证了同一命名空间下的缓存对象在逻辑上唯一,例如二级缓存中id通常是命名空间(namespace)。
  • 实现中没有任何锁或并发控制,因此PerpetualCache本身非线程安全,这正是SynchronizedCacheBlockingCache等装饰器存在的意义。

3 装饰器家族:cache.decorators 包

cache.decorators包下提供的装饰器都直接实现了Cache接口,扮演着装饰器的角色。它们会在PerpetualCache的基础上提供额外的功能,通过多个组合后满足一个特定的需求。下面逐一分析。

3.1 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<>(); } }

核心思想:按 key 加锁。假设线程 A 在BlockingCache中未查找到 keyA 对应的缓存项时,线程 A 会获取 keyA 对应的锁,这样在后续查找 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(); } }

补充完整的读取逻辑(与 docs/Mybatis/基础支持层/Mybatis-Cache.md 中的实现一致):getObject()acquireLock(key)加锁,再从delegate中取值;只有缓存命中value != null)时才立即releaseLock(key),若未命中则不释放锁,等待putObject()写入缓存后释放,从而保证同一 key 的并发查询只有一个线程真正访问数据库。

timeout属性上,setTimeout(long)可以设置加锁超时时长(毫秒);若timeout > 0则使用带超时的tryLock,超时抛CacheException,否则使用无超时的lock.lock()无限期阻塞。

3.2 FifoCache:先入先出缓存

在很多场景中,为了控制缓存的大小,系统需要按照一定的规则清理缓存。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(); } }

实现要点:

  • LinkedList实现的Deque记录 key 的插入顺序,新 key 追加到队尾(addLast);
  • 每次putObject时检查keyList.size() > size,一旦超出容量上限(默认 1024,可通过setSize调整),就从队头移除最老的 key,并同步删除delegate中对应的缓存项;
  • FifoCache只按进入顺序淘汰,不感知访问频率,因此适合对命中率要求不高的场景。

3.3 LruCache:最近最少使用缓存

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(); } }

实现要点:

  • 关键在LinkedHashMap构造方法的第三个参数true:它让keyMapaccessOrder(访问顺序)记录元素,即调用get()会将被访问的 key 移到链尾;
  • 重写removeEldestEntry():当keyMap的元素个数超过容量上限(默认 1024)时,将最久未访问的 key 记录到eldestKey
  • cycleKeyList()在写入缓存后把 key 放入keyMap,一旦eldestKey非空就删除delegate中对应的最久未使用缓存项;
  • getObject()中的keyMap.get(key)是一次“触摸(touch)”操作,用于更新访问顺序,保证热点数据不被淘汰。

3.4 SoftCache 和 WeakCache:基于引用类型的缓存

在分析SoftCacheWeakCache实现之前,先温习一下 Java 提供的 4 种引用类型:强引用StrongReference、软引用SoftReference、弱引用WeakReference和虚引用PhantomReference

  • 强引用:平时用得最多,如Object obj = new Object(),新建的 Object 对象就是被强引用的。如果一个对象被强引用,即使是 JVM 内存空间不足要抛出OutOfMemoryError,GC 也绝不会回收该对象。
  • 软引用:仅次于强引用的一种引用,使用类SoftReference表示。当 JVM 内存不足时,GC 会回收那些只被软引用指向的对象,从而避免内存溢出。软引用适合引用那些可以通过其他方式恢复的对象,例如数据库缓存中的对象可以从数据库中恢复,所以软引用可以用来实现缓存。另外,由于在程序使用软引用之前的某个时刻,其所指向的对象可能已经被 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; } }

SoftCache的设计非常精妙,核心机制有三点:

  1. 软引用存值putObject时将 value 包装成SoftEntry(继承SoftReference)存入底层缓存,value 只被软引用指向,JVM 内存紧张时 GC 可以回收它,避免缓存拖垮堆内存;
  2. 强引用“防回收”hardLinksToAvoidGarbageCollection集合保存最近访问的 value(默认最多 256 个,可通过setSize调整),相当于给最近使用的缓存项增加了强引用,使它们不会被 GC 误伤,兼顾了缓存命中率与内存安全;
  3. 引用队列“扫垃圾”SoftEntry关联了queueOfGarbageCollectedEntries引用队列,被 GC 回收的 value 对应的SoftEntry会进入队列,removeGarbageCollectedItems()遍历队列并同步删除底层缓存中的失效项,避免死数据残留。

WeakCache的实现与SoftCache基本类似,唯一的区别在于其中使用WeakEntry(继承WeakReference)封装真正的 value 对象,其他实现完全一样。由于弱引用在每次 GC 时都可能回收对象,WeakCache的缓存命中率更低、内存占用更小。

3.5 其余装饰器:ScheduledCache、LoggingCache、SynchronizedCache、SerializedCache

除了上述几个装饰器,cache.decorators包下还有如下四个:

  • ScheduledCache:周期性清理缓存的装饰器。clearInterval字段记录两次缓存清理之间的时间间隔,默认是一小时lastClear字段记录最近一次清理的时间戳。getObject()putObject()removeObject()等核心方法在执行时都会根据这两个字段检测是否需要进行清理操作,清理操作会清空缓存中所有缓存项。
  • LoggingCache:在 Cache 的基础上提供日志功能。通过hit字段和request字段记录 Cache 的命中次数和访问次数。在LoggingCache.getObject()方法中,会统计命中次数和访问次数这两个指标,并按照指定的日志输出方式输出命中率。
  • SynchronizedCache:通过在每个方法上添加synchronized关键字,为 Cache 添加同步功能,有点类似于 JDK 中CollectionsSynchronizedCollection内部类。由于PerpetualCache底层是HashMap,在多线程共享缓存时必须叠加该装饰器保证线程安全。
  • SerializedCache:提供将 value 对象序列化的功能。添加缓存项时,会将 value 对应的 Java 对象进行序列化,并将序列化后的byte[]数组作为 value 存入缓存;获取缓存项时,会将缓存项中的byte[]数组反序列化成 Java 对象。不使用SerializedCache装饰器的话,每次从缓存中获取同一 key 对应的对象时,得到的都是同一对象,任意一个线程修改该对象都会影响到其他线程以及缓存中的对象;而使用SerializedCache,每次从缓存中获取数据时都会通过反序列化得到一个全新的对象SerializedCache使用的序列化方式是 Java 原生序列化。

4 CacheKey:缓存键的设计

Cache中唯一确定一个缓存项,需要使用缓存项的 key 进行比较。MyBatis 中因为涉及动态 SQL等多方面因素,其缓存项的 key 不能仅仅通过一个String表示,所以 MyBatis 提供了CacheKey类来表示缓存项的 key。在一个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; } }

CacheKey的关键设计如下:

  • 多因素复合updateList集合中的元素共同决定两个CacheKey是否相等。一次查询产生的CacheKey一般由四个要素组成:MappedStatement的 id、查询结果集的范围参数(RowBounds的 offset 和 limit)、SQL 语句(可能包含占位符?)、SQL 语句中占位符的实际参数;
  • 分层快速比较equals()先比较hashcodechecksumcount三个整数,全部相等后才逐元素比较updateList,这种“先粗后细”的比较策略极大提升了缓存查找效率;
  • 散列累加算法update()使用multiplier(默认 37)作为乘数、DEFAULT_HASHCODE(默认 17)作为初始值,每次update都会累加checksum并重新计算hashcode,保证不同组合的要素大概率产生不同的 hash;
  • NULL_CACHE_KEYCacheKey.NULL_CACHE_KEYNullCacheKey)是一个预定义的空键,用于占位等特殊场景。

4.1 一级缓存中 CacheKey 的组装

在 MyBatis 的一级缓存中,CacheKey正是由BaseExecutor.createCacheKey()组装出来的,见 docs/Mybatis/核心处理层/5、Executor组件.md 的源码:它依次将MappedStatement的 id、RowBounds的 offset 与 limit、BoundSql的 SQL 字符串以及各个ParameterMapping对应的实参(过滤OUT输出参数)updateCacheKey,最后还会将configuration.getEnvironment()的 id 也加入其中。

这段逻辑直接印证了CacheKey注释中的四个要素,也解释了为什么“SQL 相同但参数不同”的两条查询会生成不同的CacheKey、不会共享缓存;而“完全相同”的查询(同一条 SQL、同样的参数、同一个环境)则能精确命中同一条缓存项。

5 缓存模块与一级缓存、二级缓存的关系

缓存模块是 MyBatis 一级缓存和二级缓存的公共底座,二者在本质上是相同的,都使用Cache接口的实现。

5.1 一级缓存

一级缓存是会话级(SqlSession 级)缓存。在 MyBatis 中每创建一个SqlSession对象,就表示开启一次数据库会话;在一次会话中,应用程序可能在一个事务内反复执行完全相同的查询语句,如果不缓存,每一次查询都会真正执行一次数据库查询,造成数据库资源浪费。

一级缓存的载体是BaseExecutor中的PerpetualCache localCache(以及用于缓存输出类型参数的localOutputParameterCache),默认开启、无需特殊配置。BaseExecutor.query()的执行流程是:

  1. 通过createCacheKey()创建CacheKey
  2. 根据该CacheKey查找一级缓存,命中则直接返回缓存中的结果对象;
  3. 未命中则调用queryFromDatabase()查询数据库,将结果集映射成结果对象后放入localCache并返回。

一级缓存的生命周期与SqlSession(即其中封装的Executor对象)相同:调用close()时缓存废弃;执行update()(insert/update/delete)时也会先clearLocalCache()清空缓存,保证数据一致性。另外,<select>节点配置flushCache="true"localCacheScope配置为STATEMENT时,查询后同样会清空一级缓存(相关源码同样位于 docs/Mybatis/核心处理层/5、Executor组件.md)。

5.2 二级缓存

二级缓存由CachingExecutorExecutor对象增加,它通过TransactionalCacheManager管理MappedStatement命名空间对应的Cache对象(该Cache即由PerpetualCache与各类装饰器组合而成)。从本仓库的源码看,docs/Mybatis/核心处理层/5、Executor组件.md 明确指出:MyBatis 的二级缓存在实际使用中往往利大于弊,常被 Redis 等外部缓存产品替代,因此该文档未对CachingExecutor展开分析。

6 小结

至此,MyBatis 基础支持层的主要模块就分析完了。就缓存模块而言,可以总结出以下要点:

  • 统一契约Cache接口定义了缓存的增删查、清空、大小与读写锁等基本行为,是所有缓存实现与装饰器的共同接口;
  • 被装饰者PerpetualCacheHashMap提供最基本、无并发控制的缓存能力;
  • 装饰器家族BlockingCache(防击穿)、FifoCache/LruCache(容量控制与淘汰策略)、SoftCache/WeakCache(内存友好)、ScheduledCache(定时清理)、LoggingCache(命中率统计)、SynchronizedCache(线程安全)、SerializedCache(深拷贝隔离)各司其职,通过组合即可定制出满足特定需求的完整缓存;
  • 缓存键CacheKey以“MappedStatement id + RowBounds + SQL + 实参 + 环境 id”多维复合,配合分层快速比较,为一级缓存和二级缓存的精确命中提供了可靠依据。

这些Cache接口及多个实现类的具体实现,正是 MyBatis 一级缓存和二级缓存的基础。本模块此前还分析了 MyBatis 对 Java 反射机制的封装、类型转换TypeHandler组件(数据在 Java 类型与 JDBC 类型之间转换)、DataSource模块与连接池PooledDataSourceTransaction模块,以及binding模块如何将 Mapper 接口与映射配置信息相关联,感兴趣的读者可以在 docs/Mybatis/基础支持层 目录下继续深入阅读。

【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 23:43:12

ESP32智能小车从零到一:蓝牙遥控、超声波避障与红外循迹完整实战

简介&#xff1a;面向ESP32开发者与智能小车入门者&#xff0c;这份压缩包提供从零搭建智能小车的完整源码与配套文档&#xff0c;解决硬件选型、电路设计、固件编程到无线控制的全流程问题。内容以构建指南为主线&#xff0c;覆盖ESP32双核、Wi-Fi/蓝牙特性&#xff0c;包含模…

作者头像 李华
网站建设 2026/9/20 23:43:04

极验无感验证码技术解析与实战优化

1. 项目概述"无感验证码"这个概念最近两年在互联网产品圈越来越火&#xff0c;作为从业者我亲身体验过市面上几乎所有验证码方案&#xff0c;今天要聊的极验无感验证码确实让我眼前一亮。不同于传统需要用户点击、拖拽或输入的验证方式&#xff0c;它能在用户几乎无感…

作者头像 李华
网站建设 2026/9/20 23:43:04

FT2DR操作手册实战指南:C4FM、APRS与菜单设置全解析

简介&#xff1a;这份资源是YAESU八重洲FT2DR对讲机的官方操作手册&#xff0c;以PDF格式提供&#xff0c;面向业余无线电爱好者、户外通信用户以及初次接触数字对讲机的新手&#xff0c;可解决从开箱安装、触摸屏操作到中继台、APRS与GPS功能配置的全流程使用疑问。压缩包内为…

作者头像 李华
网站建设 2026/9/20 23:42:43

oMLX分层KV缓存实战:SSD当后备存储,32GB内存跑32K上下文

先说结论&#xff1a;在 Apple Silicon 的机器上&#xff0c;把 SSD 当成 KV 缓存的后备存储&#xff0c;是让 32GB 内存跑 30B 级别模型并撑住上万 token 上下文的性价比方案。我这几个月一直在折腾 oMLX 的分层 KV 缓存&#xff0c;拿它当 Claude Code 的本地后端&#xff0c…

作者头像 李华