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

19.redis之缓存击穿

缓存击穿

1.什么是缓存击穿??

缓存击穿,是指一个key "异常火爆"的热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
🧠 记忆钩子(批注):就像防弹衣上破了一个洞,子弹集火穿过这个洞打中数据库。

2.怎样解决?

  • 穿透后线程访问数据库前加一个锁,称为分布式锁

    public Product getProduct(String id) {// === 第一重检查 ===// 刚进来,先看看缓存有没有String value = redis.get(id);if (value != null) {return value;}// 缓存没有,准备抢锁lock.lock();try {System.out.println("我是第一个拿到锁的人,我去查库。");Product p = db.query(id);// 写回 Redisredis.set(id, p);return p;} finally {lock.unlock();}
    }
    

    但是有一种情况:多个线程同时走到访问数据库这一步的话,只有一个线程能够访问,其他线程阻塞,那么当其他线程醒来的话,不也还是会进入访问数据库逻辑吗?

  • 双重检查锁 (Double Check Lock, DCL)

    public Product getProduct(String id) {
    // === 第一重检查 ===
    // 刚进来,先看看缓存有没有
    String value = redis.get(id);
    if (value != null) {return value;
    }// 缓存没有,准备抢锁
    lock.lock();
    try {// === 【重点】第二重检查 (Double Check) ===// 拿到锁之后,必须!必须!再查一次 Redis!// 为什么?因为可能在我排队的时候,前一个人已经把数据填进去了。value = redis.get(id); if (value != null) {System.out.println("好险!前一个人已经查回来了,我直接用,不用去数据库了。");return value;}// === 只有通过了两次检查,才真的去查数据库 ===System.out.println("我是第一个拿到锁的人,我去查库。");Product p = db.query(id);// 写回 Redisredis.set(id, p);return p;} finally {lock.unlock();
    }
    }
    

3.其他锁的缺点

  • 最直觉的想法,就是用 Redis 的 SETNX (Set if Not Exists) 命令。谁设置成功,谁就拿到锁。

    //这里的锁并不是并不是我们需要访问的数据,而是固定的锁,在缓存未命中时便会出发这把锁
    //String userId = 1;未命中,则进入访问数据库阶段,即下述代码// 1. 抢锁
    if (redis.setnx("lock", "1") == 1) {// 2. 抢到了,干活doSomething();  //例如,redis.setnx("userId","ylf",3600),这里是从数据库获取的值// 3. 释放锁redis.del("lock");
    }
    

    ❌ 致命 Bug: 如果代码执行到 doSomething() 的时候,服务器突然断电了,或者程序抛出异常崩了,会发生什么? redis.del("lock") 永远不会被执行! 这就叫死锁。这个 Key 会永远留在 Redis 里,以后任何人都抢不到锁,系统直接瘫痪。
    ✅ 修正方案: 必须给锁加一个“自动过期时间”。哪怕服务器炸了,过 10 秒锁也会自动消失。 (注意:要用原子命令 SET ... NX EX ...,不能分成两行写,否则两行中间断电了也是死锁)

    // 正确:设置锁的同时,指定 10秒 后自动过期
    redis.set("lock", "1", "NX", "EX", 10);
    
  • 解决“误删” (The Wrong Delete)

    现在我们加了过期时间(比如 10 秒)。但新的问题来了。

    ❌ 致命 Bug: 假如线程 A 的业务太复杂,卡顿了 15 秒才做完。
    1. T=0s:A 拿到锁(有效期 10s)。
    2. T=10s:A 还在干活,但锁自动过期了!
    3. T=11s:线程 B 来了,一看没锁,它顺理成章拿到了锁。
    4. T=15s:A 终于干完活了,它执行 redis.del("lock")。重点来了:A 删的是谁的锁?是 B 的!
    5. T=16s:线程 C 来了,一看没锁(被 A 误删了),也拿到了锁。
    • 结果:B 和 C 同时在干活,锁完全失效了。

    ✅ 修正方案: 解铃还须系铃人。谁加的锁,只能由谁来删。 我们在 value 里不存 "1" 了,存一个唯一的 UUID。

      String myUUID = UUID.randomUUID().toString();// 加锁时,把自己的签名 (UUID) 存进去redis.set("lock", myUUID, "NX", "EX", 10);// ... 干活 ...// 释放时,先检查一下:这是不是我签名的锁?if (myUUID.equals(redis.get("lock"))) {redis.del("lock"); // 是我的,才删}
    
  • 解决“原子性” (Atomicity)

    看起来刚才的代码很完美了?不,还有一个极其隐蔽的 Bug。

    ❌ 致命 Bug: 看最后释放锁的那两行代码:
    1. if (myUUID.equals(redis.get("lock"))) <-- 这一步判断通过了。
    2. (就在这毫秒之间,JVM 发生了一次垃圾回收 GC,程序停顿了)
    3. (或者锁刚好在这时候过期了,线程 B 抢到了新锁)
    4. redis.del("lock") <-- A 醒过来,执行删除。完蛋,又把 B 的新锁删掉了!
    因为“判断”和“删除”是两个动作,不是原子的。
    

    ✅ 修正方案: 必须把这两步合并成一步。Redis 自身没有这种命令,所以我们需要用 Lua 脚本(Redis 会把 Lua 脚本作为一个整体执行,中间绝不会被打断)。

    -- 这段脚本是原子的
    if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
    elsereturn 0
    end
    
http://www.jsqmd.com/news/78994/

相关文章:

  • 2025.12.12
  • 极市平台 | NeurlPS‘25开源 | 中科院新作AutoSeg3D:在线分割一切3D物体,超越ESAM!
  • APC001F
  • #题解#洛谷P1120 小木棍#搜索#剪枝
  • 云服务器的核心优势
  • 2025安全婴儿面霜测评:华西珐玛领衔,敏宝护理指南 - 资讯焦点
  • PyCausalSim:基于模拟的因果发现的Python框架
  • 爬youtube视频笔记
  • 使用vscode运行python,解释器为anaconda的虚拟环境,使用pip命令安装库失败解决方案
  • 某游戏大厂的常用面试问题解析:Netty 与 NIO - 指南
  • 软件工程学习日志2025.12.12
  • 云端算力:数字时代的核心引擎与创新基石
  • 家乐事净水器加盟费多少?0加盟费+装修补贴+区域保护,全程扶持解读 - 资讯焦点
  • 病毒学研究的关键工具:重组病毒蛋白的技术解析与应用实践
  • zz深入了解LlamaIndex实现Agent代码和原理
  • 搜维尔科技:Xsens独立项目-面向独立工作室的高端动作捕捉
  • 2025年度活动板房厂家TOP5实力评测:资质口碑场景适配全维度选型指南 - 资讯焦点
  • 解码智能指针
  • 毕业设计实战:基于SSM+MySQL的药店管理系统设计与实现,从需求到测试轻松通关!
  • linux: gdb调试器
  • 6 个最佳开源 AI 仪表盘工具
  • 红队日记 --- W1R3S
  • 深夜炸场!GPT-5.2发布;Meta被曝用阿里千问优化新模型;马斯克点赞腾讯游戏业务:他们的品味非常好 | 极客头条
  • 毕业设计实战:基于SSM+MySQL的图书商城管理系统设计与实现,从需求到测试全流程拆解,新手也能轻松通关!
  • springboot基于vue的承德市养老院智慧健康检测系统_ 用药提醒 一键呼叫 1f16uvca
  • Kate 高级文本编辑器 v26.03.70 官方中文版
  • 毕业设计实战:基于Java+MySQL的校园二手书交易平台设计与实现,从需求到上线全流程避坑指南!
  • java <T> 是什么?
  • Python 面向对象核心概念梳理
  • 毕业设计实战:SpringBoot教学资料管理系统,从0到1完整开发指南