
MyBatis缓存机制源码深度解析:一级缓存失效的5种场景你遇到过吗?
说起MyBatis缓存,很多人只知道"一级缓存默认开启,二级缓存需要配置"。
但真正踩过坑的同学都知道:一级缓存莫名其妙失效,同一个查询重复执行两次;二级缓存配置后数据不一致,查出来的是脏数据。
这篇文章从源码层面拆解MyBatis缓存机制:一级缓存实现原理、二级缓存工作流程、缓存失效场景、与Redis整合方案。
一、缓存架构总览
MyBatis缓存分为两级:
查询流程:
┌─────────────────────────────────────────────────┐
│ Executor执行器 │
│ ┌───────────────────────────────────────────┐ │
│ │ 二级缓存(Mapper级别) │ │
│ │ 命中 → 直接返回 │ │
│ │ 未命中 → 查一级缓存 │ │
│ └───────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────┐ │
│ │ 一级缓存(SqlSession级别) │ │
│ │ 命中 → 直接返回 │ │
│ │ 未命中 → 查数据库 → 写入一级缓存 │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
二、一级缓存源码解析
2.1 BaseExecutor核心实现
一级缓存的实现藏在BaseExecutor里:
publicabstractclassBaseExecutorimplementsExecutor{
protected PerpetualCache localCache; // 一级缓存
@Override
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql)throws SQLException {
List<E> list;
// 先查一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存未命中,查数据库
list = queryFromDatabase(ms, parameter, rowBounds,
resultHandler, key, boundSql);
}
return list;
}
private <E> List<E> queryFromDatabase(...)throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER); // 占位符
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key); // 移除占位符
}
localCache.putObject(key, list); // 写入缓存
return list;
}
// 清空一级缓存
@Override
publicvoidclearCache(){
localCache.clear();
}
}
2.2 CacheKey如何生成
缓存Key决定查询能否命中:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject,
RowBounds rowBounds, BoundSql boundSql){
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId()); // Statement ID
cacheKey.update(rowBounds.getOffset()); // 分页offset
cacheKey.update(rowBounds.getLimit()); // 分页limit
cacheKey.update(boundSql.getSql()); // SQL语句
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (ParameterMapping parameterMapping : parameterMappings) {
cacheKey.update(parameterObject); // 参数值
}
cacheKey.update(boundSql.getAdditionalParameter()); // 额外参数
return cacheKey;
}
CacheKey组成:
MappedStatement的ID(Mapper方法全限定名) RowBounds的分页参数 SQL语句本身 参数值 Environment的ID
任何一项不同,CacheKey就不同,缓存无法命中。
2.3 PerpetualCache底层存储
publicclassPerpetualCacheimplementsCache{
privatefinal String id;
privatefinal Map<Object, Object> cache = new HashMap<>(); // 简单HashMap
@Override
publicvoidputObject(Object key, Object value){
cache.put(key, value);
}
@Override
public Object getObject(Object key){
return cache.get(key);
}
@Override
publicvoidclear(){
cache.clear();
}
}
一级缓存底层就是一个HashMap,存储在SqlSession内部。
三、一级缓存失效的5种场景
场景1:SqlSession关闭或commit
SqlSession session1 = sqlSessionFactory.openSession();
User user1 = session1.selectOne("com.example.mapper.UserMapper.selectById", 1);
session1.commit(); // 清空一级缓存
SqlSession session2 = sqlSessionFactory.openSession();
User user2 = session2.selectOne("com.example.mapper.UserMapper.selectById", 1);
// 再次查询数据库,未命中缓存
源码:
@Override
publicvoidcommit(boolean force){
clearCache(); // commit时清空缓存
transaction.commit(force);
}
场景2:执行了insert/update/delete
SqlSession session = sqlSessionFactory.openSession();
User user1 = session.selectOne("selectById", 1); // 写入缓存
session.update("updateUser", user1); // 执行更新
User user2 = session.selectOne("selectById", 1); // 缓存失效,重新查询
源码:
@Override
publicintupdate(MappedStatement ms, Object parameter)throws SQLException {
clearCache(); // 执行更新操作前清空缓存
return doUpdate(ms, parameter);
}
场景3:查询条件不同
User user1 = session.selectOne("selectById", 1);
User user2 = session.selectOne("selectById", 2); // CacheKey不同,缓存不命中
// 或者
User user3 = session.selectOne("selectByName", "张三"); // Statement ID不同
场景4:手动清空缓存
User user1 = session.selectOne("selectById", 1);
session.clearCache(); // 手动清空
User user2 = session.selectOne("selectById", 1); // 重新查询
场景5:RowBounds分页参数不同
List<User> list1 = session.selectList("selectAll", null,
new RowBounds(0, 10));
List<User> list2 = session.selectList("selectAll", null,
new RowBounds(10, 10)); // offset不同,缓存不命中
四、二级缓存源码解析
4.1 CachingExecutor装饰器模式
二级缓存用装饰器模式包装Executor:
publicclassCachingExecutorimplementsExecutor{
privatefinal Executor delegate; // 被装饰的Executor(BaseExecutor)
private TransactionalCacheManager tcm; // 缓存管理器
@Override
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql)throws SQLException {
Cache cache = ms.getCache(); // 获取二级缓存
if (cache != null) {
flushCacheIfRequired(ms); // 检查是否需要清空
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 二级缓存未命中,委托给BaseExecutor查一级缓存和数据库
list = delegate.query(ms, parameter, rowBounds,
resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // 写入二级缓存
}
return list;
}
}
return delegate.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
}
4.2 TransactionalCache事务缓存
二级缓存不是直接写入,而是通过TransactionalCache包装:
publicclassTransactionalCacheimplementsCache{
privatefinal Cache delegate; // 真实的缓存
privatefinal Map<Object, Object> entriesToAddOnCommit; // 待提交的缓存
privatefinal Set<Object> entriesMissedInCache; // 未命中的Key
@Override
publicvoidputObject(Object key, Object value){
entriesToAddOnCommit.put(key, value); // 先存到待提交区
}
@Override
publicvoidcommit(){
// commit时才真正写入缓存
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
entriesToAddOnCommit.clear();
}
@Override
publicvoidrollback(){
// rollback时不写入,清空待提交区
entriesToAddOnCommit.clear();
}
}
关键设计:二级缓存的事务性保证:
查询结果先存到 entriesToAddOnCommit只有 commit()时才真正写入缓存rollback()时丢弃待提交数据
4.3 二级缓存配置
<!-- Mapper.xml -->
<mappernamespace="com.example.mapper.UserMapper">
<cacheeviction="LRU"flushInterval="60000"size="1024"readOnly="true"/>
<!-- 或引用其他Mapper的缓存 -->
<cache-refnamespace="com.example.mapper.OtherMapper"/>
</mapper>
cache配置参数:
eviction:淘汰策略(LRU/FIFO/SOFT/WEAK)flushInterval:自动清空间隔(毫秒)size:缓存容量readOnly:是否只读(只读返回同一对象,可读返回克隆对象)
Java注解方式:
@CacheNamespace(implementation = LruCache.class, size= 1024)
publicinterfaceUserMapper{
@CacheNamespaceRef(UserMapper.class)
List<User> selectAll();
}
五、二级缓存踩坑案例
坑1:多表关联查询缓存不一致
// UserMapper.xml
<select id="selectUserWithOrder" resultType="User">
SELECT u.*, o.order_no FROM user u
LEFT JOIN order o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
// OrderMapper.xml
<update id="updateOrder">
UPDATE order SET order_no = #{orderNo} WHERE id = #{id}
</update>
问题:
selectUserWithOrder缓存到UserMapperupdateOrder清空OrderMapper缓存UserMapper缓存未清空,查到旧数据
解决:使用cache-ref
<!-- OrderMapper.xml -->
<cache-refnamespace="com.example.mapper.UserMapper"/>
或者:多表关联查询不要使用二级缓存。
坑2:分布式环境下缓存不一致
// 应用实例A查询并缓存
User user = userMapper.selectById(1); // 写入二级缓存
// 应用实例B更新
userMapper.updateById(user); // 只清空实例B的缓存
// 应用实例A再次查询
User user2 = userMapper.selectById(1); // 命中脏缓存!
解决:分布式环境下禁用MyBatis二级缓存,使用Redis。
六、与Redis整合方案
6.1 自定义RedisCache
publicclassRedisCacheimplementsCache{
privatefinal String id;
privatefinal RedisTemplate<String, Object> redisTemplate;
publicRedisCache(String id){
this.id = id;
this.redisTemplate = SpringContextHolder.getBean("redisTemplate");
}
@Override
publicvoidputObject(Object key, Object value){
String cacheKey = generateKey(key);
redisTemplate.opsForValue().set(cacheKey, value, Duration.ofMinutes(30));
}
@Override
public Object getObject(Object key){
String cacheKey = generateKey(key);
return redisTemplate.opsForValue().get(cacheKey);
}
@Override
public Object removeObject(Object key){
String cacheKey = generateKey(key);
redisTemplate.delete(cacheKey);
returnnull;
}
@Override
publicvoidclear(){
String pattern = id + ":*";
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
private String generateKey(Object key){
return id + ":" + key.hashCode();
}
}
6.2 配置使用Redis缓存
<mappernamespace="com.example.mapper.UserMapper">
<cachetype="com.example.cache.RedisCache"/>
</mapper>
6.3 缓存Key优化
private String generateKey(Object key){
CacheKey cacheKey = (CacheKey) key;
StringBuilder sb = new StringBuilder();
sb.append(id).append(":");
for (Object update : cacheKey.getUpdateList()) {
sb.append(update.toString()).append(":");
}
return sb.toString();
}
七、最佳实践
7.1 一级缓存使用建议
7.2 二级缓存使用建议
7.3 缓存配置最佳实践
mybatis:
configuration:
cache-enabled:true# 全局开启二级缓存(默认true)
local-cache-scope:statement# 一级缓存范围(session/statement)
local-cache-scope:
session:默认,整个SqlSession共享statement:每次查询后清空,相当于禁用一级缓存
八、面试加分Q&A
Q1:一级缓存和二级缓存的区别?
A:
作用范围:一级缓存SqlSession级别,二级缓存Mapper级别 默认状态:一级缓存默认开启,二级缓存需配置 生命周期:一级缓存随SqlSession关闭,二级缓存应用级别 数据一致性:一级缓存单会话隔离,二级缓存跨会话共享需注意
Q2:为什么执行update操作后一级缓存失效?
A: 源码中BaseExecutor.update()方法会先调用clearCache()清空缓存。目的是保证数据一致性,避免查询到旧数据。
Q3:二级缓存为什么需要commit才生效?
A: 二级缓存通过TransactionalCache包装,查询结果先存到entriesToAddOnCommit待提交区。只有事务commit时才真正写入缓存,rollback时丢弃。这样保证缓存数据与数据库事务一致性。
Q4:多表关联查询能用二级缓存吗?
A: 不建议。因为一个Mapper的更新操作不会清空关联Mapper的缓存,导致数据不一致。解决方案:
使用 cache-ref引用同一缓存或禁用二级缓存,改用Redis
Q5:分布式环境如何处理MyBatis缓存?
A: 禁用MyBatis二级缓存,自定义RedisCache实现:
继承 Cache接口用Redis存储缓存数据 所有实例共享Redis,保证一致性
九、总结
MyBatis缓存机制核心设计:
记住三条铁律:
一级缓存默认开启,但update/commit/关闭SqlSession会失效 二级缓存跨SqlSession共享,多表关联和分布式环境慎用 分布式环境用Redis替代二级缓存,保证数据一致性
源码层面,理解BaseExecutor.localCache和TransactionalCache.entriesToAddOnCommit两个关键存储,就理解了MyBatis缓存的核心机制。
📖 往期推荐
▸ Java XML 解析四大技术全解析:JAXB/DOM4J/DOM/SAX 实战对比与选型指南
如果觉得有帮助,欢迎转发给需要的朋友 💙
有问题评论区见 ✨
夜雨聆风