news 2026/6/10 2:08:20

缓存三大问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
缓存三大问题
  1. 缓存穿透(数据在哪都没有)—空值缓存+布隆过滤器

    • key 从来不存在 → 攻击者恶意查询不存在的ID

    • 0【布隆过滤器前置过滤】判断id是否可能存在于数据库
      如果 bloomFilter.contains(id) == false → 直接 return null(100% 不存在,不必往下走)
      注意:布隆过滤器说"不存在"是100%准确的,说"存在"有1%误判,所以通过后还要继续查缓存和DB

    • 1 检查输入是不是非法数据
      id <= 0 或不合法 → 直接 return null / 抛异常

    • 2 检查Redis中是否有真实数据(isNotBlank(value) → 排除空格更安全)
      isNotBlank(null) = false→ key不存在
      isNotBlank("") = false→ 空值缓存
      isNotBlank(" ") = false→ 纯空格(排除)
      isNotBlank(真实数据) = true→ 反序列化 + 返回真实数据 ✅

    • 3 检查key中的value是否为""(空值缓存命中,情况:null(key不存在)或 ""(空值缓存)
      value == null→ Redis没有这个key,第一次查,继续第4步
      value == ""→ 之前查过DB确认不存在,空值缓存命中,直接 return null ❌(拦截!)
      即:if (value != null) return null

    • 4 value == null,说明Redis中完全没有这个缓存(真·第一次查)
      查数据库:
      ├── 找到了 → 写入Redis(正常TTL,如30分钟)+ 返回数据 ✅
      └── 没找到 → 写入Redis空值""(短TTL,如2分钟)+ 返回null ❌
      注意:这里同时说明布隆过滤器误判了(1%概率),空值缓存兜底

    • 【补充】布隆过滤器的维护

      • 初始化:项目启动时,把所有存在的ID批量加载进布隆过滤器(预热)
      • 新增数据:insert时同步调用 bloomFilter.add(id)
      • 删除数据:布隆过滤器不支持删除,容量满了需要重建
  2. 缓存击穿(key过期-一般是热点数据)–互斥锁重建(同步,强一致,手写锁)–逻辑过期(异步重建,高性能)

    • 缓存击穿 — 互斥锁重建(同步,强一致)

      • 0 什么是缓存击穿(和穿透的区别)
        穿透:key 从来不存在 → 攻击者恶意查询不存在的ID
        击穿:key 存在但是刚好过期 → 瞬间大量并发同时查一个热点key,全部打向DB

      • 1 从Redis查询数据
        value = redis.get(key)

      • 2 检查是否有真实数据(isNotBlank(value),同穿透方案)
        isNotBlank(真实数据) = true→ 反序列化 + 返回数据 ✅

      • 3 检查是否是空值缓存(value != null → value == “”)
        value == ""→ 空值缓存命中,直接 return null ❌(拦截!)

      • 4 执行到这里,说明缓存没命中(value == null),需要查DB
        但不能直接查!因为是热点key,可能有100个请求同时到达这一步
        → 核心思想:热点key过期时,用锁保证只有一个线程去查DB重建缓存,其他线程等待重试,直到缓存重建完成

      • 5 获取互斥锁 — 本项目有两种实现方式

        ┌─ 方式A:手写SETNX锁(CacheClient.queryWithMutex 当前使用)
        │ tryLock() → setIfAbsent(key, “1”, 10s)
        │ 获取失败 → sleep(50ms) + 递归重试(重新走一遍流程)
        │ 重试时如果缓存已被其他线程重建 → 第2步直接命中返回
        │ 重试时如果空值已被其他线程缓存 → 第3步直接拦截
        │ 获取成功 → 进入第6步
        │ 缺点:不可重入、不自动续期(10秒超时后锁可能丢了)、delete可能误删别人的锁
        │ 优点:极简,无额外依赖,教学友好

        └─ 方式B:Redisson分布式锁(推荐升级,VoucherOrderServiceImpl已使用)
        RLock lock = redissonClient.getLock(lockKey)
        lock.tryLock() → 失败阻塞/重试 → 成功继续
        优点:可重入、WatchDog看门狗每10秒自动续期、Lua脚本判断是自己的锁才释放
        缺点:引入Redisson依赖(项目中已有)

      • 6 获取锁成功后,先做双重检查(Double Check)
        再次查Redis → 如果缓存已经有了(被上个线程重建)→ 释放锁 + 直接返回
        目的:防止当前线程在sleep等待期间,其他线程已经把缓存重建好了

      • 7 双重检查通过,查数据库

        • 找到了 → 写入Redis(正常TTL)+ 释放锁 + 返回数据 ✅
        • 没找到 → 写入Redis空值""(短TTL,2分钟)+ 释放锁 + 返回null ❌
        • finally:无论如何都要释放锁
      • 【如何判断用哪种锁】
        手写SETNX:个人学习、小型项目、不涉及锁续期的场景(够用)
        Redisson:多人协作、生产环境、业务执行时间可能超过锁超时(必须续期)的场景
        本项目建议:CacheClient也升级成Redisson,保持一致,代码更简洁

    • 二、 逻辑过期(异步重建,高性能)

      • 0 核心思路
        Redis的key不设TTL(物理永不过期),自己记录一个"逻辑过期时间"字段
        所有请求永远能命中缓存,发现"逻辑过期"时,一个线程后台去异步刷新,其他线程先拿旧数据返回
      • 1 数据结构
        不用String存数据,改成存一个RedisData包装对象:
        {
        “data”: { 真实业务数据 }, // 如Shop对象
        “expireTime”: “2026-06-05 12:30:00” // 逻辑过期时间戳
        }
        Redis命令:SET shop:1 ‘{“data”:{…},“expireTime”:“…”}’ (没有EX参数,永不过期)
      • 2 预热阶段(非常重要!)
        项目启动时,主动把热点数据查出来,包装成RedisData(data + 逻辑过期时间),写入Redis
        方式:@PostConstruct 或 实现InitializingBean / CommandLineRunner
        如果忘了预热 → Redis里没key → 第3步直接返回null → 逻辑过期方案失效
        这和第4步的互斥锁不同:互斥锁可以饿死等,逻辑过期没预热就直接跪
      • 3 查询流程
        3.1 redis.get(key) → 拿到json(RedisData的序列化结果)
        3.2 如果拿不到(key不存在)
        → 直接return null(说明没预热,调用方自行兜底,比如主动初始化)
        3.3 反序列化 → RedisData对象 → 提取data和expireTime
        3.4 判断是否逻辑过期:expireTime.isAfter(now)
        • 没过期 → 直接返回 data ✅(99%的请求走这里,极快)
        • 已过期 → 进入第4步
      • 4 逻辑过期了,需要重建缓存
        4.1 先获取互斥锁 tryLock(“lock:shop:” + id)
        4.2 获取锁成功:
        → 扔到线程池(10个固定线程)异步执行:
        ① 查数据库拿到最新数据
        ② setWithLogicalExpire(key, 新数据, time, unit) → 更新RedisData(新的逻辑过期时间)
        ③ finally释放锁
        → 主线程不等,直接 return 旧数据 ✅
        4.3 获取锁失败:
        → 说明已经有其他线程在重建了
        → 不等待,直接 return 旧数据 ✅
        为什么不等?反正过一会儿缓存就被重建好了,先拿旧数据给用户看
      • 5 关键点
        ┌─ 无论过期与否,永远返回数据,用户无感知
        ├─ 重建是异步的 = 后台默默刷新,主线程不阻塞
        ├─ 互斥锁保证100个过期请求里只有1个去查DB,剩下99个拿旧数据走人
        └─ 物理永不过期 + 逻辑过期 = 杜绝雪崩(大量key同时被Redis删除的问题)
    • 【互斥锁 vs 逻辑过期 对比】

      互斥锁:

      • 同步重建,查DB时其他线程sleep(50ms)递归重试,所有人等着
      • 数据强一致,查DB拿到的最新数据才返回
      • 实现简单,就是一把锁 + 递归重试 + 双重检查
      • 代价:用户响应慢(50ms级别),极端情况持锁线程崩了要等10秒锁超时
      • 本质:牺牲可用性保一致性(CAP中的C)

      逻辑过期:

      • 异步重建,扔到线程池后台查DB,主线程拿到旧数据先返回
      • 数据短暂不一致(最多几十毫秒,等后台写完缓存就一致了)
      • 实现复杂,需要预热 + 包装RedisData + 线程池 + 锁
      • 收益:用户几乎无感知,永远不被阻塞
      • 本质:牺牲一致性保可用性(CAP中的A)

      选型:
      文章阅读量、店铺浏览、Feed流 → 逻辑过期(性能优先,看得见就行)
      库存扣减、余额查询、交易状态 → 互斥锁(一致性优先,错了不行)
      查询不存在的ID → 无论哪种方案,空值缓存+布隆过滤器都先兜底穿透

  3. 缓存雪崩 — 大量key同时过期或Redis宕机,请求瞬间全部打向DB

    • 0 什么是缓存雪崩
      同一时刻,大量缓存的key集体过期(或Redis直接挂掉)
      → 海量请求绕过缓存直接涌向数据库
      → 数据库瞬间压力暴增 → 可能直接宕机
      和大促秒杀一个道理:平时分散的流量突然集中到一点 = 雪崩

    • 1 为什么会出现雪崩
      场景1:批量设置缓存的TTL都一样
      比如晚上12点全量刷新缓存,所有key都是30分钟TTL
      → 12:30所有key同时过期 → 雪崩
      场景2:Redis宕机/重启
      缓存层直接没了 → 所有请求全部打到DB → DB扛不住
      场景3:热点数据集中过期
      秒杀/大促期间,大量商品缓存在同一时间过期

    • 2 和穿透、击穿的区别(三种问题要分清)
      穿透:key根本不存在,攻击者不断换不存在的ID打数据库(布隆 + 空值缓存)
      击穿:一个热点key刚好过期,大量并发同时抢一个key(互斥锁 / 逻辑过期)
      雪崩:一大批key同时过期,量变引起质变,数据库被大面积冲击

    • 3 解决方案

      • 方案A — 给TTL加随机值(最简单,治标)
        同一批key的过期时间不要设成一样的:
        redis.set(key, data, 30分钟 + random(0~5分钟))
        这样每个key的过期时间被错开,不会同时挂,压力被分散
        优点:改一行代码就完事
        缺点:只能缓解,不能根治;Redis宕机依然雪崩

      • 方案B — 逻辑过期(治本,用得最多)
        Redis的key不设TTL,物理永不过期 ← 从根源上消灭了"同时过期"这件事
        自己在数据里记录一个逻辑过期时间戳
        发现过期 → 异步后台刷新,主线程返回旧数据
        优点:彻底解决大量key同时过期的问题,性能高
        缺点:需要预热 + 线程池 + 数据短暂不一致(和击穿的逻辑过期是同一套方案)
        → 和缓存击穿的逻辑过期方案是完全一样的代码,复用即可

      • 方案C — Redis高可用集群(防Redis宕机)
        主从复制(Master-Slave / Sentinel):
        主节点挂了,哨兵自动选一个从节点顶上,缓存继续服务
        集群分片(Redis Cluster):
        数据分散在多台机器上,一台挂了不影响整体

      • 方案D — 多级缓存(兜底,服务降级)
        Nginx本地缓存 → Redis → 数据库,层层兜底
        Redis全挂了,还有本地缓存先扛着,给DB争取时间
        限流熔断:Redis挂了直接触发限流,保护DB不死

    • 4 实际项目怎么组合(推荐)
      逻辑过期(方案B) + 随机TTL(方案A) + Sentinel哨兵(方案C)
      ├── 普通数据:TTL + 随机值错开(简单够用)
      ├── 热点数据:逻辑过期不设物理TTL(杜绝雪崩)
      └── Redis挂掉:Sentinel主从切换 + 限流兜底

    • 【三种缓存问题总结】
      穿透 → 查不存在的东西 → 布隆过滤器 + 空值缓存
      击穿 → 一个热点突然过期 → 互斥锁(一致)/ 逻辑过期(可用)
      雪崩 → 一堆key同时过期 / Redis挂了 → 随机TTL + 逻辑过期 + 集群 + 限流

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 2:08:11

“一单一库”监管时代来临:IACheck融合AI报告审核通审Agent版构建来料资质智能核验新体系

近两年&#xff0c;检验检测行业正在经历一场深层次的数字化监管变革。从资质认定监督检查到检验检测机构专项整治&#xff0c;再到数据真实性专项治理&#xff0c;监管重点正在从结果监管逐步向过程监管延伸。其中&#xff0c;“一单一库”“一报告一溯源”“全过程留痕”等管…

作者头像 李华
网站建设 2026/6/10 2:01:09

6GB显卡本地AI效率提升实战:Ollama服务化+API调用+成本对比

你的GTX 1660Ti正在浪费&#xff01;本地AI让效率提升10倍的正确姿势前言 上周帮团队搭AI辅助编程环境&#xff0c;预算卡得很紧——每人每月API费用不能超过50块。 我算了笔账&#xff1a;GPT-4o每千token约0.15元&#xff0c;一个程序员一天调用50次、每次消耗2000token&…

作者头像 李华
网站建设 2026/6/10 1:56:07

格力空调全国 24小时 售后服务热线人工客服号码上线

格力空调售后服务电话上线400-9918812格力空调售后电话24小时人工电话:守护家庭空气健康,极速响应透明服务环境准备&#xff1a;开启你的代码之旅在正式开始编写代码之前&#xff0c;我们需要明白Python之所以在大数据和人工智能领域长盛不衰&#xff0c;核心在于其“简洁性”。…

作者头像 李华
网站建设 2026/6/10 1:56:02

El-Table 嵌套内容动态必填项校验

1、项目实际需求如上图所示是要实现的业务场景&#xff0c;实现方式是利用el-form自带的校验规则。2、代码实现<template><div id"app"><div class"container"><div class"header"><div><h2>表格动态校验示…

作者头像 李华
网站建设 2026/6/10 1:53:59

新手也能半小时上手,出库单软件的快速部署指南

从零开始&#xff1a;半小时搞定仓库数字化 很多传统档口或小微商户的老板&#xff0c;面对“数字化管理”这个词往往望而却步&#xff0c;总觉得需要请专业 IT 团队、花大价钱买服务器&#xff0c;甚至要学复杂的编程。其实&#xff0c;对于日常的出入库管理&#xff0c;完全不…

作者头像 李华