为什么 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 二级缓存的幻想,把缓存控制权拿回业务层,才是真正的"韧性设计"。

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

相关推荐
8***235526 分钟前
【wiki知识库】07.用户管理后端SpringBoot部分
java
阿蔹42 分钟前
JavaWeb-Selenium 配置以及Selenim classnotfound问题解决
java·软件测试·python·selenium·测试工具·自动化
小毅&Nora1 小时前
【后端】【C++】从裸指针到 C++20 协程:现代 C++ 内存与并发编程的双重革命
java·c++20
张np1 小时前
java基础-ArrayList
java·开发语言
Swizard1 小时前
别让 AI 假装在工作:Android "Vibe Coding" 的生存指南
android·java·vibe coding
BBB努力学习程序设计1 小时前
Java集合框架:管理数据的"超级工具箱"
java
库库林_沙琪马2 小时前
1、Hi~ SpringBoot
java·spring boot·后端
不会编程的小寒2 小时前
C / C++ 面试题
java·开发语言
BBB努力学习程序设计2 小时前
Java输入输出:让程序能与世界“对话”
java