一句话总结
MyBatis 缓存体系:一级缓存(SqlSession 级别,默认开启,HashMap 实现,同一 SqlSession 内相同查询只查一次数据库)和 二级缓存(Mapper/namespace 级别,需手动开启,CachingExecutor 装饰器实现,跨 SqlSession 共享)。核心问题:一级缓存在 Spring 整合后失效(每次查询新建 SqlSession),二级缓存多表关联易脏读(一个 namespace 更新不会清另一个 namespace 缓存)。生产环境建议用 Redis 等分布式缓存替代二级缓存。
初级理解
一级缓存(Local Cache)
# 一级缓存特点:
# 1. SqlSession 级别,默认开启,无法关闭
# 2. 实现:BaseExecutor.localCache (PerpetualCache → HashMap)
# 3. 同一 SqlSession 内,相同查询条件只查一次数据库
# 验证一级缓存:
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(同一对象)
session.close();
一级缓存失效条件
# 一级缓存失效的 4 种情况:
# 1. 不同 SqlSession
SqlSession s1 = factory.openSession();
SqlSession s2 = factory.openSession();
s1.getMapper(UserMapper.class).selectById(1); // 查库
s2.getMapper(UserMapper.class).selectById(1); // 再次查库(不同SqlSession)
# 2. 同一 SqlSession 执行了增删改(清空缓存)
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.selectById(1); // 查库,放入缓存
mapper.insert(new User()); // INSERT 清空一级缓存
mapper.selectById(1); // 再次查库
# 3. 手动清空缓存
session.clearCache();
# 4. 查询条件不同(CacheKey 不同)
mapper.selectById(1); // CacheKey 不同
mapper.selectById(2); // CacheKey 不同,各自缓存
二级缓存
# 二级缓存特点:
# 1. Mapper/namespace 级别,需手动开启
# 2. 跨 SqlSession 共享
# 3. 实现:CachingExecutor(装饰器模式)
# 开启步骤:
# ① mybatis-config.xml 全局开关(默认开启)
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
# ② Mapper.xml 添加 <cache/> 标签
<mapper namespace="com.example.mapper.UserMapper">
<cache/> <!-- 开启二级缓存 -->
</mapper>
# ③ 实体类实现 Serializable
public class User implements Serializable {
private Long id;
private String name;
}
# ④ 查询标签设置 useCache="true"(默认就是true)
<select id="selectById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
中级深入
CacheKey 原理
# CacheKey 组成要素(决定是否命中缓存):
# 1. MappedStatement.id(namespace + statementId)
# 2. rowBounds.offset(分页偏移量)
# 3. rowBounds.limit(分页大小)
# 4. boundSql.getSql()(SQL 语句)
# 5. 参数值列表
# 6. environment.id(数据源环境)
# 只要以上任一要素不同,CacheKey 就不同,缓存不命中
# CacheKey 重写了 equals() 和 hashCode()
# 通过 checksum(校验和)+ count(更新次数)+ updateList(更新列表)
# 保证相同查询条件生成相同的 CacheKey
二级缓存执行流程
# CachingExecutor.query() 核心流程:
public <E> List<E> query(MappedStatement ms, Object param,
RowBounds rowBounds, ResultHandler handler,
CacheKey key, BoundSql boundSql) {
Cache cache = ms.getCache(); // 获取二级缓存
if (cache != null) {
flushCacheIfRequired(ms); // 是否需要清空缓存
if (ms.isUseCache()) {
// 从二级缓存获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 二级缓存未命中,查一级缓存/数据库
list = delegate.query(ms, param, rowBounds,
handler, key, boundSql);
// 放入二级缓存(暂存到 TransactionalCache)
tcm.putObject(cache, key, list);
}
return list;
}
}
// 没有二级缓存,直接走 BaseExecutor
return delegate.query(ms, param, rowBounds, handler, key, boundSql);
}
# 事务提交时才真正写入二级缓存:
# TransactionalCache.commit()
# → flushPendingEntries() 将暂存数据写入底层 Cache
# → reset() 清空暂存区
# 事务回滚时清空暂存区:
# TransactionalCache.rollback()
# → clearPendingEntries()
Spring 整合后一级缓存"失效"
# Spring 整合 MyBatis 后,一级缓存为什么"失效"?
# 原因:SqlSessionTemplate 每次查询都新建 SqlSession
# Spring 管理的 SqlSession 生命周期 = 一次请求
# 源码分析:
# SqlSessionTemplate.sqlSessionProxy(JDK 动态代理)
# → SqlSessionInterceptor.invoke()
# → getSqlSession() // 每次获取新的 SqlSession
# → 执行 SQL
# → closeSqlSession() // 执行完立即关闭
# 所以:
UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
User u1 = mapper.selectById(1); // SqlSession1 查库
User u2 = mapper.selectById(1); // SqlSession2 查库(新SqlSession)
// u1 != u2,一级缓存"失效"
# 同一事务内一级缓存仍然有效:
@Transactional
public void test() {
User u1 = mapper.selectById(1); // SqlSession1 查库
User u2 = mapper.selectById(1); // SqlSession1 命中缓存
// u1 == u2,同一事务共享 SqlSession
}
高级进阶
二级缓存脏数据问题
# 场景:UserMapper 和 OrderMapper 都开启了二级缓存
# UserMapper.xml
<mapper namespace="com.example.mapper.UserMapper">
<cache/>
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
# OrderMapper.xml
<mapper namespace="com.example.mapper.OrderMapper">
<cache/>
<select id="selectByUserId" resultType="Order">
SELECT * FROM `order` WHERE user_id = #{userId}
</select>
<update id="updateOrder">
UPDATE `order` SET status = #{status} WHERE id = #{id}
</update>
</mapper>
# 问题:
# 1. OrderMapper 更新了订单状态
# 2. OrderMapper 的二级缓存被清空(同一 namespace)
# 3. 但 UserMapper 的二级缓存中,关联查询结果仍是旧数据
# 4. 导致脏读!
# 解决方案:
# 1. 使用 <cache-ref> 共享缓存
<cache-ref namespace="com.example.mapper.OrderMapper"/>
# 2. 增删改时设置 flushCache="true" 清空关联缓存
# 3. 生产环境用 Redis 替代二级缓存(推荐)
自定义缓存(整合 Redis)
# 实现 MyBatis Cache 接口,整合 Redis
public class RedisCache implements Cache {
private final String id;
private static RedisTemplate<String, Object> redisTemplate;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() { return id; }
@Override
public void putObject(Object key, Object value) {
redisTemplate.opsForValue().set(
key.toString(), value, 30, TimeUnit.MINUTES);
}
@Override
public Object getObject(Object key) {
return redisTemplate.opsForValue().get(key.toString());
}
@Override
public Object removeObject(Object key) {
return redisTemplate.delete(key.toString());
}
@Override
public void clear() {
Set<String> keys = redisTemplate.keys(id + ":*");
if (keys != null) redisTemplate.delete(keys);
}
@Override
public int getSize() {
return redisTemplate.keys(id + ":*").size();
}
}
# Mapper.xml 使用自定义缓存
<cache type="com.example.cache.RedisCache"/>
实战场景
# 场景1:二级缓存最佳实践
# 适合开启二级缓存的场景:
# 1. 查询频繁、更新少的表(字典表、配置表)
# 2. 单表查询为主
# 3. 对实时性要求不高的数据
# 不适合开启二级缓存的场景:
# 1. 多表关联查询
# 2. 更新频繁的表
# 3. 对数据一致性要求高的场景
# 场景2:Spring Boot 中配置
mybatis:
configuration:
cache-enabled: true # 开启二级缓存全局开关
# 场景3:使用 <cache> 标签属性
<cache
eviction="LRU" # 淘汰策略:LRU/FIFO/SOFT/WEAK
flushInterval="60000" # 刷新间隔(毫秒)
size="1024" # 最大缓存对象数
readOnly="true" # 只读缓存(性能更好,不安全)
blocking="true" # 阻塞等待(防止缓存击穿)
/>
# 场景4:生产环境推荐方案
# 不用 MyBatis 二级缓存,用 Spring Cache + Redis
@Service
public class UserService {
@Cacheable(value = "user", key = "#id")
public User getById(Long id) {
return userMapper.selectById(id);
}
@CacheEvict(value = "user", key = "#user.id")
public void update(User user) {
userMapper.updateById(user);
}
}
面试模拟
面试官:MyBatis 一级缓存和二级缓存的区别?
你:一级缓存是 SqlSession 级别,默认开启,HashMap 实现,同一 SqlSession 内相同查询只查一次数据库。二级缓存是 Mapper/namespace 级别,需手动开启,CachingExecutor 装饰器实现,跨 SqlSession 共享。一级缓存在 Spring 整合后因每次查询新建 SqlSession 而"失效",同一事务内仍有效。二级缓存多表关联容易脏读,生产环境建议用 Redis 替代。
面试官:为什么 Spring 整合 MyBatis 后一级缓存失效了?
你:因为 SqlSessionTemplate 每次数据库操作都通过 sqlSessionProxy 动态代理获取新的 SqlSession,执行完立即关闭。所以两次查询使用的是不同 SqlSession,一级缓存自然不共享。但在同一事务内,Spring 会复用同一个 SqlSession,一级缓存仍然有效。