MyBatis 知识框架图、性能优化与面试题

一、MyBatis 知识框架图

MyBatis
核心定位
半自动 ORM
SQL 与 Java 对象映射
灵活控制 SQL
适合复杂查询和高性能场景
基础使用
全局配置 mybatis-config.xml
Mapper XML
Mapper 接口
SqlSession
SqlSessionFactory
参数映射
结果映射
核心组件
Configuration
Executor
SimpleExecutor
ReuseExecutor
BatchExecutor
StatementHandler
ParameterHandler
ResultSetHandler
TypeHandler
MappedStatement
BoundSql
SQL 映射
select
insert
update
delete
resultMap
sql 片段
include
selectKey
动态 SQL
if
choose_when_otherwise
trim_where_set
foreach
bind
script
高级映射
一对一 association
一对多 collection
嵌套查询
嵌套结果
延迟加载
鉴别器 discriminator
缓存机制
一级缓存
SqlSession 级别
默认开启
二级缓存
Mapper namespace 级别
需显式开启
第三方缓存
Redis
Ehcache
插件机制
拦截 Executor
拦截 StatementHandler
拦截 ParameterHandler
拦截 ResultSetHandler
分页插件
审计插件
SQL 改写
Spring 集成
SqlSessionTemplate
MapperScannerConfigurer
@MapperScan
事务托管
MyBatis-Spring-Boot-Starter
性能优化
SQL 优化
索引优化
批处理
分页优化
缓存优化
连接池优化
N+1 查询治理
日志与监控
源码与原理
Mapper 代理
XML 解析
SQL 执行流程
参数绑定
结果集映射
插件责任链
缓存装饰器

二、MyBatis 核心执行流程

应用调用 Mapper 接口方法
MapperProxy 动态代理拦截
根据接口方法定位 MappedStatement
SqlSession 调用 Executor
创建 StatementHandler
ParameterHandler 设置 SQL 参数
JDBC 执行 SQL
ResultSetHandler 处理结果集
TypeHandler 完成 JDBC 类型与 Java 类型转换
返回 Java 对象或集合

完整链路可以概括为:

  1. Mapper 接口方法被 JDK 动态代理拦截。
  2. MyBatis 根据 namespace + methodName 找到对应的 MappedStatement
  3. SqlSession 委托 Executor 执行查询或更新。
  4. StatementHandler 生成并预编译 SQL。
  5. ParameterHandler 绑定参数。
  6. JDBC 访问数据库。
  7. ResultSetHandler 将结果集映射为 Java 对象。
  8. 一级缓存、二级缓存按规则参与读写。

三、MyBatis 重点知识体系

1. MyBatis 是什么

MyBatis 是一个半自动 ORM 框架。它不像 Hibernate 那样试图自动生成大多数 SQL,而是让开发者自己编写 SQL,并负责把 SQL 参数和查询结果映射到 Java 对象。

它的核心价值是:

  1. SQL 可控,适合复杂业务查询。
  2. 映射能力强,可以处理对象、集合、嵌套对象。
  3. 与 Spring 集成成熟。
  4. 比纯 JDBC 少大量模板代码。
  5. 比全自动 ORM 更容易进行 SQL 调优。

2. MyBatis 与 JDBC 的关系

MyBatis 底层仍然基于 JDBC。它封装了 JDBC 中重复且容易出错的部分,例如:

  1. 获取连接。
  2. 创建 PreparedStatement
  3. 参数绑定。
  4. 执行 SQL。
  5. 遍历 ResultSet
  6. 类型转换。
  7. 关闭资源。

但 MyBatis 不会消灭 SQL。它把 SQL 的控制权留给开发者,这是它区别于很多 ORM 框架的关键。

3. 重要对象

对象 作用
SqlSessionFactory 创建 SqlSession 的工厂,通常全局单例
SqlSession 执行 SQL 的会话对象,非线程安全
MapperProxy Mapper 接口的动态代理对象
Configuration MyBatis 全局配置中心
MappedStatement 一条 SQL 映射语句的完整描述
BoundSql 最终解析出来的 SQL 和参数信息
Executor SQL 执行器,负责缓存和数据库访问
StatementHandler 负责创建和处理 JDBC Statement
ParameterHandler 负责参数绑定
ResultSetHandler 负责结果集映射
TypeHandler 负责 Java 类型与 JDBC 类型转换

4. #{} ${} 的区别

#{} 使用 PreparedStatement 参数占位符,会进行预编译参数绑定,可以防止 SQL 注入。

${} 是字符串替换,会直接把参数拼接到 SQL 中,存在 SQL 注入风险。

推荐原则:

  1. 普通参数值使用 #{}
  2. 表名、字段名、排序方向等无法使用预编译参数的位置,才考虑 ${}
  3. 使用 ${} 时必须做白名单校验。

示例:

xml 复制代码
<!-- 推荐 -->
SELECT * FROM user WHERE id = #{id}

<!-- 有风险,必须做白名单控制 -->
SELECT * FROM user ORDER BY ${orderBy}

四、MyBatis 性能优化体系

1. SQL 层优化

SQL 优化是 MyBatis 性能优化的第一优先级,因为 MyBatis 最终还是执行 SQL。

常见策略:

  1. 只查询需要的字段,避免 SELECT *
  2. 为高频查询条件建立合适索引。
  3. 避免在索引列上使用函数或隐式类型转换。
  4. 避免大表深分页。
  5. 避免返回过大的结果集。
  6. 使用 EXPLAIN 分析执行计划。
  7. 避免在循环中频繁查询数据库。

示例:

sql 复制代码
-- 不推荐
SELECT * FROM orders WHERE DATE(create_time) = '2026-05-05';

-- 推荐
SELECT id, user_id, amount, status
FROM orders
WHERE create_time >= '2026-05-05 00:00:00'
  AND create_time <  '2026-05-06 00:00:00';

原因是 DATE(create_time) 会让索引列参与函数计算,可能导致索引失效。

2. 避免 N+1 查询

N+1 查询指先查询 1 次主表,再对每条主表记录额外查询一次关联数据。

例如:

text 复制代码
查询 100 个订单:1 次
每个订单再查用户信息:100 次
总计:101 次 SQL

优化方式:

  1. 使用 JOIN 一次查询。
  2. 先批量查询主表,再用 IN 批量查询关联表。
  3. 使用嵌套结果映射代替嵌套查询。
  4. 谨慎使用延迟加载,防止遍历对象时触发大量 SQL。

推荐批量查询:

xml 复制代码
<select id="selectUsersByIds" resultType="User">
  SELECT id, name
  FROM user
  WHERE id IN
  <foreach collection="ids" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

3. 批量写入优化

大量插入或更新时,不要一条一条提交。

可选方案:

  1. 使用 JDBC 批处理,也就是 ExecutorType.BATCH
  2. 使用 MySQL 多值插入。
  3. 分批提交,例如每 500 或 1000 条提交一次。
  4. 控制单条 SQL 大小,避免超过数据库包大小限制。

示例:

java 复制代码
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    for (int i = 0; i < users.size(); i++) {
        mapper.insert(users.get(i));
        if (i % 1000 == 0) {
            sqlSession.flushStatements();
        }
    }
    sqlSession.commit();
}

在 Spring 中一般不直接手动打开 SqlSession,而是结合事务和批处理能力设计专门的批量写入方法。

4. 分页优化

普通分页:

sql 复制代码
SELECT id, title
FROM article
ORDER BY id DESC
LIMIT 100000, 20;

当偏移量很大时,数据库需要跳过大量记录,性能会下降。

优化方案:

  1. 使用游标分页,也叫 keyset pagination。
  2. 使用覆盖索引先查 ID,再回表。
  3. 限制可跳转页数。
  4. 对后台导出任务使用异步任务,不走普通分页接口。

游标分页示例:

sql 复制代码
SELECT id, title
FROM article
WHERE id < #{lastId}
ORDER BY id DESC
LIMIT 20;

5. 缓存优化

MyBatis 有两级缓存。

一级缓存:

  1. 默认开启。
  2. 作用域是 SqlSession
  3. 同一个 SqlSession 中相同查询可以复用缓存。
  4. 执行增删改、提交、回滚、关闭会清理缓存。

二级缓存:

  1. 作用域是 Mapper namespace。
  2. 需要显式开启。
  3. 要求缓存对象可序列化。
  4. 数据一致性要求高的场景要谨慎使用。

二级缓存适合:

  1. 读多写少。
  2. 数据变化不频繁。
  3. 对短时间旧数据可容忍。
  4. 查询结果体积可控。

不适合:

  1. 强一致性账户余额。
  2. 库存扣减。
  3. 高频写入数据。
  4. 多表复杂关联且更新入口分散的场景。

6. 连接池优化

MyBatis 自身可以配置数据源,但生产中通常交给 Spring Boot 和 HikariCP 管理。

重点参数:

  1. 最大连接数。
  2. 最小空闲连接。
  3. 连接超时时间。
  4. 空闲连接存活时间。
  5. 慢 SQL 日志。
  6. 泄漏检测。

连接池不是越大越好。连接数过大可能导致数据库上下文切换变多,吞吐反而下降。一般要结合数据库最大连接数、应用实例数、请求并发量和 SQL 平均耗时综合评估。

7. ResultMap 优化

复杂 resultMap 会增加映射成本,尤其是嵌套对象和集合映射。

优化建议:

  1. 简单查询使用 resultType
  2. 复杂对象使用 resultMap
  3. 一对多映射注意去重成本。
  4. 大结果集避免一次性映射成复杂对象树。
  5. 报表或列表页可以使用轻量 DTO。

8. 日志与监控

优化不能只靠猜,应通过观测定位。

建议关注:

  1. SQL 执行次数。
  2. 单条 SQL 耗时。
  3. 慢 SQL 分布。
  4. 数据库连接池等待时间。
  5. 返回行数。
  6. 是否出现 N+1 查询。
  7. 缓存命中率。
  8. 事务持有连接的时间。

常用工具:

  1. 数据库慢查询日志。
  2. EXPLAIN
  3. p6spy。
  4. Arthas。
  5. APM,例如 SkyWalking、Pinpoint、Prometheus + Grafana。

五、MyBatis 入门级面试题

1. MyBatis 是什么?它解决了什么问题?

答案:

MyBatis 是一个基于 Java 的持久层框架,属于半自动 ORM。它封装了 JDBC 的重复代码,例如连接管理、参数设置、结果集遍历和对象映射,同时保留开发者手写 SQL 的能力。

它主要解决:

  1. JDBC 代码冗余。
  2. 手动参数绑定繁琐。
  3. 手动封装结果对象麻烦。
  4. SQL 与 Java 代码混杂。
  5. 数据库访问层难维护。

相比全自动 ORM,MyBatis 的优势是 SQL 更可控,适合复杂查询和性能敏感场景。

2. MyBatis 和 Hibernate 有什么区别?

答案:

MyBatis 是半自动 ORM,需要开发者编写 SQL;Hibernate 是全自动 ORM,更多通过对象关系映射自动生成 SQL。

主要区别:

对比项 MyBatis Hibernate
SQL 控制 手写 SQL,控制强 自动生成 SQL 为主
学习成本 相对低 相对高
复杂查询 更灵活 复杂查询可能不直观
数据库迁移 SQL 与数据库耦合较强 数据库无关性更强
性能调优 直接调 SQL 需要理解 ORM 生成 SQL
适用场景 复杂业务、报表、高性能查询 标准 CRUD、领域模型驱动

面试中可以这样总结:MyBatis 更像增强版 JDBC,Hibernate 更像完整 ORM。MyBatis 牺牲了一部分自动化,换来了 SQL 可控性。

3. SqlSessionFactorySqlSession 有什么区别?

答案:

SqlSessionFactory 是创建 SqlSession 的工厂,通常在应用启动时创建一次,全局单例使用。它是线程安全的。

SqlSession 是执行 SQL 的会话对象,包含数据库连接、事务上下文和一级缓存。它不是线程安全的,不能作为单例共享。每次请求或事务通常使用独立的 SqlSession

在 Spring 集成中,开发者通常不直接操作 SqlSession,而是通过 Mapper 接口调用,底层由 SqlSessionTemplate 管理线程安全和事务。

4. Mapper 接口为什么不用写实现类?

答案:

因为 MyBatis 使用 JDK 动态代理为 Mapper 接口生成代理对象。

调用 Mapper 接口方法时,实际会进入 MapperProxyMapperProxy 根据接口全限定名和方法名拼出 statement id,例如:

text 复制代码
com.example.UserMapper.selectById

然后去 Configuration 中查找对应的 MappedStatement,最终交给 SqlSession 执行 SQL。

所以 Mapper 接口方法必须能和 XML 中的 namespace + id 对应起来。

5. #{}${} 的区别是什么?

答案:

#{} 是预编译参数占位符,底层会被转换成 ?,然后由 PreparedStatement 设置参数。它可以防止 SQL 注入。

${} 是字符串替换,会把参数直接拼接进 SQL。它不能防止 SQL 注入。

示例:

xml 复制代码
SELECT * FROM user WHERE name = #{name}

最终类似:

sql 复制代码
SELECT * FROM user WHERE name = ?

而:

xml 复制代码
SELECT * FROM user ORDER BY ${orderBy}

会直接拼接字段名。

结论:

  1. 查询条件值使用 #{}
  2. 表名、字段名、排序字段这类无法预编译的位置才使用 ${}
  3. 使用 ${} 必须做白名单校验。

6. MyBatis 如何传递多个参数?

答案:

常见方式有四种:

  1. 使用 @Param 注解。
  2. 使用 JavaBean。
  3. 使用 Map
  4. 使用 DTO 或 Query 对象。

推荐使用 @Param 或专门的 DTO。

示例:

java 复制代码
User selectByNameAndStatus(@Param("name") String name, @Param("status") Integer status);

XML:

xml 复制代码
<select id="selectByNameAndStatus" resultType="User">
  SELECT *
  FROM user
  WHERE name = #{name}
    AND status = #{status}
</select>

如果不使用 @Param,MyBatis 会使用 param1param2arg0arg1 作为默认参数名,可读性较差。

7. resultTyperesultMap 有什么区别?

答案:

resultType 用于简单映射,适合数据库字段名和 Java 属性名一致,或者开启驼峰映射后能自动匹配的场景。

resultMap 用于复杂映射,例如:

  1. 字段名和属性名差异较大。
  2. 一对一映射。
  3. 一对多映射。
  4. 嵌套对象。
  5. 继承或鉴别器映射。

示例:

xml 复制代码
<resultMap id="UserMap" type="User">
  <id property="id" column="id"/>
  <result property="userName" column="user_name"/>
</resultMap>

简单场景用 resultType,复杂场景用 resultMap

8. MyBatis 支持哪些动态 SQL 标签?

答案:

常用动态 SQL 标签包括:

  1. if:条件判断。
  2. choosewhenotherwise:类似 switch。
  3. where:自动处理 WHERE 和多余 AND/OR。
  4. set:用于动态更新,自动处理逗号。
  5. trim:自定义前缀、后缀、去除规则。
  6. foreach:遍历集合,常用于 IN 和批量插入。
  7. bind:绑定变量。

示例:

xml 复制代码
<select id="selectByCondition" resultType="User">
  SELECT *
  FROM user
  <where>
    <if test="name != null and name != ''">
      AND name = #{name}
    </if>
    <if test="status != null">
      AND status = #{status}
    </if>
  </where>
</select>

六、MyBatis 进阶级面试题

1. MyBatis 的一级缓存是什么?

答案:

一级缓存是 SqlSession 级别的缓存,默认开启,不能关闭。

在同一个 SqlSession 中,如果执行相同的 SQL、相同参数、相同分页和相同环境,MyBatis 会优先从一级缓存中取结果。

一级缓存失效场景:

  1. 不同 SqlSession
  2. 执行了 insert、update、delete。
  3. 调用了 commitrollback
  4. 手动调用 clearCache()
  5. 查询语句配置了 flushCache=true

一级缓存的主要作用是减少同一会话内重复查询,但在 Spring 中通常一个事务对应一个会话,所以它的作用范围不会跨请求。

2. MyBatis 的二级缓存是什么?

答案:

二级缓存是 Mapper namespace 级别的缓存,需要显式开启。

开启方式:

xml 复制代码
<cache/>

二级缓存的特点:

  1. 多个 SqlSession 可以共享。
  2. 作用域是同一个 Mapper namespace。
  3. 查询结果在 SqlSession 提交或关闭后才会进入二级缓存。
  4. 执行增删改默认会刷新缓存。
  5. 缓存对象通常需要可序列化。

二级缓存适合读多写少的场景。对于写频繁、强一致性要求高、多表关联复杂的业务,不建议依赖 MyBatis 二级缓存。

3. 为什么 MyBatis 二级缓存可能导致脏数据?

答案:

因为二级缓存默认按 Mapper namespace 隔离。如果一个查询涉及多张表,但更新操作发生在另一个 Mapper 中,当前 Mapper 的缓存可能不会被清理,从而返回旧数据。

示例:

  1. OrderMapper 中有查询订单和用户信息的关联 SQL。
  2. UserMapper 更新了用户名称。
  3. OrderMapper 的二级缓存没有被刷新。
  4. 再次查询订单详情时,可能拿到旧的用户名称。

解决思路:

  1. 对复杂多表查询谨慎使用二级缓存。
  2. 使用统一 Mapper 管理相关查询和更新。
  3. 使用 Redis 等外部缓存并显式设计失效策略。
  4. 对强一致性数据不使用二级缓存。

4. MyBatis 的延迟加载是什么?如何触发?

答案:

延迟加载是指查询主对象时,关联对象暂时不查询,等真正访问关联属性时再执行 SQL。

它常用于一对一、一对多关联查询。

配置示例:

xml 复制代码
<settings>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

触发方式:

java 复制代码
Order order = orderMapper.selectById(1L);
User user = order.getUser(); // 访问 user 属性时触发关联查询

优点是避免不必要查询。缺点是如果在循环中访问关联属性,可能导致 N+1 查询。

5. MyBatis 如何实现一对多映射?

答案:

可以使用 collection 标签。

示例:

xml 复制代码
<resultMap id="OrderMap" type="Order">
  <id property="id" column="order_id"/>
  <result property="amount" column="amount"/>
  <collection property="items" ofType="OrderItem">
    <id property="id" column="item_id"/>
    <result property="productName" column="product_name"/>
  </collection>
</resultMap>

SQL:

sql 复制代码
SELECT
  o.id AS order_id,
  o.amount,
  i.id AS item_id,
  i.product_name
FROM orders o
LEFT JOIN order_item i ON o.id = i.order_id
WHERE o.id = #{id}

MyBatis 会根据 <id> 标签识别主对象是否已经存在,从而把多行结果合并到同一个订单对象的 items 集合中。

6. associationcollection 的区别是什么?

答案:

association 用于一对一或多对一映射,对应单个对象属性。

collection 用于一对多映射,对应集合属性。

示例:

xml 复制代码
<!-- 一个订单对应一个用户 -->
<association property="user" javaType="User"/>

<!-- 一个订单对应多个明细 -->
<collection property="items" ofType="OrderItem"/>

一句话总结:对象用 association,集合用 collection

7. MyBatis 插件机制能拦截哪些对象?

答案:

MyBatis 插件可以拦截四类核心对象:

  1. Executor:拦截查询、更新、提交、回滚等。
  2. StatementHandler:拦截 SQL 预编译、参数设置等。
  3. ParameterHandler:拦截参数绑定。
  4. ResultSetHandler:拦截结果集处理。

常见用途:

  1. 分页插件。
  2. SQL 日志。
  3. 数据权限。
  4. 多租户。
  5. 字段加解密。
  6. 自动填充审计字段。
  7. SQL 性能监控。

插件底层使用责任链和动态代理实现。

8. MyBatis 分页插件的基本原理是什么?

答案:

分页插件通常拦截 StatementHandlerExecutor,在 SQL 执行前改写 SQL。

例如原始 SQL:

sql 复制代码
SELECT * FROM user ORDER BY id DESC

MySQL 分页改写后:

sql 复制代码
SELECT * FROM user ORDER BY id DESC LIMIT ?, ?

同时可能额外执行一条 count SQL:

sql 复制代码
SELECT COUNT(*) FROM user

核心步骤:

  1. 拦截 SQL 执行。
  2. 获取原始 SQL 和分页参数。
  3. 根据数据库方言改写 SQL。
  4. 重新设置 BoundSql
  5. 执行分页查询。
  6. 可选执行 count 查询。

9. ExecutorType.SIMPLEREUSEBATCH 有什么区别?

答案:

类型 特点 适用场景
SIMPLE 每次执行都创建新的 Statement 默认通用场景
REUSE 复用相同 SQL 的 Statement 高频重复 SQL
BATCH 批量执行更新语句 批量插入、批量更新

SIMPLE 是默认执行器。

REUSE 可以减少 Statement 创建成本,但使用场景不如默认执行器普遍。

BATCH 适合大批量写入,但要注意内存占用、事务大小和错误定位成本。

10. MyBatis 如何防止 SQL 注入?

答案:

主要依赖 #{} 的预编译参数绑定。

防御原则:

  1. 查询条件值使用 #{}
  2. 不直接使用用户输入拼接 SQL。
  3. 使用 ${} 时进行白名单校验。
  4. 排序字段、排序方向、表名等动态部分用枚举控制。
  5. 模糊查询不要直接拼接字符串。

模糊查询推荐:

xml 复制代码
<bind name="pattern" value="'%' + keyword + '%'"/>
SELECT *
FROM user
WHERE name LIKE #{pattern}

不要这样写:

xml 复制代码
WHERE name LIKE '%${keyword}%'

七、MyBatis 高级面试题

1. MyBatis Mapper 方法调用的底层原理是什么?

答案:

Mapper 接口本身没有实现类,MyBatis 通过 JDK 动态代理创建实现对象。

核心流程:

  1. MapperRegistry 保存 Mapper 接口与 MapperProxyFactory 的关系。
  2. 获取 Mapper 时,MapperProxyFactory 创建代理对象。
  3. 调用接口方法时进入 MapperProxy.invoke()
  4. 如果是 Object 方法,比如 toString,直接执行。
  5. 否则封装为 MapperMethod
  6. 根据接口名和方法名找到 MappedStatement
  7. 判断 SQL 类型:select、insert、update、delete。
  8. 调用 SqlSession 对应方法。
  9. 最终由 Executor 执行 SQL。

这也是为什么 Mapper XML 的 namespace 必须与 Mapper 接口全限定名一致,SQL 标签的 id 必须与方法名对应。

2. MyBatis 插件为什么只能拦截四大对象?

答案:

因为 MyBatis 在创建这四类对象时,会通过 InterceptorChain.pluginAll() 进行代理包装。

这四类对象是:

  1. Executor
  2. StatementHandler
  3. ParameterHandler
  4. ResultSetHandler

只有经过插件链包装的对象,才能被插件拦截。普通内部类或任意对象没有经过该代理流程,所以不能直接拦截。

插件实现通常包括:

java 复制代码
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class MyPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }
}

3. MyBatis 的缓存 key 是如何生成的?

答案:

MyBatis 会通过 CacheKey 标识一次查询。通常包含:

  1. MappedStatement 的 id。
  2. 分页参数 offset
  3. 分页参数 limit
  4. 最终 SQL。
  5. SQL 参数值。
  6. 环境 id。

只有这些信息都一致,才会命中同一个缓存。

这也解释了为什么同一个 Mapper 方法,如果参数不同或动态 SQL 生成的最终 SQL 不同,就不会命中相同缓存。

4. MyBatis 一级缓存为什么会出现脏读?如何解决?

答案:

一级缓存是 SqlSession 级别的缓存。如果同一个 SqlSession 中先查询了某条数据,之后数据库被其他事务修改,而当前 SqlSession 再次查询相同 SQL,可能会直接返回一级缓存中的旧数据。

解决方法:

  1. 缩短 SqlSession 生命周期。
  2. 在关键查询前调用 clearCache()
  3. 更新后及时提交或回滚。
  4. localCacheScope 设置为 STATEMENT,让一级缓存只在语句执行期间有效。

配置示例:

xml 复制代码
<setting name="localCacheScope" value="STATEMENT"/>

但设置为 STATEMENT 后,一级缓存复用能力会下降。

5. localCacheScope=SESSIONSTATEMENT 有什么区别?

答案:

SESSION 是默认值,表示一级缓存作用于整个 SqlSession。同一个会话内的相同查询可以复用缓存。

STATEMENT 表示一级缓存只作用于一次语句执行过程。语句执行完后缓存基本不再复用。

区别:

配置 作用范围 优点 缺点
SESSION 整个 SqlSession 减少重复查询 可能读取到旧数据
STATEMENT 单条语句 数据更新感知更及时 缓存收益更小

强一致性要求更高时,可以考虑 STATEMENT

6. MyBatis 是如何解析动态 SQL 的?

答案:

MyBatis 在解析 XML 时,会把动态 SQL 节点解析成一组 SqlNode

常见节点包括:

  1. StaticTextSqlNode
  2. TextSqlNode
  3. IfSqlNode
  4. ChooseSqlNode
  5. ForEachSqlNode
  6. WhereSqlNode
  7. SetSqlNode
  8. TrimSqlNode
  9. MixedSqlNode

执行时,MyBatis 会根据参数对象和 OGNL 表达式动态拼接 SQL,最终生成 BoundSql

大致流程:

  1. XML 被解析为 SqlSource
  2. 动态 SQL 使用 DynamicSqlSource
  3. 执行时遍历 SqlNode
  4. 根据参数计算条件。
  5. 拼接最终 SQL。
  6. 生成 BoundSql
  7. 交给后续执行器处理。

7. MyBatis 中 TypeHandler 的作用是什么?

答案:

TypeHandler 负责 Java 类型和 JDBC 类型之间的转换。

例如:

  1. Java String 与 JDBC VARCHAR
  2. Java Integer 与 JDBC INTEGER
  3. Java LocalDateTime 与 JDBC TIMESTAMP
  4. Java 枚举与数据库字段。

自定义枚举转换示例:

java 复制代码
public class StatusTypeHandler extends BaseTypeHandler<Status> {
    @Override
    public void setNonNullParameter(
            PreparedStatement ps,
            int i,
            Status parameter,
            JdbcType jdbcType
    ) throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public Status getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return Status.fromCode(rs.getInt(columnName));
    }

    @Override
    public Status getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return Status.fromCode(rs.getInt(columnIndex));
    }

    @Override
    public Status getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return Status.fromCode(cs.getInt(columnIndex));
    }
}

适用场景:

  1. 枚举映射。
  2. JSON 字段映射。
  3. 加密字段解密。
  4. 特殊时间类型转换。
  5. 数据库自定义类型。

8. MyBatis 如何处理驼峰命名?

答案:

可以通过 mapUnderscoreToCamelCase 开启下划线到驼峰的自动映射。

配置:

xml 复制代码
<setting name="mapUnderscoreToCamelCase" value="true"/>

效果:

text 复制代码
user_name -> userName
create_time -> createTime

注意:

  1. 它主要影响结果集字段到 Java 属性的映射。
  2. SQL 中仍然要写数据库真实字段名。
  3. 如果字段别名已经和属性名一致,也可以不依赖该配置。

9. MyBatis 中 RowBounds 是物理分页还是逻辑分页?

答案:

默认情况下,RowBounds 是逻辑分页。

逻辑分页指数据库先返回较多数据,然后 MyBatis 在内存中跳过 offset,再取 limit。大数据量场景性能较差。

生产环境通常使用分页插件,将分页参数改写到 SQL 中,实现物理分页。

例如 MySQL:

sql 复制代码
SELECT *
FROM user
ORDER BY id DESC
LIMIT ?, ?

所以面试回答重点是:原生 RowBounds 默认不是数据库物理分页,除非插件拦截并改写 SQL。

10. MyBatis 的 selectOne 如果查到多条记录会怎样?

答案:

selectOne 期望返回 0 或 1 条记录。

如果查到 0 条,返回 null

如果查到 1 条,返回该对象。

如果查到多条,会抛出 TooManyResultsException

因此,使用 selectOne 的 SQL 必须保证业务唯一性,例如通过唯一索引、主键或明确的 LIMIT 1 控制。

八、MyBatis 专家级面试题

1. 请描述 MyBatis 从 XML 配置加载到执行 SQL 的完整源码主线。

答案:

完整主线可以分为启动解析阶段和运行执行阶段。

启动解析阶段:

  1. SqlSessionFactoryBuilder 读取配置文件。
  2. XMLConfigBuilder 解析 mybatis-config.xml
  3. 构建 Configuration 对象。
  4. 解析 settings、typeAliases、plugins、environments、mappers 等配置。
  5. XMLMapperBuilder 解析 Mapper XML。
  6. 每个 SQL 标签被解析为一个 MappedStatement
  7. 动态 SQL 被解析为 SqlNodeSqlSource
  8. MappedStatement 注册到 Configuration.mappedStatements

运行执行阶段:

  1. 应用调用 Mapper 接口方法。
  2. MapperProxy 拦截调用。
  3. MapperMethod 根据方法签名判断执行类型和返回类型。
  4. 通过 SqlSession 调用 select、insert、update 或 delete。
  5. Executor 处理一级缓存、二级缓存和数据库访问。
  6. StatementHandler 准备 SQL。
  7. ParameterHandler 绑定参数。
  8. JDBC 执行 SQL。
  9. ResultSetHandler 映射结果。
  10. 返回 Java 对象。

这条链路中的核心对象是 Configuration -> MappedStatement -> SqlSource -> BoundSql -> Executor -> StatementHandler -> ParameterHandler -> ResultSetHandler

2. MyBatis 为什么使用装饰器实现缓存?

答案:

MyBatis 缓存体系使用了装饰器模式,基础缓存通常是 PerpetualCache,外层可以包装多个增强能力。

常见装饰器包括:

  1. LruCache:最近最少使用淘汰。
  2. FifoCache:先进先出淘汰。
  3. SoftCache:软引用缓存。
  4. WeakCache:弱引用缓存。
  5. ScheduledCache:定时清理。
  6. SerializedCache:序列化缓存。
  7. LoggingCache:日志统计。
  8. SynchronizedCache:同步控制。
  9. TransactionalCache:事务缓存。

使用装饰器的好处:

  1. 缓存能力可以灵活组合。
  2. 避免一个缓存类承担过多职责。
  3. 符合开闭原则。
  4. 便于扩展不同缓存策略。

3. TransactionalCache 的作用是什么?

答案:

TransactionalCache 用于让二级缓存和事务提交保持一致。

查询结果不会立刻写入二级缓存,而是先暂存在事务缓存中。只有当 SqlSession 提交时,缓存内容才会真正写入二级缓存。如果事务回滚,则丢弃暂存数据。

这样可以避免未提交事务的数据污染二级缓存。

核心行为:

  1. 查询结果先放入临时缓存。
  2. commit 时刷新到真正二级缓存。
  3. rollback 时清理临时缓存。
  4. 更新操作会标记需要清空二级缓存。

4. MyBatis 插件链的执行顺序是怎样的?

答案:

MyBatis 会按插件注册顺序依次包装目标对象,形成多层代理。

如果配置顺序为:

xml 复制代码
<plugins>
  <plugin interceptor="PluginA"/>
  <plugin interceptor="PluginB"/>
</plugins>

目标对象可能被包装为:

text 复制代码
PluginB代理(PluginA代理(原始对象))

调用时通常外层插件先进入,所以后注册的插件可能先执行拦截逻辑。

因此多个插件同时改写 SQL、分页、加租户条件、加数据权限时,要特别注意顺序,否则可能出现 SQL 被重复包裹、count SQL 错误、参数映射不一致等问题。

5. 如何设计一个 MyBatis 数据权限插件?

答案:

常见设计是拦截 StatementHandler.prepareExecutor.query,在 SQL 执行前追加数据权限条件。

设计要点:

  1. 从当前登录上下文获取用户、角色、部门、租户等信息。
  2. 判断当前 SQL 是否需要加权限条件。
  3. 解析 SQL,而不是简单字符串拼接。
  4. 对 SELECT 添加权限 WHERE 条件。
  5. 对 UPDATE、DELETE 也要考虑权限约束。
  6. 保持参数映射与新增参数一致。
  7. 支持白名单或注解跳过。
  8. 对复杂 SQL、子查询、UNION 做兼容测试。

推荐使用 SQL 解析库,例如 JSqlParser,而不是手写字符串拼接。

风险点:

  1. SQL 改写导致语法错误。
  2. count SQL 与分页 SQL 不一致。
  3. 多插件顺序冲突。
  4. 权限条件漏加导致越权。
  5. 参数绑定错误。
  6. SQL 过于复杂导致解析失败。

6. 如何定位 MyBatis 查询很慢的问题?

答案:

定位顺序:

  1. 确认慢的是接口、MyBatis 映射、数据库 SQL,还是网络和连接池等待。
  2. 打印最终 SQL 和参数。
  3. 在数据库中用真实参数执行 SQL。
  4. 使用 EXPLAIN 查看执行计划。
  5. 检查索引是否命中。
  6. 检查返回行数是否过大。
  7. 检查是否存在 N+1 查询。
  8. 检查是否有复杂 resultMap 映射。
  9. 检查连接池是否耗尽。
  10. 检查事务是否过长。

常见原因:

  1. 缺少索引。
  2. 索引失效。
  3. 深分页。
  4. 返回大字段或大结果集。
  5. 嵌套查询触发 N+1。
  6. 锁等待。
  7. 数据库连接池等待。
  8. 应用层映射对象过重。
  9. 日志打印过多。

优秀回答要强调:不要只盯着 MyBatis,最终瓶颈往往在 SQL、索引、连接池和数据量。

7. MyBatis 中如何处理大数据量查询?

答案:

大数据量查询要避免一次性把所有数据加载到内存。

可选方案:

  1. 分页查询。
  2. 游标查询。
  3. 流式处理。
  4. 限制返回字段。
  5. 异步导出。
  6. 使用只读事务。
  7. 控制 fetch size。

MyBatis 支持 Cursor

java 复制代码
Cursor<User> scanUsers();

使用时要注意:

  1. Cursor 必须在 SqlSession 打开期间消费。
  2. Spring 事务不能提前结束。
  3. 不要把 Cursor 结果再全部收集到 List,否则失去意义。
  4. 数据库驱动可能需要额外 fetch size 配置。

适合导出、批处理、离线任务等场景。

8. MyBatis 如何实现多租户?

答案:

常见方案有三种:

  1. 独立数据库:每个租户一个库。
  2. 独立 schema:每个租户一个 schema。
  3. 共享表:表中增加 tenant_id

MyBatis 中常见实现:

  1. 通过插件自动给 SQL 添加 tenant_id 条件。
  2. 通过动态数据源路由到不同数据库。
  3. 通过 Mapper XML 显式编写租户条件。
  4. 结合拦截器和注解控制是否忽略租户。

共享表插件方案要重点解决:

  1. SELECT 自动加 tenant_id
  2. INSERT 自动填充 tenant_id
  3. UPDATE 和 DELETE 必须带 tenant_id 条件。
  4. 防止用户传入任意租户 ID。
  5. 管理员跨租户查询要走受控白名单。

9. 如何在 MyBatis 中实现乐观锁?

答案:

乐观锁通常通过版本号字段实现。

表结构:

sql 复制代码
version INT NOT NULL DEFAULT 0

更新 SQL:

xml 复制代码
<update id="updateWithVersion">
  UPDATE product
  SET stock = #{stock},
      version = version + 1
  WHERE id = #{id}
    AND version = #{version}
</update>

如果返回更新行数为 1,说明更新成功。

如果返回 0,说明版本已变化,发生并发冲突,需要重试或提示失败。

乐观锁适合读多写少、冲突概率不高的场景。如果冲突非常频繁,重试成本可能很高,需要考虑悲观锁、队列化或库存专用扣减方案。

10. MyBatis 项目中 Mapper XML 很多,如何治理?

答案:

治理重点是规范、复用、测试和可观测。

建议:

  1. 按业务边界拆分 Mapper。
  2. XML namespace 与接口全限定名一致。
  3. SQL id 使用清晰命名。
  4. 公共字段用 <sql> 片段复用。
  5. 避免超长动态 SQL。
  6. 复杂查询沉淀为专门 DTO。
  7. 对高风险 SQL 添加集成测试。
  8. 引入 SQL lint 或代码评审规则。
  9. 建立慢 SQL 监控。
  10. 对大 XML 文件按领域拆分。

不要为了复用把所有 SQL 塞进一个通用 Mapper。过度通用会让 SQL 难读、难调试、难做权限控制。

九、性能优化面试专题题

1. 一个接口调用了 200 次 SQL,你会怎么优化?

答案:

先判断这 200 次 SQL 是否属于 N+1 查询。

排查步骤:

  1. 打开 SQL 日志或链路追踪。
  2. 统计 SQL 类型、次数和参数差异。
  3. 如果是循环中查询关联数据,改为批量查询。
  4. 如果是懒加载触发,考虑关闭该场景懒加载或改成 JOIN。
  5. 如果是权限、字典、配置类数据重复查,考虑缓存。
  6. 如果业务确实需要多次查询,评估是否可以聚合接口或预加载。

优化方案:

  1. 一次 JOIN 查询。
  2. 两阶段批量查询。
  3. 本地缓存或 Redis 缓存。
  4. 调整 resultMap,避免嵌套查询。
  5. 将循环内查询移到循环外。

优秀答案要强调:先观测,再判断是否 N+1,最后按数据一致性和结果规模选择 JOIN 或批量查询。

2. MyBatis 批量插入很慢怎么办?

答案:

优化方向:

  1. 使用批处理执行器 ExecutorType.BATCH
  2. 使用多值 insert。
  3. 每 500 到 1000 条分批提交。
  4. 关闭不必要的 SQL 日志。
  5. 减少索引数量或延后创建索引,适合离线导入场景。
  6. 检查数据库事务日志和锁等待。
  7. 控制单事务大小。

多值 insert 示例:

xml 复制代码
<insert id="batchInsert">
  INSERT INTO user(name, age)
  VALUES
  <foreach collection="list" item="item" separator=",">
    (#{item.name}, #{item.age})
  </foreach>
</insert>

注意:

  1. 单条 SQL 不要过大。
  2. 数据库有最大包大小限制。
  3. 批量失败时错误定位更复杂。
  4. 需要结合业务决定是否允许部分失败。

3. 深分页为什么慢?如何优化?

答案:

深分页慢是因为数据库需要扫描并跳过大量记录。

例如:

sql 复制代码
LIMIT 1000000, 20

数据库可能要先找到前 1000020 条,再丢弃前 1000000 条。

优化方式:

  1. 使用游标分页。
  2. 使用覆盖索引查询 ID,再回表。
  3. 限制最大页码。
  4. 搜索类场景使用搜索引擎。
  5. 导出类场景走异步任务。

覆盖索引优化示例:

sql 复制代码
SELECT a.*
FROM article a
JOIN (
  SELECT id
  FROM article
  ORDER BY id DESC
  LIMIT 1000000, 20
) t ON a.id = t.id;

更推荐游标分页:

sql 复制代码
SELECT id, title
FROM article
WHERE id < #{lastId}
ORDER BY id DESC
LIMIT 20;

4. resultMap 很复杂会影响性能吗?

答案:

会。

复杂 resultMap,尤其是一对多嵌套结果,会带来对象创建、去重、集合合并和属性填充成本。如果结果集很大,映射成本会明显上升。

优化建议:

  1. 列表页使用轻量 DTO。
  2. 详情页再查询完整对象树。
  3. 控制返回行数。
  4. 给嵌套结果正确配置 <id>,帮助 MyBatis 去重。
  5. 避免无必要的一次性深层级映射。
  6. 对报表查询直接映射到扁平 DTO。

5. 如何判断 MyBatis 慢是 SQL 慢还是映射慢?

答案:

可以分层计时。

排查方法:

  1. 从应用日志获取接口耗时和 SQL 耗时。
  2. 在数据库直接执行最终 SQL,看数据库耗时。
  3. 对比数据库耗时和接口耗时。
  4. 如果 SQL 快但接口慢,检查结果集大小、对象映射、序列化和网络传输。
  5. 如果 SQL 慢,继续分析执行计划、索引和锁等待。

例如:

text 复制代码
接口耗时:3000ms
数据库 SQL 耗时:200ms

这种情况就不应只优化 SQL,还要看:

  1. 是否返回了几万行数据。
  2. 是否复杂 resultMap 映射。
  3. 是否 JSON 序列化慢。
  4. 是否网络传输大对象。
  5. 是否日志打印了完整结果。

十、高频追问速记

问题 简答
MyBatis 是全自动 ORM 吗 不是,是半自动 ORM
SqlSession 线程安全吗 不线程安全
Mapper 接口实现类谁生成 JDK 动态代理
#{} 能防 SQL 注入吗 能,使用预编译参数
${} 能防 SQL 注入吗 不能,是字符串替换
一级缓存默认开启吗
一级缓存作用域 SqlSession
二级缓存默认开启吗 不完全是,需要显式配置使用
二级缓存作用域 Mapper namespace
selectOne 多条结果会怎样 TooManyResultsException
RowBounds 默认是物理分页吗 不是,默认逻辑分页
插件能拦截哪些对象 Executor、StatementHandler、ParameterHandler、ResultSetHandler
动态 SQL 最终生成什么 BoundSql
简单映射用什么 resultType
复杂映射用什么 resultMap
批处理用什么执行器 ExecutorType.BATCH
大数据查询推荐一次性 List 吗 不推荐
大数据查询可用什么 分页、Cursor、流式处理
慢查询先看什么 SQL、执行计划、索引、返回行数
二级缓存适合什么 读多写少、允许短暂旧数据的场景

十一、学习路线:入门到专家

入门阶段

目标:会使用 MyBatis 完成基础 CRUD。

需要掌握:

  1. Mapper 接口。
  2. Mapper XML。
  3. selectinsertupdatedelete
  4. #{} 参数绑定。
  5. resultType
  6. Spring Boot 集成。

初级进阶

目标:能写可维护的动态 SQL。

需要掌握:

  1. 动态 SQL 标签。
  2. resultMap
  3. 驼峰映射。
  4. 多参数传递。
  5. 分页查询。
  6. 批量插入。

中级阶段

目标:能处理复杂业务映射和性能问题。

需要掌握:

  1. 一对一、一对多映射。
  2. 延迟加载。
  3. 一级缓存和二级缓存。
  4. N+1 查询治理。
  5. SQL 优化。
  6. 连接池优化。
  7. 插件基本原理。

高级阶段

目标:理解 MyBatis 核心原理并能扩展。

需要掌握:

  1. Mapper 动态代理。
  2. MappedStatement
  3. BoundSql
  4. Executor
  5. StatementHandler
  6. TypeHandler
  7. 插件机制。
  8. 缓存装饰器。

专家阶段

目标:能设计复杂持久层架构和排查线上问题。

需要掌握:

  1. MyBatis 源码主线。
  2. 多租户插件。
  3. 数据权限插件。
  4. 分库分表集成。
  5. 复杂 SQL 治理。
  6. 大数据量查询方案。
  7. 慢 SQL 体系化治理。
  8. 缓存一致性设计。
  9. 事务边界设计。
  10. 线上故障定位。

十二、面试回答模板

遇到 MyBatis 面试题,可以按这个结构回答:

  1. 先给定义。
  2. 再说底层原理。
  3. 说明使用场景。
  4. 补充风险点。
  5. 给出优化或最佳实践。

例如问:二级缓存能不能随便用?

优秀回答:

二级缓存不能随便用。它是 Mapper namespace 级别的缓存,适合读多写少、数据变化不频繁、允许短时间旧数据的场景。它的问题是缓存失效粒度较粗,复杂多表查询时,如果更新发生在其他 Mapper,当前 namespace 的缓存可能不会被清理,导致旧数据。因此在强一致性、高频写、多表关联复杂的业务里,不建议依赖 MyBatis 二级缓存,更推荐使用 Redis 并设计明确的 key、过期时间和失效策略。

相关推荐
江南十四行3 小时前
Python性能优化完全指南——剖析、缓存与C扩展
python·缓存·性能优化
Devin~Y3 小时前
大厂Java面试:Spring Boot + Redis/Kafka + Spring Cloud + JVM + RAG/向量检索(小Y翻车实录)
java·jvm·spring boot·redis·spring cloud·kafka·mybatis
zxrhhm3 小时前
PostgreSQL 分页性能优化 FETCH WITH TIES 与传统 LIMIT/OFFSET 的对比
数据库·postgresql·性能优化
剑神一笑3 小时前
Linux find 命令深度解析:从递归遍历到性能优化的完整实现
linux·运维·性能优化
Hello--_--World3 小时前
React:解释什么是虚拟Dom?它的工作原理及其性能优化机制,深入理解 JSX、如何理解 UI = f(state)?
react.js·ui·性能优化
小短腿的代码世界4 小时前
QCefView深度解析:在Qt中嵌入Chromium的架构设计与性能优化实战
开发语言·qt·性能优化
不会编程的懒洋洋5 小时前
WPF 性能优化+异步+渲染
开发语言·笔记·性能优化·c#·wpf·图形渲染·线程
Java成神之路-20 小时前
面试题:MyBatis延迟加载的底层原理
mybatis
敖正炀1 天前
Spring Boot + MyBatis 企业级数据访问层实战:从选型到分库分表的深度演进
mybatis