一、核心概念
MyBatis 提供了两级缓存机制来减少数据库访问:
- 一级缓存(Session 级别):
SqlSession级别的缓存,默认开启,无法关闭 - 二级缓存(Mapper 级别):跨
SqlSession的缓存,作用于Namespace,默认关闭,需手动配置
核心区别:
- 作用域:一级缓存仅在单个
SqlSession内有效;二级缓存在多个SqlSession间共享 - 生命周期:一级缓存随
SqlSession关闭而清空;二级缓存持续存在直到应用重启或缓存失效
二、一级缓存(SqlSession 级别)
1. 工作原理
一级缓存是 MyBatis 的默认行为,基于 SqlSession 的 HashMap 实现:
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<>(); // 缓存容器
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
}
缓存 Key 组成:
CacheKey = statementId + offset + limit + sql + params + environmentId
2. 生命周期
// 创建 SqlSession
SqlSession session = sqlSessionFactory.openSession();
// 第一次查询:查数据库,结果放入一级缓存
User user1 = session.selectOne("selectUserById", 1); // 执行 SQL
System.out.println(user1);
// 第二次查询:相同参数,直接从一级缓存返回
User user2 = session.selectOne("selectUserById", 1); // 不执行 SQL
System.out.println(user2);
System.out.println(user1 == user2); // true,同一个对象
// 关闭 SqlSession,一级缓存清空
session.close();
3. 缓存失效场景
一级缓存在以下情况下失效:
// ❌ 场景 1:执行增删改操作
session.selectOne("selectUserById", 1); // 查询,放入缓存
session.update("updateUser", user); // 更新操作
session.selectOne("selectUserById", 1); // 缓存失效,重新查询数据库
// ❌ 场景 2:手动清空缓存
session.selectOne("selectUserById", 1); // 查询,放入缓存
session.clearCache(); // 手动清空
session.selectOne("selectUserById", 1); // 缓存失效,重新查询
// ❌ 场景 3:不同的 SqlSession
SqlSession session1 = factory.openSession();
SqlSession session2 = factory.openSession();
session1.selectOne("selectUserById", 1); // session1 的缓存
session2.selectOne("selectUserById", 1); // session2 没有缓存,查数据库
// ❌ 场景 4:查询参数不同
session.selectOne("selectUserById", 1); // 查询 ID=1
session.selectOne("selectUserById", 2); // 参数不同,缓存未命中
4. 潜在问题(脏读)
在 Spring 集成环境中,一级缓存可能导致数据不一致:
// @Transactional 注解下,同一个 SqlSession
@Transactional
public void updateUser() {
User user = userMapper.selectById(1); // 查询,放入缓存
System.out.println(user.getName()); // "张三"
// 其他线程或服务修改了数据库
// UPDATE user SET name = '李四' WHERE id = 1
User user2 = userMapper.selectById(1); // 从缓存读取
System.out.println(user2.getName()); // 仍然是"张三"(脏读)
}
解决方案:
<!-- 设置一级缓存作用域为 STATEMENT(每次查询后立即清空) -->
<setting name="localCacheScope" value="STATEMENT"/>
三、二级缓存(Mapper 级别)
1. 工作原理
二级缓存作用于 Namespace,多个 SqlSession 共享同一个 Mapper 的缓存:
SqlSession1 ──┐
├──> Mapper Namespace 缓存(共享)
SqlSession2 ──┘
启用步骤:
<!-- 1. MyBatis 全局配置(mybatis-config.xml) -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 2. Mapper XML 中开启二级缓存 -->
<mapper namespace="com.example.mapper.UserMapper">
<cache
eviction="LRU" <!-- 缓存回收策略:LRU/FIFO/SOFT/WEAK -->
flushInterval="60000" <!-- 刷新间隔:60秒 -->
size="512" <!-- 缓存对象数量 -->
readOnly="false"/> <!-- 是否只读(false 会返回缓存对象的拷贝) -->
<select id="selectUserById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
实体类必须实现 Serializable:
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
// getters & setters
}
2. 生命周期
// Session1:查询 + 提交
SqlSession session1 = factory.openSession();
User user1 = session1.selectOne("selectUserById", 1); // 查数据库
session1.commit(); // ⚠️ 必须提交,数据才会放入二级缓存
session1.close();
// Session2:从二级缓存读取
SqlSession session2 = factory.openSession();
User user2 = session2.selectOne("selectUserById", 1); // 命中二级缓存
session2.close();
System.out.println(user1 == user2); // false(readOnly=false 会返回副本)
关键点:
- 必须 提交事务(
commit())或 关闭SqlSession(close())后,查询结果才会进入二级缓存 readOnly=false时,返回的是缓存对象的深拷贝(防止并发修改)
3. 缓存失效机制
<!-- 增删改操作会刷新二级缓存 -->
<update id="updateUser" flushCache="true"> <!-- 默认 true -->
UPDATE user SET name = #{name} WHERE id = #{id}
</update>
<!-- 查询操作默认不刷新缓存 -->
<select id="selectAll" resultType="User" flushCache="false"> <!-- 默认 false -->
SELECT * FROM user
</select>
4. 缓存回收策略
| 策略 | 说明 |
|---|---|
LRU | 最近最少使用(默认):移除最长时间不被使用的对象 |
FIFO | 先进先出:按对象进入缓存的顺序移除 |
SOFT | 软引用:基于 JVM 垃圾回收器和软引用规则移除 |
WEAK | 弱引用:更积极地基于垃圾回收器和弱引用规则移除 |
四、使用场景与选择
1. 一级缓存使用场景
推荐场景:
- ✅ 单次会话内多次查询相同数据(如事务内多次读取同一用户)
- ✅ 批量操作后的关联查询(如插入订单后查询订单详情)
不推荐场景:
- ❌ 分布式环境(不同服务节点无法共享
SqlSession) - ❌ 数据频繁变更且需要实时性的场景
最佳实践:
// Spring 集成时,建议关闭一级缓存或设置为 STATEMENT 级别
@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setLocalCacheScope(LocalCacheScope.STATEMENT); // 关键配置
factory.setConfiguration(config);
return factory.getObject();
}
}
2. 二级缓存使用场景
推荐场景:
- ✅ 查询频繁、更新较少的数据(如字典表、配置表)
- ✅ 读多写少的业务场景(如商品分类、地区信息)
- ✅ 单体应用且数据一致性要求不高
不推荐场景:
- ❌ 分布式/微服务架构(缓存无法跨服务同步,容易脏读)
- ❌ 数据实时性要求高(如库存、订单状态)
- ❌ 存在多表关联查询(更新一张表无法刷新关联表缓存)
典型问题示例:
// 问题:多表关联查询的缓存一致性
<select id="selectOrderWithUser" resultType="Order">
SELECT o.*, u.name AS userName
FROM order o
LEFT JOIN user u ON o.user_id = u.id
WHERE o.id = #{id}
</select>
// 如果只更新了 user 表,order 的二级缓存不会失效
userMapper.updateUser(user); // 更新用户名
orderMapper.selectOrderWithUser(1); // 返回旧的用户名(脏数据)
3. 实际生产环境建议
现代架构推荐方案:
// ❌ 不推荐:使用 MyBatis 二级缓存
// 原因:分布式环境无法保证一致性
// ✅ 推荐:使用专业缓存中间件
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String, User> redisTemplate;
public User getUserById(Long id) {
// 1. 先查 Redis
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user == null) {
// 2. Redis 未命中,查数据库
user = userMapper.selectById(id);
// 3. 写入 Redis,设置过期时间
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
}
return user;
}
@Transactional
public void updateUser(User user) {
// 1. 更新数据库
userMapper.updateUser(user);
// 2. 删除 Redis 缓存(保证一致性)
redisTemplate.delete("user:" + user.getId());
}
}
优势:
- 支持分布式部署(多服务节点共享缓存)
- 灵活的过期策略(TTL、LRU、LFU)
- 更强大的数据结构(String、Hash、List、Set、ZSet)
- 缓存预热、缓存穿透/击穿/雪崩的解决方案
五、面试总结
推荐回答思路
- 先说两级缓存的定义:
- 一级缓存:
SqlSession级别,默认开启,无法关闭 - 二级缓存:
Namespace级别,默认关闭,需配置
- 一级缓存:
- 对比作用域和生命周期:
- 一级缓存:单个
SqlSession内有效,关闭即清空 - 二级缓存:多个
SqlSession共享,应用级别持久化
- 一级缓存:单个
- 说明使用场景:
- 一级缓存:事务内多次查询相同数据,Spring 环境建议设为
STATEMENT级别 - 二级缓存:读多写少的字典表,但不推荐在分布式环境使用
- 一级缓存:事务内多次查询相同数据,Spring 环境建议设为
- 补充现代方案:
- 生产环境推荐 Redis/Caffeine 等专业缓存中间件
- 提供更好的分布式支持、过期策略和监控能力
加分项
- 能说出一级缓存 Key 的组成(statementId + params)
- 提到 Spring 集成时事务内的一级缓存可能导致脏读
- 了解二级缓存的
readOnly参数(性能 vs 安全性) - 说出多表关联查询时二级缓存的一致性问题
- 推荐使用 Redis 等分布式缓存替代二级缓存
核心记忆点
一级缓存:默认开启,
SqlSession级别,适合事务内重复查询
二级缓存:手动开启,Namespace级别,适合读多写少的单体应用
生产环境:优先使用 Redis/Caffeine,避免 MyBatis 二级缓存在分布式场景的一致性问题