MyBatis 动态 SQL 与缓存机制深度解析

发布于:2025-07-02 ⋅ 阅读:(21) ⋅ 点赞:(0)

在Java持久层技术体系中,MyBatis凭借其灵活的SQL映射和强大的动态SQL能力,成为企业级应用开发的首选框架。本文从动态SQL核心语法、缓存实现原理、性能优化及面试高频问题四个维度,结合源码与工程实践,系统解析MyBatis的核心特性与最佳实践。

一、动态SQL核心语法与实现原理

1.1 动态SQL标签体系

标签 作用 示例场景
<if> 条件判断,按需拼接SQL片段 动态查询(如多条件筛选)
<choose> 类似于Java的switch语句,多选一执行 单条件查询(不同条件互斥)
<where> 智能处理WHERE子句,自动剔除多余的AND/OR 动态WHERE条件
<set> 智能处理UPDATE语句,自动剔除多余的逗号 动态更新(部分字段更新)
<foreach> 遍历集合,生成批量SQL 批量插入、IN条件查询
<trim> 自定义前缀、后缀处理,可替代<where><set> 高级SQL片段处理

1.2 动态SQL执行流程

动态SQL执行流程图

关键步骤解析:
  1. SQL节点解析
    • XML配置中的动态标签(如<if>)被解析为SqlNode对象(如IfSqlNode)。
  2. OGNL表达式计算
    • 使用OGNL(Object Graph Navigation Language)计算动态条件(如#{username} != null)。
  3. 参数绑定
    • 通过TypeHandler将Java对象转换为JDBC类型(如String → VARCHAR)。

1.3 高级应用:自定义SQL提供器

1. 使用@Provider注解
@SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")  
List<User> selectByCondition(Map<String, Object> params);  

// 自定义SQL提供器  
public class UserSqlProvider {  
    public String selectByCondition(Map<String, Object> params) {  
        SQL sql = new SQL();  
        sql.SELECT("*").FROM("users");  
        if (params.containsKey("username")) {  
            sql.WHERE("username = #{username}");  
        }  
        if (params.containsKey("age")) {  
            sql.WHERE("age >= #{age}");  
        }  
        return sql.toString();  
    }  
}  
2. 流式SQL构建(SQL类)
String sql = new SQL()  
    .SELECT("id", "username")  
    .FROM("users")  
    .WHERE("status = 'ACTIVE'")  
    .ORDER_BY("create_time DESC")  
    .toString();  

二、缓存机制深度解析

2.1 一级缓存(本地缓存)

1. 核心特性
  • 作用域SqlSession级别(同一个会话内共享)。
  • 生命周期:与SqlSession一致,会话关闭时缓存清空。
  • 实现类PerpetualCache(基于HashMap)。
2. 源码关键逻辑
public class DefaultSqlSession implements SqlSession {  
    private final Executor executor;  

    @Override  
    public <T> T selectOne(String statement, Object parameter) {  
        List<T> list = this.selectList(statement, parameter);  
        // 一级缓存逻辑在Executor中实现  
        return list.isEmpty() ? null : list.get(0);  
    }  
}  

public class BaseExecutor implements Executor {  
    private final PerpetualCache localCache;  

    @Override  
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {  
        BoundSql boundSql = ms.getBoundSql(parameter);  
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);  
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);  
    }  

    @Override  
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {  
        // 先从一级缓存获取  
        List<E> list = (List<E>) localCache.getObject(key);  
        if (list != null) {  
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);  
            return list;  
        } else {  
            // 缓存未命中,执行数据库查询  
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);  
            return list;  
        }  
    }  
}  

2.2 二级缓存(全局缓存)

1. 核心特性
  • 作用域namespace级别(跨会话共享)。
  • 配置方式
    <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>  
    
  • 默认实现PerpetualCache(可替换为Redis、Ehcache等)。
2. 缓存配置参数
参数 作用
eviction 缓存淘汰策略(LRU/FIFO/SOFT/WEAK)
flushInterval 刷新间隔(毫秒,默认不刷新)
size 缓存最大容量(元素个数)
readOnly 是否只读(true则返回缓存对象的引用,性能更高)

2.3 缓存工作流程

缓存工作流程图

关键注意点:
  • 缓存失效
    增删改操作(INSERT/UPDATE/DELETE)默认会清空所在namespace的二级缓存。
  • 嵌套查询
    嵌套查询(<association><collection>)可能导致二级缓存失效(取决于useCache属性)。

三、缓存集成与性能优化

3.1 第三方缓存集成(Redis示例)

1. 添加依赖
<dependency>  
    <groupId>org.mybatis.caches</groupId>  
    <artifactId>mybatis-redis</artifactId>  
    <version>1.0.0-beta2</version>  
</dependency>  
2. 配置Redis缓存
<cache type="org.mybatis.caches.redis.RedisCache"/>  

<!-- redis.properties -->  
host=127.0.0.1  
port=6379  
timeout=2000  

3.2 性能优化策略

1. 合理使用缓存级别
  • 一级缓存:默认开启,适合短期高频查询(如同一请求内多次查询相同数据)。
  • 二级缓存:需显式配置,适合全局共享且读多写少的数据(如字典表、配置信息)。
2. 缓存参数调优
<!-- 高并发场景优化配置 -->  
<cache  
    eviction="LRU"  
    flushInterval="300000" <!-- 5分钟刷新一次 -->  
    size="2048"          <!-- 增大缓存容量 -->  
    readOnly="true"/>    <!-- 只读模式提升性能 -->  
3. 避免缓存穿透与雪崩
  • 缓存穿透
    查询不存在的数据导致每次都访问数据库,可通过布隆过滤器或缓存空值解决。
  • 缓存雪崩
    大量缓存同时失效导致瞬间数据库压力剧增,可通过设置随机过期时间避免。

四、面试高频问题深度解析

4.1 基础概念类问题

Q:MyBatis动态SQL与Hibernate Criteria API的区别?
A:

维度 MyBatis动态SQL Hibernate Criteria API
SQL控制 完全手动控制,灵活性高 通过API生成,灵活性低
学习成本 较低(熟悉XML标签即可) 较高(需掌握对象化查询API)
性能 接近原生SQL,性能优化空间大 可能生成冗余SQL,优化难度高
适用场景 复杂SQL场景(如多表关联、嵌套查询) 简单增删改查场景

Q:MyBatis一级缓存与二级缓存的区别?
A:

特性 一级缓存 二级缓存
作用域 SqlSession级别 Namespace级别
生命周期 会话关闭后失效 应用启动到关闭
默认开启
缓存共享 同一个会话内共享 跨会话共享
实现类 PerpetualCache 可自定义(如RedisCache)

4.2 实现原理类问题

Q:MyBatis如何实现动态SQL的条件判断?
A:

  1. 通过OGNL表达式计算条件(如#{username} != null)。
  2. 解析为对应的SqlNode实现类(如IfSqlNode)。
  3. 在SQL执行时动态决定是否包含该SQL片段。

Q:二级缓存的嵌套查询会导致什么问题?如何解决?
A:

  • 问题:嵌套查询默认不使用二级缓存,可能导致重复查询数据库。
  • 解决方案
    1. 设置useCache="true"flushCache="false"
    2. 使用<resultMap>的嵌套映射替代嵌套查询。

4.3 实战调优类问题

Q:如何解决MyBatis缓存与数据库一致性问题?
A:

  1. 更新策略
    • 增删改操作后强制刷新缓存(默认行为)。
    • 设置合理的缓存过期时间(如5分钟)。
  2. 读写分离场景
    • 主库写操作后立即刷新缓存。
    • 从库读操作使用缓存,通过数据库主从同步保证最终一致性。

Q:MyBatis动态SQL中<where>标签与<trim>标签的区别?
A:

  • <where>
    自动添加WHERE关键字,并剔除多余的AND/OR。
  • <trim>
    可自定义前缀、后缀处理,如:
    <trim prefix="WHERE" prefixOverrides="AND |OR ">  
        ...  
    </trim>  
    
    更灵活,可替代<where>标签。

总结:动态SQL与缓存的最佳实践

动态SQL设计原则

  1. 简洁优先:避免过度复杂的动态SQL,优先使用<if><where>等基础标签。
  2. 参数校验:在Java代码中进行参数校验,避免在动态SQL中处理复杂逻辑。
  3. SQL片段复用:使用<sql>标签定义公共SQL片段,通过<include>复用。

缓存使用策略

  1. 读多写少场景:启用二级缓存,如字典表、配置信息。
  2. 写操作频繁场景:禁用二级缓存,避免频繁刷新影响性能。
  3. 分布式环境:使用Redis等分布式缓存替代默认实现,保证跨节点缓存一致性。

通过系统化掌握MyBatis动态SQL与缓存机制的核心原理及最佳实践,面试者可在回答中精准匹配问题需求,例如分析“如何优化复杂查询性能”时,能结合动态SQL优化与缓存策略,展现对持久层技术的深度理解与工程实践能力。


网站公告

今日签到

点亮在社区的每一天
去签到