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)工作流程
- 同一
sqlsession执行相同查询(相同 sql + 参数):- 首次查询:查数据库,结果存入一级缓存。
- 二次查询:直接从缓存获取,不执行 sql。
- 触发缓存清空的场景:
- 执行
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)工作流程
- 开启二级缓存后,
sqlsession关闭时,一级缓存中的数据会写入二级缓存。 - 新
sqlsession执行相同查询(同一 namespace + 相同 sql + 参数):- 先查二级缓存,命中则返回。
- 未命中则查数据库,结果存入一级缓存,
sqlsession关闭后同步到二级缓存。
- 触发缓存清空的场景:
- 同一 namespace 下执行
insert/update/delete操作(自动清空当前 namespace 的二级缓存)。 - 配置
flushinterval自动刷新(如 60 秒)。 - 手动调用
sqlsessionfactory.getconfiguration().getcache(namespace).clear()。
- 同一 namespace 下执行
(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),需根据内存调整 |
readonly | false(默认):缓存对象可修改(返回序列化副本);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 缓存清空,导致脏数据。
三、延迟加载与缓存的协同工作
- 延迟加载的关联查询也会触发缓存:首次调用
getter执行关联查询后,结果会存入一级缓存,sqlsession关闭后同步到二级缓存。 - 缓存优先级:二级缓存 > 一级缓存 > 数据库(查询时先查二级缓存,再查一级缓存,最后查数据库)。
- 协同优化场景:查询主对象(如用户)时用延迟加载(避免关联数据冗余),主对象和关联对象均开启二级缓存(减少重复查询),适合 “主数据不常变、关联数据按需加载” 的场景(如用户信息 + 历史订单)。
到此这篇关于mybatis中的延迟加载和一二级缓存深入解析的文章就介绍到这了,更多相关mybatis延迟加载和缓存内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论