Skip to content

MyBatis 一/二级缓存深度解析

面试官:MyBatis 有缓存机制吗?

:有,分一级缓存和二级缓存。一级缓存是 SqlSession 级别的,默认开启;二级缓存是 Mapper(namespace)级别的,需要手动开启。

面试官:那你知道一级缓存什么时候会失效吗?在 Spring 环境中有什么坑?

:一级缓存在执行增删改、手动清空缓存、SqlSession 关闭时会失效。在 Spring 中,如果没有开启事务,每次 Mapper 调用都是新的 SqlSession,一级缓存形同虚设。

面试官:二级缓存有什么问题?为什么生产环境不建议用?

这个追问直击要害——能说出二级缓存脏读问题的候选人,才算真正理解 MyBatis 缓存的局限性。


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 / deleteExecutor.update() 会调用 clearLocalCache()清空整个一级缓存
手动清空缓存sqlSession.clearCache()强制清空 localCache清空整个一级缓存
SqlSession 关闭sqlSession.close()SqlSession 销毁,缓存随之销毁缓存无法复用
配置 STATEMENT 级别localCacheScope=STATEMENT每次查询后立即清空一级缓存降级为语句级别
Spring 无事务每次 Mapper 调用每次都是新的 SqlSession一级缓存完全失效
查询条件不同不同的参数CacheKey 不同无法命中缓存

关键陷阱:Spring 环境中的一级缓存

// ❌ 场景1:无事务,一级缓存失效
@Service
public 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:有事务,一级缓存有效
@Service
public 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

配置一级缓存级别

mybatis-config.xml
<settings>
<!-- SESSION:会话级别(默认),SqlSession 生命周期内有效 -->
<!-- STATEMENT:语句级别,每次查询后立即清空 -->
<setting name="localCacheScope" value="SESSION"/>
</settings>

Q3:一级缓存有什么问题?如何解决?高频

Section titled “Q3:一级缓存有什么问题?如何解决?”

问题:同一 SqlSession 内可能读到脏数据

@Transactional
public 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:对一致性要求高的查询,调用前手动清空缓存
@Transactional
public 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 等外部缓存

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);
}

缓存淘汰策略对比

策略实现类淘汰规则适用场景
LRULruCache最近最少使用默认,适合大多数场景
FIFOFifoCache先进先出数据访问无热点
SOFTSoftCache软引用,内存不足时回收内存敏感场景
WEAKWeakCache弱引用,GC 时回收缓存对象较小
PERPETUALPerpetualCache永不淘汰数据量可控

Q5:二级缓存的执行流程是什么?高频

Section titled “Q5:二级缓存的执行流程是什么?”

二级缓存工作流程

┌──────────────────────────────────────────────────────────────┐
│ SqlSession A │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 1. selectById(1) │ │
│ │ - 查二级缓存(namespace 级别)→ 未命中 │ │
│ │ - 查一级缓存(SqlSession A 级别)→ 未命中 │ │
│ │ - 查数据库 → 写入一级缓存 │ │
│ │ - 返回结果 │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 2. commit() │ │
│ │ - 一级缓存数据写入二级缓存 │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ SqlSession B(新的会话) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 3. selectById(1) │ │
│ │ - 查二级缓存(namespace 级别)→ 命中! │ │
│ │ - 直接返回(不查数据库,不查一级缓存) │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

关键源码分析

// CachingExecutor.query()(二级缓存装饰器)
@Override
public <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 示例

OrderMapper.xml
<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 缓存实现示例

@Service
public 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 等外部缓存,精细控制缓存失效策略。


┌────────────────────────────────────────────────────────────────┐
│ 应用层调用 │
│ 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 时写入二级缓存(延迟) │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘


案例 1:Spring 事务中的一级缓存失效

Section titled “案例 1:Spring 事务中的一级缓存失效”
// ❌ 错误:嵌套事务导致一级缓存失效
@Service
public 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;
}
}
@Service
public class OrderItemService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新事务!
public List<OrderItem> getItemsByOrderId(Long orderId) {
return orderItemMapper.selectByOrderId(orderId);
}
}
// 问题:REQUIRES_NEW 会挂起当前事务,创建新 SqlSession
// 新事务结束后,恢复原事务,但原 SqlSession 的一级缓存仍然有效
// ✅ 正确:理解事务传播机制
@Transactional
public Order getOrder(Long orderId) {
Order order = orderMapper.selectById(orderId); // 写入一级缓存
// 不开启新事务,复用当前 SqlSession
List<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
Order order2 = orderMapper.selectById(orderId); // 命中一级缓存
return order;
}
// ❌ 问题:二级缓存脏读
@Service
public 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();
}
@Service
public 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 好

  1. 一级缓存:SqlSession 级别,默认开启,Spring 无事务时失效
  2. 二级缓存:namespace 级别,需手动开启,commit 后才写入
  3. 缓存 Key:StatementId + SQL + 参数 + 环境(6 个要素)
  4. 一级缓存失效:增删改、手动清空、close、STATEMENT 级别
  5. 二级缓存脏读:多表关联时,不同 namespace 互不感知
  6. 生产建议:禁用二级缓存,用 Redis 替代(除非单表字典数据)
  7. cache-ref:共享缓存,但任一修改都清空,效果打折
  8. 对象引用:一级缓存返回对象引用,可能被意外修改