Java缓存踩坑记:明明设置了过期时间,怎么还是读到脏数据?

上周四凌晨,我被一条报警短信惊醒------核心服务的缓存命中率突然跌到70%以下,同时数据库负载飙升。登录服务器一看,大量请求在缓存中拿到了过期的用户资料,而Redis里这些Key的TTL明明设置的是30分钟。这场景是不是很熟悉?今天就来扒一扒这个看似简单却暗藏杀机的缓存过期问题。

现象:缓存"幽灵数据"之谜

在我们的会员系统中,用户资料缓存设计如下:

java 复制代码
// 错误写法:典型的"先删后写"模式
public void updateUserProfile(long userId, UserProfile newProfile) {
    // 先删缓存
    redisTemplate.delete("user:" + userId);
    // 再写数据库(耗时操作)
    userDao.updateProfile(userId, newProfile);
    // 理论上缓存失效后下次查询会回种
}

表面看逻辑没问题,但压测时发现:当更新操作密集时,约5%的请求会读到旧数据,尽管Redis中的Key确实设置了30分钟过期时间。你可能会问:"缓存不是删了吗?下次查询自然会回填新数据啊?"

根因:缓存失效的"时间差陷阱"

问题出在缓存删除到数据库更新完成这个时间窗口。在高并发场景下:

  1. 线程A删除缓存
  2. 线程B在A更新完DB前查询缓存,发现miss后回查数据库(此时读到的是旧数据)
  3. 线程B将旧数据写入缓存
  4. 线程A完成DB更新
  • 结果*:缓存里停留的是脏数据,而30分钟的TTL会让这个错误持续半小时!这就是所谓的"缓存双写不一致"问题。

解决方案:从"先删后写"到"延迟双删"

修正后的代码引入二次删除 和短暂延迟:

java 复制代码
// 正确写法:延迟双删策略
public void updateUserProfile(long userId, UserProfile newProfile) {
    // 第一次删除
    redisTemplate.delete("user:" + userId);
    // 更新数据库
    userDao.updateProfile(userId, newProfile);
    // 提交事务后延迟二次删除
    transactionTemplate.execute(status -> {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.schedule(() -> {
            redisTemplate.delete("user:" + userId);
        }, 500, TimeUnit.MILLISECONDS); // 延迟500ms
        return null;
    });
}

关键点:

  1. 第二次删除延迟500ms(根据业务压测调整),确保数据库主从同步完成
  2. 放在事务提交后执行,避免事务回滚导致误删

实测对比:

方案 脏数据概率 平均耗时
先删后写 5.2% 78ms
延迟双删 0.03% 83ms

深挖:那些比想象中复杂的缓存问题

你以为这就完了?还有更隐蔽的坑:

坑1:热点Key的"死锁回填"

当某个Key突然成为热点(比如明星离婚新闻触发用户主页访问暴涨),如果缓存同时失效,会导致所有请求穿透到DB。解决方案是用redisTemplate.opsForValue().setIfAbsent()加分布式锁,只允许一个线程回填缓存。

坑2:本地缓存的"时间漂移"

如果你的系统用了多级缓存(比如Redis+Guava Cache),Guava的过期时间是相对时间 (从写入开始算),而Redis是绝对时间。集群机器时钟不同步时,可能导致本地缓存比Redis存活更久。

坑3:TTL的"批量失效"风暴

给所有Key设置相同的TTL(比如30分钟整),可能导致缓存大规模同时失效。正确做法是加上随机扰动:

java 复制代码
// 好的TTL设置:基础时间+随机偏移
int baseTtl = 1800; // 30分钟
int randomTtl = baseTtl + ThreadLocalRandom.current().nextInt(300); // 加0~5分钟随机
redisTemplate.opsForValue().set(key, value, randomTtl, TimeUnit.SECONDS);

避坑清单:缓存失效的生存法则

  1. 永远假设缓存会失效:代码要能容忍缓存miss和脏数据,做好降级策略
  2. 慎用"先删后写":优先考虑"先更新DB再删缓存"(Cache Aside Pattern)
  3. 监控缓存命中率与失效时间分布:突然的命中率下跌往往预示问题
  4. 区分冷热数据:热点数据用永不过期策略+后台刷新,避免集体失效
  5. 考虑消息队列保障最终一致:对于强一致性要求高的场景,可以用binlog监听触发缓存更新

最后说个扎心的事实:没有完美的缓存方案,只有适合当前业务场景的权衡。你在项目里还遇到过哪些诡异的缓存问题?欢迎在评论区分享你的"战损版"经验。

相关推荐
靠coding维持生计的potato44 分钟前
使用 unified diff 为 pnpm 依赖编写可复现补丁
前端
用户8662978480081 小时前
数据库报错"坏块"别急着删库重来,试试 KingbaseES 的离线修复工具
后端
jose1 小时前
URL 里多写了一个端口,为什么请求还能调通?
java·后端
艺杯羹1 小时前
Windows跨平台编码地雷全解:从GBK静默崩溃到内核级原子写入防御
人工智能·python·桌面客户端
用户8356290780511 小时前
使用 Python 在 Excel 中识别并高亮重复数据与唯一数据
后端·python
CCblandy1 小时前
Agent 工具调用怎么编排?本机实测:并行只到 2.63 倍,慢的先发省 30ms,CPU 活加 worker 无效
人工智能·后端·python·性能优化
云智慧AIOps社区1 小时前
MSP 的增长瓶颈不在客户,在交付模式:人效与规模如何兼得?
运维·人工智能·自动化运维
Yolo566Q1 小时前
AI-XGBoost 植被与土地利用识别、土壤碳氮空间预测、生物多样性驱动机制、土壤微生物功能预测、生态退化与风险识、论文写作
人工智能
茉莉玫瑰花茶1 小时前
GO [ 标准库 ]
开发语言·后端·golang