引言
在这篇文章,我们详细解释一下二级缓存的实现原理。
开始
介绍
MyBatis 的二级缓存是 Mapper 命名空间级别 的缓存,它的核心设计目标是通过共享缓存数据,减少跨会话的重复查询。与一级缓存不同,二级缓存的作用域不再局限于单个 SqlSession,而是扩展到同一个 Mapper 的命名空间(Namespace)。这意味着,无论多个 SqlSession 是否相同,只要它们操作的是同一个 Mapper 的 SQL,就可以共享缓存数据。因此,二级缓存也被称为 namespace 级别的缓存,其作用域范围比一级缓存更广。
在设计二级缓存时,核心思路是为 Mapper XML 解析后的 MappedStatement 映射器语句提供缓存服务。具体来说,当某个 SqlSession 的生命周期结束时,需要将其一级缓存中的数据刷新到二级缓存中,以便后续在相同命名空间下执行相同 SQL 时可以直接使用缓存数据,避免重复查询数据库。
为了实现这一目标,首先需要在 XML 解析阶段添加全局缓存配置的支持,允许用户通过配置开启或关闭二级缓存。由于缓存的作用域是 Mapper 的命名空间级别,因此在解析 MappedStatement 时,还需要为其指定缓存策略。MyBatis 提供了四种缓存策略:LRU(最近最少使用)、FIFO(先进先出)、SOFT(软引用) 和 WEAK(弱引用)。在本文中,我们以 FIFO 先进先出策略 为例进行实现。
当用户配置开启二级缓存后,在创建 SqlSession 时,会将其执行器(Executor)用缓存执行器(CachingExecutor)进行装饰。这种装饰器模式的设计是为了将事务中的缓存操作统一管理起来。具体来说,在事务提交或会话关闭时,缓存执行器会负责将一级缓存中的数据刷新到二级缓存中。这样,在后续执行相同命名空间下的相同 SQL 时,就可以直接从二级缓存中获取数据,而无需再次访问数据库。
通过这种设计,MyBatis 的二级缓存不仅实现了跨会话的数据共享,还通过事务管理和缓存策略的灵活配置,确保了缓存数据的一致性和高效性。
创建二级缓存队列
MyBatis 的二级缓存设计具有高度的灵活性,允许开发者根据实际需求对缓存策略进行多种调整。通过配置,你可以选择不同的缓存策略,如 LRU(最近最少使用)、FIFO(先进先出)、SOFT(软引用) 和 WEAK(弱引用),同时还可以设置数据刷新策略、对象存储限制等参数,以满足不同场景下的性能优化需求。
以下是 MyBatis 支持的缓存策略及其特点:
LRU(Least Recently Used,最近最少使用) 该策略会优先移除最长时间未被访问的缓存对象。LRU 是 MyBatis 默认的缓存策略,适合大多数场景,能够有效提高缓存命中率。
FIFO(First In First Out,先进先出) 该策略按照对象进入缓存的顺序移除过期对象。适合缓存数据量较大且访问频率相对均匀的场景。
SOFT(软引用) 该策略基于垃圾回收器的状态和软引用规则移除对象。当内存不足时,垃圾回收器会优先回收软引用对象,适合需要平衡内存使用和缓存性能的场景。
WEAK(弱引用) 该策略比软引用更加主动,基于垃圾回收器的状态和弱引用规则移除对象。适合缓存数据生命周期较短或需要频繁更新的场景。
除了内置的缓存策略外,MyBatis 还支持开发者自定义缓存实现,或者与第三方内存缓存库(如 Redis、Ehcache 等)进行集成,从而满足更复杂的业务需求。在本文的框架开发中,我们仅实现 FIFO 缓存策略,作为二级缓存的基础功能。
通过这种灵活的缓存设计,MyBatis 能够帮助开发者在不同场景下优化数据访问性能,同时兼顾内存使用效率和数据一致性。
FIFO(先进先出)缓存的核心设计是基于 Deque(双端队列) 维护的一个链表结构。它通过队列的特性来实现缓存对象的淘汰策略,而具体的缓存操作(如存放、获取、移除、清空)则委托给底层的 Cache 对象完成。这种设计采用了典型的 装饰器模式,既保留了缓存的基本功能,又增加了 FIFO 的淘汰机制。
在 FifoCache 的实现中,主要提供了以下几个方法:
存放(**putObject**)将缓存对象存入底层 Cache,同时将其键值添加到队列的末尾。如果队列的长度超过了预设的 size 值,则会调用 cycleKeyList 方法移除队列的第一个元素,确保缓存总量不超过限制。
获取(**getObject**)直接从底层 Cache 中获取缓存对象,不改变队列的顺序。
移除(**removeObject**)从底层 Cache 中移除指定键的缓存对象,同时将其从队列中删除。
清空(**clear**)清空底层 Cache 中的所有缓存对象,并重置队列。
其中,cycleKeyList 方法是实现 FIFO 淘汰策略的关键。它的作用是在添加新记录时,检查当前队列的长度是否超过了 size 值。如果超出,则移除队列的第一个元素(即最早添加的缓存对象),从而保证缓存的总量始终控制在预设范围内。
通过这种设计,FifoCache 不仅能够高效地管理缓存对象,还能确保缓存的淘汰行为符合 FIFO 的原则,是一种简单而实用的缓存实现。
创建事务缓存管理
TransactionalCache 的核心职责是管理一级缓存在事务期间的数据操作,并在会话结束时将数据刷新到二级缓存中。具体来说,当一级缓存的作用域范围内的会话因为提交(commit)或关闭(close)而结束时,TransactionalCache 会调用 flushPendingEntries 方法。这个方法会遍历所有暂存的缓存数据,并通过 delegate.putObject(entry.getKey(), entry.getValue()) 将这些数据刷新到二级缓存队列中,确保其他会话可以共享这些缓存数据。
另一方面,如果会话执行了回滚操作(rollback),TransactionalCache 则会清空所有暂存的缓存数据,避免无效或错误的数据被写入二级缓存。这种设计不仅保证了事务的一致性,还确保了缓存数据的有效性。
通过这种机制,TransactionalCache 在事务管理和缓存共享之间起到了关键作用。它既支持在事务提交时将数据刷新到二级缓存,也能够在事务回滚时及时清理缓存,从而维护缓存数据的正确性和一致性。
创建事务缓存管理器
事务缓存管理器(TransactionalCacheManager)的核心作用是对事务缓存(TransactionalCache)进行统一管理和包装。它在缓存执行器(CachingExecutor)创建时被实例化,负责管理执行器范围内的所有事务缓存操作。
事务缓存管理器的主要功能是,在执行器范围内对事务缓存进行批量提交或回滚时,统一处理缓存数据的刷新操作。具体来说:
批量提交(commit) 当事务提交时,事务缓存管理器会遍历所有关联的 TransactionalCache 实例,并调用其 commit 方法。这会将暂存的事务缓存数据刷新到二级缓存中,确保其他会话可以访问这些数据。
批量回滚(rollback) 当事务回滚时,事务缓存管理器会遍历所有关联的 TransactionalCache 实例,并调用其 rollback 方法。这会清空所有暂存的事务缓存数据,避免无效或错误的数据被写入二级缓存。
通过这种设计,事务缓存管理器将事务缓存的操作集中管理,简化了缓存执行器的逻辑,同时确保了在事务提交或回滚时,缓存数据的刷新行为是统一且一致的。这种批量处理机制不仅提高了性能,还增强了缓存数据管理的可靠性。
装饰缓存执行
public Executor newExecutor(Transaction transaction) {
Executor executor = new SimpleExecutor(this, transaction);
// 配置开启缓存,创建 CachingExecutor(默认就是有缓存)装饰者模式
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
return executor;
}
二级缓存的流转 当会话结束时(如提交 commit 或关闭 close),CachingExecutor 会将一级缓存中的数据刷新到二级缓存中。这样,其他会话在相同命名空间下执行相同 SQL 时,可以直接从二级缓存中获取数据,而无需再次访问数据库。
事务管理 在事务提交或回滚时,CachingExecutor 会通过事务缓存管理器(TransactionalCacheManager)统一处理缓存数据的刷新或清理,确保缓存数据的一致性和正确性。
通过这种装饰器模式的设计,CachingExecutor 将一级缓存和二级缓存的功能无缝整合,既保留了 SimpleExecutor 的基础能力,又扩展了缓存的跨会话共享能力,从而显著提升了 MyBatis 的性能和灵活性。
创建CachingExecutor
在 CachingExecutor 的实现中,核心关注点在于会话期间的数据查询如何利用缓存。具体来说,query 方法中的 delegate.<E>query 操作是关键,这里的 delegate 实际上是 SimpleExecutor 的实例。SimpleExecutor 负责执行具体的数据库查询操作,而 CachingExecutor 则在此基础上添加了缓存逻辑。
当查询操作在会话中执行时,CachingExecutor 会首先检查二级缓存(即 MappedStatement 中配置的 Cache 缓存队列,如本章实现的 FifoCache 先进先出缓存)。如果二级缓存中未命中,则委托 SimpleExecutor 执行数据库查询,并将结果写入一级缓存(会话级别缓存)。随着会话的生命周期结束,缓存数据会根据事务状态(提交或回滚)进行流转:
提交(commit):通过 TransactionalCacheManager 事务缓存管理器,将一级缓存中的数据刷新到二级缓存中,确保其他会话可以共享这些数据。
回滚(rollback):清空事务缓存中的数据,避免无效或错误的数据被写入二级缓存。
简而言之,CachingExecutor 通过装饰器模式将 SimpleExecutor 包装起来,在查询操作中优先使用缓存,同时通过 TransactionalCacheManager 管理缓存的流转,确保数据在会话结束时能够正确同步到二级缓存或清空。这种设计不仅提升了查询性能,还保证了缓存数据的一致性和事务安全性。