MyBatis 一级缓存和二级缓存?

2025年 阅读约 12 分钟 面试指南 · MyBatis

深入解析MyBatis一级缓存和二级缓存:缓存原理与实现、生命周期与失效条件、脏数据问题与解决方案、与Spring整合时的注意事项,附面试模拟问答。

一句话总结

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,一级缓存仍然有效。