【MyBatis】——执行过程
MyBatis核心机制详解:执行流程、延迟加载与缓存
引言
MyBatis 作为一款优秀的持久层框架,以其灵活性、高性能和易于上手的特点,在Java开发中得到了广泛应用。要真正精通MyBatis,仅仅会编写SQL是远远不够的,深入理解其内部执行流程和核心机制至关重要。本文将详细解析MyBatis的执行流程、延迟加载以及一级与二级缓存的原理和使用。
一、MyBatis 执行流程
配置文件:
MyBatis的执行流程可以看作是“如何通过接口方法最终执行SQL并返回结果”的故事。其核心过程如下图所示,我们将逐步分解:
详细步骤分解:
二、MyBatis 延迟加载(Lazy Loading)
1. 什么是延迟加载?
延迟加载,也称为懒加载,是一种对象关系映射(ORM)的优化策略。它的核心思想是:只有在真正使用到对象的关联数据时,才执行SQL语句去数据库查询加载,否则不加载。这可以有效减少不必要的数据库查询,提升性能。
例如:查询 Order
(订单)对象时,其关联的 User
(用户)信息可能并不需要立即显示。使用延迟加载,只有在调用 order.getUser()
方法时,MyBatis才会发起查询获取User数据。
2. 如何配置使用?
- 全局开启:在
mybatis-config.xml
核心配置文件中设置。
<settings><!-- 开启延迟加载 --><setting name="lazyLoadingEnabled" value="true"/><!-- 禁止 aggressive lazy loading(按需加载) --><setting name="aggressiveLazyLoading" value="false"/></settings>
* `lazyLoadingEnabled`:延迟加载的全局开关。 * `aggressiveLazyLoading`:如果设置为 `true`,任何方法的调用都会加载所有延迟加载属性。设置为 `false` 则表示每个属性按需加载。
2. 局部配置:在具体的 association
或 collection
标签上使用 fetchType
属性覆盖全局配置。
3. 实现原理
MyBatis的延迟加载是通过 动态代理 实现的。
开启了延迟加载后,在判断orderlist是否为空是,就会判断为是
- 创建代理对象:当主对象(如
Order
)被查询出来后,MyBatis并不会直接填充其关联属性(如user
),而是为该关联属性创建一个代理对象(比如User
的代理)。 - 拦截方法调用:这个代理对象实现了所有的目标对象方法,并持有一个用于执行延迟加载SQL的
Method
句柄。 - 触发加载:当应用程序第一次调用代理对象的任何方法时(如
order.getUser().getName()
),拦截器(LazyLoader
)会被触发。 - 执行查询:拦截器会通过之前保存的SQL信息和参数,执行一次真实的数据库查询,获取完整的关联对象数据。
- 替换代理:查询完成后,MyBatis会用真实的关联对象数据替换掉原来的代理对象。此后,对该属性的所有调用都将直接作用于这个真实对象,延迟加载过程结束。
注意事项:延迟加载在提高性能的同时,也带来了“N+1查询问题”的风险。如果遍历一个订单列表,并对每个订单都访问其延迟加载的用户信息,就会产生1次查询订单 + N次查询用户(N是订单数量)的性能灾难。需根据业务场景谨慎使用。
三、MyBatis 一级缓存与二级缓存
缓存是MyBatis提升性能的重要手段,它提供了两级缓存结构。
1. 一级缓存(Local Cache)
- 作用范围:SqlSession级别。默认开启,无法关闭。
- 生命周期:与
SqlSession
相同。当SqlSession
被close()
、commit()
、rollback()
或执行了 update(增删改)操作时,该SqlSession
下的一级缓存会被清空。 - 工作原理:
- 在同一个
SqlSession
中执行两次相同的SQL查询(相同的SQL和参数),第一次会查询数据库并将结果放入缓存,第二次则直接从缓存中获取,不再访问数据库。 - 它内部是一个简单的
HashMap
,Key由StatementId + SQL + 参数值
等构成。
- 在同一个
- 注意事项:在分布式或Web应用中,由于
SqlSession
的生命周期通常很短(例如在请求开始时创建,请求结束时关闭),一级缓存的作用非常有限,主要用于减少在一个业务事务内的重复查询。
2. 二级缓存(Second Level Cache)
- 作用范围:Mapper级别(Namespace级别)。多个
SqlSession
可以共享同一个二级缓存。 - 生命周期:与应用生命周期基本相同。只有当数据被执行了 update 操作(增删改)时,MyBatis才会清空该
Mapper
对应的二级缓存,以确保数据一致性。 - 如何开启:
- 全局开关:在
mybatis-config.xml
中设置。
- 全局开关:在
<settings><setting name="cacheEnabled" value="true"/> <!-- 默认为true,通常不用改 --></settings>
2. **在Mapper.xml中开启**:在需要开启二级缓存的 `Mapper.xml` 文件中添加 `<cache/>` 标签。
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.mapper.UserMapper"><cache/> <!-- 启用二级缓存 -->...</mapper>
3. **POJO序列化**:二级缓存中的数据可能被写入磁盘或通过网络传输,因此对应的实体类必须实现 `java.io.Serializable` 接口。
工作流程:
- 查询顺序:二级缓存 -> 一级缓存 -> 数据库。
- 当
SqlSession
被关闭或提交时,它的一级缓存中的数据才会被转存到其对应的二级缓存中。这意味着,未提交的事务数据不会被其他SqlSession
看到,避免了脏读。
缓存策略与自定义:
<cache>
标签可以配置多种属性,如eviction
(回收策略:LRU、FIFO等)、flushInterval
(刷新间隔)、size
(引用数目)等。- MyBatis还支持集成第三方缓存库,如 Ehcache、Redis 等,只需实现
org.apache.ibatis.cache.Cache
接口,并在<cache type="...">
中指定类型即可。这使得二级缓存可以部署在分布式环境中,解决单机缓存的数据一致性问题。
注意事项:
拓展:serializable接口有哪些作用
java.io.Serializable
是 Java 提供的一个标记接口(Marker Interface)。它内部没有任何需要实现的方法(public abstract
方法)。它的作用仅仅是为一个类“打上标记”,向 Java 虚拟机(JVM)声明:“这个类的对象可以被序列化。”
核心作用:对象序列化与反序列化
Serializable
接口的核心作用围绕着两个过程:
序列化(Serialization):
- 将内存中的 Java 对象(Object) 的状态(即其成员变量的值)转换成一个字节序列(byte sequence) 的过程。
- 这个字节序列可以被轻松地写入到文件(
.ser
等)、存入数据库的BLOB
字段、或者通过网络发送到另一台主机。
反序列化(Deserialization):
- 与序列化相反。将之前序列化得到的字节序列,重新构造成一个与原始对象状态完全相同的 Java 对象 的过程。
简单比喻:
- 序列化就像把一辆汽车拆解成零件清单(字节序列),以便装箱运输或存入仓库。
- 反序列化就像在目的地根据零件清单重新组装出一辆一模一样的汽车(对象)。
为什么需要序列化?
序列化机制解决了几个关键问题:
持久化(Persistence):
- 将对象的状态永久保存到存储设备(如硬盘)上,即使程序关闭后,下次启动仍然可以通过反序列化恢复这个对象。例如:保存游戏进度、应用程序的配置信息等。
网络通信(Network Communication):
- 在分布式系统或远程方法调用(如 RMI、Web Services)中,需要将对象从一个JVM(一台机器)传输到另一个JVM(另一台机器)。序列化将对象转换为字节流,可以通过网络Socket进行传输,接收方再反序列化获得对象。
缓存(Caching):
- 这正是MyBatis二级缓存要求
Serializable
的原因! - 缓存系统(如Redis、Memcached,甚至MyBatis自己的二级缓存)通常是独立于应用程序进程的。它们存储的是数据,而不是活的Java对象。
- 为了将一个Java对象放入缓存,必须先将它序列化成字节数组形式进行存储。
- 当从缓存中读取数据时,再将字节数组反序列化回Java对象供程序使用。
- 如果对象不可序列化,缓存系统就无法正确地存储和重建它。
- 这正是MyBatis二级缓存要求
3. 一级缓存与二级缓存对比总结
特性 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession 内部 | Mapper (Namespace) 级别,跨 SqlSession |
默认状态 | 开启,且不可关闭 | 需在Mapper.xml中手动配置 <cache/> 开启 |
生命周期 | 随 SqlSession 的销毁而销毁 | 随应用程序的生命周期,除非被更新操作清空 |
缓存数据 | 存储在内存中 | 可配置存储介质(内存、磁盘、第三方缓存服务器) |
共享性 | 不能共享 | 可以共享 |
应用场景 | 避免在一个会话内重复查询 | 避免在不同会话间对读多写少的相同数据进行重复查询 |
总结
深入理解MyBatis的执行流程是理解其所有特性的基础,它揭示了SQL从方法调用到结果返回的完整生命周期。延迟加载是一种重要的优化手段,通过动态代理实现按需加载,但需警惕N+1问题。一级缓存和二级缓存构成了MyBatis的性能加速器,一级缓存作用于会话内部,二级缓存则服务于全局应用,正确配置和使用缓存能极大提升系统性能,尤其是在读多写少的场景下。
掌握这三部分内容,意味着你对MyBatis的核心运行机制有了深刻的认识,能够更好地在实际开发中进行性能调优和问题排查。