🚫 一、为什么 MyBatis 原生二级缓存"难以修复"?
MyBatis 二级缓存的底层设计存在结构性缺陷,不是加个插件就能完美解决的:
| 问题 | 原因 | 高并发影响 |
|---|---|---|
| 缓存 Key 粒度粗糙 | Key = namespace + sql + params,但 params 是对象哈希,分页/排序等参数容易哈希冲突或忽略语义差异 |
返回错误数据(如分页错乱) |
| 无 TTL / 过期机制 | 默认使用 PerpetualCache,数据永不过期 |
内存泄漏 + 脏读 |
| 跨 JVM 无法共享 | 缓存是本地内存(JVM 内),多实例部署时各节点缓存不一致 | 数据分裂,用户看到不同结果 |
| 写操作不自动失效关联缓存 | 更新 User 表,不会自动清理 OrderMapper 中关联的 user_orders 缓存 |
脏读持续存在 |
⚠️ 这些是架构级缺陷 ,靠拦截器或装饰器插件无法根本解决。
🔧 二、社区增强插件方案(可选,但需谨慎)
虽然不能"修复",但有插件替换缓存实现,提升可用性:
1. mybatis-redis(最常用)
- GitHub: https://github.com/mybatis/redis-cache
- 原理:用 Redis 作为二级缓存的存储后端,解决多节点一致性问题
- 使用方式:
java
@Mapper
@CacheNamespace(implementation = RedisCache.class)
public interface OrderMapper {
// ...
}
- ✅ 优点:跨实例一致、可设 TTL(需自定义)
- ❌ 缺点:
- 仍无法解决 Key 语义问题(分页/动态 SQL 缓存 key 仍可能冲突)
- 每次查询都多一次 Redis 网络 IO,在 10w QPS 下可能成为瓶颈
- 序列化成本高(Java 对象需序列化为 byte[])
💡 适用场景:读多写少、数据变更不频繁、对延迟不敏感的配置类数据(如字典表)
2. 自定义 Cache 实现(继承 MyBatis Cache 接口)
你可以自己实现带版本号 或逻辑过期的缓存:
java
public class VersionedCache implements Cache {
private final String id;
private final RedisTemplate redis;
@Override
public void putObject(Object key, Object value) {
// key = "order:123", value = { data: {...}, version: 20251203 }
redis.opsForHash().put("mybatis:cache:" + id, serialize(key), wrapWithVersion(value));
}
@Override
public Object getObject(Object key) {
Object cached = redis.opsForHash().get("mybatis:cache:" + id, serialize(key));
if (cached != null && isVersionValid(cached)) {
return unwrap(cached);
}
return null;
}
}
- ✅ 优点:可控制缓存结构、加版本、加 TTL
- ❌ 缺点:开发维护成本高,且仍受限于 MyBatis 缓存 key 生成逻辑
🛑 三、我们的选择:弃用二级缓存,自建业务缓存层
在高并发订单系统中,我们最终彻底关闭 MyBatis 二级缓存,原因如下:
- 控制权不足:MyBatis 缓存是"黑盒",无法插入降级、熔断、监控逻辑
- 异常难追溯:缓存脏数据问题往往延迟暴露,修复成本高
- 违背"可观测性"原则:缓存命中/失效无法对接 Prometheus / SkyWalking
✅ 替代方案:业务层 + Redis + 幂等 + 版本号
java
@Service
public class OrderService {
public Order getOrder(Long id) {
String cacheKey = "order:v2:" + id;
Order order = redis.get(cacheKey, Order.class);
if (order != null) return order;
// 双检锁 + 空值缓存防穿透
synchronized (getLockKey(id)) {
order = redis.get(cacheKey, Order.class);
if (order == null) {
order = orderMapper.findById(id);
if (order != null) {
redis.setex(cacheKey, 300, order); // 5分钟过期
} else {
redis.setex("empty:" + cacheKey, 60, "1"); // 防缓存穿透
}
}
}
return order;
}
@Transactional
public void updateOrder(Order order) {
orderMapper.update(order);
// 主动失效缓存(旁路删除)
redis.delete("order:v2:" + order.getId());
}
}
✅ 优势:
- 缓存 key 语义清晰(含版本 v2,便于灰度/回滚)
- 支持 TTL、空值缓存、主动失效
- 可集成 缓存命中率监控 、慢查询告警
- 与 Flink/Canal 数据对齐 机制无缝衔接
🧠 总结:二级缓存不是"能不能修",而是"值不值得用"
| 方案 | 适合场景 | 高并发推荐度 |
|---|---|---|
| MyBatis 原生二级缓存 | 单机、低频、只读数据 | ❌ 不推荐 |
| mybatis-redis 插件 | 多实例、读多写少、容忍一定延迟 | ⚠️ 谨慎评估 |
| 自定义 Cache 实现 | 有强缓存治理能力的团队 | ⚠️ 成本高 |
| 业务层自建缓存 | 高并发、强一致性、可观测性要求高 | ✅ 强烈推荐 |
最终口诀更新:
"二级缓存看似香,架构缺陷难躲藏;
分页排序易冲突,多机部署更遭殃;
插件替换治标难,自建缓存才稳当;
Key 带版本加 TTL,高并发下不慌张!"
如果你正在设计高并发系统,放弃对 MyBatis 二级缓存的幻想,把缓存控制权拿回业务层,才是真正的"韧性设计"。
关注我,从零开始构建可观测、可降级、可修复的高并发系统。