SQL

Redis --- 缓存雪崩、击穿、穿透与数据库缓存双一致性

2021-06-08  本文已影响0人  _code_x

写在前

在看redis缓存雪崩、击穿和穿透之前,先回答一下几个缓存的问题。

为什么要用 redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存

Redis 与 Memcached的区别

两者都是非关系型(NoSql)内存键值数据库,主要有以下不同:

(1)数据类型。Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。

(2)数据持久化。Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。

(3)分布式。Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。Redis Cluster 实现了分布式的支持。

(4)内存管理机制。在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘(设置过期时间),而 Memcached 的数据则会一直在内存中。Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。

(5)Memcached是多线程,⾮阻塞IO复⽤的⽹络模型;Redis使⽤单线程的多路 IO 复⽤模型。

使用Redis有什么缺点?存在的问题?

(1)缓存和数据库双写一致性问题

(2)缓存雪崩问题

(3)缓存击穿问题

(4)缓存穿透(并发竞争)问题

缓存雪崩

为了使查询速度更快,我们选择使用缓存来保存数据,使原本每次请求都需要查询数据库的操作变成先查询缓存,缓存有直接返回,缓存没有则查询数据库然后再写入缓存中,通常缓存都是有有效时长的,否则就会一直占用内存空间。

问题描述:当大量请求在访问都会先从缓存查询,如果此时大部分缓存同时过期失效,那么这些请求都查询不到缓存,此时他们会全部将请求到数据库,当请求数量足够大时此时将会把数据库压垮。简言之,如果缓存挂掉了,就意味着大量的请求都跑到数据库去了,压垮数据库,这就是缓存雪崩。

解决方案

Redis是如何判断数据是否过期的呢?

Redis 通过一个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键是一个指针,这个指针指向键空间中的某个键对象( 也即是某个数据库键)。过期字典的值是一个long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间:一个毫秒精度的UNIX 时间戳。

过期字典是存储在 redisDb 这个结构里的:键空间+键的过期时间

typedef struct redisDb {
    ...

    dict *dict;     //数据库键空间,保存着数据库中所有键值对
    dict *expires   // 过期字典,保存着键的过期时间
    ...
} redisDb;

通过过期字典,程序可以用以下步骤检查一个给定键是否过期:

(1)检查给定键是否存在于过期字典: 如果存在,那么取得键的过期时间。

(2)检查当前UNIX 时间戳是否大于键的过期时间: 如果是的话,那么键已经过期;否则的话,键未过期。

Redis 给缓存数据设置过期时间有啥用?

(1)有助于缓解内存的消耗,避免长时间占用内存。如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。

(2)实际业务场景需要。很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在1分钟内有效,用户登录的 token 可能只在 1 天内有效。如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。

127.0.0.1:6379> exp key  60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56

注意:Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间, ttl查看键还有多久过期

redis 设置过期时间,怎么处理过期数据呢?(过期键删除策略)

Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置⼀个过期时间。作为⼀个 缓存数据库,这是⾮常实⽤的。如我们⼀般项⽬中的 token 或者⼀些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理⽅式,⼀般都是⾃⼰判断过期,这样⽆疑会严重影响项⽬性 。

通过key设置过期时间:我们 set key 的时候,都可以给⼀个 expire time,就是过期时间。通过过期时间我们可以指定这个key可以存活的时间。如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢(策略)

所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。

Redis 内存淘汰机制

作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。

redis 4.0 版本后增加以下两种(针对最少使用的淘汰机制):

ps:MySQL⾥有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?如何提高缓存命中率?

缓存击穿(缓存雪崩的另一个场景,热点数据在某一时刻过期失效)

问题描述:对于一些设置了过期时间的key,当redis缓存中有一个key是大量请求同时访问的热点数据,如果突然这个key时间到了,那么大量的请求在缓存中获取不到该key,穿过缓存直接来到数据库导致数据库崩溃,这样因为单个key失效而穿过缓存到数据库称为缓存击穿

热点缓存失效解决方案

ps:上述两种问题,针对redis服务器不可用情况:

缓存穿透

问题描述:缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据,则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。这样,如果请求的数据在缓存大量不命中,导致大量的请求走向数据库,就很可能将数据库搞垮,导致整个服务瘫痪。这种通常是恶意查询和被攻击几率较大

解决方案

ps:布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端(无效的请求),存在的话才会走下面的流程。
但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

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

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

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

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

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

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

更多关于布隆过滤器的内容参考:《不了解布隆过滤器?一文给你整的明明白白!》

缓存与数据库双写一致性

问题描述:从理论上来说,只要我们设置了键的过期时间,我们就能够保证缓存和数据库的数据最终一致性。因为只要缓存数据过期了,就会被删除,下次读的时候因为缓存里面没有,就会从数据库中查询并更新到缓存中。但是,在缓存数据没过期的时间内,缓存数据和数据库数据是不同步的。

怎样保证在写入数据库的同时,同步更新缓存中的数据。就是缓存与数据库双写一致性问题。

怎样解决缓存与数据库双写一致性问题?

方案:解决思路基本上都是删除缓存。因为这样的话,下一次读就会到数据库中读到缓存中,保证缓存的一致性。就算数据库更新操作失败了,也不会有缓存数据与数据库数据不一致的问题,即使缓存数据和数据库数据都是旧数据。只是删除缓存的时机不同会引发不同的问题

如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。这里聊聊,Cache Aside Pattern(旁路缓存模式)更新数据库,删除缓存,如果数据库删除成功,缓存删除失败,解决方案:

ps:为什么是删除缓存,而不是更新缓存?

(1)缓存可能复杂:很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。

(2)更新代价高:另外更新缓存的代价有时候是很高的。

总结

补充

什么是缓存预热?

缓存预热是一个比较常见的概念,就是指在系统上线后,先将相关的缓存数据直接加载到缓存系统。这样,用户请求的时候就不需要先去查询数据库,再将数据放入缓存了,用户可以直接拿到实现被预热的缓存数据。

什么是缓存(熔断)降级?

熔断机制:“我们提供过载保护。当某个服务故障或者异常发生时,若这个异常条件需要我们处理,我们会采取一些保护措施---直接熔断整个服务,而不是一直等到此服务超时,从而防止整个系统的故障。”

什么是缓存降级?

拿日志级别设置预案作为参考:

巨人的肩膀

https://www.cnblogs.com/yanggb/p/11110706.html

https://blog.csdn.net/qq_38550836/article/details/108044871

https://www.cnblogs.com/bigsai/p/13951123.html

https://www.it610.com/article/1292457092094959616.htm

上一篇 下一篇

猜你喜欢

热点阅读