MyBatis二级缓存翻车实录:改个昵称,全公司用户头像集体“穿越”?!
作者:互联网
2026-03-27
凌晨2:18,钉钉炸出灵魂拷问
我猛灌半杯冰美式,盯着屏幕:
数据库查证:用户B头像URL未变
前端Network:返回的base64头像数据确是用户A的
服务日志:无异常堆栈,无SQL报错
最魔幻的是:
- 刷新3次,头像在“用户B原图”“用户A旧图”“空白”间随机切换
- 重启服务瞬间恢复正常,10分钟后复现
- 仅发生在用户修改资料后
我后背发凉:这哪是bug,这是缓存成精了啊!
三小时硬核排查(附真实命令)
第一回合:甩锅Redis?
redis-cli> KEYS user:avatar:*
# 结果:空!项目根本没接Redis缓存!
测试小王弱弱补刀:“哥...你上周说‘简单功能用MyBatis二级缓存就行’..."
我:???(记忆碎片开始闪回)
第二回合:Arthas锁死缓存轨迹
# 监控Mapper方法返回值
watch com.xxx.mapper.UserMapper selectAvatarByUid '{returnObj}' -x 3 -n 5
关键输出:
[第1次调用] Avatar{id=1002, url="b_old.jpg"} ← 用户B的头像
[第2次调用] Avatar{id=1001, url="a_old.jpg"} ← 竟是用户A的旧头像!
[第3次调用] null
突破口:返回对象的id字段都错乱了!缓存污染实锤!
第三回合:翻出“罪证”XML
<mapper namespace="com.xxx.mapper.UserMapper">
<cache eviction="LRU" size="1024" readOnly="false"/>
<select id="selectAvatarByUid" resultType="Avatar">
SELECT id, url FROM avatar WHERE user_id = #{uid}
select>
mapper>
<mapper namespace="com.xxx.mapper.UserMapper">
<cache eviction="LRU" size="512" readOnly="true"/>
<update id="updateNickname">
UPDATE user SET nickname=#{name} WHERE id=#{id}
update>
mapper>
瞳孔地震:
两个Mapper共用同一个namespace!
MyBatis二级缓存以namespace为隔离单位 → 所有操作共享同一块缓存区域 → 头像数据被昵称更新操作污染!
深扒MyBatis缓存源码(3.5.13版)
缓存key生成逻辑(CacheKey.java)
// 拼接缓存key的核心逻辑
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
// 按参数、SQL、offset等生成唯一key
hashCode = hashCode * 31 + ArrayUtil.hashCode(object);
}
}
关键真相:
- 缓存key =
namespace + sql + params + offset... - 但namespace相同时,不同Mapper的SQL会混用同一缓存池!
ProfileMapper.updateNickname执行时,触发缓存清空(因readOnly=false)- 但清空的是整个namespace的缓存 → 头像查询缓存被误删 → 下次查询时,因缓存miss+并发,脏数据混入
为什么重启能暂时恢复?
// CachingExecutor.java
public List query(...) {
if (ms.getCache() != null) {
flushCacheIfRequired(ms); // 更新操作会清空整个namespace缓存
...
}
}
重启 → JVM内存清空 → 缓存重建 → 短暂“干净” → 随着操作累积,污染循环开始
️ 三招根治(已上线30天零复发)
方案1:紧急止血(10分钟上线)
<settings>
<setting name="cacheEnabled" value="false"/>
settings>
适用场景:分布式环境、数据强一致性要求高、缓存收益低
方案2:规范namespace(治本之策)
<mapper namespace="com.xxx.mapper.ProfileMapper">
mapper>
团队公约:
- namespace = Mapper接口全限定名(IDEA自动生成)
- 禁止手动修改namespace
- Code Review必查项:
grep "
方案3:用Redis替代(高阶方案)
// 自定义Cache实现,接入Redis
public class RedisCache implements Cache {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
@Override
public void putObject(Object key, Object value) {
// 序列化存入Redis,key=namespace:md5(sql+params)
redisTemplate.opsForValue().set(buildKey(key), value, 10, TimeUnit.MINUTES);
}
// ... 其他方法实现
}
优势:
- 多节点共享缓存
- 精细化过期策略
- 避免JVM内存压力
血泪避坑清单(打印贴工位!)
表格
| 误区 | 真相 | 行动指南 |
|---|---|---|
| “二级缓存开箱即用” | 分布式环境必翻车 | 单机只读场景慎用,分布式直接关 |
| “readOnly=true很安全” | 更新操作仍会清空整个namespace缓存 | 避免在含写操作的Mapper开缓存 |
| “namespace随便起” | 缓存隔离的唯一依据 | 严格等于Mapper接口全路径 |
| “缓存能提升性能” | 小数据量场景,序列化开销>收益 | 压测验证:QPS提升<5%?不如关掉 |
| “MyBatis缓存很智能” | 无分布式锁、无穿透保护 | 高并发场景必接Redis+本地缓存 |
灵魂三问(上线前必答) :
1️⃣ 项目是单机还是集群?→ 集群?二级缓存退退退!
2️⃣ 数据允许短暂不一致吗?→ 用户资料?必须强一致!
3️⃣ 缓存命中率实测多少?→ 用Arthas统计:monitor -c 5 com.xxx.mapper.XxxMapper selectXxx
写在晨光微露时
天快亮时,我给团队Wiki加了一页:
测试小王发来新奶茶:“哥,这次排查笔记能发我学习吗?”
我笑着回:“下次上线前,咱俩一起过缓存设计。”
技术没有“小配置”,只有“大敬畏”。
那些深夜翻源码的狼狈,终会沉淀为代码里的从容。
相关推荐
专题
+ 收藏
+ 收藏
+ 收藏
+ 收藏
+ 收藏
最新数据
相关文章
哇!绝了!原来这么简单!我的 Java 项目代码终于被 “拯救” 了!
Redis 缓存三大经典问题:穿透、击穿与雪崩
AI智能应用开发(Java)从起点到终点-面向对象
Netty:从“网络搬砖”到“流水线大师”的奇幻之旅
Java线程池的执行流程与常见配置
AQS 同步器——Java 并发框架的核心底座全解析
Netty是如何处理websoceket协议的
JVM - 运行时内存模型
使用 Arthas 精准排查 SpringBoot 多模块项目中未使用的类(安全清理无用代码)
4. Spring Boot 数据持久化(JPA)
AI精选
