MyBatis 一/二级缓存深度解析
面试官:MyBatis 有缓存机制吗?
你:有,分一级缓存和二级缓存。一级缓存是 SqlSession 级别的,默认开启;二级缓存是 Mapper(namespace)级别的,需要手动开启。
面试官:那你知道一级缓存什么时候会失效吗?在 Spring 环境中有什么坑?
你:一级缓存在执行增删改、手动清空缓存、SqlSession 关闭时会失效。在 Spring 中,如果没有开启事务,每次 Mapper 调用都是新的 SqlSession,一级缓存形同虚设。
面试官:二级缓存有什么问题?为什么生产环境不建议用?
这个追问直击要害——能说出二级缓存脏读问题的候选人,才算真正理解 MyBatis 缓存的局限性。
链式追问一:一级缓存原理
Section titled “链式追问一:一级缓存原理”Q1:一级缓存是什么?作用范围是什么?必考
Section titled “Q1:一级缓存是什么?作用范围是什么?”一级缓存(Local Cache) 是 SqlSession 级别的缓存,默认开启,无法完全关闭。
核心特性:
作用范围:当前 SqlSession 会话内生命周期:SqlSession 创建到关闭存储位置:BaseExecutor 的 localCache 字段(PerpetualCache)底层实现:HashMap线程安全:线程私有,无需考虑并发缓存效果演示:
// 同一个 SqlSession 内的两次相同查询SqlSession session = sqlSessionFactory.openSession();try { UserMapper mapper = session.getMapper(UserMapper.class);
User u1 = mapper.selectById(1); // 第 1 次:查数据库,写入一级缓存 User u2 = mapper.selectById(1); // 第 2 次:直接从缓存返回,不查数据库
System.out.println(u1 == u2); // true(同一个对象引用)} finally { session.close();}缓存 Key 的组成(6 个要素):
// CacheKey 生成逻辑(简化版)CacheKey key = new CacheKey();key.update(ms.getId()); // 1. StatementId(SQL 唯一标识)key.update(rowBounds.getOffset()); // 2. 分页偏移量key.update(rowBounds.getLimit()); // 3. 分页大小key.update(boundSql.getSql()); // 4. SQL 语句key.update(parameterObject); // 5. 参数对象key.update(environment.getId()); // 6. 环境 ID(多数据源场景)
// 示例:// CacheKey = "com.example.UserMapper.selectById_0_2147483647_SELECT * FROM user WHERE id=?_1_development"Q2:一级缓存什么时候会失效?必考
Section titled “Q2:一级缓存什么时候会失效?”这是面试高频考点,失效场景要全面掌握。
一级缓存失效场景对比表:
| 失效场景 | 触发方法 | 原因 | 影响 |
|---|---|---|---|
| 执行增删改 | insert / update / delete | Executor.update() 会调用 clearLocalCache() | 清空整个一级缓存 |
| 手动清空缓存 | sqlSession.clearCache() | 强制清空 localCache | 清空整个一级缓存 |
| SqlSession 关闭 | sqlSession.close() | SqlSession 销毁,缓存随之销毁 | 缓存无法复用 |
| 配置 STATEMENT 级别 | localCacheScope=STATEMENT | 每次查询后立即清空 | 一级缓存降级为语句级别 |
| Spring 无事务 | 每次 Mapper 调用 | 每次都是新的 SqlSession | 一级缓存完全失效 |
| 查询条件不同 | 不同的参数 | CacheKey 不同 | 无法命中缓存 |
关键陷阱:Spring 环境中的一级缓存
// ❌ 场景1:无事务,一级缓存失效@Servicepublic class UserService { @Autowired private UserMapper userMapper;
public void test() { User u1 = userMapper.selectById(1); // SqlSession A,查数据库 User u2 = userMapper.selectById(1); // SqlSession B,再次查数据库 System.out.println(u1 == u2); // false(不同对象) }}// 原因:每次 Mapper 调用都创建新的 SqlSession,用完就关闭,缓存无法复用
// ✅ 场景2:有事务,一级缓存有效@Servicepublic class UserService { @Autowired private UserMapper userMapper;
@Transactional public void test() { User u1 = userMapper.selectById(1); // SqlSession A,查数据库 User u2 = userMapper.selectById(1); // SqlSession A(复用),命中缓存 System.out.println(u1 == u2); // true(同一对象) }}// 原因:@Transactional 会在方法开始时创建 SqlSession,方法结束才关闭,中间复用Spring 事务管理的 SqlSession 生命周期:
@Transactional 方法开始 └── Spring 事务管理器创建 SqlSession └── 绑定到 ThreadLocal(TransactionSynchronizationManager) └── 第一次 Mapper 调用 └── 从 ThreadLocal 获取 SqlSession(已存在) └── 执行 SQL,写入一级缓存 └── 第二次 Mapper 调用 └── 从 ThreadLocal 获取 SqlSession(同一个) └── 命中一级缓存,直接返回 └── @Transactional 方法结束 └── 提交事务,关闭 SqlSession配置一级缓存级别:
<settings> <!-- SESSION:会话级别(默认),SqlSession 生命周期内有效 --> <!-- STATEMENT:语句级别,每次查询后立即清空 --> <setting name="localCacheScope" value="SESSION"/></settings>Q3:一级缓存有什么问题?如何解决?高频
Section titled “Q3:一级缓存有什么问题?如何解决?”问题:同一 SqlSession 内可能读到脏数据
@Transactionalpublic void dirtyReadProblem() { User u1 = userMapper.selectById(1); // 查 DB: name="张三"
// 此时另一个线程(或另一个事务)更新了数据库 // UPDATE user SET name='李四' WHERE id=1
User u2 = userMapper.selectById(1); // 命中缓存!返回 name="张三"(脏读) System.out.println(u2.getName()); // 输出:张三(但数据库已是李四)}场景分析:
| 场景 | 是否脏读 | 原因 |
|---|---|---|
| 单线程、无外部修改 | ❌ 否 | 缓存数据与数据库一致 |
| 多线程、同一事务 | ✅ 是 | 外部提交不可见,缓存未更新 |
| 分布式环境 | ✅ 是 | 多实例间缓存不共享 |
解决方案:
// 方案1:对一致性要求高的查询,调用前手动清空缓存@Transactionalpublic void safeQuery() { User u1 = userMapper.selectById(1);
// 执行前清空一级缓存 sqlSession.clearCache();
User u2 = userMapper.selectById(1); // 重新查数据库}
// 方案2:配置 STATEMENT 级别(性能下降)<setting name="localCacheScope" value="STATEMENT"/>
// 方案3:select 标签强制刷新缓存<select id="selectById" resultType="User" flushCache="true"> SELECT * FROM user WHERE id = #{id}</select>
// 方案4:直接查询数据库(推荐)// 对一致性要求高的场景,不依赖 MyBatis 缓存,用 Redis 等外部缓存链式追问二:二级缓存原理
Section titled “链式追问二:二级缓存原理”Q4:二级缓存是什么?如何开启?必考
Section titled “Q4:二级缓存是什么?如何开启?”二级缓存 是 Mapper(namespace)级别的缓存,多个 SqlSession 共享,需要手动开启。
一级缓存 vs 二级缓存对比:
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用范围 | SqlSession 级别 | namespace(Mapper)级别 |
| 共享性 | 当前会话私有 | 多会话共享 |
| 默认状态 | 默认开启,无法完全关闭 | 默认关闭,需手动开启 |
| 生命周期 | SqlSession 生命周期 | 应用生命周期 |
| 底层实现 | PerpetualCache(HashMap) | 可配置(内存、磁盘、Redis) |
| 线程安全 | 线程私有,安全 | 需考虑并发(默认同步) |
| 数据时机 | 查询后立即写入 | SqlSession commit/close 后写入 |
| 脏读风险 | 低(会话内隔离) | 高(多表关联时) |
开启二级缓存(三步):
<!-- 第一步:全局开关(mybatis-config.xml) --><settings> <setting name="cacheEnabled" value="true"/> <!-- 默认 true,可省略 --></settings>
<!-- 第二步:Mapper XML 中启用(每个 Mapper 单独配置) --><mapper namespace="com.example.UserMapper"> <!-- 简单配置 --> <cache/>
<!-- 详细配置 --> <cache eviction="LRU" <!-- 淘汰策略:LRU/FIFO/SOFT/WEAK --> flushInterval="60000" <!-- 刷新间隔:60秒 --> size="512" <!-- 最多缓存 512 个对象引用 --> readOnly="true" <!-- 只读(默认 false,返回拷贝) --> blocking="false" <!-- 是否阻塞(默认 false) --> /></mapper>
<!-- 第三步:实体类实现 Serializable -->public class User implements Serializable { private Long id; private String name; // ...}注解方式开启:
@CacheNamespace( eviction = LruCache.class, flushInterval = 60000, size = 512, readWrite = false // true=读写(返回拷贝),false=只读(返回引用))public interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User selectById(Long id);}缓存淘汰策略对比:
| 策略 | 实现类 | 淘汰规则 | 适用场景 |
|---|---|---|---|
| LRU | LruCache | 最近最少使用 | 默认,适合大多数场景 |
| FIFO | FifoCache | 先进先出 | 数据访问无热点 |
| SOFT | SoftCache | 软引用,内存不足时回收 | 内存敏感场景 |
| WEAK | WeakCache | 弱引用,GC 时回收 | 缓存对象较小 |
| PERPETUAL | PerpetualCache | 永不淘汰 | 数据量可控 |
Q5:二级缓存的执行流程是什么?高频
Section titled “Q5:二级缓存的执行流程是什么?”二级缓存工作流程:
┌──────────────────────────────────────────────────────────────┐│ SqlSession A ││ ┌────────────────────────────────────────────────────────┐ ││ │ 1. selectById(1) │ ││ │ - 查二级缓存(namespace 级别)→ 未命中 │ ││ │ - 查一级缓存(SqlSession A 级别)→ 未命中 │ ││ │ - 查数据库 → 写入一级缓存 │ ││ │ - 返回结果 │ ││ └────────────────────────────────────────────────────────┘ ││ ┌────────────────────────────────────────────────────────┐ ││ │ 2. commit() │ ││ │ - 一级缓存数据写入二级缓存 │ ││ └────────────────────────────────────────────────────────┘ │└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐│ SqlSession B(新的会话) ││ ┌────────────────────────────────────────────────────────┐ ││ │ 3. selectById(1) │ ││ │ - 查二级缓存(namespace 级别)→ 命中! │ ││ │ - 直接返回(不查数据库,不查一级缓存) │ ││ └────────────────────────────────────────────────────────┘ │└──────────────────────────────────────────────────────────────┘关键源码分析:
// CachingExecutor.query()(二级缓存装饰器)@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) { BoundSql boundSql = ms.getBoundSql(parameter);
// 1. 生成 CacheKey(包含 SQL、参数等) CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 2. 查二级缓存 Cache cache = ms.getCache(); // 获取 namespace 级别的 Cache if (cache != null) { // 尝试从二级缓存获取 List<E> cachedList = (List<E>) cache.getObject(key); if (cachedList != null) { return cachedList; // 缓存命中,直接返回 }
// 缓存未命中,查询数据库 List<E> list = delegate.query(ms, parameter, rowBounds, resultHandler); // 委托给 BaseExecutor
// 写入二级缓存(通过 TransactionalCacheManager 延迟写入) tcm.putObject(cache, key, list);
return list; }
// 3. 无二级缓存,直接查询数据库 return delegate.query(ms, parameter, rowBounds, resultHandler);}
// TransactionalCacheManager:事务提交时才真正写入二级缓存public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); // 将待写入的缓存数据真正写入 Cache }}Q6:二级缓存的脏读问题是什么?为什么生产环境不建议用?必考
Section titled “Q6:二级缓存的脏读问题是什么?为什么生产环境不建议用?”这是二级缓存最大的隐患,也是面试官最喜欢追问的点。
脏读场景:多表关联时的缓存不一致
场景:Order 关联 OrderItem,两个 Mapper 分别有自己的二级缓存
┌────────────────────────────────────────────────────────────┐│ 步骤1:SqlSession A 查询 Order(id=1) ││ SELECT * FROM `order` WHERE id=1 ││ 缓存内容:{id:1, totalAmount:100, items:[...]} ││ 写入 OrderMapper 的二级缓存 │└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐│ 步骤2:SqlSession B 更新 OrderItem ││ UPDATE order_item SET price=200 ││ WHERE order_id=1 AND item_id=10 ││ 清空 OrderItemMapper 的二级缓存 ││ 但 OrderMapper 的二级缓存未清空!(不同 namespace) │└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐│ 步骤3:SqlSession C 查询 Order(id=1) ││ 命中 OrderMapper 二级缓存 ││ 返回旧数据:{id:1, totalAmount:100, items:[旧数据]} ││ 脏读!OrderItem 已被修改,但 Order 缓存未更新 │└────────────────────────────────────────────────────────────┘问题根源:
OrderMapper 缓存(namespace=com.example.OrderMapper) └── 包含 Order + OrderItem 的关联数据
OrderItemMapper 缓存(namespace=com.example.OrderItemMapper) └── 只包含 OrderItem 数据
两个 namespace 的缓存互不感知!修改 OrderItem 时,只会清空 OrderItemMapper 的缓存,不会清空 OrderMapper 的缓存。解决方案对比:
| 方案 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| cache-ref | <cache-ref namespace="com.example.OrderItemMapper"/> | 简单 | 缓存共享,任一修改都清空,效果打折 | 少量关联表 |
| 禁用二级缓存 | <setting name="cacheEnabled" value="false"/> | 彻底解决 | 需要其他缓存方案 | 大多数生产环境 |
| 单表查询 | 不用关联查询,应用层组装 | 缓存独立 | 代码复杂,性能下降 | 关联关系简单的场景 |
| 用 Redis 替代 | 外部缓存,手动控制失效 | 精细控制 | 开发成本高 | 对一致性有要求的场景 |
cache-ref 示例:
<mapper namespace="com.example.OrderMapper"> <cache/>
<!-- 引用 OrderItemMapper 的缓存,两个 Mapper 共享缓存 --> <cache-ref namespace="com.example.OrderItemMapper"/>
<select id="selectOrderWithItems" resultMap="orderResultMap"> SELECT o.*, oi.* FROM `order` o LEFT JOIN order_item oi ON o.id = oi.order_id WHERE o.id = #{id} </select></mapper>
<!-- OrderItemMapper.xml --><mapper namespace="com.example.OrderItemMapper"> <cache/> <!-- 两个 Mapper 共享这个 Cache -->
<update id="updateOrderItem"> UPDATE order_item SET price=#{price} WHERE id=#{id} </update></mapper>问题:cache-ref 导致两个 Mapper 共享缓存,任一 Mapper 执行增删改都会清空缓存,缓存命中率大幅下降。
Q7:什么时候用 MyBatis 二级缓存?什么时候用 Redis?实战
Section titled “Q7:什么时候用 MyBatis 二级缓存?什么时候用 Redis?”选型决策表:
| 维度 | MyBatis 二级缓存 | Redis 缓存 | 推荐方案 |
|---|---|---|---|
| 数据一致性 | 难以保证(多表关联时) | 可手动控制 | Redis |
| 缓存粒度 | namespace 级别(粗粒度) | 可精细控制(key-value) | Redis |
| 分布式支持 | 不支持(默认内存) | 天然支持 | Redis |
| 失效策略 | 只能按 namespace 全量清空 | 支持精细失效(key 模式匹配) | Redis |
| 容量限制 | 受 JVM 堆内存限制 | 可配置淘汰策略,容量大 | Redis |
| 持久化 | 不支持(重启丢失) | 支持(RDB/AOF) | Redis |
| 监控统计 | 不支持 | 丰富的监控工具 | Redis |
| 开发成本 | 低(零配置) | 中(需手动编码) | 看场景 |
| 性能 | 高(本地内存) | 中(网络 IO) | MyBatis 缓存 |
适用场景对比:
✅ MyBatis 二级缓存适用场景:1. 单表查询,无关联关系2. 数据极少变动(如字典表、配置表)3. 单机应用,无需分布式4. 对一致性要求不高5. 读多写极少(QPS < 100)
示例:- 省市区字典表(一年更新一次)- 系统配置表(很少变动)- 商品分类表(变动频率低)
❌ MyBatis 二级缓存不适用场景:1. 多表关联查询2. 数据频繁变动3. 分布式部署(多实例缓存不共享)4. 对一致性要求高5. 高并发写入
示例:- 订单表(频繁更新)- 用户表(关联多表)- 商品表(库存实时变动)生产环境最佳实践:
<!-- 推荐:禁用二级缓存,用 Redis 替代 --><settings> <setting name="cacheEnabled" value="false"/></settings>
<!-- 特殊场景:仅对极少变动的字典表开启 --><mapper namespace="com.example.DictMapper"> <cache eviction="LRU" flushInterval="86400000" readOnly="true"/> <!-- 24小时刷新 --></mapper>Redis 缓存实现示例:
@Servicepublic class UserService { @Autowired private UserMapper userMapper; @Autowired private RedisTemplate<String, Object> redisTemplate;
public User getUserById(Long id) { String cacheKey = "user:" + id;
// 1. 查 Redis 缓存 User user = (User) redisTemplate.opsForValue().get(cacheKey); if (user != null) { return user; }
// 2. 查数据库 user = userMapper.selectById(id); if (user != null) { // 3. 写入 Redis 缓存 redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS); }
return user; }
public void updateUser(User user) { // 1. 更新数据库 userMapper.updateById(user);
// 2. 删除缓存(保证一致性) String cacheKey = "user:" + user.getId(); redisTemplate.delete(cacheKey);
// 或:更新缓存 // redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS); }}本质一句话:MyBatis 二级缓存适合单表、极少变动的字典数据;对一致性有要求、分布式部署的场景,必须用 Redis 等外部缓存,精细控制缓存失效策略。
缓存层级全景图
Section titled “缓存层级全景图”┌────────────────────────────────────────────────────────────────┐│ 应用层调用 ││ userMapper.selectById(1) │└──────────────────────────┬─────────────────────────────────────┘ │ ▼┌────────────────────────────────────────────────────────────────┐│ 二级缓存(namespace 级别) ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 检查条件: │ ││ │ - cacheEnabled=true(全局开关) │ ││ │ - Mapper XML 中配置 <cache> │ ││ │ - SqlSession 已 commit 或 close │ ││ └──────────────────────────────────────────────────────────┘ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 缓存结构: │ ││ │ - Key: CacheKey(StatementId + SQL + 参数 + 环境) │ ││ │ - Value: 查询结果(List<User>) │ ││ │ - Scope: 整个 namespace(多个 SqlSession 共享) │ ││ └──────────────────────────────────────────────────────────┘ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 特性: │ ││ │ - 写入时机:SqlSession commit/close 后 │ ││ │ - 淘汰策略:LRU / FIFO / SOFT / WEAK │ ││ │ - 问题:多表关联时脏读风险高 │ ││ └──────────────────────────────────────────────────────────┘ │└──────────────────────────┬─────────────────────────────────────┘ │ 未命中 ▼┌────────────────────────────────────────────────────────────────┐│ 一级缓存(SqlSession 级别) ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 检查条件: │ ││ │ - 默认开启,无法完全关闭 │ ││ │ - localCacheScope=SESSION(默认) │ ││ │ - 未执行增删改、未手动清空 │ ││ └──────────────────────────────────────────────────────────┘ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 缓存结构: │ ││ │ - Key: CacheKey(StatementId + SQL + 参数 + 环境) │ ││ │ - Value: 查询结果(对象引用) │ ││ │ - Scope: 当前 SqlSession(线程私有) │ ││ └──────────────────────────────────────────────────────────┘ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 特性: │ ││ │ - 写入时机:查询后立即写入 │ ││ │ - 失效时机:增删改、手动清空、close、STATEMENT 级别 │ ││ │ - 问题:Spring 无事务时失效,对象引用可能被意外修改 │ ││ └──────────────────────────────────────────────────────────┘ │└──────────────────────────┬─────────────────────────────────────┘ │ 未命中 ▼┌────────────────────────────────────────────────────────────────┐│ 数据库查询 ││ SELECT * FROM user WHERE id=1 ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 执行流程: │ ││ │ 1. Executor.doQuery() │ ││ │ 2. StatementHandler.prepare() │ ││ │ 3. ParameterHandler.setParameters() │ ││ │ 4. PreparedStatement.executeQuery() │ ││ │ 5. ResultSetHandler.handleResultSets() │ ││ └──────────────────────────────────────────────────────────┘ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 结果处理: │ ││ │ 1. 写入一级缓存(立即) │ ││ │ 2. SqlSession commit 时写入二级缓存(延迟) │ ││ └──────────────────────────────────────────────────────────┘ │└────────────────────────────────────────────────────────────────┘高频面试题总结
Section titled “高频面试题总结”实战案例:缓存优化
Section titled “实战案例:缓存优化”案例 1:Spring 事务中的一级缓存失效
Section titled “案例 1:Spring 事务中的一级缓存失效”// ❌ 错误:嵌套事务导致一级缓存失效@Servicepublic class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderItemService orderItemService;
@Transactional public Order getOrder(Long orderId) { Order order = orderMapper.selectById(orderId); // 查询 Order
// 调用其他 Service 方法(嵌套事务) List<OrderItem> items = orderItemService.getItemsByOrderId(orderId); // 新事务,新 SqlSession
// 再次查询 Order(想命中一级缓存,但实际不会命中) Order order2 = orderMapper.selectById(orderId); // 同一个 SqlSession,但缓存已失效? return order; }}
@Servicepublic class OrderItemService { @Transactional(propagation = Propagation.REQUIRES_NEW) // 新事务! public List<OrderItem> getItemsByOrderId(Long orderId) { return orderItemMapper.selectByOrderId(orderId); }}// 问题:REQUIRES_NEW 会挂起当前事务,创建新 SqlSession// 新事务结束后,恢复原事务,但原 SqlSession 的一级缓存仍然有效
// ✅ 正确:理解事务传播机制@Transactionalpublic Order getOrder(Long orderId) { Order order = orderMapper.selectById(orderId); // 写入一级缓存
// 不开启新事务,复用当前 SqlSession List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
Order order2 = orderMapper.selectById(orderId); // 命中一级缓存 return order;}案例 2:二级缓存导致的脏读
Section titled “案例 2:二级缓存导致的脏读”// ❌ 问题:二级缓存脏读@Servicepublic class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderItemMapper orderItemMapper;
public Order getOrder(Long orderId) { return orderMapper.selectOrderWithItems(orderId); // 查询 Order + OrderItem }
public void updateOrderItemPrice(Long itemId, BigDecimal price) { orderItemMapper.updatePrice(itemId, price); // 更新 OrderItem // 问题:只清空了 OrderItemMapper 的二级缓存 // OrderMapper 的缓存仍然存在,包含旧的 OrderItem 数据 }}
// ✅ 解决方案1:禁用二级缓存<setting name="cacheEnabled" value="false"/>
// ✅ 解决方案2:使用 cache-ref(共享缓存)<!-- OrderMapper.xml --><cache-ref namespace="com.example.OrderItemMapper"/><!-- 缺点:任一 Mapper 修改都会清空缓存,缓存效果大打折扣 -->
// ✅ 解决方案3:手动清空关联缓存public void updateOrderItemPrice(Long itemId, BigDecimal price) { OrderItem item = orderItemMapper.selectById(itemId); orderItemMapper.updatePrice(itemId, price);
// 手动清空 OrderMapper 的缓存 sqlSession.getConfiguration().getCache("com.example.OrderMapper").clear();}案例 3:Redis 替代二级缓存
Section titled “案例 3:Redis 替代二级缓存”@Servicepublic class DictService { @Autowired private DictMapper dictMapper; @Autowired private RedisTemplate<String, Object> redisTemplate;
// 查询字典数据 public List<Dict> getDictByType(String type) { String cacheKey = "dict:" + type;
// 1. 查 Redis List<Dict> dicts = (List<Dict>) redisTemplate.opsForValue().get(cacheKey); if (dicts != null) { return dicts; }
// 2. 查数据库 dicts = dictMapper.selectByType(type);
// 3. 写入 Redis(缓存 24 小时) redisTemplate.opsForValue().set(cacheKey, dicts, 24, TimeUnit.HOURS);
return dicts; }
// 更新字典数据 public void updateDict(Dict dict) { // 1. 更新数据库 dictMapper.updateById(dict);
// 2. 删除缓存(精确失效) String cacheKey = "dict:" + dict.getType(); redisTemplate.delete(cacheKey); }}
// 优势:// 1. 精确控制缓存失效(只删除相关 key,而非整个 namespace)// 2. 分布式环境共享缓存// 3. 支持持久化、监控、统计// 4. 容量大(不受 JVM 堆内存限制)性能对比:
| 场景 | MyBatis 二级缓存 | Redis 缓存 | 性能差异 |
|---|---|---|---|
| 本地查询(单机) | 0.1ms(本地内存) | 1ms(网络 IO) | MyBatis 快 10x |
| 分布式查询(3 节点) | 缓存不共享,每次查 DB(50ms) | 共享缓存,命中(1ms) | Redis 快 50x |
| 更新后查询 | namespace 全量清空,命中率低 | 精确删除 key,命中率高 | Redis 好 |
关键知识点速记
Section titled “关键知识点速记”- 一级缓存:SqlSession 级别,默认开启,Spring 无事务时失效
- 二级缓存:namespace 级别,需手动开启,commit 后才写入
- 缓存 Key:StatementId + SQL + 参数 + 环境(6 个要素)
- 一级缓存失效:增删改、手动清空、close、STATEMENT 级别
- 二级缓存脏读:多表关联时,不同 namespace 互不感知
- 生产建议:禁用二级缓存,用 Redis 替代(除非单表字典数据)
- cache-ref:共享缓存,但任一修改都清空,效果打折
- 对象引用:一级缓存返回对象引用,可能被意外修改