缓存击穿、缓存雪崩、缓存穿透以及数据库缓存双写不一致问题

在项目中,我们所需要的数据通常存储在数据库中,但是数据库的数据保存在硬盘上,硬盘的读写操作很慢,为了避免直接访问数据库,我们可以使用 Redis 作为缓存层,缓存通常存储在内存中,内存的读写速度远超硬盘‌,而引入缓存层后,会出现三种缓存异常问题:缓存穿透缓存雪崩缓存击穿,以及数据库与缓存的双写不一致问题。

1.缓存击穿(Cache Penetration)

定义:缓存击穿是指一个设置了过期时间的缓存项在过期之后,恰好有很多并发请求访问这个过期数据,这些请求发现缓存中没有值后,会去查询数据库,引起数据库压力骤增。

解决方案

互斥锁:在访问缓存的代码块中,使用互斥锁(如 Redis 的 SETNX 命令)来保证在缓存失效的瞬间只有一个请求能查询数据库并更新缓存,其他请求则在锁释放后从缓存中获取数据。(这篇博客有讲:如何通过redis实现分布式锁)。

永久缓存:对于某些不经常改变的数据,可以考虑设置为永久缓存,这样就不会有击穿的问题。

同步锁:使用synchronized加锁排队,通过双重检查锁(DCL)即在进入synchronized前查询一次redis,进入后再查询一次,防止上一个抢到锁的线程已经更新过redis(适用于仅查询的场景,如果要进行更新操作就不太适用该方法,因为synchronized不是分布式锁,在多台服务器上操作可能会导致数据不一致问题)。

String key = "product:" + id;
//先查一次缓存
String data = redisTemplate.opsForValue().get(key);
if (data != null) {//如果查到直接返回return data;
}
synchronized (this) {// 再次检查缓存,防止当很多请求到这里直接访问数据库data = redisTemplate.opsForValue().get(key);if (data != null) {return data;}// 查询数据库data = userMapper.queryFromDatabase(id);if (data != null) {// 将数据存入缓存redisTemplate.opsForValue().set(key,data);}
}

2.缓存雪崩(Cache Avalanche)

定义:缓存雪崩是指缓存在同一时间大面积的失效,这时又来了一波请求,所有的请求都去查数据库,造成数据库压力瞬间过大,宕机,结果是缓存和数据库都挂了,就像雪崩一样。(缓存集中过期或者服务器宕机都会造成雪崩)

解决方案

设置过期时间的随机性:给缓存的过期时间增加一个随机值,避免集体失效。(可通过random随机生成)

使用集群:通过增加机器来分散压力或者哨兵模式避免单一节点压力过大。

限流降级:在应用层面对请求进行限流,或者在系统负载过高时进行服务降级。

服务熔断:使用熔断机制,当检测到系统负载过高时,自动熔断一部分服务,减轻系统压力。

3. 缓存穿透(Cache Bypass)

定义:缓存穿透是指查询一个一定不存在的数据,由于缓存不命中后,还需要去查询数据库,引起大量请求直接穿透到数据库,给数据库造成巨大压力,甚至宕机。

解决方案

布隆过滤器:布隆过滤器可以快速判断一个元素是否存在于某个集合中,可以用来过滤掉不存在的数据请求。

基于redisson实现布隆过滤器示例代码:

@Autowired
private RedissonClient redissonClient;
​
public RBloomFilter<String> initBloomFilter() {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("productBloomFilter");// 预期元素数量为 100000,误判率 1%bloomFilter.tryInit(100000L, 0.01);// 预热数据(示例:从数据库加载所有合法ID)List<Long> productIds = orderService.getAllProductIds();productIds.forEach(id -> bloomFilter.add("product_" + id));return bloomFilter;}

空值缓存:如果一个查询返回的数据为空(null),也将其存储到缓存中,设置一个较短的过期时间,例如几分钟。这样再次请求相同数据时会快速响应,不会每次都去查询数据库。

 public String getData(Long id) {String key = "product:" + id;// 先从缓存中获取数据String data = redisTemplate.opsForValue().get(key);if (data == null) {return null;}else{return data;}// 查询数据库data = userMapper.queryFromDatabase(id);if (data == null) {// 将空值存入缓存redisTemplate.opsForValue().set(key,null);} else {// 将数据存入缓存redisTemplate.opsForValue().set(key,data);}return data;
}

参数校验:提前校验一些数据库中没有的数据。

4.数据库缓存双写不一致问题

数据库和缓存双写不一致‌是指在多用户并发操作中,数据库和缓存中的数据未能保持一致的状态。这种情况通常发生在以下场景:

1‌)先更新数据库再更新缓存‌如果在更新数据库后,更新缓存之前发生故障,缓存中的数据会与数据库中的数据不一致。此外,多个并发请求可能导致缓存被多次更新,而数据库中的数据未能及时更新,从而产生不一致‌。

2)先更新缓存再更新数据库‌:如果在更新缓存后,更新数据库之前发生故障,数据库中的数据会与缓存中的数据不一致。多个并发请求可能导致缓存被覆盖,而数据库中的数据未能及时更新,从而产生不一致‌。

解决方案:为了解决数据库和缓存双写不一致的问题,有以下几种常见的解决方案

1.先更新数据库再删除缓存‌:这种方法避免了缓存和数据库之间的数据不一致问题。更新数据库后删除缓存,下次读取时会从数据库中重新加载最新数据到缓存中。

缺点:然而,在高并发情况下,可能会在删除缓存和更新数据库之间出现短暂的数据不一致,可以通过设置缓存过期时间或使用缓存更新策略来减少这种情况的发生‌。

 @Transactional//保证事物原子性public void updateUser(User user) {// 先更新数据库userMapper.update(user);// 再删除缓存String key = "user:" + user.getId();redisTemplate.delete(key);}

2‌.异步更新缓存‌:通过消息队列来实现异步更新缓存。数据库更新完成后,将操作命令放入消息队列,由缓存系统消费这些命令进行更新。这种方法可以保证数据操作顺序一致性,确保缓存系统的数据正常‌。

缺点:引入了额外的中间件,增加了系统的复杂度和维护成本。

@Transactional
public void updateUser(User user) {// 先更新数据库userMapper.update(user);// 发送消息到消息队列rabbitTemplate.convertAndSend("workExchange", "workRoutingKey", user);
}
​
​
@RabbitListener(queues = "workExchange")
public void handleCacheUpdate(User user) {// 删除缓存String key = "user:" + user.getId();redisTemplate.delete(key);
}

3.延迟双删策略:先删除缓存,再更新数据库,然后在一段时间后再次删除缓存 ,以确保在更新数据库和删除缓存之间读取到旧缓存数据的线程在后续操作中也能获取到最新数据。

第二次删除缓存的原因:

为了解决读写并发请求导致数据不一致的情况,所以要再删除一次缓存(延迟)。

如果不延迟,可能存在如下情况:

  •  写请求:删除缓存
  •  读请求:缓存未命中、读取数据库的值20
  •  写请求:更新数据库的值为21
  •  写请求:第二次删除缓存
  •  读请求:更新缓存值为20还是会导致数据不一致,所以第二次删除缓存需要延迟一段时间,直到 并发的读请求都已经将旧值缓存好这时候再去删除,可以删除掉旧的缓存值。

需要延迟多久这个时间非常不好设定,因为我们不知道并发的读请求写入缓存什么时候能够结束,能够保证第二次删除缓存是能够删除旧值,经验值一般设置为500ms-1s, 如果发生了并发读请求,那么在这段时间内数据是不一致的,外界读取的是旧值第二次删除就一定能成功吗。

如果第二次删除失败了怎么办,会导致长时间的数据不一致,所以还是要引入重试机制(消息队列重试)

缺点:但是需要额外设置延迟时间,若时间设置不合理,可能仍会出现数据不一致的情况;增加了系统的处理时间,降低了系统的响应性能。

@Transactional
public void updateUser(User user) {// 先更新数据库userMapper.update(user);// 再删除缓存String key = "user:" + user.getId();redisTemplate.delete(key);// 延迟一段时间后再次删除缓存new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500);redisTemplate.delete(key);} catch (InterruptedException e) {e.printStackTrace();}}).start();
}

4.基于数据库的 Binlog 同步:借助数据库的 Binlog(二进制日志)来捕捉数据库的变更信息,再通过中间件将这些变更信息同步到 Redis 中。

操作步骤

  1.  开启数据库的 Binlog 功能。
  2. 利用 Canal(以 MySQL 为例)等中间件监听数据库的 Binlog 变化。
  3. 中间件将捕获到的变更信息发送给处理程序。
  4. 处理程序根据变更信息更新 Redis 中的缓存数据。 

缺点:引入了额外的中间件,增加了系统的复杂度和维护成本;需要处理中间件的高可用和数据传输的稳定性问题。

5.基于读写锁:读写锁允许多个读操作同时进行,但在写操作时会阻塞其他读操作和写操作,确保在同一时间内只有一个写操作可以访问资源,以此来保证数据的完整性和一致性。

缺点:在一些复杂的业务场景下,可能存在多个数据源之间的数据依赖和更新顺序问题,如果不同数据源之间的更新顺序没有正确处理,仍然可能导致数据不一致。。

6.使用分布式锁‌:在更新操作时使用分布式锁,确保同一时间只有一个请求可以操作缓存和数据库,从而避免数据不一致的问题‌。 (这篇博客有讲:如何通过redis实现分布式锁)

缺点:增加了系统的复杂度和性能开销;分布式事务的实现和维护难度较大。

7.消息队列重试

  1. 在代码里,更新MySQL数据,同步删除缓存,如果删除失败,则发送一个消息队列的删除缓存的消息
  2. 再由一个消费者监听对应的消息,将缓存数据删除
  3. 如果删除失败,触发重试机制,重试删除。如果重试超过一定次数,则需要记录异常且告警。

缺点:该方法对代码入侵性比较强,且引入了消息队列,提升了系统的复杂性,提高了服务的风险性。

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

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

相关文章

可灵2.1 vs Veo 3:AI视频生成谁更胜一筹?

在Google发布Veo 3几天后,可灵显然感受到了压力,发布了即将推出的视频模型系列可灵 2.1的早期体验版。 据我了解,有三种不同的模式: 可灵 2.1 标准模式: 720p分辨率 仅支持图像转视频(生成更快,一致性更好) 5秒视频仍需20积分 可灵 2.1 专业模式: 1080p分辨率 仅在图…

解决Docker存储空间不足问题

虚拟机磁盘扩展实战&#xff1a;解决Docker存储空间不足问题 问题背景 在虚拟机中运行的Linux系统上&#xff0c;Docker服务因根分区空间不足而无法正常运行。初始状态如下&#xff1a; [rootlocalhost ~]# df -h / 文件系统 容量 已用 可用 已用% 挂载点…

Redis 中如何保证缓存与数据库的数据一致性?

在 Redis 中保证缓存与数据库的数据一致性&#xff0c;需结合业务场景选择以下策略&#xff1a; 核心策略总结 Cache Aside&#xff08;旁路缓存&#xff09;模式 读操作&#xff1a;先查缓存&#xff0c;未命中则查数据库并写入缓存。写操作&#xff1a;先更新数据库&#xf…

晶振频率稳定性:5G 基站与航天设备的核心竞争力

在当今科技飞速发展的时代&#xff0c;电子设备的性能和可靠性至关重要。晶振作为电子设备中的核心部件&#xff0c;为系统提供精确的时间和频率基准。晶振的频率稳定性直接影响着设备的整体性能&#xff0c;从日常生活中广泛使用的智能手机、智能穿戴设备&#xff0c;到对精度…

PDFGear——完全免费且功能强大的PDF处理软件

关键词 &#xff1a;PDFGear、免费、跨平台、多功能、OCR 概要 &#xff1a;PDFGear是一款完全免费且功能强大的PDF处理软件&#xff0c;支持Windows、macOS、iOS和Android等多平台使用。它集PDF阅读、编辑、格式转换、OCR识别及AI智能助手于一体&#xff0c;满足用户多样化文档…

【笔记】在 MSYS2(MINGW64)中正确安装 Rust

#工作记录 1. 环境信息 Windows系统: MSYS2 MINGW64当前时间: 2025年6月1日Rust 版本: rustc 1.87.0 (17067e9ac 2025-05-09) (Rev2, Built by MSYS2 project) 2. 安装步骤 步骤 1: 更新系统包数据库并升级已安装的包 首先&#xff0c;确保我们的 MSYS2 系统是最新状态。打…

WIN11+VSCODE搭建的c/c++环境调试报错解决

解决调试报错 前面win11vscode搭建的c/c环境&#xff0c;ctrlshiftB生成正常&#xff0c;cttlF5运行正常。今天打断点逐步调试时报错&#xff0c;提示找不到库文件。解决方案如下&#xff1a; 下载mingw-w64源码库&#xff1a;&#xff08;两种途径&#xff09; 通过MSYS2 UC…

React项目在ios和安卓端要做一个渐变色背景,用css不支持,可使用react-native-linear-gradient

以上有个模块是灰色逐渐到白的背景色过渡 如果是css&#xff0c;以下代码就直接搞定 background: linear-gradient(180deg, #F6F6F6 0%, #FFF 100%);但是在RN中不支持这种写法&#xff0c;那应该写呢&#xff1f; 1.引入react-native-linear-gradient插件&#xff0c;我使用的是…

android-studio-2024.3.2.14如何用WIFI连接到手机(给数据线说 拜拜!)

原文&#xff1a;Android不用数据线就能调试真机的方法—给数据线说 拜拜&#xff01;&#xff08;adb远程调试&#xff09; android-studio-2024.3.2.14是最新的版本&#xff0c;如何连接到手机&#xff0c;可用WIFI&#xff0c;可不用数据线&#xff0c;拜拜 第一步&#xf…

【前端】JS引擎 v.s. 正则表达式引擎

JS引擎 v.s. 正则表达式引擎 它们的转义符都是\ 经过JS引擎会进行一次转义 经过正则表达式会进行一次转义在一次转义中\\\\\的转义过程&#xff1a; 第一个 \ (转义符) 会“吃掉”第二个 \&#xff0c;结果是得到一个字面量的 \。 第三个 \ (转义符) 会“吃掉”第四个 \&#x…

ReactHook有哪些

React 中常用的 Hooks 列表及用法 React Hooks 是 React 16.8 版本引入的一项重要特性&#xff0c;它极大地简化和优化了函数组件的开发过程。以下是 React 中常用的 Hooks 列表及其详细用法&#xff1a; 1. useState useState 是用于在函数组件中添加状态的 Hook。通过调用…

【PyQt5】PyQt5初探 - 一个简单的例程

PyQt5初探 - 一个简单的例程 引言一、安装配置二、使用2.1 PyQt5简单例程2.2 与c Qt深入对比 三、相关教程 引言 PyQt5是一个比较流行的Python图形用户界面(GUI)库&#xff0c;它基于Qt库&#xff08;一个跨平台的C库&#xff0c;用于开发应用程序的图形界面&#xff09;为Pyt…

图文详解Java并发面试题

文章目录 1、并发与并行2、线程安全3、线程、进程、协程4、线程间通信5、线程创建方式6、8G内存创建的线程数7、普通Java程序含有的线程8、start()、run()9、线程调度、6种状态、强制停止线程、上下文切换10、守护线程、用户线程11、 volatile 、synchronized12、sleep() 、 wa…

飞牛fnNAS存储空间模式详解

目录 一、NAS的存储空间 二、多硬盘对NAS速度的提升原理 三、多硬盘对数据安全的提升原理 四、多硬盘对容量的提升原理 五、磁盘阵列模式 六、飞牛NAS支持的存储模式 七、具体如何选择存储空间模式 在数字化时代,数据是个人和企业发展的核心资产,但面临硬盘损坏、病毒…

OpenCv高阶(二十)——dlib脸部轮廓绘制

文章目录 一、人脸面部轮廓绘制代码实现1、定义绘制直线段的函数2、定义绘制凸包轮廓的函数3、读取输入图像4、初始化dlib的人脸检测器5、使用检测器在图像中检测人脸&#xff08;参数0表示不进行图像缩放&#xff09;6、加载dlib的68点人脸关键点预测模型7、遍历检测到的每个人…

WEBSTORM前端 —— 第3章:移动 Web —— 第3节:移动适配

目录 一、移动Web基础 1.谷歌模拟器 2.屏幕分辨率 3.视口 4.二倍图 二、适配方案 三、rem 适配方案 四、less 1.less – 简介 2.less – 注释 3.less – 运算 4.less – 嵌套 5.less – 变量 6.less – 导入 7.less – 导出 8.less – 禁止导出 五…

Altium Disigner(16.1)学习-原理图绘制以及必要操作

一、下载软件 通过网盘分享的文件&#xff1a;Altium Designer 16.zip 链接: https://pan.baidu.com/s/1uBHeoJJ-iA2tXw3NRjCcdA?pwd7c3h 提取码: 7c3h 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 --来自百度网盘超级会员v5的分享 二、建立工程 添加proje…

AI炼丹日志-25 - OpenAI 开源的编码助手 Codex 上手指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇&#xff1a; MyBatis 更新完毕目前开始更新 Spring&#xff0c;一起深入浅出&#xff01; 大数据篇 300&#xff1a; Hadoop&…

Redis:安装与常用命令

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;Redis &#x1f525; 安装 Redis 使⽤apt安装 apt install redis -y⽀持远程连接 修改 /etc/redis/redis.conf 修改 bind 127.0.0.1 为 bind 0.0.0.0 修改 protected-mode yes 为 protected-mo…

02 APP 自动化-Appium 运行原理详解

环境搭建见 01 APP 自动化-环境搭建 文章目录 一、Appium及Appium自动化测试原理二、Appium 自动化配置项三、常见 ADB 命令四、第一个 app 自动化脚本 一、Appium及Appium自动化测试原理 Appium 跨平台、开源的 app 自动化测试框架&#xff0c;用来测试 app 应用程序&#x…