Redis缓存问题(穿透, 击穿, 雪崩, 污染, 一致性)

目录

1.什么是Redis缓存问题?

2.缓存穿透

3.缓存击穿

4.缓存雪崩

5.缓存污染(或满了)

   5.1 最大缓存设置多大

   5.2 缓存淘汰策略

6.数据库和缓存一致性

   6.1 4种相关模式

   6.2 方案:队列+重试机制

   6.3 方案:异步更新缓存(基于订阅binlog的同步机制)


1.什么是Redis缓存问题?

在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问Mysql等数据库。这样可以大大缓解数据库的压力。

当缓存库出现时,必须要考虑如下问题:

  • 缓存穿透
  • 缓存穿击
  • 缓存雪崩
  • 缓存污染(或者满了)
  • 缓存和数据库一致性

2.缓存穿透

什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

举个例子:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。也就是说这些请求最终都落到了数据库上,对数据库造成了巨大的压力。

有什么解决办法吗?

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效 key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下:SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

如果用 Java 代码展示的话,差不多是下面这样的:

public Object getObjectInclNullById(Integer id) {// 从缓存中获取数据Object cacheValue = cache.get(id);// 缓存为空if (cacheValue == null) {// 从数据库中获取Object storageValue = storage.get(key);// 缓存空对象cache.set(key, storageValue);// 如果存储数据为空,需要设置一个过期时间(300秒)if (storageValue == null) {// 必须设置过期时间,否则有被攻击的风险cache.expire(key, 60 * 5);}return storageValue;}return cacheValue;
}

2)布隆过滤器

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,是不是感觉布隆过滤器就是我们想要找的那个正确答案?。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

加入布隆过滤器之后的缓存处理流程图如下。

但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!

我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)

3.缓存击穿

什么是缓存击穿?

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

举个例子:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。

有什么解决办法吗?

  • 设置热点数据永不过期或者过期时间比较长。
  • 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  • 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。

缓存穿透和缓存击穿有什么区别?

缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。 

4.缓存雪崩

什么是缓存雪崩?

实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。

举个例子:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。

有哪些解决办法?

针对 Redis 服务不可用的情况:

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 限流,避免同时处理大量的请求。

针对热点缓存失效的情况:

  1. 设置不同的失效时间比如随机设置缓存的失效时间。
  2. 缓存永不失效(不太推荐,实用性太差)。
  3. 设置二级缓存。

缓存雪崩和缓存击穿有什么区别?

缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。 

5.缓存污染(或满了)

缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。

缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响Redis性能。这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。

   5.1 最大缓存设置多大

系统的设计选择是一个权衡的过程:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,我会建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销

对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,你就可以使用下面这个命令来设定缓存的大小了:

不过,缓存被写满是不可避免的, 所以需要数据淘汰策略。 

   5.2 缓存淘汰策略

Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。

怎么理解呢?主要看分三类看:

  • 不淘汰
    • noeviction (v4.0后默认的)
  • 对设置了过期时间的数据中进行淘汰
    • 随机:volatile-random
    • ttl:volatile-ttl
    • lru:volatile-lru
    • lfu:volatile-lfu
  • 全部数据进行淘汰
    • 随机:allkeys-random
    • lru:allkeys-lru
    • lfu:allkeys-lfu

具体分析如下:

1.noeviction

该策略是Redis的默认策略。在这种策略下,一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。这种策略不会淘汰数据,所以无法解决缓存污染问题。一般生产环境不建议使用。

其他七种规则都会根据自己相应的规则来选择数据进行删除操作。

2.volatile-random

这个算法比较简单,在设置了过期时间的键值对中,进行随机删除。因为是随机删除,无法把不再访问的数据筛选出来,所以可能依然会存在缓存污染现象,无法解决缓存污染问题。

3.volatile-ttl

这种算法判断淘汰数据时参考的指标比随机删除时多进行一步过期时间的排序。Redis在筛选需删除的数据时,越早过期的数据越优先被选择。

4.volatile-lru

LRU算法:LRU 算法的全称是 Least Recently Used,按照最近最少使用的原则来筛选数据。这种模式下会使用 LRU 算法筛选设置了过期时间的键值对。

详细LRU算法可看此博客:LRU缓存淘汰算法详解与实现_北~笙的博客-CSDN博客

Redis优化的LRU算法实现

Redis会记录每个数据的最近一次被访问的时间戳。在Redis在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。通过随机读取待删除集合,可以让Redis不用维护一个巨大的链表,也不用操作链表,进而提升性能。

Redis 选出的数据个数 N,通过 配置参数 maxmemory-samples 进行配置。个数N越大,则候选集合越大,选择到的最久未被使用的就更准确,N越小,选择到最久未被使用的数据的概率也会随之减小。

5.volatile-lfu

会使用 LFU 算法选择设置了过期时间的键值对。

LFU 算法:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。 Redis的LFU算法实现:

当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。

Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255,这样在访问快速的情况下,如果每次被访问就将访问次数加一,很快某条数据就达到最大值255,可能很多数据都是255,那么退化成LRU算法了。所以Redis为了解决这个问题,实现了一个更优的计数规则,并可以通过配置项,来控制计数器增加的速度。

6.allkeys-lru

使用 LRU 算法在所有数据中进行筛选。具体LFU算法跟上述 volatile-lru 中介绍的一致,只是筛选的数据范围是全部缓存,这里就不在重复。

7.allkeys-random

从所有键值对中随机选择并删除数据。volatile-random 跟 allkeys-random算法一样,随机删除就无法解决缓存污染问题。

8.allkeys-lfu 使用 LFU 算法在所有数据中进行筛选。具体LFU算法跟上述 volatile-lfu 中介绍的一致,只是筛选的数据范围是全部缓存,这里就不在重复。

allkeys-lfu 策略是 Redis 4.0 后新增。

6.数据库和缓存一致性

  • 问题来源

使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库:

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:

1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。 

   6.1 4种相关模式

更新缓存的的Design Pattern有四种:Cache aside Pattern, Read through, Write through, Write behind caching; 

这里主要来看最常用的Cache Aside Pattern, 总结来说就是

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

其具体逻辑如下:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

注意,我们的更新是先更新数据库,成功后,让缓存失效。那么,这种方式是否可以没有文章前面提到过的那个问题呢?我们可以脑补一下。

一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。

那么,是不是Cache Aside这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

   6.2 方案:队列+重试机制

流程如下所示

  • 更新数据库数据;
  • 缓存因为种种问题删除失败
  • 将需要删除的key发送至消息队列
  • 自己消费消息,获得需要删除的key
  • 继续重试删除操作,直到成功

然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

   6.3 方案:异步更新缓存(基于订阅binlog的同步机制)

整体思路

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1)读Redis:热数据基本都在Redis

2)写MySQL: 增删改都是操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)
  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

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

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

相关文章

[C++11]

文章目录 1. 自动类型推导1.1 auto1.1.1 推导规则1.1.2 auto的限制1.1.3 auto的应用1.1.4 范围for 1.2 decltype1.2.1 推导规则1.2.2 decltype的应用 1.3 返回类型后置 2.可调用对象包装器、绑定器2.1 可调用对象包装器2.1.1 基本用法2.1.2 作为回调函数使用 2.2 绑定器 3. usi…

idea创建javaweb项目,jboss下没有web application

看看下图这个地方有没有web application

C++_模板进阶_非类型模板参数_模板特化_分离编译

一、非类型模板参数 模板参数,分为类型形参和非类型形参。 类型形参就是在模板中跟在typename和class之后的参数类型名称,非类型形参就是用一个常量作为类模板或者函数模板的一个参数,在类模板和函数模板中,可以将该参数当作常量…

【JUC系列-01】深入理解JMM内存模型的底层实现原理

一,深入理解JMM内存模型 1,什么是可见性 在谈jmm的内存模型之前,先了解一下并发并发编程的三大特性,分别是:可见性,原子性,有序性。可见性指的就是当一个线程修改某个变量的值之后&#xff0c…

卷积神经网络全解:(AlexNet/VGG/ GoogLeNet/LeNet/ResNet/卷积/激活/池化/全连接)、现代卷积神经网络、经典卷积神经网络

CNN,卷积神经网络,Convolution Neural Network 卷积计算公式:N (W-F2p)/s1 这个公式每次都得看看,不能忘 1 经典网络 按照时间顺序 1.1 LeNet LeNet是 Yann LeCun在1998年提出,用于解决手…

Lua 数据结构

一、Lua 中的数据结构 Lua 中并没有像 java、kotlin 语言有专门的数组、列表、集合等数据结构的类,而只提供 table 类型,但他很强大而且也很灵活,而且也在很多场景中天然的就解决了如何对接和使用的问题(例如模块的引入&#xff…

MYSQL完全卸载、安装与账号创建、权限控制

一、卸载mysql CentOS 卸载 MySQL 1. 查看安装情况 使用以下命令查看当前安装mysql情况,查找以前是否装有mysql rpm -qa|grep -i mysql这里显示我安装的 MySQL 服务有有: 2. 停止 mysql 服务、删除之前安装的 mysql 删除命令:rpm -e –n…

C++笔记之条件变量(Condition Variable)与cv.wait 和 cv.wait_for的使用

C笔记之条件变量(Condition Variable)与cv.wait 和 cv.wait_for的使用 参考博客:C笔记之各种sleep方法总结 code review! 文章目录 C笔记之条件变量(Condition Variable)与cv.wait 和 cv.wait_for的使用1.条件变量&…

v8引擎编译全过程

环境vs2019 cmd 命令行需要设置成为代理模式 set http_proxyhttp://127.0.0.1:10809 set https_proxyhttp://127.0.0.1:10809 这个必须带上,不然报错,告诉编译器win系统的模式 set DEPOT_TOOLS_WIN_TOOLCHAIN0 源码 GitHub: GitHub - v8/v8: The…

Eclipse如何设置快捷键

在eclopse设置注释行和取消注释行 // 打开eclipse,依次打开:Window -> Preferences -> General -> Key,

solidwords(6)

从右视图开始,分上下两部分 标题 这里的薄壁要留意一下怎么算的(单向:默认向内;如果想向外记得选反向)

【Spring专题】Spring之Bean的生命周期源码解析——阶段二(二)(IOC之属性填充/依赖注入)

目录 前言阅读准备阅读指引阅读建议 课程内容一、依赖注入方式(前置知识)1.1 手动注入1.2 自动注入1.2.1 XML的autowire自动注入1.2.1.1 byType:按照类型进行注入1.2.1.2 byName:按照名称进行注入1.2.1.3 constructor:…

idea 新建servlet 访问提示404 WebServlet注解找不到包 报错

检查访问路径是否设置正确 如果设置为name “/testServlet”,则会404 WebServlet注解报错找不到包 检查是否引入了tomcat依赖包

线性代数的学习和整理8: 方阵和行列式相关(草稿-----未完成)

1.4.1 方阵 矩阵里,行数列数的矩阵叫做方阵方阵有很多很好的特殊属性 1.4.2 行列式 行列式是方阵的一种特殊运算如果矩阵行数列数相等,那么这个矩阵是方阵。行列数的计算方式和矩阵的不同只有方阵才有行列式行列式其实是,矩阵变化的一个面…

超越函数界限:探索JavaScript函数的无限可能

🎬 岸边的风:个人主页 🔥 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想,就是为了理想的生活 ! 目录 📚 前言 📘 1. 函数的基本概念 📟 1.1 函数的定义和调用 📟 1.2 …

动态内存管理

目录 为什么要用动态内存开辟 动态内存有关函数 void* malloc (size_t size); void free (void* ptr); void* calloc (size_t num, size_t size); void* realloc (void* ptr, size_t size); C/C程序的内存开辟 柔性数组 特点: 柔性数组的使用: 为什么要用…

【nodejs】用Node.js实现简单的壁纸网站爬虫

1. 简介 在这个博客中,我们将学习如何使用Node.js编写一个简单的爬虫来从壁纸网站获取图片并将其下载到本地。我们将使用Axios和Cheerio库来处理HTTP请求和HTML解析。 2. 设置项目 首先,确保你已经安装了Node.js环境。然后,我们将创建一个…

学习笔记|基于Delay实现的LED闪烁|u16是什么|a--和--a的区别|STC32G单片机视频开发教程(冲哥)|第六集(上):实现LED闪烁

文章目录 摘要软件更新什么是闪烁Tips:u16是什么? 语法分析:验证代码Tips:a--和--a的区别(--ms 的用法)测试代码: 摘要 1.基于Delay实现的LED闪烁 2.函数的使用 3,新建文件,使用模块化编程 软件更新 打…

macOS(m1/m2)破解Sublime Text和Navicat16

破解Sublime Text 说明:全程使用的是终端操作 1. 下载Sublime Text,建议使用brew下载 2. 进入到下载的app的文件夹 cd "/Applications/Sublime Text.app/Contents/MacOS/"3. 执行以下操作以确认版本是否匹配 md5 -q sublime_text | grep -i…

消息中间件的选择:RabbitMQ是一个明智的选择

💗wei_shuo的个人主页 💫wei_shuo的学习社区 🌐Hello World ! MQ(Message Queue) MQ(消息队列)是一种用于在应用程序之间进行异步通信的技术;允许应用程序通过发送和接收…