MyBatis 缓存是如何工作的?
MyBatis 的缓存工作机制主要围绕一级缓存和二级缓存展开:
一级缓存 (SqlSession 级别):
- 开启与作用域: 默认开启,作用域是
SqlSession
。每个SqlSession
内部维护一个简单的HashMap
作为缓存。 - 工作流程:
- 当
SqlSession
执行一个查询时,它会先根据特定的规则生成一个缓存Key
。 - 使用这个
Key
尝试在当前SqlSession
的一级缓存 (HashMap
) 中查找结果。 - 缓存命中 (Hit): 如果找到了对应的结果对象(引用),则直接返回该对象引用,不再查询数据库。
- 缓存未命中 (Miss): 如果缓存中没有找到,则执行数据库查询。
- 查询数据库后,将获取到的结果对象(引用)存入当前
SqlSession
的一级缓存中,Key
就是之前生成的那个。
- 当
- 生命周期与失效:
- 一级缓存的生命周期与
SqlSession
完全一致。当SqlSession
关闭 (close()
) 时,一级缓存被清空。 - 执行
commit()
操作时,会清空一级缓存(即使没有修改操作,因为无法确定是否有其他会话修改了数据)。 - 执行任何
INSERT
,UPDATE
,DELETE
操作时,会清空一级缓存,以保证缓存数据的准确性。 - 手动调用
sqlSession.clearCache()
会清空一级缓存。
- 一级缓存的生命周期与
- 开启与作用域: 默认开启,作用域是
二级缓存 (Mapper Namespace 级别):
- 开启与作用域: 默认关闭,需要显式配置开启。作用域是 Mapper 的
Namespace
,可以被多个SqlSession
共享。通常使用一个实现了Cache
接口的类来存储(默认是PerpetualCache
,内部也是HashMap
,但可配置为 Ehcache, Redis 等)。 - 工作流程:
- 当一个
SqlSession
执行查询时,如果该 Mapper 配置了二级缓存 (<cache/>
标签) 并且全局开关已打开 (cacheEnabled=true
):- 它会先尝试根据规则生成的
Key
在二级缓存中查找。 - 二级缓存命中: 如果找到,并且缓存配置为
readOnly="false"
(默认),则返回结果对象的反序列化副本;如果配置为readOnly="true"
,则返回对象引用(性能高但有线程安全风险)。 - 二级缓存未命中: 继续查找一级缓存。
- 一级缓存命中: 返回一级缓存中的对象引用。
- 一级缓存也未命中: 执行数据库查询。
- 查询结果存入一级缓存。
- 它会先尝试根据规则生成的
- 数据进入二级缓存: 当
SqlSession
提交 (commit()
) 或关闭 (close()
) 时,一级缓存中与配置了二级缓存的 Mapper 相关的数据,会被刷新(放入)到该 Mapper Namespace 对应的二级缓存中(通常是序列化存储)。
- 当一个
- 生命周期与失效:
- 二级缓存的生命周期与应用程序(
SqlSessionFactory
)相关,除非被策略性清除或手动清除。 - 当同一个
Namespace
下执行了任何INSERT
,UPDATE
,DELETE
操作(且flushCache="true"
,这是默认值)时,该Namespace
的二级缓存会被清空。 - 可以通过
<cache/>
标签的flushInterval
属性设置定时清空。 - 缓存达到最大容量 (
size
属性) 时,会根据eviction
策略进行淘汰。 - 可以手动获取
Cache
对象并调用clear()
方法。
- 二级缓存的生命周期与应用程序(
- 开启与作用域: 默认关闭,需要显式配置开启。作用域是 Mapper 的
缓存的 Key 是如何生成的?
缓存的 Key
对于确保缓存的正确性至关重要。MyBatis 需要确保只有在完全相同的查询条件下才能命中缓存。缓存 Key
(CacheKey
对象)的生成通常由以下几个部分组成,以保证其唯一性:
- MappedStatement 的 ID: 即 Mapper 接口的全限定名 + 方法名(或 XML 中的
<select>
等标签的 ID)。这唯一标识了你执行的是哪条 SQL 定义。 - 查询参数 (Parameter Object): 传递给 SQL 语句的所有参数的值。不同的参数值会导致不同的查询结果,因此必须包含在 Key 中。MyBatis 会遍历参数对象(或 Map)的属性值并加入 Key 的计算。
- 分页信息 (RowBounds): 即查询的偏移量 (offset) 和限制数量 (limit)。不同的分页参数会返回不同的数据子集。
- SQL 语句本身 (BoundSql): 经过动态 SQL 解析后生成的最终可执行 SQL 字符串。虽然
MappedStatement
ID 和参数通常能确定 SQL,但为了处理一些极端或复杂的动态 SQL 场景,将最终的 SQL 字符串也纳入 Key 的计算能提供更严格的唯一性保证。 - 环境 ID (Environment ID): 如果在
mybatis-config.xml
中配置了多个环境 (<environments>
),当前使用的环境 ID 也会作为 Key 的一部分,以隔离不同数据源环境下的缓存。
生成过程简述:
- 在执行器 (
Executor
) 准备执行查询时,会调用createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)
方法。 - 该方法创建一个
CacheKey
对象。 CacheKey
对象内部维护一个updateList
(一个ArrayList
)。- 依次调用
CacheKey
的update()
方法,将上述几个组成部分(MappedStatement ID、RowBounds 的 offset 和 limit、BoundSql 的 SQL 字符串、参数值)添加到updateList
中。 - 最终,
CacheKey
会根据updateList
中所有元素的hashCode
和equals
方法来计算自身的hashCode
和实现equals
逻辑。
这个 CacheKey
对象随后被用作在一级缓存和二级缓存(通常是 HashMap
或类似结构)中存取值的键。
缓存的淘汰策略有哪些?
缓存的淘汰策略定义了当缓存达到其配置的最大容量时,应该移除哪些条目以便为新条目腾出空间。这些策略在 Mapper XML 文件的 <cache/>
标签的 eviction
属性中配置:
LRU
(Least Recently Used): 最近最少使用 (默认)- 移除最长时间未被访问的缓存条目。
- 它假设最近被访问的数据将来也更可能被访问。
- 这是 MyBatis 的默认淘汰策略,适用于大多数场景。
FIFO
(First In First Out): 先进先出- 移除最早进入缓存的条目,无论它最近是否被访问过。
- 实现简单,但如果旧条目仍然被频繁访问,则效率不高。
SOFT
(Soft Reference): 软引用- 基于 Java 的软引用 (
java.lang.ref.SoftReference
)。 - 缓存条目会被垃圾回收器标记,只有在内存不足时,垃圾回收器才会回收这些被软引用指向的对象。
- 缓存的存活时间与 JVM 的内存压力有关。
- 基于 Java 的软引用 (
WEAK
(Weak Reference): 弱引用- 基于 Java 的弱引用 (
java.lang.ref.WeakReference
)。 - 缓存条目会被更积极地回收。只要发生垃圾回收,无论内存是否充足,被弱引用指向的对象都可能被回收。
- 适用于那些可以轻易重新创建或获取,并且不希望它们长时间占用内存的缓存对象。这种策略用得相对较少。
- 基于 Java 的弱引用 (
配置示例:
<mapper namespace="com.example.mapper.UserMapper">
<!-- 使用 LRU 策略,缓存最多 1024 个对象引用,60 秒刷新一次,允许读写 -->
<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="false"/>
<!-- ... -->
</mapper>
选择哪种淘汰策略取决于具体的应用场景和对缓存数据访问模式的预估。LRU
是最常用且通常效果不错的选择。