为什么 MyBatis 原生二级缓存“难以修复”?

🚫 一、为什么 MyBatis 原生二级缓存"难以修复"?

MyBatis 二级缓存的底层设计存在结构性缺陷,不是加个插件就能完美解决的:

问题 原因 高并发影响
缓存 Key 粒度粗糙 Key = namespace + sql + params,但 params 是对象哈希,分页/排序等参数容易哈希冲突或忽略语义差异 返回错误数据(如分页错乱)
无 TTL / 过期机制 默认使用 PerpetualCache,数据永不过期 内存泄漏 + 脏读
跨 JVM 无法共享 缓存是本地内存(JVM 内),多实例部署时各节点缓存不一致 数据分裂,用户看到不同结果
写操作不自动失效关联缓存 更新 User 表,不会自动清理 OrderMapper 中关联的 user_orders 缓存 脏读持续存在

⚠️ 这些是架构级缺陷 ,靠拦截器或装饰器插件无法根本解决


🔧 二、社区增强插件方案(可选,但需谨慎)

虽然不能"修复",但有插件替换缓存实现,提升可用性:

1. mybatis-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 二级缓存,原因如下:

  1. 控制权不足:MyBatis 缓存是"黑盒",无法插入降级、熔断、监控逻辑
  2. 异常难追溯:缓存脏数据问题往往延迟暴露,修复成本高
  3. 违背"可观测性"原则:缓存命中/失效无法对接 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 二级缓存的幻想,把缓存控制权拿回业务层,才是真正的"韧性设计"。

关注我,从零开始构建可观测、可降级、可修复的高并发系统。

相关推荐
橘子海全栈攻城狮12 分钟前
【最新源码】养老院系统管理A013
java·spring boot·后端·web安全·微信小程序
逻辑驱动的ken19 分钟前
Java高频面试考点18
java·开发语言·数据库·算法·面试·职场和发展·哈希算法
冷雨夜中漫步1 小时前
Claude Code源码分析——Claude Code Agent Loop 详细设计文档
java·开发语言·人工智能·ai
直奔標竿1 小时前
Java开发者AI转型第二十六课!Spring AI 个人知识库实战(五)——联网搜索增强实战
java·开发语言·人工智能·spring boot·后端·spring
one_love_zfl2 小时前
java面试-微服务组件篇
java·微服务·面试
一只大袋鼠2 小时前
Java进阶:CGLIB动态代理解析
java·开发语言
环流_2 小时前
HTTP 协议的基本格式
java·网络协议·http
爱滑雪的码农2 小时前
Java基础十三:Java中的继承、重写(Override)与重载(Overload)详解
java·开发语言
【 】4232 小时前
C++&STL(Standard Template Library,标准模板库)
java·开发语言·c++
茉莉玫瑰花茶2 小时前
LangChain 核心组件 [ 2 ]
java·数据库·langchain