【MyBatis延迟加载与缓存机制:深入剖析性能优化核心原理】

MyBatis延迟加载与缓存机制:深入剖析性能优化核心原理

1 延迟加载:概念与背景

在现代软件开发中,数据访问性能一直是开发者关注的重点。当应用程序需要处理大量数据或复杂对象关系时,延迟加载(Lazy Loading)技术应运而生,成为优化性能的关键策略。延迟加载,也称为懒加载,其核心思想是将数据的加载推迟到真正需要访问时才执行,从而避免不必要的资源消耗和性能开销。

延迟加载的概念在计算机科学中早有应用,对应的术语是"惰性求值"(Lazy Evaluation)。这是一种编程策略,在定义对象时并不立即计算其实际值,而是在对象被实际调用时才进行求值。这种方法可以最小化计算机在不必要计算上的开销,提高程序的运行效率。

在持久层框架中,延迟加载的应用场景尤为广泛。例如,在一个电商系统中,查询用户信息时,如果使用延迟加载策略,只有当需要查看该用户的订单信息时,才会去数据库查询订单数据,而不是在查询用户信息时就把所有关联数据一并加载。这种按需加载的方式可以显著减少数据库查询次数,降低系统负载,提高响应速度。

1.1 延迟加载的基本原理

延迟加载的基本原理可以通过以下流程图直观展示:

应用程序调用主对象
延迟加载代理对象
方法调用被拦截
检查关联数据是否已加载?
直接返回已加载数据
执行关联查询SQL
将查询结果填充到主对象
返回数据给应用程序
继续执行原方法调用

从技术实现角度看,延迟加载主要依赖于代理模式的应用。当查询主对象时,框架并不会立即加载所有关联数据,而是返回一个代理对象。这个代理对象封装了实际数据加载逻辑,只有当应用程序真正访问关联属性时,才会触发数据加载操作。

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代理的基本原理是:

  1. 使用CGLIB创建目标对象的代理对象
  2. 当调用目标方法时,进入拦截器invoke方法
  3. 发现目标方法是null值,执行SQL查询
  4. 获取数据以后,调用set方法设置属性值
  5. 再继续查询目标方法,就有值了

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:指定哪些方法调用会触发延迟加载,默认包括equalsclonehashCodetoString
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 延迟加载的触发条件

理解延迟加载的触发条件对于正确使用和优化性能至关重要。以下是具体的触发条件:

  1. 调用延迟属性的getter方法:如user.getOrderList()
  2. 对延迟集合属性进行操作:如orderList.size()orderList.isEmpty()、遍历操作等
  3. 调用特定的对象方法:如equalsclonehashCodetoString等方法

需要注意的是,仅获取代理对象引用不会触发加载:

// 以下操作不会触发延迟加载
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方法可以看出,一级缓存的命中条件包括:

  1. 同一个statementID
  2. 相同的参数值
  3. 相同的分页参数
  4. 相同的SQL语句
  5. 相同的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 一级缓存的清空时机

一级缓存在以下情况下会被清空:

  1. 执行更新操作:包括insertupdatedelete语句
  2. 手动清空缓存:调用SqlSession.clearCache()方法
  3. 设置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的环境中,需要特别注意一级缓存的行为:

  1. 未开启事务时:每次数据库操作都会创建新的SqlSession,一级缓存不生效
  2. 开启事务时:在同一个事务范围内使用同一个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 二级缓存的工作机制

二级缓存的工作机制可以通过以下流程图展示:

应用程序执行查询
二级缓存是否开启?
执行数据库查询
生成缓存Key
缓存中是否存在结果?
从缓存返回结果
执行数据库查询
将结果存入二级缓存
返回查询结果
应用程序执行更新
执行数据库更新
清空相关二级缓存
5.4.1 缓存命中流程
  1. 应用程序执行查询操作
  2. MyBatis检查对应的Mapper是否配置了二级缓存
  3. 如果配置了二级缓存,根据查询参数生成CacheKey
  4. 在二级缓存中查找是否存在对应的结果
  5. 如果存在(缓存命中),直接返回缓存结果
  6. 如果不存在(缓存未命中),执行数据库查询,并将结果存入二级缓存
5.4.2 缓存更新流程
  1. 应用程序执行插入、更新或删除操作
  2. MyBatis执行对应的数据库操作
  3. 根据操作所在的Mapper namespace,清空对应的二级缓存
  4. 确保下次查询时能获取到最新数据

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 延迟加载在缓存环境下的行为

当延迟加载与缓存结合使用时,需要特别注意延迟加载的触发时机和缓存的生命周期。以下是延迟加载在缓存环境下的典型行为:

  1. 一级缓存与延迟加载:在同一个SqlSession内,延迟加载的关联对象第一次被访问时,会触发查询并将结果存储在一级缓存中。后续再次访问相同的关联对象时,会直接从一级缓存中获取。

  2. 二级缓存与延迟加载:当使用二级缓存时,延迟加载的关联对象查询结果也可以被缓存。但需要注意,关联对象的缓存是独立于主对象的。即使主对象已经被缓存,关联对象仍然可能触发额外的查询。

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. 第一次执行时,仍然会有1+N次查询
  2. 但查询结果会被缓存到二级缓存中
  3. 后续执行相同的查询时,关联对象的查询可能直接从缓存中获取,减少数据库访问次数

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 缓存粒度控制

根据业务需求合理控制缓存粒度:

  1. 细粒度缓存:缓存单个对象或小批量数据,更新频繁但命中率高
  2. 粗粒度缓存:缓存大量数据或整个查询结果,更新不频繁但查询性能好
<!-- 细粒度缓存配置 -->
<cache eviction="LRU" size="5000"/>

<!-- 粗粒度缓存配置 -->  
<cache eviction="FIFO" size="100" flushInterval="3600000"/>
7.2.2 缓存过期策略

根据数据更新频率设置合适的缓存过期策略:

  1. 频繁更新数据:设置较短的过期时间或使用LRU淘汰策略
  2. 静态数据:设置较长的过期时间或不过期
  3. 重要业务数据:使用主动更新策略,确保数据一致性

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 最佳实践总结

  1. 合理使用延迟加载:对于数据量大、访问频率低的关联对象使用延迟加载,核心数据使用即时加载

  2. 缓存策略优化:根据数据特性和业务需求选择合适的缓存粒度和过期策略

  3. 避免N+1查询:通过批量查询、JOIN查询等方式优化延迟加载可能带来的性能问题

  4. 监控与调优:建立完善的监控机制,定期分析缓存命中率和查询性能

  5. 事务管理:注意在Spring等框架中,事务边界对一级缓存的影响

通过合理运用MyBatis的延迟加载和缓存机制,可以构建出高性能、可扩展的数据访问层,为应用程序提供良好的用户体验和系统性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值