mybatis中的延迟加载和一二级缓存

MyBatis 延迟加载与一二级缓存核心知识点

一、延迟加载(Lazy Loading)

1. 核心定义

延迟加载是 MyBatis 对关联查询 的优化机制:查询主对象时,不立即查询其关联对象(如用户和账户的一对多关系),仅在实际使用关联对象 (调用 getter 方法)时,才触发关联查询的 SQL 执行。

2. 核心对比(延迟加载 vs 立即加载)

加载方式 执行时机 适用场景 优点 缺点
延迟加载 调用关联对象 getter 关联数据不常使用、关联表数据量大 减少无效查询,提升性能 可能触发 N+1 问题(循环查询时)
立即加载 查询主对象时(如 LEFT JOIN 关联) 关联数据必用、数据量小 一次查询完成,避免多次数据库交互 冗余数据加载,性能损耗

3. 实现原理

基于 动态代理 :查询主对象时,MyBatis 返回主对象的代理实例;当调用关联对象的 getter 方法时,代理对象拦截该调用,触发关联查询 SQL 执行,查询结果赋值后返回。

4. 配置要求(全局开启)

需在 SqlMapConfig.xml<settings> 标签中配置(MyBatis 默认为关闭):

xml

复制代码
<settings>
    <!-- 全局开启延迟加载(核心) -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 关闭积极加载(MyBatis 3.4.1+ 默认 false,低版本需手动设置)
         避免一次性加载所有关联对象 -->
    <setting name="aggressiveLazyLoading" value="false"/>
    <!-- 可选:指定触发延迟加载的方法(默认 equals/clone/hashCode/toString)
         调用这些方法不会触发延迟加载 -->
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

5. 关联查询配置(XML 示例)

一对多(用户 -> 账户)

xml

复制代码
<!-- UserMapper.xml -->
<resultMap id="userAccountMap" type="cn.tx.domain.User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <!-- collection 配置一对多关联,select 指定关联查询方法 -->
    <collection 
        property="accounts"       <!-- 主对象中关联属性名 -->
        ofType="cn.tx.domain.Account"  <!-- 关联对象类型 -->
        column="id"              <!-- 关联条件(主表主键 -> 从表外键) -->
        select="cn.tx.mapper.AccountMapper.findAccountsByUid"/>  <!-- 关联查询 SQL -->
</resultMap>

<!-- 主查询:仅查询用户,不加载账户 -->
<select id="findUserById" resultMap="userAccountMap">
    SELECT id, username FROM user WHERE id = #{id}
</select>
一对一(用户 -> 身份证)

xml

复制代码
<resultMap id="userIdCardMap" type="cn.tx.domain.User">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <!-- association 配置一对一关联 -->
    <association 
        property="idCard" 
        javaType="cn.tx.domain.IdCard"  <!-- 一对一用 javaType -->
        column="id" 
        select="cn.tx.mapper.IdCardMapper.findByIdCardByUid"/>
</resultMap>

6. 关键注意事项

  • SqlSession 存活要求 :延迟加载需通过 SqlSession 执行关联查询,因此调用 getter 前,SqlSession 不能关闭(否则报 PersistenceException)。
  • N+1 问题规避 :循环查询多个主对象时,避免逐个触发关联查询(1 次主查询 + N 次关联查询);大量数据建议用 LEFT JOIN 立即加载。
  • 实体类无特殊要求 :无需实现接口,仅需保证关联属性有 getter/setter 方法(代理对象需通过 getter 触发加载)。

二、MyBatis 缓存机制(一级缓存 + 二级缓存)

MyBatis 缓存的核心目的是减少数据库查询次数,提升性能,分为一级缓存(本地缓存)和二级缓存(全局缓存)。

1. 一级缓存(Local Cache)

(1)核心定义
  • 作用范围:单个 SqlSession 内部(会话级缓存),不同 SqlSession 互不共享。
  • 存储介质:内存(HashMap),默认开启,无需额外配置。
(2)工作流程
  1. 同一 SqlSession 执行相同查询(相同 SQL + 参数):
    • 首次查询:查数据库,结果存入一级缓存。
    • 二次查询:直接从缓存获取,不执行 SQL。
  2. 触发缓存清空的场景:
    • 执行 insert/update/delete 操作(自动清空当前 SqlSession 的一级缓存,保证数据一致性)。
    • 调用 sqlSession.clearCache() 手动清空。
    • SqlSession 关闭(缓存失效)。
(3)配置说明

默认开启,可通过 localCacheScope 调整作用域(全局配置):

xml

复制代码
<settings>
    <!-- SESSION(默认):缓存作用于整个 SqlSession -->
    <!-- STATEMENT:缓存仅作用于当前 SQL 语句,执行后立即清空 -->
    <setting name="localCacheScope" value="SESSION"/>
</settings>

2. 二级缓存(Second Level Cache)

(1)核心定义
  • 作用范围:全局共享 (跨 SqlSession),按 Mapper 接口(namespace)隔离(同一 namespace 共享缓存)。
  • 存储介质:默认内存(HashMap),可集成 Redis/Ehcache 等第三方缓存(持久化)。
  • 依赖要求:缓存的实体类必须实现 Serializable 接口(缓存对象需序列化存储)。
(2)工作流程
  1. 开启二级缓存后,SqlSession 关闭时,一级缓存中的数据会写入二级缓存。
  2. SqlSession 执行相同查询(同一 namespace + 相同 SQL + 参数):
    • 先查二级缓存,命中则返回。
    • 未命中则查数据库,结果存入一级缓存,SqlSession 关闭后同步到二级缓存。
  3. 触发缓存清空的场景:
    • 同一 namespace 下执行 insert/update/delete 操作(自动清空当前 namespace 的二级缓存)。
    • 配置 flushInterval 自动刷新(如 60 秒)。
    • 手动调用 sqlSessionFactory.getConfiguration().getCache(namespace).clear()
(3)完整配置步骤
步骤 1:实体类实现 Serializable

java

运行

复制代码
public class User implements Serializable { // 必须实现,否则缓存序列化失败
    private Integer id;
    private String username;
    private List<Account> accounts;
    // getter/setter/toString
}
步骤 2:全局开启二级缓存(SqlMapConfig.xml)

xml

复制代码
<settings>
    <!-- 全局开启二级缓存(默认 true,显式配置更清晰) -->
    <setting name="cacheEnabled" value="true"/>
    <!-- 可选:全局缓存自动刷新时间(毫秒),默认不自动刷新 -->
    <setting name="cacheFlushInterval" value="60000"/>
</settings>
步骤 3:Mapper 开启缓存(XML 方式)

在 Mapper XML 的 mapper 根标签下添加 <cache> 标签:

xml

复制代码
<!-- UserMapper.xml -->
<mapper namespace="cn.tx.mapper.UserMapper">
    <!-- 开启当前 namespace 的二级缓存 -->
    <cache 
        eviction="LRU"          <!-- 缓存回收策略(默认 LRU) -->
        flushInterval="60000"   <!-- 60 秒自动刷新 -->
        size="1024"             <!-- 缓存最大容量(默认 1024 个对象) -->
        readOnly="false"/>      <!-- false:可修改(返回副本);true:只读(返回原对象,性能高) -->

    <!-- 查询方法:默认 useCache="true"(启用二级缓存) -->
    <select id="findUserById" resultMap="userAccountMap" useCache="true">
        SELECT id, username FROM user WHERE id = #{id}
    </select>

    <!-- 增删改:默认 flushCache="true"(清空缓存),可省略 -->
    <update id="updateUser" flushCache="true">
        UPDATE user SET username = #{username} WHERE id = #{id}
    </update>
</mapper>
步骤 4:注解方式配置(备选)

java

运行

复制代码
// UserMapper.java(注解开启二级缓存)
@CacheNamespace(
    implementation = PerpetualCache.class, // 缓存实现类(默认)
    eviction = LruCache.class,             // 回收策略
    flushInterval = 60000,
    size = 1024,
    readWrite = true // 等价于 readOnly="false"
)
public interface UserMapper {
    @Options(useCache = true) // 启用二级缓存
    User findUserById(@Param("id") Integer id);

    @Options(flushCache = true) // 清空缓存
    void updateUser(User user);
}
(4)<cache> 标签核心属性
属性 取值说明
eviction 缓存回收策略(4 种):- LRU(默认):最近最少使用,移除最长时间未使用对象- FIFO:先进先出,按存入顺序移除- SOFT:软引用,内存不足时移除- WEAK:弱引用,垃圾回收时移除
flushInterval 自动刷新时间(毫秒),默认不自动刷新(仅增删改触发)
size 缓存最大对象数(默认 1024),需根据内存调整
readOnly false(默认):缓存对象可修改(返回序列化副本);true:只读(返回原对象,性能更高)

3. 一二级缓存核心对比

特性 一级缓存(Local Cache) 二级缓存(Second Level Cache)
作用范围 单个 SqlSession 全局(跨 SqlSession),按 namespace 隔离
开启方式 默认开启,无需配置 全局开关 + Mapper 单独开启
存储介质 内存(HashMap) 默认内存,可集成第三方缓存
序列化要求 实体类必须实现 Serializable
数据一致性 会话内一致(自动清空) 跨会话一致(增删改自动清空)
适用场景 单次会话内重复查询 多会话共享数据(如字典、静态数据)

4. 缓存使用注意事项

  • 关联查询缓存一致性:如果 Mapper A 关联 Mapper B 的查询,建议 Mapper B 也开启二级缓存,避免关联数据缓存不一致。

  • 禁用部分查询缓存 :实时性要求高的数据(如订单状态),可通过 useCache="false" 禁用二级缓存:

    xml

    复制代码
    <select id="findRealTimeOrder" resultType="Order" useCache="false">
        SELECT * FROM order WHERE id = #{id}
    </select>
  • 第三方缓存集成 :生产环境建议用 Redis/Ehcache 替代默认内存缓存(默认缓存重启项目失效),需添加对应依赖(如 mybatis-redis)并配置缓存实现类。

  • 避免缓存脏数据:多表关联查询(跨 namespace)不建议用二级缓存,容易因某张表更新未触发其他 namespace 缓存清空,导致脏数据。

三、延迟加载与缓存的协同工作

  1. 延迟加载的关联查询也会触发缓存:首次调用 getter 执行关联查询后,结果会存入一级缓存,SqlSession 关闭后同步到二级缓存。
  2. 缓存优先级:二级缓存 > 一级缓存 > 数据库(查询时先查二级缓存,再查一级缓存,最后查数据库)。
  3. 协同优化场景:查询主对象(如用户)时用延迟加载(避免关联数据冗余),主对象和关联对象均开启二级缓存(减少重复查询),适合 "主数据不常变、关联数据按需加载" 的场景(如用户信息 + 历史订单)。
相关推荐
IT枫斗者枫哥6 分钟前
Java 文件导出:写完了,为什么还不能标记成功?
java
陈皮波比茶12 分钟前
java笔记-typora快捷键
java
蚂蚁雅嘿21 分钟前
maven标签日常总结
java·maven
洋不写bug32 分钟前
二叉树(二) 常见基础操作解析|结点数、树高、查找结点、判断完全二叉树
java·开发语言·数据结构·完全二叉树·二叉树结点数·树高·查找结点
菠萝加点糖43 分钟前
Maven 坐标系统说明
java·maven
hm宋1 小时前
Canal 升级 jar 包操作文档(以升级 FastJSON 为例)
java·canal·jar
后台模板学习1 小时前
学习的心态高频面试题
java·数据库·学习
用户3126874877201 小时前
线程池到底怎么管理线程的?从 ThreadPoolExecutor 到拒绝策略全链路拆解
java
cpolar技术支持2 小时前
Kafka Streams 窗口统计怎么验收:本地跑订单流聚合,用 cpolar 给同事看只读结果页
java·docker·kafka·cpolar·kafka streams
爱读源码的大都督2 小时前
DeepSeek面试官问:生产RAG系统回答不准确,该如何定位和优化?这样回答,能让面试官当场给你Offer!
java·后端·python