mybatis中的延迟加载和一二级缓存深入解析

 更新时间:2025年12月02日 11:19:44   作者:不光头强  
延迟加载是MyBatis对关联查询的优化机制,查询主对象时,不立即查询其关联对象(如用户和账户的一对多关系),仅在实际使用关联对象(调用 getter 方法)时,才触发关联查询的 SQL 执行,这篇文章给大家介绍mybatis中的延迟加载和一二级缓存,感兴趣的朋友一起看看吧

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

一、延迟加载(Lazy Loading)

1. 核心定义

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

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

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

3. 实现原理

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

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

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

<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 示例)

一对多(用户 -> 账户)
<!-- 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>
一对一(用户 -> 身份证)
<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 调整作用域(全局配置):

<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

public class User implements Serializable { // 必须实现,否则缓存序列化失败
    private Integer id;
    private String username;
    private List<Account> accounts;
    // getter/setter/toString
}

步骤 2:全局开启二级缓存(SqlMapConfig.xml)

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

步骤 3:Mapper 开启缓存(XML 方式)

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

<!-- 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:注解方式配置(备选)

// 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),需根据内存调整
readOnlyfalse(默认):缓存对象可修改(返回序列化副本);true:只读(返回原对象,性能更高)

3. 一二级缓存核心对比

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

4. 缓存使用注意事项

  • 关联查询缓存一致性:如果 Mapper A 关联 Mapper B 的查询,建议 Mapper B 也开启二级缓存,避免关联数据缓存不一致。
  • 禁用部分查询缓存:实时性要求高的数据(如订单状态),可通过 useCache="false" 禁用二级缓存:
<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. 协同优化场景:查询主对象(如用户)时用延迟加载(避免关联数据冗余),主对象和关联对象均开启二级缓存(减少重复查询),适合 “主数据不常变、关联数据按需加载” 的场景(如用户信息 + 历史订单)。

到此这篇关于mybatis中的延迟加载和一二级缓存深入解析的文章就介绍到这了,更多相关mybatis延迟加载和缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 在Java8中如何避开空指针异常

    在Java8中如何避开空指针异常

    这篇文章主要给大家介绍了关于在Java8中如何风骚走位的避开空指针异常的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Java8具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-06-06
  • java web开发之实现购物车功能

    java web开发之实现购物车功能

    这篇文章主要为大家详细介绍了java web开发之实现购物车功能的相关资料,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • spring根据controller中接收请求参数不同走不同service的实现方法

    spring根据controller中接收请求参数不同走不同service的实现方法

    这篇文章主要给大家介绍了关于spring实现根据controller中接收请求参数不同走不同service的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2018-11-11
  • 基于redis setIfAbsent的使用说明

    基于redis setIfAbsent的使用说明

    这篇文章主要介绍了基于redis setIfAbsent的使用说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • Java实现万年历效果

    Java实现万年历效果

    这篇文章主要为大家详细介绍了Java实现万年历效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-06-06
  • Java实现拓扑排序算法的示例代码

    Java实现拓扑排序算法的示例代码

    在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。本文将为大家讲讲拓扑排序算法的原理及实现,需要的可以参考一下
    2022-07-07
  • java selenium元素定位大全

    java selenium元素定位大全

    本文主要介绍java selenium元素定位,这里整理了selenium元素定位的相关资料,有兴趣的小伙伴可以参考下
    2016-08-08
  • 关于@PostConstruct、afterPropertiesSet和init-method的执行顺序

    关于@PostConstruct、afterPropertiesSet和init-method的执行顺序

    这篇文章主要介绍了关于@PostConstruct、afterPropertiesSet和init-method的执行顺序,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • Springboot如何通过路径映射获取本机图片资源

    Springboot如何通过路径映射获取本机图片资源

    项目中对图片的处理与查看是必不可少的,本文将讲解如何通过项目路径来获取到本机电脑的图片资源,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-08-08
  • 解决IDEA删除子模块并重建后MAVEN无法识别的问题

    解决IDEA删除子模块并重建后MAVEN无法识别的问题

    这篇文章主要介绍了解决IDEA删除子模块并重建后MAVEN无法识别的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02

最新评论