|
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用范围 | SqlSession 级别(同一个会话) |
Mapper 接口级别(同一个 namespace) |
| 默认状态 | 默认开启,无法关闭 | 默认关闭,需手动开启 |
| 缓存键 | SqlSession + MappedStatement + 参数 |
Mapper namespace + MappedStatement + 参数 |
| 失效时机 | SqlSession 关闭或执行写操作(insert/update/delete) |
同一 namespace 执行写操作,或缓存过期 |
| 存储介质 | 内存 | 内存(可配置第三方缓存如 Redis) |
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询:从数据库获取
User user1 = mapper.selectById(1);
// 第二次查询:命中一级缓存
User user2 = mapper.selectById(1);
System.out.println(user1 == user2); // true(同一对象)
}
全局开启二级缓存:
<settings>
<setting name="cacheEnabled" value="true"/> <!-- 默认true,可省略 -->
</settings>
在 Mapper.xml 中配置缓存:
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache
eviction="LRU" <!-- 淘汰策略:LRU(最近最少使用) -->
flushInterval="60000" <!-- 自动刷新时间(毫秒) -->
size="1024" <!-- 最大缓存对象数 -->
readOnly="false"/>
<!-- 配置SQL使用缓存 -->
<select id="selectById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 写操作刷新缓存 -->
<update id="updateById" flushCache="true">
UPDATE user SET username = #{username} WHERE id = #{id}
</update>
</mapper>
实体类实现序列化:
public class User implements Serializable { ... }
SqlSession 的生命周期即可面试点睛:需明确两级缓存的作用范围和失效机制,以及二级缓存的适用场景限制。
MyBatis 默认的二级缓存基于内存,不适合分布式环境,整合 Redis 作为二级缓存的步骤如下:
引入依赖:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
配置 Redis 缓存:
在 src/main/resources 下创建 redis.properties:
redis.host=localhost
redis.port=6379
redis.timeout=2000
redis.password=
redis.database=0
redis.keyPrefix=mybatis:cache:
redis.expire=3600 # 缓存过期时间(秒)
在 Mapper 中使用 Redis 缓存:
<mapper namespace="com.example.mapper.DictMapper">
<!-- 指定使用Redis缓存 -->
<cache type="org.mybatis.caches.redis.RedisCache"/>
<!-- 查询方法使用缓存 -->
<select id="selectByType" resultType="Dict" useCache="true">
SELECT * FROM dict WHERE type = #{type}
</select>
</mapper>
自定义 Redis 缓存(可选):
如需自定义序列化方式或缓存逻辑,可继承 RedisCache:
public class CustomRedisCache extends RedisCache {
public CustomRedisCache(String id) {
super(id);
}
@Override
public void putObject(Object key, Object value) {
// 自定义存入逻辑(如使用JSON序列化)
super.putObject(key, value);
}
@Override
public Object getObject(Object key) {
// 自定义获取逻辑
return super.getObject(key);
}
}
并在 Mapper 中引用:
<cache type="com.example.cache.CustomRedisCache"/>
MyBatis 的缓存接口 Cache 是扩展点,RedisCache 实现了该接口,将缓存数据存储到 Redis 而非内存,实现分布式环境下的缓存共享。
面试点睛:重点说明 MyBatis 缓存的扩展机制(通过实现
Cache接口),以及分布式缓存的必要性。
MyBatis 的插件机制基于拦截器模式,允许在 SQL 执行过程中插入自定义逻辑,可拦截的四大核心对象包括:
Executor:执行器(update/query/flushStatements/commit/rollback 等方法)ParameterHandler:参数处理器(getParameterObject/setParameters 方法)ResultSetHandler:结果集处理器(handleResultSets/handleOutputParameters 方法)StatementHandler:语句处理器(prepare/parameterize/batch/update/query 方法)实现 Interceptor 接口:
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 2. 获取当前SQL信息
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
// 3. 判断是否需要分页(假设参数中包含Page对象)
Object parameter = statementHandler.getParameterHandler().getParameterObject();
if (parameter instanceof Page) {
Page<?> page = (Page<?>) parameter;
// 4. 重写SQL,添加分页条件(以MySQL为例)
String pageSql = sql + " LIMIT " + page.getOffset() + ", " + page.getPageSize();
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
// 5. 执行原方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// 生成代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取插件配置参数
}
}
配置插件:
<plugins>
<plugin interceptor="com.example.plugin.PageInterceptor">
<!-- 可选配置参数 -->
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
使用分页插件:
Page<User> page = new Page<>(1, 10); // 第1页,每页10条
List<User> users = userMapper.selectByPage(page);
long total = page.getTotal(); // 总条数(需额外查询count)
MyBatis 通过 JDK 动态代理为被拦截对象生成代理,当调用被拦截方法时,会先执行插件的 intercept 方法,再通过 invocation.proceed() 执行原方法。
面试点睛:需说明插件可拦截的对象和方法,以及拦截器的实现要点(
@Intercepts注解、Plugin.wrap方法)。
延迟加载是 MyBatis 的关联查询优化机制,指在查询主对象时不立即加载关联对象,而是在真正使用关联对象时才执行查询,减少不必要的数据库访问。
MyBatis 通过 CGLIB 或 JDK 动态代理为关联对象创建代理对象,当调用关联对象的 getter 方法时,触发代理逻辑,执行关联查询 SQL。
全局开启懒加载:
<settings>
<setting name="lazyLoadingEnabled" value="true"/> <!-- 开启懒加载 -->
<setting name="aggressiveLazyLoading" value="false"/> <!-- 关闭积极加载(按需加载) -->
</settings>
在 resultMap 中配置关联查询:
<!-- 订单结果映射 -->
<resultMap id="orderMap" type="Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<!-- 懒加载订单项(一对多) -->
<collection
property="items"
ofType="OrderItem"
select="com.example.mapper.OrderItemMapper.selectByOrderId"
column="id"
fetchType="lazy"/> <!-- 显式指定懒加载 -->
</resultMap>
<select id="selectOrder" resultMap="orderMap">
SELECT id, order_no FROM `order` WHERE id = #{id}
</select>
订单项查询:
<mapper namespace="com.example.mapper.OrderItemMapper">
<select id="selectByOrderId" resultType="OrderItem">
SELECT * FROM order_item WHERE order_id = #{orderId}
</select>
</mapper>
Order order = orderMapper.selectOrder(1L);
// 此时未查询订单项,只查询了订单主表
System.out.println(order.getOrderNo()); // 不触发关联查询
List<OrderItem> items = order.getItems();
// 调用getItems()时,触发懒加载,执行selectByOrderId查询
items.forEach(System.out::println);
association/collection)SqlSession 生命周期内使用懒加载的关联对象,否则会抛出异常fetchType="eager" 为特定关联配置立即加载面试点睛:核心是动态代理机制,需说明懒加载的触发时机和配置要点,以及与 N+1 查询问题的关系。
MyBatis 的 Mapper 代理通过 JDK 动态代理实现,核心是 MapperProxy 类,其工作流程如下:
获取 Mapper 接口代理对象:
当调用 SqlSession.getMapper(Class<T> type) 时,MyBatis 通过 MapperProxyFactory 创建代理对象。
// MapperRegistry类
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
return mapperProxyFactory.newInstance(sqlSession);
}
// MapperProxyFactory类
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 创建JDK动态代理
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy
);
}
代理对象执行方法:
当调用 Mapper 接口方法时,会触发 MapperProxy 的 invoke 方法。
// MapperProxy类
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 处理Object类的方法(如toString、hashCode)
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 处理接口默认方法
if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
// 处理Mapper接口方法
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
方法执行映射:
MapperMethod 将接口方法映射为 SqlSession 的对应操作。
// MapperMethod类
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
// 处理更新
}
case DELETE: {
// 处理删除
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
// 处理带ResultHandler的查询
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args); // 处理集合查询
} else if (method.returnsMap()) {
// 处理Map查询
} else {
// 处理单个对象查询
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
// 处理返回结果
return result;
}
通过 "接口全限定名 + 方法名" 与 Mapper.xml 中 SQL 的 id 进行匹配,将接口方法调用转换为对 SqlSession 相应方法的调用,实现无侵入式的 SQL 执行。
面试点睛:这是 MyBatis 的核心设计,需结合动态代理和方法映射的过程说明,体现对框架底层的理解。
N+1 查询问题是 MyBatis 关联查询中常见的性能陷阱,指查询 N 条主表数据后,每条主表数据又触发一次子表查询,导致总共 N+1 次数据库交互。
// 1. 查询所有订单(1次查询)
List<Order> orders = orderMapper.selectAll();
// 2. 遍历订单,查询每个订单的明细(N次查询)
for (Order order : orders) {
List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
order.setItems(items);
}
// 总计:1 + N 次查询
association 或 collection 进行 JOIN 查询,一次性获取所有数据。
<resultMap id="orderWithItemsMap" type="Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<collection property="items" ofType="OrderItem">
<id column="item_id" property="id"/>
<result column="product_id" property="productId"/>
</collection>
</resultMap><select id="selectOrdersWithItems" resultMap="orderWithItemsMap">
SELECT
o.id, o.order_no,
oi.id AS item_id, oi.product_id
FROM order o
LEFT JOIN order_item oi ON o.id = oi.order_id
</select>
- **延迟加载 + 批量查询**:
结合懒加载和 `foreach` 实现 "1+1" 次查询。
```xml
<!-- 订单映射(懒加载) -->
<resultMap id="orderMap" type="Order">
<id column="id" property="id"/>
<collection
property="items"
select="com.example.mapper.OrderItemMapper.selectByOrderIds"
column="id"
fetchType="lazy"/>
</resultMap>
<!-- 订单项批量查询 -->
<select id="selectByOrderIds" resultType="OrderItem">
SELECT * FROM order_item WHERE order_id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
@TableName 和 @TableField 注解简化关联查询。面试点睛:需说明 N+1 问题的成因和两种解决方案的适用场景(关联查询适合数据量小的场景,延迟加载 + 批量查询适合数据量大的场景)。
MyBatis 的性能优化需从 SQL、缓存、连接池等多方面入手,常见手段包括:
SQL 优化:
SELECT *,只查询必要字段缓存优化:
SqlSession 生命周期)连接池优化:
<dataSource type="POOLED">
<property name="poolMaximumActiveConnections" value="20"/> <!-- 最大活跃连接 -->
<property name="poolMaximumIdleConnections" value="10"/> <!-- 最大空闲连接 -->
<property name="poolMaximumCheckoutTime" value="20000"/> <!-- 最大 checkout 时间 -->
</dataSource>批量操作优化:
BatchExecutor 执行批量插入 / 更新foreach 批量操作
<insert id="batchInsert">
INSERT INTO user (username, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>其他优化:
resultMap 代替 resultType,避免字段映射开销fetchSize 控制 JDBC 的每次抓取行数EXPLAIN 分析并优化面试点睛:性能优化是企业级应用的重点,需结合具体场景说明优化手段,体现实战经验。
MyBatis 支持调用数据库存储过程,通过 statementType="CALLABLE" 配置,并使用 #{parameterMode=IN/OUT/INOUT} 指定参数模式。
创建存储过程:
-- 根据用户ID查询用户信息,并返回总记录数
DELIMITER //
CREATE PROCEDURE select_user(
IN userId INT,
OUT total INT
)
BEGIN
SELECT COUNT(1) INTO total FROM user;
SELECT * FROM user WHERE id = userId;
END //
DELIMITER ;
Mapper.xml 配置:
<select id="callSelectUser" statementType="CALLABLE" resultType="User">
{
call select_user(
#{userId, mode=IN, jdbcType=INTEGER},
#{total, mode=OUT, jdbcType=INTEGER}
)
}
</select>
Mapper 接口:
public interface UserMapper {
User callSelectUser(Map<String, Object> params);
}
调用存储过程:
Map<String, Object> params = new HashMap<>();
params.put("userId", 1);
// 执行存储过程
User user = userMapper.callSelectUser(params);
// 获取OUT参数
Integer total = (Integer) params.get("total");
对于返回多个结果集的存储过程,需使用 resultSets 属性指定结果集映射:
<select id="callMultiResultProc" statementType="CALLABLE"
resultSets="users,orders"
resultMap="userMap,orderMap">
{call get_user_orders(#{userId, mode=IN})}
</select>
MyBatis 作为企业级应用的主流 ORM 框架,其面试考察点涵盖基础概念、核心原理、配置使用、性能优化和源码理解等多个层面。准备面试时,建议:
SqlSession、Executor、MapperProxy 等核心类的实现。通过系统学习这些知识点,并结合实际项目经验,就能在 MyBatis 相关面试中脱颖而出。