MyBatis 原生二级缓存"难以修复"的原因解析及解决方案

 更新时间:2025年12月04日 10:28:42   作者:旷野说  
文章主要讨论了MyBatis原生二级缓存存在的问题,包括结构性缺陷、难以修复等,社区提供了多种增强插件方案,但这些方案也有各自的优缺点,本文结合实例代码介绍的非常详细,感兴趣的朋友跟随小编一起看看吧

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

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

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

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

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

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

1.mybatis-redis(最常用)

@Mapper
@CacheNamespace(implementation = RedisCache.class)
public interface OrderMapper {
    // ...
}
  • ✅ 优点:跨实例一致、可设 TTL(需自定义)
  • ❌ 缺点:
    • 无法解决 Key 语义问题(分页/动态 SQL 缓存 key 仍可能冲突)
    • 每次查询都多一次 Redis 网络 IO,在 10w QPS 下可能成为瓶颈
    • 序列化成本高(Java 对象需序列化为 byte[])

💡 适用场景:读多写少、数据变更不频繁、对延迟不敏感的配置类数据(如字典表)

2.自定义 Cache 实现(继承 MyBatisCache接口)

你可以自己实现带版本号逻辑过期的缓存:

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 + 幂等 + 版本号

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

到此这篇关于MyBatis 原生二级缓存“难以修复”的原因解析及解决方案的文章就介绍到这了,更多相关mybatis二级缓存修复内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 一篇文章带你入门Java多线程

    一篇文章带你入门Java多线程

    这篇文章主要介绍了java多线程编程实例,分享了几则多线程的实例代码,具有一定参考价值,加深多线程编程的理解还是很有帮助的,需要的朋友可以参考下
    2021-08-08
  • java实现树形菜单对象

    java实现树形菜单对象

    这篇文章主要为大家详细介绍了java实现树形菜单对象,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-05-05
  • img 加载网络图片失败 显示默认图片的方法

    img 加载网络图片失败 显示默认图片的方法

    下面小编就为大家带来一篇img 加载网络图片失败 显示默认图片的方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • MyBatis图文并茂讲解注解开发一对一查询

    MyBatis图文并茂讲解注解开发一对一查询

    这篇文章主要介绍了SpringBoot中Mybatis注解一对一查询的实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Mybatis如何使用动态语句实现批量删除(delete结合foreach)

    Mybatis如何使用动态语句实现批量删除(delete结合foreach)

    这篇文章主要介绍了Mybatis如何使用动态语句实现批量删除(delete结合foreach),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • Java超详细分析抽象类和接口的使用

    Java超详细分析抽象类和接口的使用

    在类中没有包含足够的信息来描绘一个具体的对象,这样的类称为抽象类,接口是Java中最重要的概念之一,它可以被理解为一种特殊的类,不同的是接口的成员没有执行体,是由全局常量和公共的抽象方法所组成,本文给大家介绍Java抽象类和接口,感兴趣的朋友一起看看吧
    2022-04-04
  • 微信随机生成红包金额算法java版

    微信随机生成红包金额算法java版

    这篇文章主要为大家详细介绍了java和php版的微信随机生成红包金额算法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-07-07
  • Spring Boot应用启动后立即关闭的原因及解决实现步骤

    Spring Boot应用启动后立即关闭的原因及解决实现步骤

    我们在新建SpringBoot项目的时候,有时在运行的时候,会遇到一会就自动关闭了的问题,这篇文章主要介绍了Spring Boot应用启动后立即关闭的原因及解决实现步骤,需要的朋友可以参考下
    2025-09-09
  • java唯一字符串ID生成方案详解

    java唯一字符串ID生成方案详解

    这篇文章主要给大家介绍了关于java唯一字符串ID生成方案的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • RocketMQ4.5.X 实现修改生产者消费者日志保存路径

    RocketMQ4.5.X 实现修改生产者消费者日志保存路径

    这篇文章主要介绍了RocketMQ4.5.X 实现修改生产者消费者日志保存路径方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07

最新评论