缓存雪崩、击穿、穿透全中招?别让缓存与数据库的“爱恨情仇”毁了你的系统!

你有没有经历过这样的深夜告警:Redis 响应延迟飙升,数据库 CPU 直冲 100%,接口大面积超时?一查日志,发现大量请求绕过缓存直怼数据库——典型的缓存击穿 + 穿透组合拳。更惨的是,修复后数据对不上了:用户看到的订单状态是“已支付”,数据库里却是“待支付”。

这不是 bug,这是缓存与数据库一致性失控的灾难现场

作为在高并发系统里摸爬滚打多年的老兵,“北风朝向”可以负责任地告诉你:缓存不是银弹,用不好就是定时炸弹。今天我们就来直面这个让无数架构师夜不能寐的问题——如何真正解决缓存与数据库的一致性问题


一致性难题的本质:异步世界的同步幻想

我们总希望缓存和数据库“同时更新、永不掉队”。但现实很骨感:

  • 数据库是持久化权威源(Source of Truth)
  • 缓存是易失性加速层(Speed Layer)
  • 两者更新必然存在时间窗口,哪怕只有几毫秒

在这个窗口内,若发生并发读写或异常中断,就会出现:

  • 脏读:读到旧缓存
  • 空穿透:缓存失效后大量请求打到 DB
  • 中间态暴露:先删缓存还是先改 DB?顺序错了就出事

要破局,必须从更新策略、异常处理、重试机制、兜底方案四维出击。


❌ 坑1:先更新数据库,再删除缓存 —— 看似合理,实则埋雷

这是最常见也最容易出问题的做法。你以为很安全?

@Service
public class OrderService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate OrderMapper orderMapper;// ❌ 错误示范:先更新DB,再删缓存@Transactionalpublic void updateOrderStatus(Long orderId, String status) {// 1. 更新数据库orderMapper.updateStatus(orderId, status);// 2. 删除缓存(假设 key 是 "order:123")redisTemplate.delete("order:" + orderId);}
}
问题在哪?看这个并发场景:
ClientAClientBDBCache更新DB (status=PAID)删除缓存时间T1查询缓存 → MISS查询DB → 得到 PAID写入缓存(status=PAID)时间T2,在A删除之后、写入之前(无操作)T2 < T1+Δ,缓存又被写回旧值!ClientAClientBDBCache

看到了吗?ClientB 在 A 删除缓存后、事务提交前读到了“中间状态”的数据并回填缓存,导致缓存中仍然是旧值!这就是经典的缓存不一致窗口期问题


✅ 解法1:延时双删 + 删除重试,堵住时间窗漏洞

既然无法完全避免窗口期,那就主动延长观察期,并二次清理。

@Service
public class OrderService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate ExecutorService asyncExecutor; // 自定义线程池// ✅ 改进版:延时双删@Transactionalpublic void updateOrderStatusSafe(Long orderId, String status) {// 第一次删除缓存deleteCache(orderId);// 更新数据库orderMapper.updateStatus(orderId, status);// 异步延时第二次删除(如500ms后)asyncExecutor.submit(() -> {try {Thread.sleep(500); // 可配置为动态值deleteCache(orderId);} catch (InterruptedException e) {Thread.currentThread().interrupt();}});}private void deleteCache(Long orderId) {redisTemplate.delete("order:" + orderId);}
}

🔍 关键点解析

  • 第一次删:防止后续请求命中旧缓存
  • 延时双删:给可能在此期间写入缓存的查询留出时间,再删一遍
  • 异步执行:不影响主流程性能

但这还不够健壮——如果删除失败怎么办?


✅ 解法2:基于消息队列的最终一致性保障

当业务复杂度上升,建议引入消息中间件(如 Kafka/RocketMQ),将“缓存操作”解耦为异步任务。

@Service
public class OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate KafkaTemplate<String, String> kafkaTemplate;// ✅ 使用MQ实现最终一致性@Transactionalpublic void updateOrderStatusWithMQ(Long orderId, String status) {// 1. 更新数据库orderMapper.updateStatus(orderId, status);// 2. 发送消息通知缓存更新String message = buildDeleteCacheMessage(orderId);kafkaTemplate.send("cache-invalidate-topic", "order:" + orderId, message);}private String buildDeleteCacheMessage(Long orderId) {return "{\"type\":\"DELETE\",\"key\":\"order:" + orderId + "\"}";}
}// 消费者服务(独立部署)
@Component
public class CacheInvalidateConsumer {@KafkaListener(topics = "cache-invalidate-topic")public void consume(String message) {try {// 解析消息并删除缓存deleteCacheFromMessage(message);} catch (Exception e) {// 记录失败日志,进入死信队列或重试机制log.error("缓存删除失败,加入重试队列", e);retryLater(message); // 可放入 Redis ZSet 按时间重试}}private void retryLater(String message) {// 实现指数退避重试逻辑}
}

优势

  • 解耦业务逻辑与缓存操作
  • 失败可重试,保证最终一致性
  • 易于扩展为多级缓存同步

⚠️ 注意:需处理消息重复消费问题(幂等性)


❌ 坑2:缓存穿透 —— 黑客最爱的攻击方式

当恶意请求查询不存在的数据时,每次都会击穿缓存直达数据库。

// ❌ 危险代码:未处理空值
public Order getOrder(Long orderId) {String key = "order:" + orderId;// 1. 先查缓存Order order = (Order) redisTemplate.opsForValue().get(key);if (order != null) {return order;}// 2. 查数据库order = orderMapper.selectById(orderId);if (order != null) {redisTemplate.opsForValue().set(key, order, Duration.ofMinutes(10));}// else 不做任何处理 → 下次还得查DB!return order;
}

攻击者只需遍历 orderId=99999999 这类无效ID,就能轻松压垮数据库。


✅ 解法3:布隆过滤器 + 空值缓存,双重防护

方案一:布隆过滤器前置拦截
@Component
public class BloomFilterCacheService {private BloomFilter<String> bloomFilter;@PostConstructpublic void init() {// 初始化布隆过滤器(可通过后台任务定期加载所有有效ID)Set<String> allOrderIds = orderMapper.selectAllIds().stream().map(String::valueOf).collect(Collectors.toSet());bloomFilter = BloomFilter.create(Funnels.stringFunnel(), allOrderIds.size(), 0.01); // 误判率1%allOrderIds.forEach(bloomFilter::put);}public boolean mightExist(Long orderId) {return bloomFilter.mightContain(String.valueOf(orderId));}
}@Service
public class OrderService {@Autowiredprivate BloomFilterCacheService bloomFilter;public Order getOrderWithBloom(Long orderId) {// 1. 布隆过滤器快速判断if (!bloomFilter.mightExist(orderId)) {return null; // 绝对不存在}// 2. 正常走缓存 → DB流程return getOrderFromCacheOrDB(orderId);}
}
方案二:空值缓存(Null Value Caching)
// ✅ 对查询为空的结果也进行缓存(短 TTL)
public Order getOrderSafe(Long orderId) {String key = "order:" + orderId;Order order = (Order) redisTemplate.opsForValue().get(key);if (order != null) {return order;}// 缓存缺失,查数据库order = orderMapper.selectById(orderId);if (order != null) {redisTemplate.opsForValue().set(key, order, Duration.ofMinutes(10));} else {// 🔐 即使为空也缓存,防止穿透redisTemplate.opsForValue().set(key, NULL_PLACEHOLDER, Duration.ofMinutes(2));}return order;
}

📌 建议组合使用:Bloom Filter + 空值缓存,既高效又安全。


❌ 坑3:缓存雪崩 —— 大量Key同时过期

当缓存集群重启或大批热点Key在同一时间过期,瞬间海量请求涌向数据库。

// ❌ 所有缓存都设置固定过期时间
redisTemplate.opsForValue().set("order:123", order, Duration.ofHours(1)); // 都是1小时

一旦这些Key集中失效,后果不堪设想。


✅ 解法4:随机过期时间 + 多级缓存 + 热点探测

// ✅ 设置带随机偏移的过期时间
public void setCacheWithRandomExpire(String key, Object value) {// 基础TTL:1小时long baseSeconds = 3600;// 随机增加0~1800秒(0~30分钟)long randomExtra = ThreadLocalRandom.current().nextLong(0, 1800);Duration expire = Duration.ofSeconds(baseSeconds + randomExtra);redisTemplate.opsForValue().set(key, value, expire);
}

💡 更进一步:

  • 使用 本地缓存(Caffeine)+ Redis 构成多级缓存
  • 对热点数据启用永不过期 + 后台异步刷新
  • 结合监控系统自动识别并保护热点Key

总结:一致性保障的四大黄金法则

策略推荐场景关键要点
延时双删简单系统、低频更新控制延迟时间,避免过度影响性能
消息队列异步更新中大型系统保证消息幂等、支持失败重试
布隆过滤器 + 空值缓存防穿透标配Bloom Filter 定期重建
随机过期 + 多级缓存防雪崩核心热点数据特殊对待

最后的忠告:没有强一致,只有最终一致

请记住:在分布式环境下,缓存与数据库不可能做到实时强一致。我们的目标不是消灭延迟,而是控制不一致的时间窗口,使其对业务无感

当你设计缓存策略时,不妨问自己三个问题:

  1. 如果用户读到的是5秒前的数据,会影响核心流程吗?
  2. 如果缓存短暂不一致,能否通过补偿任务修复?
  3. 是否有监控能及时发现异常并告警?

真正的高手,不是追求理论完美,而是在可用性、一致性、性能之间找到最优平衡点

下次再遇到缓存问题,别急着甩锅Redis——先看看自己的代码,是不是又忘了“删缓存”?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/97949.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/97949.shtml
英文地址,请注明出处:http://en.pswp.cn/web/97949.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于 Python charm 库实现的一些 Pairing 密码学算法

基于 Python charm 库实现了一些 Pairing 密码学算法&#xff0c;放在了 https://github.com/BatchClayderman/Cryptography-Schemes 里面。 在正确部署了 Python charm 库后&#xff0c;所有的 Python 脚本都是独立的&#xff0c;即该存储库中不存在一个脚本调用另一个脚本的…

用户体验五大要点:从问题到解决方案的完整指南

在互联网产品设计和运营的过程中&#xff0c;用户体验&#xff08;User Experience&#xff0c;简称 UX&#xff09; 已经成为决定产品成败的关键因素。一个功能再强大的产品&#xff0c;如果用户用得不舒服、不信任&#xff0c;甚至觉得没有价值&#xff0c;最终都会被抛弃。那…

MySQL 外键约束:表与表之间的 “契约”,数据一致性的守护者

MySQL 外键约束&#xff1a;表与表之间的 “契约”&#xff0c;数据一致性的守护者 在 MySQL 数据库设计中&#xff0c;外键约束&#xff08;FOREIGN KEY&#xff09;是维护表之间关联关系的核心工具。它就像表与表之间的一份 “契约”&#xff0c;确保从表&#xff08;如订单…

《投资-54》元宇宙

元宇宙&#xff08;Metaverse&#xff09;是一个近年来备受关注的概念&#xff0c;它描绘了一个虚拟与现实交融、由多个互连的3D虚拟世界组成的沉浸式数字环境。用户可以通过虚拟现实&#xff08;VR&#xff09;、增强现实&#xff08;AR&#xff09;、互联网和其他技术&#x…

【数据结构】Java集合框架:List与ArrayList

文章目录一、认识List接口1.1 List的定义与继承关系1.2 Collection接口的核心方法1.3 List接口的独特方法二、线性表与顺序表基础2.1 线性表2.2 顺序表自定义顺序表&#xff08;MyArrayList&#xff09;实现1. 前期准备&#xff1a;自定义异常类2. MyArrayList核心结构3. 工具方…

K8S里的“豌豆荚”:Pod

1. 为什么要有podPod 这个词原意是“豌豆荚”&#xff0c;后来又延伸出“舱室”“太空舱”等含义&#xff0c;你可以看一下这张图片&#xff0c;形 象地来说 Pod 就是包含了很多组件、成员的一种结构。之前的容器技术让进程在一个“沙盒”环境里运行&#xff0c;具有良好的隔离…

vue3 基本教程-运行一个最小demo

Vue 3 基本教程 - 运行一个最小 Demo 1. 创建项目 使用 Vue 官方脚手架工具创建一个新项目&#xff1a; # 安装 Vue CLI (如果尚未安装) npm install -g vue/cli# 创建一个新项目 vue create vue3-demo# 选择 Vue 3 预设 # 使用方向键选择 "Default (Vue 3)" 然后按 …

大数据新视界 -- Hive 集群搭建与配置的最佳实践(2 - 16 - 13)

💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的博客,正是这样一个温暖美好的所在。在这里,你们不仅能够收获既富有趣味又极为实…

C/C++ 转 Java 的数据结构初阶对比指南

一、先遣了解和回顾1、预览快速对比表格数据结构​​​​C/C 实现​​​​Java 实现​​​​关键区别​​​​数组​​int arr[5];int[] arr new int[5];语法类似&#xff0c;Java 数组是对象​​动态数组​​vector<int> v;ArrayList<Integer> list new ArrayLi…

长连接和短连接

在网络通信中&#xff0c;长连接&#xff08;Long Connection&#xff09;和短连接&#xff08;Short Connection&#xff09;是两种核心的连接管理策略&#xff0c;其区别主要体现在连接生命周期、资源占用和适用场景上。以下是两者的详细解析&#xff1a;一、核心概念对比特性…

Java:使用spring-cloud-gateway的应用报DnsNameResolverTimeoutException原因和解决方法

使用spring-cloud-gateway时&#xff0c;有时会报DnsNameResolverTimeoutException异常。堆栈信息类似&#xff1a;Caused by: java.net.UnknownHostException: Failed to resolve cloudconnector.linkup-sage.comat io.netty.resolver.dns.DnsResolveContext.finishResolve(Dn…

SpringCloud概述

目录 一、概念 1.1 微服务架构 1.2 SpringCloud概念 1.3 核心价值 1.4 能力边界 1.5 微服务总体架构图 二、生态圈 2.1 不同生态圈组件对比 2.2 组件介绍 2.2.1 服务发现与注册 2.2.2 配置管理 2.2.3 API网关 2.2.4 容错与熔断 2.2.5 客户端负载均衡 2.2.6 服务…

光伏电站环境监测仪—专为光伏电站设计的气象监测设备

光伏电站环境监测仪是专为光伏电站设计的气象监测设备&#xff0c;通过实时采集关键环境参数&#xff0c;为光伏系统的发电效率评估、运维决策和安全预警提供数据支撑。监测参数太阳辐射采用高精度总辐射表&#xff0c;测量水平面总辐射和倾斜面辐射&#xff0c;精度达 2% 以内…

Node.js ≥ 18 安装教程

Windows 安装 下载安装包&#xff1a;访问 Node.js官网&#xff0c;下载最新的 LTS 版本&#xff08;确保版本 ≥ 18&#xff09;运行安装程序&#xff1a;双击下载的安装文件&#xff0c;按照向导完成安装验证安装&#xff1a;打开命令提示符或PowerShell&#xff0c;输入以下…

电脑 hdmi 没有声音问题解决

问题现象&#xff1a;电脑耳机声音正常输出&#xff0c;使用hdmi连接电视后&#xff0c;没有声音输出。&#xff08;正常会通过hdmi 在电视上播放视频和声音&#xff09;解决方案:出现该情况很可能原因是 显卡的驱动不对。网上找了各种方法都没有解决&#xff0c;最后系统升级后…

学习日记-XML-day55-9.14

1.xml基本介绍知识点核心内容重点XML定义可扩展标记语言&#xff0c;用于数据存储和传输与HTML的区别&#xff08;HTML用于展示&#xff0c;XML用于结构化数据&#xff09;XML用途1. 配置文件&#xff08;Spring的beans.xml、Tomcat的server.xml&#xff09;;2. 数据交换&#…

【系统架构设计(27)】信息安全技术集成

文章目录一、本文知识覆盖范围二、信息安全基础要素详解1、机密性保障技术2、完整性验证技术3、可用性保障技术4、可控性管理技术5、可审查性追溯技术三、网络安全威胁与防护策略1、非授权访问防护2、拒绝服务攻击防护3、恶意软件传播防护四、加密技术体系与应用1、对称加密技术…

什么是 SaaS 安全?

什么是 SaaS 安全&#xff1f; SaaS 安全专注于保护云中的数据、应用程序和用户身份。它旨在应对基于云的软件所面临的挑战&#xff0c;以确保信息的安全性和可用性。SaaS 安全致力于降低未授权访问、数据泄露等风险&#xff0c;同时增强 SaaS 应用程序的安全性。 SaaS 安全不仅…

mysql和postgresql如何选择

h5打开以查看 简单来说&#xff1a; MySQL&#xff1a;更像是一个“快速、可靠的工匠”&#xff0c;注重速度、简单和稳定性&#xff0c;尤其在读操作密集的Web应用中是经典选择。 PostgreSQL&#xff1a;更像是一个“功能强大的学者”&#xff0c;追求功能的完备性、标准的符…

Redis最佳实践——安全与稳定性保障之数据持久化详解

Redis 在电商应用的安全与稳定性保障之数据持久化全面详解一、持久化机制深度解析 1. 持久化策略矩阵策略触发方式数据完整性恢复速度适用场景RDB定时快照分钟级快容灾备份/快速恢复AOF实时追加日志秒级慢金融交易/订单关键操作混合模式RDBAOF同时启用秒级中等高安全要求场景无…