当前位置: 首页 > news >正文

MyBatis缓存穿透深度解析:从原理到实战解决方案

引言

作为Java开发中常用的ORM框架,MyBatis的缓存机制(一级缓存、二级缓存)能显著提升查询效率,但实际使用中,“缓存穿透”问题却像个隐藏的“坑”——明明查的是数据库里没有的数据,请求却像潮水一样反复冲向数据库,轻则增加DB压力,重则导致DB崩溃。

今天这篇文章,笔者将从原理到解决方案,带大家彻底搞懂MyBatis缓存穿透!

一、什么是缓存穿透?MyBatis场景下的典型表现

1. 缓存穿透的定义

缓存穿透是指:客户端查询一个数据库中不存在的数据,由于缓存中也无该数据记录,请求绕过缓存直接访问数据库。如果这类无效请求高频发生(比如恶意攻击或参数错误),数据库可能被压垮。

2. MyBatis中的特殊场景

MyBatis的一级缓存(SqlSession级别)和二级缓存(Mapper级别)对“空值”的处理差异,是导致穿透的关键:

  • 一级缓存:默认会缓存null结果(同一SqlSession中重复查询会直接命中缓存)。但如果SqlSession关闭(比如HTTP请求结束),缓存失效,下次请求仍会穿透。
  • 二级缓存:默认不缓存null结果(比如PerpetualCache实现)。即使第一次查询返回null,缓存中也不会存储该键,后续相同查询依然会穿透到数据库。

举个真实例子
之前做用户系统时,前端传了一个id=-1的查询请求(业务中id是自增正整数)。第一次查询时,MyBatis二级缓存没命中,查数据库返回null,但缓存不存null;第二次同样传id=-1,又穿透到数据库……由于前端埋点错误,这个无效请求被高频触发,DB瞬间压力飙升!

二、MyBatis缓存穿透的根因分析

1. 二级缓存不缓存null值(核心原因)

MyBatis二级缓存的默认实现(如PerpetualCache)设计逻辑是“只缓存有效数据”,数据库查不到的结果不会存入缓存。这意味着,同一个无效id的多次查询,每次都会绕过缓存直接打DB

2. 数据动态变化导致缓存失效

即使缓存了有效数据,若数据被删除(如用户注销),缓存会被清除。此时查询已删除的id(数据库无记录),又会触发穿透。

3. 恶意攻击或参数错误

  • 恶意用户故意传入不存在的id(如id=0、超大数值)。
  • 前端/客户端生成参数时逻辑错误(如循环递增id,超出数据库最大值)。

三、实战解决方案:从简单到进阶

针对MyBatis缓存穿透,需要“多层防御”——从缓存策略、前置校验到流量控制,逐层拦截无效请求。以下是我在实际项目中验证过的有效方案:

方案1:缓存空值(Null Caching)—— 最直接的拦截

核心思路:将数据库查询结果为null的键存入缓存(标记为“不存在”),后续相同请求直接从缓存获取,避免穿透。

实现步骤(以Redis为二级缓存为例)
  1. 配置MyBatis使用Redis缓存
    引入mybatis-redis依赖,在Mapper接口上添加@CacheNamespace注解,指定自定义的Redis缓存实现。

    @CacheNamespace(implementation = CustomRedisCache.class)
    public interface UserMapper {User selectById(Long id); // 查询方法
    }
    
  2. 自定义Redis缓存类,处理空值
    重写putObjectgetObject方法,将null结果序列化为特定标识(如"NULL")存入Redis,并设置短过期时间(防止脏数据)。

    public class CustomRedisCache implements Cache {private final RedisTemplate<String, Object> redisTemplate;private static final long NULL_TTL = 300; // 空值缓存5分钟@Overridepublic void putObject(Object key, Object value) {String cacheKey = "mybatis:cache:" + key.toString(); // 自定义缓存键格式if (value == null) {// 存入"NULL"标识,替代nullredisTemplate.opsForValue().set(cacheKey, "NULL", NULL_TTL, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(cacheKey, value);}}@Overridepublic Object getObject(Object key) {String cacheKey = "mybatis:cache:" + key.toString();Object value = redisTemplate.opsForValue().get(cacheKey);// 若缓存值为"NULL",返回null;否则返回实际值return "NULL".equals(value) ? null : value;}// 其他方法(如removeObject、getSize等)按需实现...
    }
    
  3. 验证效果
    第一次查询id=-1时,数据库返回null,缓存存入"NULL";后续相同查询直接从缓存获取null,不再穿透DB。

方案2:布隆过滤器(Bloom Filter)—— 海量数据的前置拦截

核心思路:在查询数据库或缓存前,先用布隆过滤器判断id是否存在。若不存在,直接返回,避免访问缓存和DB。

实现步骤(基于Guava BloomFilter)
  1. 初始化布隆过滤器
    启动时加载所有有效id到布隆过滤器(适用于数据量稳定场景,如用户表、商品表)。

    @PostConstruct // Spring启动后初始化
    public void initBloomFilter() {// 预期插入100万条数据,误判率1%BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1_000_000, 0.01);// 从数据库加载所有有效id(如用户表的id)List<Long> allUserIds = userMapper.selectAllIds();allUserIds.forEach(bloomFilter::put);// 将布隆过滤器注入到Service中this.bloomFilter = bloomFilter;
    }
    
  2. 在Service层拦截无效请求
    查询前通过布隆过滤器校验id是否存在,不存在则直接返回null(或抛异常)。

    public User getUserById(Long id) {// 布隆过滤器前置校验if (!bloomFilter.mightContain(id)) {log.warn("拦截无效id:{}", id);return null;}// 命中缓存或数据库return userMapper.selectById(id);
    }
    

注意:布隆过滤器存在误判率(可能将不存在的id判定为存在),因此即使校验通过,仍需查询缓存/DB二次验证;若数据动态变化(如新增id),需定期更新布隆过滤器(可通过定时任务重新加载全量数据)。

方案3:热点参数校验+限流—— 拦截恶意请求

核心思路:针对已知非法参数(如负数、超长id)或高频无效参数,在网关或Service层添加校验,直接拦截。

实现示例
public User getUserById(Long id) {// 校验1:id必须为正数(业务逻辑约束)if (id == null || id <= 0) {log.warn("非法id请求:{}", id);return null;}// 校验2:id不能超过数据库最大可能值(如MySQL的BIGINT最大值)if (id > Long.MAX_VALUE - 1000) { log.warn("id超出合理范围:{}", id);return null;}// 正常查询逻辑...
}

扩展:结合Sentinel等限流工具,对特定无效id(如id=0)的请求限流,防止恶意攻击。

方案4:分布式锁—— 防缓存击穿(穿透的极端场景)

核心思路:当缓存未命中时,仅允许一个线程查询数据库,其他线程等待结果,避免大量线程同时穿透到DB(适用于高并发热点id场景)。

实现步骤(基于Redis分布式锁)
public User getUserById(Long id) {String cacheKey = "user:cache:" + id;// 1. 先查缓存User user = redisTemplate.opsForValue().get(cacheKey);if (user != null) {return user;}// 2. 缓存未命中,尝试加锁(防止大量线程同时查DB)String lockKey = "user:lock:" + id;boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); // 锁10秒if (!locked) {// 加锁失败,等待100ms后重试(避免无限重试)try {Thread.sleep(100);return getUserById(id); // 递归重试(实际项目中建议限制重试次数)} catch (InterruptedException e) {Thread.currentThread().interrupt();return null;}}try {// 3. 加锁成功,查数据库user = userMapper.selectById(id);// 4. 回写缓存(缓存空值或有效数据,设置合理TTL)redisTemplate.opsForValue().set(cacheKey, user != null ? user : "NULL", user != null ? 3600 : 300, // 有效数据缓存1小时,空值缓存5分钟TimeUnit.SECONDS);return user;} finally {// 5. 释放锁(Lua脚本保证原子性)redisTemplate.execute(new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class), Collections.singletonList(lockKey), "1");}
}

四、方案对比与选择建议

方案优点缺点适用场景
缓存空值实现简单,快速拦截重复无效请求需合理设置TTL,可能存储短时间脏数据无效参数固定、数据量小
布隆过滤器内存占用低,适合海量数据存在误判,需二次校验数据量大、动态变化
热点参数校验逻辑简单,拦截明显非法请求无法处理合法但数据库无记录的id已知非法参数范围
分布式锁彻底避免缓存击穿(穿透的极端情况)实现复杂,可能影响性能高并发、热点id场景

总结:多层防御,让缓存穿透无处可逃

MyBatis缓存穿透的本质是“无效请求绕过缓存直连DB”,解决思路是拦截无效请求+减少无效查询。实际项目中,建议:

  1. 优先用缓存空值拦截重复请求(简单高效);
  2. 海量数据场景补充布隆过滤器前置校验;
  3. 恶意请求用参数校验+限流精准打击;
  4. 高并发热点场景用分布式锁防击穿。

记住:没有完美的方案,只有最适合业务的组合!

如果你在实际项目中遇到过更复杂的缓存穿透问题,欢迎在评论区留言讨论~ 😊

http://www.dtcms.com/a/274191.html

相关文章:

  • Selenium 自动化实战技巧【selenium】
  • frame 与新窗口切换操作【selenium 】
  • 【JMeter】调试方法
  • Conda 安装与配置详解及常见问题解决
  • 从代码学习深度强化学习 - PPO PyTorch版
  • 番外-linux系统运行.net framework 4.0的项目
  • 《Java EE与中间件》实验三 基于Spring Boot框架的购物车
  • 【flutter】flutter网易云信令 + im + 声网rtm从0实现通话视频文字聊天的踩坑
  • 如何蒸馏 设计 中文nl2sql 数据集,进行grpo强化学习 训练qwen3-8b,可以提升大模型nl2sql的能力,支持300行sql生成
  • Redis 分片集群
  • mysql报错服务没有报告任何错误
  • AIGC(生成式AI)试用 33 -- 用AI学AI名词
  • 彻底解决JavaFx在Linux中文无法正常显示的问题(究其根本原因)
  • 剑指offer——数组:数组中重复的数字
  • 博客系统开发全流程解析(前端+后端+数据库)与 AI 协作初体验
  • [大模型问数]实现大模型调用MYSQL(03)【MCP笔记】
  • Prometheus+Grafana部署及企业微信邮件/群消息告警通知配置
  • Shader面试题100道之(81-100)
  • 中学物理模拟实验Python程序集成打包
  • 牛客周赛 Round 99
  • Spring 声明式事务:从原理到实现的完整解析
  • 破解多宠管理难题,端侧AI重新定义宠物智能硬件
  • 《Spring 中上下文传递的那些事儿》Part 10:上下文敏感信息脱敏与审计日志记录
  • ESP32_启动日志分析
  • 【TCP/IP】17. 移动 IP
  • Linux权限的概念
  • 硬件加速(FPGA)
  • 函数指针指针函数 智能指针
  • 通过ETL工具,高效完成达梦数据库数据同步至数仓Oracle的具体实现
  • MDSE模型驱动的软件工程和敏捷开发相结合的案例