火柴人武林大会
156.74M · 2026-02-04
我猛灌半杯冰美式,盯着屏幕:
数据库查证:用户B头像URL未变
前端Network:返回的base64头像数据确是用户A的
服务日志:无异常堆栈,无SQL报错
最魔幻的是:
我后背发凉:这哪是bug,这是缓存成精了啊!
redis-cli> KEYS user:avatar:*
# 结果:空!项目根本没接Redis缓存!
测试小王弱弱补刀:“哥...你上周说‘简单功能用MyBatis二级缓存就行’..."
我:???(记忆碎片开始闪回)
# 监控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字段都错乱了!缓存污染实锤!
<!-- UserMapper.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>
<!-- ProfileMapper.xml(致命复制粘贴) -->
<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为隔离单位 → 所有操作共享同一块缓存区域 → 头像数据被昵称更新操作污染!
CacheKey.java)// 拼接缓存key的核心逻辑
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
// 按参数、SQL、offset等生成唯一key
hashCode = hashCode * 31 + ArrayUtil.hashCode(object);
}
}
关键真相:
namespace + sql + params + offset...ProfileMapper.updateNickname执行时,触发缓存清空(因readOnly=false)// CachingExecutor.java
public <E> List<E> query(...) {
if (ms.getCache() != null) {
flushCacheIfRequired(ms); // 更新操作会清空整个namespace缓存
...
}
}
重启 → JVM内存清空 → 缓存重建 → 短暂“干净” → 随着操作累积,污染循环开始
<!-- 所有Mapper.xml 删除 <cache> 标签 -->
<!-- 或全局关闭(mybatis-config.xml) -->
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
适用场景:分布式环境、数据强一致性要求高、缓存收益低
<!-- ProfileMapper.xml -->
<mapper namespace="com.xxx.mapper.ProfileMapper"> <!-- 唯一且语义清晰 -->
<!-- 移除<cache>,交由业务层控制 -->
</mapper>
团队公约:
grep "<mapper namespace" *.xml | sort | uniq -d// 自定义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);
}
// ... 其他方法实现
}
优势:
表格
| 误区 | 真相 | 行动指南 |
|---|---|---|
| “二级缓存开箱即用” | 分布式环境必翻车 | 单机只读场景慎用,分布式直接关 |
| “readOnly=true很安全” | 更新操作仍会清空整个namespace缓存 | 避免在含写操作的Mapper开缓存 |
| “namespace随便起” | 缓存隔离的唯一依据 | 严格等于Mapper接口全路径 |
| “缓存能提升性能” | 小数据量场景,序列化开销>收益 | 压测验证:QPS提升<5%?不如关掉 |
| “MyBatis缓存很智能” | 无分布式锁、无穿透保护 | 高并发场景必接Redis+本地缓存 |
灵魂三问(上线前必答) :
1️⃣ 项目是单机还是集群?→ 集群?二级缓存退退退!
2️⃣ 数据允许短暂不一致吗?→ 用户资料?必须强一致!
3️⃣ 缓存命中率实测多少?→ 用Arthas统计:monitor -c 5 com.xxx.mapper.XxxMapper selectXxx
天快亮时,我给团队Wiki加了一页:
测试小王发来新奶茶:“哥,这次排查笔记能发我学习吗?”
我笑着回:“下次上线前,咱俩一起过缓存设计。”
技术没有“小配置”,只有“大敬畏”。
那些深夜翻源码的狼狈,终会沉淀为代码里的从容。