引言:一场“血淋淋”的面试
“请介绍一下 MyBatis 中 #{} 和 ${} 的区别。”
面试官面无表情地抛出第一个问题,我心中一喜——这不是背烂了的八股文吗?于是流利地回答:“#{} 是预编译处理,可以防止 SQL 注入;${} 是字符串替换,不能防止 SQL 注入……”
“嗯,那 MyBatis 的一级缓存是 SqlSession 级别的,二级缓存是 Mapper 级别的,你能说说它们的工作流程吗?”
我继续作答,但逐渐感到面试官的问题越来越深入:“一级缓存什么时候会被清空?”“二级缓存如何实现分布式缓存?”“MyBatis 的插件原理是什么?如何分页?”
随着问题层层递进,我开始支支吾吾,最终被吊打得体无完肤……
如果你也曾经历过类似场景,或者想彻底掌握 MyBatis 的核心原理与面试要点,那么这篇 2万字详解 将是你的救命稻草。本文将以面试问答形式,从基础到源码,全面梳理 MyBatis 的知识体系。
1. MyBatis 基础概念
1.1 什么是 MyBatis?
面试官:请简单介绍一下 MyBatis 是什么,它和 Hibernate 有什么不同?
答:MyBatis 是一个半自动化的 ORM(对象关系映射)框架,它通过 XML 或注解的方式将 Java 对象与 SQL 语句映射起来。与 Hibernate 这种全自动 ORM 不同,MyBatis 需要开发者编写 SQL,因此灵活性更高,适合 SQL 优化复杂的场景。
核心特点:
-
轻量级,学习成本低。
-
支持动态 SQL,可以灵活组装查询条件。
-
与 JDBC 相比,减少了大量冗余代码。
-
支持与 Spring 等框架无缝集成。
与 Hibernate 对比:
| SQL 控制 | 全手动编写 SQL | 自动生成 SQL(也可手写) |
| 学习曲线 | 平缓 | 较陡峭 |
| 数据库无关性 | 弱(SQL 依赖数据库) | 强(HQL 屏蔽差异) |
| 性能调优 | 容易(直接优化 SQL) | 较复杂(需理解缓存、懒加载等) |
| 适用场景 | 需细粒度 SQL 优化的项目 | 业务逻辑简单的 CRUD 项目 |
1.2 MyBatis 的核心组件
面试官:MyBatis 的核心组件有哪些?它们的作用分别是什么?
答:MyBatis 的核心组件包括:
-
SqlSessionFactoryBuilder:用于构建 SqlSessionFactory,一般从 XML 配置文件或 Configuration 对象构建,构建完成后即可丢弃。
-
SqlSessionFactory:工厂接口,用于创建 SqlSession。它是线程安全的,通常一个应用只有一个实例。
-
SqlSession:代表一次数据库会话,提供了执行 SQL、提交/回滚事务、获取 Mapper 接口等方法。它不是线程安全的,作用域通常限定在方法内。
-
Mapper:由 SqlSession 动态生成的代理对象,封装了 SQL 语句的调用细节。开发者通过 Mapper 接口调用方法,即可执行对应的 SQL。
工作流程:
通过 SqlSessionFactoryBuilder 读取配置文件,创建 SqlSessionFactory。
调用 sqlSessionFactory.openSession() 获取 SqlSession。
SqlSession 根据 Mapper 接口的字节码生成代理对象。
调用 Mapper 方法,代理对象根据方法签名找到对应的 SQL 语句并执行。
关闭 SqlSession(或由 Spring 管理)。
2. MyBatis 配置文件详解
MyBatis 的全局配置文件(通常命名为 mybatis-config.xml)包含了影响框架行为的设置。下面逐一讲解。
2.1 properties 属性
作用:用于引入外部 properties 文件或定义属性值,方便在配置文件中使用占位符。
xml
<properties resource="db.properties">
<property name="username" value="root"/>
</properties>
之后在数据源配置中可引用:${driver}、${url} 等。属性加载顺序(后覆盖前):方法参数传递 > 文件 resource/url 属性 > 内部 property 子元素。
2.2 settings 设置
作用:全局参数配置,影响 MyBatis 运行时行为。例如:
xml
<settings>
<!– 开启二级缓存 –>
<setting name="cacheEnabled" value="true"/>
<!– 开启延迟加载 –>
<setting name="lazyLoadingEnabled" value="true"/>
<!– aggressiveLazyLoading –>
<setting name="aggressiveLazyLoading" value="false"/>
<!– 使用驼峰命名自动转换 –>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!– 打印 SQL 日志 –>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
每个 setting 都有默认值,常用配置包括缓存、懒加载、执行器类型等。
2.3 typeAliases 类型别名
作用:为 Java 类型设置短名称,减少 XML 中全限定名的冗余。
xml
<typeAliases>
<typeAlias alias="User" type="com.example.model.User"/>
<!– 扫描包,默认别名是类名首字母小写 –>
<package name="com.example.model"/>
</typeAliases>
内置别名如 _int、string、map、list 等。
2.4 typeHandlers 类型处理器
作用:实现 JDBC 类型与 Java 类型的相互转换。MyBatis 内置了常用类型处理器,如 String、Integer、Date 等。如果需要自定义(如处理 JSON 字段),可以实现 TypeHandler 接口或继承 BaseTypeHandler。
xml
<typeHandlers>
<typeHandler handler="com.example.handler.JsonTypeHandler" javaType="com.example.model.JsonData" jdbcType="VARCHAR"/>
</typeHandlers>
2.5 plugins 插件
作用:拦截 MyBatis 核心对象的方法调用,实现分页、性能监控等功能。后面单独章节详解。
2.6 environments 环境配置
作用:配置数据源和事务管理器,可以设置多个环境(开发、测试、生产),通过 default 指定。
xml
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
事务管理器类型:JDBC(使用 Connection 的提交回滚)、MANAGED(容器管理事务)。数据源类型:UNPOOLED(每次打开新连接)、POOLED(连接池)、JNDI。
2.7 mappers 映射器
作用:注册 SQL 映射文件或 Mapper 接口。
xml
<mappers>
<!– 使用类路径加载 XML –>
<mapper resource="com/example/mapper/UserMapper.xml"/>
<!– 使用 Mapper 接口(需同名同路径的 XML 或注解 SQL) –>
<mapper class="com.example.mapper.UserMapper"/>
<!– 扫描包(推荐) –>
<package name="com.example.mapper"/>
</mappers>
3. 映射器 XML 文件详解
映射器 XML 文件包含了 SQL 语句、结果映射、缓存配置等。一个典型的映射器文件结构如下:
xml
<?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/>
<!– 结果映射 –>
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
</resultMap>
<!– SQL 片段 –>
<sql id="baseColumns">id, name, age</sql>
<!– 查询语句 –>
<select id="selectUser" resultMap="userMap">
SELECT <include refid="baseColumns"/> FROM user WHERE id = #{id}
</select>
</mapper>
3.1 CRUD 操作
-
<insert>、<update>、<delete>、<select> 标签对应增删改查。
-
常用属性:id(方法名)、parameterType(参数类型)、resultType/resultMap(返回值类型)、flushCache(是否清空一级/二级缓存)、useCache(是否使用二级缓存)等。
示例:
xml
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name, age) VALUES(#{name}, #{age})
</insert>
3.2 resultMap 高级结果映射
resultMap 是 MyBatis 最强大的特性之一,用于解决复杂查询结果到 Java 对象的映射。
基本属性:
-
id:唯一标识。
-
type:映射的 Java 类型。
-
extends:继承其他 resultMap。
子元素:
-
<id>:标记主键,可提升性能。
-
<result>:普通字段映射。
-
<association>:一对一关联。
-
<collection>:一对多关联。
-
<discriminator>:鉴别器(根据字段值选择不同的映射)。
关联映射示例:
xml
<!– 用户和订单的一对多关系 –>
<resultMap id="userWithOrders" type="User">
<id property="id" column="u_id"/>
<result property="name" column="u_name"/>
<collection property="orders" ofType="Order">
<id property="id" column="o_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
<select id="selectUserWithOrders" resultMap="userWithOrders">
SELECT u.id u_id, u.name u_name, o.id o_id, o.order_no
FROM user u
LEFT JOIN order o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
延迟加载:在 MyBatis 中,可以通过配置 lazyLoadingEnabled=true 和 aggressiveLazyLoading=false 来启用延迟加载。同时,association 和 collection 可以设置 fetchType="lazy" 覆盖全局配置。
xml
<association property="address" column="address_id" select="selectAddress" fetchType="lazy"/>
3.3 动态 SQL
动态 SQL 是 MyBatis 的核心优势,可以在 XML 中编写类似 JSTL 的条件判断、循环等逻辑。
常用标签:
-
<if>:条件判断。
-
<choose>、<when>、<otherwise>:多分支选择。
-
<where>、<set>、<trim>:智能处理 AND/OR 和逗号。
-
<foreach>:循环集合。
-
<bind>:创建变量(如模糊查询拼接)。
示例:
xml
<select id="findActiveUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
<update id="updateUser">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</set>
WHERE id = #{id}
</update>
<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="searchUsers" resultType="User">
<bind name="pattern" value="'%' + keyword + '%'"/>
SELECT * FROM user WHERE name LIKE #{pattern}
</select>
4. 缓存机制
4.1 一级缓存(Local Cache)
面试官:请说明 MyBatis 一级缓存的工作原理,以及它可能导致的问题。
答:一级缓存是 SqlSession 级别的缓存,默认开启且无法关闭(在 MyBatis 3 中可配置 localCacheScope 为 STATEMENT 来关闭,但实际是清空缓存)。它基于 PerpetualCache 实现,底层是一个 HashMap。
工作流程:
执行查询时,先从缓存中根据 CacheKey 查找结果。
如果找到,直接返回缓存对象。
如果未找到,查询数据库,然后将结果放入缓存。
CacheKey 的组成:
-
MappedStatement 的 id。
-
查询参数的组合(包括分页参数等)。
-
具体的 SQL 语句(包括动态 SQL 生成的 SQL)。
-
分页的 RowBounds 等。
一级缓存失效(清空)情况:
-
执行 SqlSession 的 update、delete、insert 操作(即使影响行数为0),会清空一级缓存。
-
手动调用 clearCache() 方法。
-
执行 commit()、rollback() 或 close() 后,SqlSession 关闭,缓存自然消失。
-
如果配置了 localCacheScope=STATEMENT,则每次查询后清空缓存(相当于缓存只在当前语句有效)。
注意问题:由于一级缓存是 SqlSession 级别的,在 Spring 整合中,如果 Service 方法未开启事务,每次 Mapper 操作都会新建 SqlSession(或使用 SqlSessionTemplate 的线程绑定机制),一级缓存不共享,因此不会出现脏数据。但在事务方法内,多次查询可能使用同一 SqlSession,此时需注意缓存可能带来的数据不一致问题(如其他事务修改了数据,但当前 SqlSession 缓存未更新)。
4.2 二级缓存(Second Level Cache)
面试官:那二级缓存呢?它如何工作?有什么优缺点?
答:二级缓存是 Mapper 级别(namespace)的缓存,可以被多个 SqlSession 共享。它基于装饰器模式,默认也是用 PerpetualCache,但可以配置如 Redis、EhCache 等第三方缓存实现。
开启方式:
在全局配置文件 <settings> 中设置 cacheEnabled=true(默认就是 true)。
在 Mapper XML 中添加 <cache/> 标签,或使用 @CacheNamespace 注解。
查询语句需要设置 useCache=true(默认 true)。
<cache/> 标签属性:
-
eviction:缓存回收策略(LRU、FIFO、SOFT、WEAK),默认 LRU。
-
flushInterval:刷新间隔(毫秒),默认不设置(仅当调用更新语句时刷新)。
-
size:引用数目,默认 1024。
-
readOnly:是否只读,true 则返回缓存对象本身(速度快但可能并发修改),false 则返回拷贝对象(安全但性能稍低),默认 false。
-
type:自定义缓存实现类。
工作流程:
二级缓存作用域为 namespace,所有 SqlSession 共享。
当 SqlSession 执行查询时,先查二级缓存(若开启),再查一级缓存,最后查数据库。
当执行更新操作(insert/update/delete)时,会清空该 namespace 下的二级缓存(包括关联的其他 namespace 如果配置了 <cache-ref>)。
与一级缓存的关系:
-
二级缓存的数据在 SqlSession 提交或关闭后才生效(因为事务未提交时数据可能回滚,不能放入二级缓存)。
-
查询时数据加载顺序:二级缓存 → 一级缓存 → 数据库。
脏数据问题:
-
由于二级缓存是 namespace 级别的,当不同 namespace 操作关联表时可能导致数据不一致。例如,UserMapper 缓存了用户数据,但 OrderMapper 更新了用户订单(不涉及用户表),此时 User 缓存未失效,查询到旧数据。
-
解决方案:使用 <cache-ref> 让多个 namespace 共享同一个缓存区域,或避免在关联查询中使用二级缓存。
分布式缓存:
可以通过实现 Cache 接口,将缓存数据存放到 Redis 等中间件中,实现分布式环境下的缓存共享。MyBatis 提供了 org.mybatis.caches.redis.RedisCache 等第三方集成包。
4.3 自定义缓存
面试官:如果需要集成 Redis 作为二级缓存,你会怎么做?
答:可以自定义一个实现 org.apache.ibatis.cache.Cache 接口的类,并在 <cache type="…"/> 中指定。主要实现方法:getId、putObject、getObject、removeObject、clear、getSize 等。
示例(伪代码):
java
public class RedisCache implements Cache {
private final String id;
private JedisPool jedisPool;
public RedisCache(String id) {
this.id = id;
// 初始化连接池
}
@Override
public void putObject(Object key, Object value) {
// 序列化 key 和 value,存入 Redis
}
@Override
public Object getObject(Object key) {
// 从 Redis 获取并反序列化
}
// … 其他方法
}
然后在 Mapper 中配置:
xml
<cache type="com.example.RedisCache" eviction="LRU" flushInterval="60000"/>
5. 插件开发
5.1 插件原理
面试官:MyBatis 的插件(Interceptor)是如何工作的?你能实现一个分页插件吗?
答:MyBatis 允许拦截以下接口的方法:
-
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
-
ParameterHandler (getParameterObject, setParameters)
-
ResultSetHandler (handleResultSets, handleOutputParameters)
-
StatementHandler (prepare, parameterize, batch, update, query)
插件通过动态代理实现,每个被拦截的对象都会被代理对象包装,执行时先调用插件的 intercept 方法。
实现步骤:
实现 Interceptor 接口。
使用 @Intercepts 和 @Signature 注解指定要拦截的方法。
在全局配置文件中注册插件。
5.2 分页插件实现
分页是常见需求,我们可以通过拦截 Executor 的 query 方法,在 SQL 后拼接 limit 语句。
java
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
// 判断是否需要分页(例如参数中带有 Page 对象)
if (parameter instanceof Page) {
Page page = (Page) parameter;
// 获取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
// 拼接 count 查询获取总记录数(省略)
// 拼接分页 SQL
String pageSql = sql + " LIMIT " + page.getOffset() + "," + page.getPageSize();
// 通过反射修改 BoundSql 中的 SQL
ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可读取配置参数
}
}
注意:实际实现更复杂,需要考虑方言(不同数据库分页语法)、预编译参数处理等。现在已有成熟分页插件如 PageHelper。
6. 与 Spring 整合
6.1 整合方式
面试官:MyBatis 如何与 Spring 整合?整合后有什么好处?
答:Spring 通过 mybatis-spring 桥接包提供无缝整合。主要配置:
-
SqlSessionFactoryBean:用于创建 MyBatis 的 SqlSessionFactory,替代原生 SqlSessionFactoryBuilder。
-
MapperFactoryBean 或 MapperScannerConfigurer:用于自动生成 Mapper 代理并注入 Spring 容器。
-
SqlSessionTemplate:替代原生的 SqlSession,保证线程安全,并与 Spring 事务同步。
配置示例(JavaConfig 方式):
java
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.setTypeAliasesPackage("com.example.model");
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(sqlSessionFactory());
}
}
整合好处:
-
Spring 管理数据源和事务,MyBatis 操作自动参与事务。
-
Mapper 接口可直接注入,无需手动获取 SqlSession。
-
SqlSessionTemplate 线程安全,避免原生 SqlSession 的线程不安全问题。
6.2 事务管理
面试官:在 Spring 整合中,MyBatis 是如何与 Spring 事务保持一致的?
答:当使用 SqlSessionTemplate 时,它内部会通过 SpringManagedTransaction 管理事务。SqlSessionTemplate 的每个方法都会检查当前是否存在 Spring 事务(通过 TransactionSynchronizationManager),如果有,则复用同一个 SqlSession;如果没有,则创建新的 SqlSession,并在操作完成后自动提交/回滚。这样可以保证在同一个事务中,所有 Mapper 操作使用同一个数据库连接,保持事务一致性。
7. 常见面试题深度解析
7.1 #{} 和 ${} 的区别
问题:请详细说明 #{} 和 ${} 的区别,并举例说明各自的使用场景。
答:
-
#{}:预编译处理,MyBatis 会将 SQL 中的 #{} 替换为 ?,然后通过 PreparedStatement 的 set 方法赋值。这可以防止 SQL 注入,并且能够自动进行类型转换。
-
${}:字符串替换,MyBatis 会将 SQL 中的 ${} 直接替换为参数值(原样拼接),不经过预编译。容易引发 SQL 注入,但适用于动态表名、列名等无法使用占位符的场景。
示例:
xml
<select id="selectUser" resultType="User">
SELECT * FROM user WHERE name = #{name} <!– 安全 –>
</select>
<select id="selectByTable" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id} <!– 表名动态传入 –>
</select>
注意:使用 ${} 时,传入的参数值需要做好安全过滤(如白名单校验)。
7.2 一级缓存失效场景
问题:MyBatis 一级缓存什么时候会失效(被清空)?为什么执行 update 操作会清空缓存?
答:
一级缓存失效的场景包括:
执行 insert、update、delete 操作(MyBatis 认为这些操作可能导致数据变化,因此清空缓存)。
手动调用 sqlSession.clearCache()。
执行 commit、rollback、close 操作(SqlSession 生命周期结束)。
如果设置了 localCacheScope=STATEMENT,则每次查询后缓存会被清空(相当于只缓存当前语句)。
为什么 update 会清空缓存:因为 MyBatis 无法知道 update 语句具体修改了哪些数据,为了保证数据一致性,最简单的做法就是清空该 SqlSession 的所有缓存。这也是一级缓存粒度粗的体现。
7.3 二级缓存脏数据问题
问题:请说明二级缓存可能产生脏数据的原因,以及如何避免。
答:
原因:二级缓存是 namespace 级别的,如果两个不同的 namespace 操作了同一张表(例如 UserMapper 和 OrderMapper 都涉及 user 表),但缓存却隔离,就会导致数据不一致。例如:
-
UserMapper 查询用户列表并缓存。
-
OrderMapper 更新了某个用户的订单(但不涉及用户表),不会触发 UserMapper 的缓存清空。
-
之后再次通过 UserMapper 查询用户列表,返回的是旧缓存数据(用户信息可能已过时)。
解决方案:
-
避免在多 namespace 中使用二级缓存,或确保所有操作同一张表的 Mapper 都通过 <cache-ref> 共享同一个缓存区域。
-
在更新操作时,手动清空相关 namespace 的缓存(不推荐)。
-
使用更细粒度的缓存策略,如按主键缓存,但 MyBatis 二级缓存本身不支持这种细粒度(缓存 key 是整个查询语句+参数)。
-
在分布式环境下,可采用集中式缓存(如 Redis),并通过缓存失效策略解决。
7.4 MyBatis 的执行器类型
问题:MyBatis 有哪几种执行器(Executor)?分别有什么特点?
答:
MyBatis 有三种内置执行器:
-
SIMPLE:默认执行器,每次执行 SQL 都会创建一个新的 PreparedStatement,执行完立即关闭。
-
REUSE:可重用执行器,会将 PreparedStatement 缓存起来(通过 Map<String, PreparedStatement>),下次执行同一条 SQL 时重用,减少预编译开销。
-
BATCH:批处理执行器,用于批量更新操作,将多个 SQL 语句批量发送到数据库执行,提高性能。但需要注意,BatchExecutor 在执行 query 或 commit 时会强制刷新批处理语句。
可以在全局配置文件中通过 <setting name="defaultExecutorType" value="REUSE"/> 修改,也可以在创建 SqlSession 时指定。
7.5 如何获取自增主键
问题:在插入数据后,如何获取数据库自动生成的主键?
答:
有两种方式:
使用 useGeneratedKeys 和 keyProperty(推荐):
xml
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(name) VALUES(#{name})
</insert>
执行后,传入的 user 对象的 id 属性会被自动赋值为生成的主键。
使用 selectKey 标签(适用于非自增主键或复杂生成逻辑):
xml
<insert id="insertUser">
<selectKey keyProperty="id" order="AFTER" resultType="int">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO user(name) VALUES(#{name})
</insert>
order 属性可以是 BEFORE 或 AFTER,BEFORE 用于获取序列等场景。
7.6 MyBatis 的延迟加载原理
问题:MyBatis 的延迟加载是如何实现的?它有什么优缺点?
答:
实现原理:
-
MyBatis 通过创建目标对象的代理(CGLIB 或 Javassist)来实现延迟加载。
-
当配置了延迟加载的关联属性(如 association 或 collection)时,MyBatis 会在映射时设置一个代理对象。
-
第一次访问该属性时,代理对象会触发后续查询(执行预先定义的 SQL),从数据库加载数据并填充。
配置:需要开启全局 lazyLoadingEnabled=true,并且关联查询中设置 fetchType="lazy"。
优点:按需加载,避免一次性加载过多数据,提高性能。
缺点:可能引发 N+1 查询问题(循环查询),需要在设计时权衡;同时延迟加载需要代理支持,可能带来少许性能开销。
N+1 问题示例:
查询所有用户(1 条 SQL),然后遍历每个用户获取其订单(N 条 SQL)。解决方案:使用关联查询一次性加载(join),或使用延迟加载的批量模式(配合 aggressiveLazyLoading 等)。
7.7 MyBatis 的分页方式
问题:MyBatis 有哪些分页实现方式?逻辑分页和物理分页有什么区别?
答:
-
逻辑分页:使用 MyBatis 自带的 RowBounds 对象,它在查询所有结果后,在内存中进行截取(offset, limit)。适合数据量小的场景,数据量大时性能差。
-
物理分页:在 SQL 语句中添加 LIMIT 等数据库分页语法,只查询需要的数据。可通过手动拼接 SQL 或使用分页插件实现。
-
分页插件:如 PageHelper,基于拦截器在 SQL 执行前动态拼接分页语句,并自动查询总记录数,对业务代码无侵入。
区别:逻辑分页查询所有数据,消耗网络和内存;物理分页只查询部分数据,效率高。
7.8 MyBatis 的批量操作
问题:如何使用 MyBatis 进行高效的批量插入或更新?
答:
有两种方式:
使用 BatchExecutor:
通过 sqlSessionFactory.openSession(ExecutorType.BATCH) 获取 SqlSession,然后连续执行多次 insert/update 操作,最后 commit。MyBatis 会将多条 SQL 合并到一次网络请求中(取决于数据库驱动支持),提高效率。
使用 foreach 拼接 SQL:
在 Mapper XML 中,使用 <foreach> 一次性拼接多条 insert 语句,如:
xml
<insert id="batchInsert">
INSERT INTO user(name) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name})
</foreach>
</insert>
这种方式只需执行一次 SQL,效率更高,但注意 SQL 长度限制(max_allowed_packet)。
推荐:对于大量数据,采用 BatchExecutor 方式,避免 SQL 过长;对于中小量数据,使用 foreach 方式简单高效。
7.9 MyBatis 和 Spring 整合后的事务管理
问题:在 Spring 中,如果 Service 方法上有 @Transactional 注解,MyBatis 的 SqlSession 是如何获取和管理的?
答:
-
Spring 通过 SqlSessionTemplate 的 getSqlSessionFactory() 获取 SqlSession,内部通过 SqlSessionUtils 的 getSqlSession 方法获取当前事务绑定的 SqlSession。
-
该方法会检查当前是否存在 Spring 事务(通过 TransactionSynchronizationManager 的资源绑定)。如果有,则从 ThreadLocal 中获取绑定的 SqlSession;如果没有,则创建新的 SqlSession,并在方法结束后自动关闭。
-
同时,Spring 会将 MyBatis 的 SqlSession 包装成 SqlSessionInterceptor 的代理,实现事务同步:当 Spring 事务提交时,会通过事务同步机制调用 MyBatis 的 commit;回滚时调用 rollback。
-
这样,一个事务内的所有 Mapper 操作都使用同一个 SqlSession,保证了事务的原子性和一致性。
7.10 MyBatis 的二级缓存与 Redis 集成时,如何保证缓存一致?
问题:当使用 Redis 作为二级缓存,多个应用实例同时操作数据库时,如何保证缓存的一致性?
答:
使用 Redis 作为二级缓存时,缓存是应用实例共享的,但仍然面临缓存与数据库的一致性问题。常见的解决方案有:
-
设置合理的缓存过期时间,让缓存最终一致。
-
更新数据库后主动清空相关缓存。可以在 Service 层手动删除 Redis 中的缓存键,或者在 MyBatis 的缓存实现中监听更新操作(但 MyBatis 二级缓存本身会在更新时调用 clear 方法,如果自定义 RedisCache 的 clear 方法实现为删除 namespace 下的所有键,则可达到目的)。
-
使用缓存模式:如 Cache-Aside(旁路缓存),先更新数据库,再删除缓存;或者先删除缓存,再更新数据库(需处理并发问题)。
-
对于强一致性要求高的场景,建议不要使用二级缓存,或者使用分布式事务等复杂方案。
需要注意的是,MyBatis 的二级缓存是基于 namespace 的,如果采用自定义 RedisCache,clear 方法通常需要删除该 namespace 下的所有键,这可能导致缓存失效范围过大。一些优化方案如:使用多级缓存,或针对每个查询结果单独缓存键,但 MyBatis 本身的 CacheKey 设计已经包含了查询参数,因此更新操作无法精确知道哪些查询键需要失效,只能全清。
8. 源码简析:从配置到执行
为了更深入理解 MyBatis,我们简要过一下核心源码流程。
8.1 构建 SqlSessionFactory
java
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
build 方法会创建 XMLConfigBuilder 解析配置文件,生成 Configuration 对象,然后返回 DefaultSqlSessionFactory。
Configuration 是 MyBatis 的核心配置类,包含了所有配置信息(environment、mapper 注册、缓存等)。它内部维护了一个 Map<String, MappedStatement>,key 为 namespace + id,value 是封装了 SQL 语句、参数映射、结果映射等信息的 MappedStatement。
8.2 获取 SqlSession 和 Mapper
java
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
openSession 根据配置的 Executor 类型创建执行器,并包装事务。getMapper 通过 MapperProxyFactory 创建动态代理 MapperProxy。
8.3 执行 Mapper 方法
以 mapper.selectUser(1) 为例:
MapperProxy 的 invoke 方法被调用。
根据方法名从 Configuration 中获取 MappedStatement。
创建 Executor 执行查询。如果开启二级缓存,会先创建 CachingExecutor 包装原始 Executor。
在 CachingExecutor 中,先根据 MappedStatement 的 cache 配置尝试从二级缓存获取结果。
如果二级缓存没有命中,则调用实际 Executor(如 SimpleExecutor)的 query 方法。
SimpleExecutor 会创建 StatementHandler,并通过 ParameterHandler 设置参数,通过 ResultSetHandler 处理结果集。
在执行过程中,一级缓存会通过 LocalCache 处理(在 BaseExecutor 中)。
最终返回结果。
8.4 缓存查找逻辑
CachingExecutor 的 query 方法:
java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
// 判断是否需要刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 从二级缓存获取
List<E> list = (List<E>) cache.getObject(key);
if (list == null) {
list = delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
cache.putObject(key, list);
}
return list;
}
}
return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
BaseExecutor 的 query 方法处理一级缓存:
java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// … 省略
List<E> list = localCache.getObject(key);
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
return list;
}
queryFromDatabase 在查询后会放入 localCache。
9. 性能优化与最佳实践
9.1 SQL 层面优化
-
合理使用索引,避免全表扫描。
-
尽量减少关联查询,或使用冗余字段。
-
使用批量操作减少数据库交互。
-
对分页查询使用物理分页,并优化 count 查询。
9.2 缓存优化
-
合理使用一级缓存和二级缓存,避免脏数据。
-
对于查询频率高、更新频率低的数据,开启二级缓存。
-
二级缓存适合只读或读写比高的场景,注意序列化。
9.3 插件使用
-
使用分页插件统一处理分页逻辑。
-
使用性能监控插件(如慢 SQL 日志)帮助优化。
9.4 配置调优
-
数据源连接池大小设置合理(根据并发数和数据库最大连接数)。
-
调整 fetchSize 减少网络传输(大数据量查询时)。
-
使用 REUSE 执行器减少 SQL 预编译开销。



