MyBatis延迟加载与缓存机制:深入剖析性能优化核心原理
1 延迟加载:概念与背景
在现代软件开发中,数据访问性能一直是开发者关注的重点。当应用程序需要处理大量数据或复杂对象关系时,延迟加载(Lazy Loading)技术应运而生,成为优化性能的关键策略。延迟加载,也称为懒加载,其核心思想是将数据的加载推迟到真正需要访问时才执行,从而避免不必要的资源消耗和性能开销。
延迟加载的概念在计算机科学中早有应用,对应的术语是"惰性求值"(Lazy Evaluation)。这是一种编程策略,在定义对象时并不立即计算其实际值,而是在对象被实际调用时才进行求值。这种方法可以最小化计算机在不必要计算上的开销,提高程序的运行效率。
在持久层框架中,延迟加载的应用场景尤为广泛。例如,在一个电商系统中,查询用户信息时,如果使用延迟加载策略,只有当需要查看该用户的订单信息时,才会去数据库查询订单数据,而不是在查询用户信息时就把所有关联数据一并加载。这种按需加载的方式可以显著减少数据库查询次数,降低系统负载,提高响应速度。
1.1 延迟加载的基本原理
延迟加载的基本原理可以通过以下流程图直观展示:
从技术实现角度看,延迟加载主要依赖于代理模式的应用。当查询主对象时,框架并不会立即加载所有关联数据,而是返回一个代理对象。这个代理对象封装了实际数据加载逻辑,只有当应用程序真正访问关联属性时,才会触发数据加载操作。
1.2 延迟加载的适用场景
延迟加载特别适用于以下场景:
- 关联关系复杂:对象之间存在多层嵌套关系,如用户-订单-订单明细
- 数据量较大:关联数据可能很大,但并非每次都需要访问
- 性能敏感:需要优化初始加载时间,提高系统响应速度
- 网络环境差:减少不必要的数据传输,节省带宽资源
然而,延迟加载并非万能解决方案,它也存在一些缺点。最主要的问题是N+1查询问题,即当需要遍历一个集合并访问每个元素的延迟加载属性时,会导致主查询1次加上每个对象的延迟查询N次,总共N+1次查询。此外,延迟加载还可能导致代理对象序列化问题,以及会话关闭后无法加载的问题。
2 MyBatis延迟加载的底层原理
2.1 动态代理机制的实现
MyBatis延迟加载的核心实现依赖于动态代理技术。具体来说,当开启延迟加载功能后,MyBatis会为关联对象创建代理对象,而不是直接加载真实数据。这些代理对象拦截所有方法调用,在适当的时候触发数据加载操作。
MyBatis支持两种动态代理方式:JDK动态代理和CGLIB动态代理。选择哪种代理方式取决于目标类是否实现了接口。如果目标类实现了接口,MyBatis会优先使用JDK动态代理;如果目标类没有实现接口,则使用CGLIB动态代理。
以下是MyBatis代理工厂的选择逻辑:
// MyBatis ProxyFactory选择逻辑(简化版)
public class ProxyFactory {
public static Object createProxy(Object target, ResultLoaderMap lazyLoader,
Configuration configuration, ObjectFactory objectFactory,
List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
// 判断目标类是否为接口或者代理类
boolean isJdkProxy = target.getClass().getInterfaces().length > 0
&& !Proxy.isProxyClass(target.getClass());
if (isJdkProxy) {
// 使用JDK动态代理(优先选择,性能略优且符合Java标准)
return JdkProxyFactory.createProxy(target, lazyLoader, configuration,
objectFactory, constructorArgTypes, constructorArgs);
} else {
// 使用CGLIB动态代理(目标是非接口的普通类时)
return CglibProxyFactory.createProxy(target, lazyLoader, configuration,
objectFactory, constructorArgTypes, constructorArgs);
}
}
}
2.2 JDK动态代理实现机制
JDK动态代理基于接口实现。当MyBatis开启延迟加载后,对于关联对象,会为其创建一个实现了相同接口的代理对象。当调用代理对象的方法时,代理对象会拦截该调用,判断关联对象是否已经加载,如果未加载,则触发相应的数据库查询操作。
以下是一个简化的JDK动态代理示例:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 定义接口
interface Department {
void showDepartmentInfo();
}
// 真实对象
class RealDepartment implements Department {
@Override
public void showDepartmentInfo() {
System.out.println("加载部门信息,执行数据库查询...");
}
}
// 代理处理器
class DepartmentProxyHandler implements InvocationHandler {
private Object target;
public DepartmentProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (target == null) {
// 模拟延迟加载,创建真实对象
target = new RealDepartment();
}
return method.invoke(target, args);
}
}
// 测试类
public class JdkProxyExample {
public static void main(String[] args) {
Department realDepartment = null;
DepartmentProxyHandler handler = new DepartmentProxyHandler(realDepartment);
Department proxyDepartment = (Department) Proxy.newProxyInstance(
Department.class.getClassLoader(),
new Class<?>[]{Department.class},
handler
);
proxyDepartment.showDepartmentInfo(); // 此时才会触发真实加载
}
}
2.3 CGLIB动态代理实现机制
对于没有实现接口的类,MyBatis使用CGLIB动态代理。CGLIB通过继承目标类,生成子类代理对象,重写父类的方法来实现拦截。
CGLIB代理的基本原理是:
- 使用CGLIB创建目标对象的代理对象
- 当调用目标方法时,进入拦截器invoke方法
- 发现目标方法是null值,执行SQL查询
- 获取数据以后,调用set方法设置属性值
- 再继续查询目标方法,就有值了
2.4 ResultLoaderMap:延迟加载的核心容器
ResultLoaderMap是MyBatis用于管理延迟加载任务的容器,它存储了属性名与对应的ResultLoader的映射关系。每个延迟属性对应一个ResultLoader,当属性被访问时,通过ResultLoader执行对应的子查询并填充数据。
以下是ResultLoaderMap的简化概念示意:
// ResultLoaderMap简化概念示意
public class ResultLoaderMap {
// 存储属性名到ResultLoader的映射
private final Map<String, LoadPair> loaderMap = new HashMap<>();
// 检查是否有指定属性的加载器
public boolean hasLoader(String property) {
return loaderMap.containsKey(property);
}
// 触发指定属性的加载
public void load(String property) throws SQLException {
LoadPair pair = loaderMap.get(property);
if (pair != null) {
pair.load(); // 执行SQL查询并填充结果
loaderMap.remove(property); // 加载后移除该加载器
}
}
}
// 加载器,包含了执行查询所需的全部信息
class LoadPair {
private final String property;
private final MetaObject metaResultObject;
private final ResultLoader resultLoader;
public void load() throws SQLException {
// 执行SQL查询获取结果
Object value = resultLoader.loadResult();
// 将结果设置到目标对象的属性上
metaResultObject.setValue(property, value);
}
}
ResultLoaderMap是会话级(SqlSession)容器,线程安全由SqlSession的线程隔离性保证。在高并发场景下,每个请求使用独立SqlSession,避免线程间数据污染。
3 MyBatis延迟加载的配置与使用
3.1 延迟加载的配置方式
MyBatis提供了灵活的配置选项来控制延迟加载行为。配置可以在两个层面进行:全局配置和局部配置。
3.1.1 全局配置
在MyBatis的核心配置文件中,可以使用setting标签修改全局的加载策略:
<configuration>
<settings>
<!-- 开启延迟加载功能 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 设置激进延迟加载策略 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 延迟加载触发方法 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
</configuration>
配置参数说明:
- lazyLoadingEnabled:设置为
true时开启延迟加载功能,默认值为false - aggressiveLazyLoading:设置为
false时,按需加载对象属性;设置为true时,任何对对象方法的调用都会触发所有延迟加载属性的加载 - lazyLoadTriggerMethods:指定哪些方法调用会触发延迟加载,默认包括
equals、clone、hashCode、toString
3.1.2 局部配置
除了全局配置外,还可以在关联查询中单独设置延迟加载策略:
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="userResultMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- 一对一关联,使用延迟加载 -->
<association property="author" column="author_id"
select="selectAuthor" fetchType="lazy"/>
<!-- 一对多关联,使用延迟加载 -->
<collection property="posts" ofType="com.example.entity.Post"
column="id" select="selectPostsForBlog" fetchType="lazy"/>
</resultMap>
<select id="selectUserWithLazyAssociations" resultMap="userResultMap">
SELECT id, username, author_id FROM user WHERE id = #{id}
</select>
<select id="selectAuthor" resultType="com.example.entity.Author">
SELECT id, name FROM author WHERE id = #{authorId}
</select>
<select id="selectPostsForBlog" resultType="com.example.entity.Post">
SELECT id, title, content FROM post WHERE user_id = #{userId}
</select>
</mapper>
通过fetchType属性可以覆盖全局的延迟加载设置,值为lazy表示使用延迟加载,eager表示立即加载。
3.2 延迟加载的触发条件
理解延迟加载的触发条件对于正确使用和优化性能至关重要。以下是具体的触发条件:
- 调用延迟属性的getter方法:如
user.getOrderList() - 对延迟集合属性进行操作:如
orderList.size()、orderList.isEmpty()、遍历操作等 - 调用特定的对象方法:如
equals、clone、hashCode、toString等方法
需要注意的是,仅获取代理对象引用不会触发加载:
// 以下操作不会触发延迟加载
User user = userMapper.getUserById(1);
List<Order> orderList = user.getOrderList(); // 仅获取引用,不会触发加载
// 以下操作会触发延迟加载
int size = user.getOrderList().size(); // 调用size()方法触发加载
boolean isEmpty = user.getOrderList().isEmpty(); // 调用isEmpty()方法触发加载
for (Order order : user.getOrderList()) { // 遍历触发加载
// 处理订单
}
3.3 延迟加载的实际案例
下面通过一个完整的示例来演示MyBatis延迟加载的实际应用:
3.3.1 实体类定义
public class User implements Serializable {
private Integer id;
private String username;
private List<Order> orderList; // 延迟加载的关联对象
// getter和setter方法
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public List<Order> getOrderList() { return orderList; }
public void setOrderList(List<Order> orderList) { this.orderList = orderList; }
}
public class Order implements Serializable {
private Integer id;
private String orderNo;
private Double amount;
private Integer userId;
// getter和setter方法
// ...
}
3.3.2 MyBatis配置和Mapper文件
<!-- mybatis-config.xml -->
<configuration>
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
</configuration>
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="userResultMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- 配置延迟加载 -->
<collection property="orderList" ofType="com.example.entity.Order"
column="id" select="getOrdersByUserId" fetchType="lazy"/>
</resultMap>
<select id="getUserById" resultMap="userResultMap" parameterType="int">
SELECT id, username FROM user WHERE id = #{id}
</select>
<select id="getOrdersByUserId" resultType="com.example.entity.Order" parameterType="int">
SELECT id, order_no, amount, user_id FROM orders WHERE user_id = #{userId}
</select>
</mapper>
3.3.3 使用示例
public class LazyLoadingDemo {
public static void main(String[] args) {
// 使用try-with-resources确保SqlSession正确关闭
try (SqlSession sqlSession = MyBatisUtil.getSqlSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 查询用户信息
User user = userMapper.getUserById(1);
System.out.println("用户名: " + user.getUsername());
// 此时还没有执行订单查询的SQL
System.out.println("=== 分割线,以上SQL不包含订单查询 ===");
// 访问订单信息时,才会触发延迟加载,执行订单查询SQL
// 注意:延迟加载依赖活动的SqlSession,建议在会话关闭前完成所有延迟属性的访问
List<Order> orderList = user.getOrderList();
System.out.println("订单数量: " + orderList.size());
// 后续再次访问不会触发SQL查询,因为已缓存在一级缓存中
System.out.println("再次访问订单: " + user.getOrderList().size());
} // SqlSession自动关闭
// 注意:在此处访问user.getOrderList()会抛出异常
// 因为延迟加载依赖活动的SqlSession
}
}
4 MyBatis一级缓存详解
4.1 一级缓存的基本概念
MyBatis的一级缓存是SqlSession级别的缓存,默认开启且不能关闭。在同一个SqlSession范围内,执行相同的SQL查询,MyBatis会优先从缓存中获取结果,而不是直接查询数据库。
一级缓存的作用范围限定在同一个SqlSession内。当应用程序执行查询时,查询结果会被缓存到SqlSession的一级缓存中。后续在同一个SqlSession内执行的相同查询,可以直接从缓存中获取结果,避免重复的数据库访问。
4.2 一级缓存的底层实现
一级缓存的实现位于BaseExecutor类中,具体通过PerpetualCache实现:
public abstract class BaseExecutor implements Executor {
// 这个属性就是一级缓存,它内部有一个map
protected PerpetualCache localCache;
// 查询方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 生成缓存key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
// 生成缓存key的方法
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId()); // statementId
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit())); // mybatis的分页参数
cacheKey.update(boundSql.getSql()); // 要发送给数据库的sql
// 参数值
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value); // sql参数
}
}
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId()); // mybaits的环境对象
}
return cacheKey;
}
}
从createCacheKey方法可以看出,一级缓存的命中条件包括:
- 同一个statementID
- 相同的参数值
- 相同的分页参数
- 相同的SQL语句
- 相同的MyBatis环境
4.3 一级缓存的存储与读取
一级缓存的存储和读取逻辑在BaseExecutor.query方法中实现:
public abstract class BaseExecutor implements Executor {
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 错误上下文设置...
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 这里是从一级缓存中查询
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 一级缓存中查不到时再从数据库查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
// 延迟加载处理...
return list;
}
// 查询数据库的方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 把查询到的结果放入一级缓存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
}
4.4 一级缓存的清空时机
一级缓存在以下情况下会被清空:
- 执行更新操作:包括
insert、update、delete语句 - 手动清空缓存:调用
SqlSession.clearCache()方法 - 设置flushCache属性:在Mapper配置中设置
flushCache="true"
当执行更新操作时,MyBatis会调用BaseExecutor.update方法,其中会清空一级缓存:
public abstract class BaseExecutor implements Executor {
public int update(MappedStatement ms, Object parameter) throws SQLException {
// 错误上下文设置...
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 这里就是在清空一级缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
}
4.5 一级缓存在Spring环境中的注意事项
在Spring整合MyBatis的环境中,需要特别注意一级缓存的行为:
- 未开启事务时:每次数据库操作都会创建新的SqlSession,一级缓存不生效
- 开启事务时:在同一个事务范围内使用同一个SqlSession对象,一级缓存生效
这意味着在Spring管理中,一级缓存的有效性取决于事务的配置。如果希望利用一级缓存提升性能,需要确保相关操作在同一个事务中执行。
5 MyBatis二级缓存详解
5.1 二级缓存的基本概念
MyBatis的二级缓存是Mapper级别的缓存,默认关闭,需要手动开启。与一级缓存不同,二级缓存可以跨SqlSession共享,即多个SqlSession可以访问同一个二级缓存。
二级缓存的作用范围是Mapper的namespace级别。当开启二级缓存后,同一个namespace下的所有操作语句(select、insert、update、delete)都会影响缓存。执行查询时,数据会先被缓存到二级缓存中;执行更新操作时,会清空对应的缓存。
5.2 二级缓存的开启与配置
5.2.1 全局开启二级缓存
在MyBatis配置文件中开启二级缓存:
<configuration>
<settings>
<!-- 开启全局二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
5.2.2 在Mapper文件中配置二级缓存
在需要开启二级缓存的Mapper文件中添加<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
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
<select id="findById" resultType="com.example.entity.User">
SELECT * FROM t_user WHERE user_id=#{userId}
</select>
</mapper>
缓存配置属性说明:
- eviction:缓存回收策略,可选值包括LRU(最近最少使用)、FIFO(先进先出)、SOFT(软引用)、WEAK(弱引用)
- flushInterval:缓存刷新间隔,单位毫秒
- size:引用数目,代表缓存最多可以存储多少个对象
- readOnly:是否只读,如果为true,则所有相同的查询返回同一个实例;如果为false,则返回拷贝实例(序列化反序列化)
5.3 二级缓存的底层实现
二级缓存的实现基于装饰器模式,通过CachingExecutor对基本的Executor进行装饰:
// Executor创建过程
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
// ... 事务创建逻辑
// 创建基本的Executor
final Executor executor = configuration.newExecutor(tx, execType);
// 根据cacheEnabled配置决定是否使用CachingExecutor装饰
if (configuration.isCacheEnabled()) {
executor = new CachingExecutor(executor);
}
// ... 插件拦截器处理
return new DefaultSqlSession(configuration, executor, autoCommit);
}
}
// CachingExecutor核心逻辑
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
// 获取MappedStatement对应的缓存
Cache cache = ms.getCache();
if (cache != null) {
// 检查是否需要清空缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 处理存储过程相关逻辑...
// 从缓存中获取结果
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 缓存未命中,委托给底层Executor查询数据库
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 将结果存入缓存
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
}
5.4 二级缓存的工作机制
二级缓存的工作机制可以通过以下流程图展示:
5.4.1 缓存命中流程
- 应用程序执行查询操作
- MyBatis检查对应的Mapper是否配置了二级缓存
- 如果配置了二级缓存,根据查询参数生成CacheKey
- 在二级缓存中查找是否存在对应的结果
- 如果存在(缓存命中),直接返回缓存结果
- 如果不存在(缓存未命中),执行数据库查询,并将结果存入二级缓存
5.4.2 缓存更新流程
- 应用程序执行插入、更新或删除操作
- MyBatis执行对应的数据库操作
- 根据操作所在的Mapper namespace,清空对应的二级缓存
- 确保下次查询时能获取到最新数据
5.5 二级缓存的注意事项
使用二级缓存时需要注意以下问题:
5.5.1 数据一致性问题
由于二级缓存是跨SqlSession共享的,可能会出现数据一致性问题。当多个应用实例或同一应用的多个线程同时操作数据库时,需要确保缓存能及时更新。
解决方案:
- 设置合理的缓存过期时间
- 在关键业务操作中手动清空缓存
- 使用Redis等分布式缓存替代MyBatis内置缓存
5.5.2 对象序列化要求
如果缓存配置为readOnly=“false”,MyBatis会通过序列化和反序列化返回缓存对象的拷贝。这就要求缓存的对象必须实现Serializable接口。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ... 属性和方法
}
5.5.3 缓存作用范围
二级缓存的作用范围是Mapper的namespace级别,这意味着:
- 同一个namespace下的所有操作共享同一个缓存
- 不同namespace的缓存相互独立
- 关联查询可能涉及多个namespace,需要谨慎处理
6 延迟加载与缓存的结合使用
6.1 延迟加载在缓存环境下的行为
当延迟加载与缓存结合使用时,需要特别注意延迟加载的触发时机和缓存的生命周期。以下是延迟加载在缓存环境下的典型行为:
-
一级缓存与延迟加载:在同一个SqlSession内,延迟加载的关联对象第一次被访问时,会触发查询并将结果存储在一级缓存中。后续再次访问相同的关联对象时,会直接从一级缓存中获取。
-
二级缓存与延迟加载:当使用二级缓存时,延迟加载的关联对象查询结果也可以被缓存。但需要注意,关联对象的缓存是独立于主对象的。即使主对象已经被缓存,关联对象仍然可能触发额外的查询。
6.2 缓存对N+1查询问题的缓解
延迟加载可能导致N+1查询问题,而缓存可以在一定程度上缓解这个问题:
// 典型的N+1查询问题
List<User> users = userMapper.getAllUsers(); // 1次查询,获取所有用户
for (User user : users) {
// 对每个用户,访问延迟加载的订单列表
List<Order> orders = user.getOrderList(); // N次查询
System.out.println("用户 " + user.getUsername() + " 有 " + orders.size() + " 个订单");
}
当启用二级缓存后,上述代码的行为会有所改善:
- 第一次执行时,仍然会有1+N次查询
- 但查询结果会被缓存到二级缓存中
- 后续执行相同的查询时,关联对象的查询可能直接从缓存中获取,减少数据库访问次数
6.3 最佳实践配置
以下是一个结合延迟加载和缓存的最佳实践配置示例:
<!-- mybatis-config.xml -->
<configuration>
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 非激进延迟加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 配置二级缓存 -->
<cache eviction="LRU" flushInterval="300000" size="1024" readOnly="true"/>
<resultMap id="userDetailMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- 延迟加载的关联对象 -->
<collection property="orders" ofType="Order"
select="selectOrdersByUserId" column="id" fetchType="lazy"/>
</resultMap>
<select id="selectUserById" resultMap="userDetailMap" useCache="true">
SELECT id, username FROM user WHERE id = #{id}
</select>
<select id="selectOrdersByUserId" resultType="Order" useCache="true">
SELECT id, user_id, amount, create_time
FROM orders
WHERE user_id = #{userId}
</select>
</mapper>
7 性能优化与常见问题解决方案
7.1 延迟加载的N+1查询问题解决方案
延迟加载虽然能提升初始加载性能,但可能引发N+1查询问题。以下是几种解决方案:
7.1.1 使用批量查询优化
<!-- 批量查询配置 -->
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectUsersByIds" resultMap="userResultMap">
SELECT id, username FROM user
WHERE id IN
<foreach collection="list" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
</mapper>
<!-- OrderMapper.xml -->
<mapper namespace="com.example.mapper.OrderMapper">
<select id="selectOrdersByUserIds" resultType="Order">
SELECT id, user_id, amount, create_time
FROM orders
WHERE user_id IN
<foreach collection="list" item="userId" open="(" close=")" separator=",">
#{userId}
</foreach>
</select>
</mapper>
// Java代码中的批量查询优化
public class UserService {
public void processUsersWithOrders(List<Integer> userIds) {
// 批量查询用户
List<User> users = userMapper.selectUsersByIds(userIds);
// 批量查询所有订单
List<Order> allOrders = orderMapper.selectOrdersByUserIds(userIds);
// 建立用户ID到订单列表的映射
Map<Integer, List<Order>> userOrderMap = allOrders.stream()
.collect(Collectors.groupingBy(Order::getUserId));
// 将订单设置到对应用户
for (User user : users) {
List<Order> userOrders = userOrderMap.getOrDefault(user.getId(),
Collections.emptyList());
user.setOrderList(userOrders);
}
// 处理业务逻辑...
}
}
7.1.2 使用JOIN查询避免N+1问题
在某些场景下,可以使用JOIN查询一次性获取所有数据:
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="userWithOrdersMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="amount" column="amount"/>
<result property="createTime" column="create_time"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="userWithOrdersMap">
SELECT u.id, u.username, o.id as order_id, o.amount, o.create_time
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
</mapper>
7.2 缓存优化策略
7.2.1 缓存粒度控制
根据业务需求合理控制缓存粒度:
- 细粒度缓存:缓存单个对象或小批量数据,更新频繁但命中率高
- 粗粒度缓存:缓存大量数据或整个查询结果,更新不频繁但查询性能好
<!-- 细粒度缓存配置 -->
<cache eviction="LRU" size="5000"/>
<!-- 粗粒度缓存配置 -->
<cache eviction="FIFO" size="100" flushInterval="3600000"/>
7.2.2 缓存过期策略
根据数据更新频率设置合适的缓存过期策略:
- 频繁更新数据:设置较短的过期时间或使用LRU淘汰策略
- 静态数据:设置较长的过期时间或不过期
- 重要业务数据:使用主动更新策略,确保数据一致性
7.3 监控与诊断
7.3.1 缓存命中率监控
通过日志或监控工具跟踪缓存命中率:
public class CacheMonitor {
private static final Logger logger = LoggerFactory.getLogger(CacheMonitor.class);
public static void logCacheHitRate(String cacheName, long hitCount, long totalCount) {
double hitRate = (double) hitCount / totalCount * 100;
logger.info("缓存[{}]命中率: {}/{} ({}%)",
cacheName, hitCount, totalCount, String.format("%.2f", hitRate));
if (hitRate < 80) {
logger.warn("缓存[{}]命中率较低,建议优化", cacheName);
}
}
}
7.3.2 慢查询诊断
识别和优化慢查询:
<!-- 配置SQL执行时间监控 -->
<plugin interceptor="com.example.plugin.SqlExecutionTimeInterceptor">
<property name="threshold" value="1000"/>
</plugin>
8 总结
MyBatis的延迟加载和缓存机制是提升应用性能的重要技术手段。通过深入理解其底层原理和合理配置,可以显著优化数据库访问性能。
8.1 技术对比
| 特性 | 延迟加载 | 一级缓存 | 二级缓存 |
|---|---|---|---|
| 作用范围 | 关联对象级别 | SqlSession级别 | Mapper级别 |
| 默认状态 | 关闭 | 开启且不可关闭 | 关闭 |
| 共享性 | 不支持跨Session共享 | 不支持跨Session共享 | 支持跨Session共享 |
| 实现原理 | 动态代理 | PerpetualCache本地缓存 | 装饰器模式+CachingExecutor |
| 适用场景 | 关联对象数据量大、不总是需要 | 同一Session内重复查询 | 跨Session的数据共享 |
8.2 最佳实践总结
-
合理使用延迟加载:对于数据量大、访问频率低的关联对象使用延迟加载,核心数据使用即时加载
-
缓存策略优化:根据数据特性和业务需求选择合适的缓存粒度和过期策略
-
避免N+1查询:通过批量查询、JOIN查询等方式优化延迟加载可能带来的性能问题
-
监控与调优:建立完善的监控机制,定期分析缓存命中率和查询性能
-
事务管理:注意在Spring等框架中,事务边界对一级缓存的影响
通过合理运用MyBatis的延迟加载和缓存机制,可以构建出高性能、可扩展的数据访问层,为应用程序提供良好的用户体验和系统性能。
1294

被折叠的 条评论
为什么被折叠?



