Redis【一】缓存常见问题与解决方案

序言

在分布式系统盛行的今天,尤其是在一些用户体量比较大的互联网业务系统里面,缓存充当着扛压屏障的作用。当前各互联网系统可以扛住动辄数万甚至数十万的并发请求量,缓存机制功不可没。而一旦缓存出现问题,对系统的影响往往也是致命的。所以在缓存的使用时必须要考虑完备的兜底与灾难应对策略。

数据淘汰机制

  • 数据过期,是缓存系统的一个正常逻辑,是符合业务预期的一种数据删除机制。即设定了有效期的缓存数据,过期之后从缓存中移除。
  • 数据淘汰,是缓存系统的一种“有损自保”的降级策略,是业务预期之外的一种数据删除手段。指的是所存储的数据没达到过期时间,但缓存空间满了,对于新的数据想要加入缓存中时,缓存模块需要执行的一种应对策略。

缓存雪崩

为了限制缓存的数量,很多的缓存记录都会设置一定的有效期,到期后自动失效。这种在一些批量缓存构建或者全量缓存重建时,因为设置了相同的失效时间,会导致大量甚至全部的缓存数据在短时间内集体失效,这样会导致大量的请求无法命中缓存而直接流转到了下游模块,导致系统瘫痪,也即缓存雪崩。
在这里插入图片描述

解决思路:一种简单的策略,就是批量加载的场景,将过期时间在一个固定时间段内以毫秒级别进行随机打散,比如本来要设置每条记录过期时间为5分钟,则批量加载的时候可以设置过期时间为5~10分钟之间的任意一个毫秒数。这样就可以有效的避免数据集中失效,避免出现缓存雪崩而影响业务稳定。

如果某个独立缓存真的出现了缓存雪崩,业务层面应该如何将受损范围控制在仅自身模块、避免殃及数据库以及下游公共服务模块,进而避免业务出现系统性瘫痪呢?这个就需要结合服务治理中的一些手段来综合防范了,比如服务降级、服务熔断、以及接口限流等策略

缓存击穿

正如前面所提到的,基于内存的缓存,受内存容量限制,往往都会加载一些热点数据。而这些热点缓存数据,可以命中大部分的业务请求。少部分没有命中缓存的数据,则直接转由业务模块进行处理(比如从MySQL里面进行查询)。

先来看一个例子:

互动论坛系统,使用Redis作为缓存,缓存最近1年的帖子信息。如果用户查看的帖子是最近1年的,则直接从Redis中查询并返回,如果用户查看的帖子是1年前的,则从MySQL中进行捞取并返回。

因为论坛系统中,大部分人会阅读或者查看的都是最近新发的帖子,只有极少数的人可能会偶尔“挖坟”查看一年前的历史帖子。

系统上线前会根据冷热请求的比例与总量情况,评估需要部署的硬件规模,以确保可以支撑住线上正常的访问请求。但为了避免缓存数据被无限撑满,一般业务缓存数据都会设置一个过期时间,来保证缓存数据的定期清理与更新。

近段时间,娱乐圈的雷声不断,各种新鲜的大瓜也让吃瓜群众撑到打嗝。

有一天,娱乐圈当红流量明星李某某突然被爆料与某网红存在某些不正当的关系,甚至被爆有多次PC被捕的惊天大瓜,引起粉丝和路人的强烈关注。

吃瓜群众们群情高涨、热搜一波盖过一波、帖子的浏览量光速攀升,论坛系统在缓存模块的加持下,虽然整体CPU和内存占用都飙升上去了,倒也相安无事。

但天有不测风云,恰好这个时候,这条帖子的记录在缓存中过期被删除了。然后狂涛巨浪般的请求涌向了后端的数据库,让数据库原地瘫痪,进而陆陆续续殃及了整个论坛系统。这就是典型的一个缓存击穿的问题。

在这里插入图片描述
缓存击穿和前面提到的缓存雪崩产生的原因其实很相似。区别点在于:

  • 缓存雪崩是大面积的缓存失效导致大量请求涌入数据库。

  • 缓存击穿是少量缓存失效的时候恰好失效的数据遭遇大并发量的请求,导致这些请求全部涌入数据库中。

针对这种情况,我们可以为热点数据设置一个过期时间续期的操作,比如每次请求的时候自动将过期时间续期一下。

此外,也可以在数据库记录访问的时候借助分布式锁来防止缓存击穿问题的出现。当缓存不可用时,仅持锁的线程负责从数据库中查询数据并写入缓存中,其余请求重试时先尝试从缓存中获取数据,避免所有的并发请求全部同时打到数据库上。如下图所示:
在这里插入图片描述

缓存穿透

我们的系统对外开放并运行的时候,面对的环境险象环生。你不知道请求是来自一个正常用户还是某些别有用心的盗窃者、亦或是个纯粹的破坏者。

还是上面的论坛的例子:

用户在互动论坛上点击帖子并查看内容的时候,界面调用查询帖子详情接口时会传入帖子ID,然后后端基于帖子ID先去缓存中查询,如果缓存中存在则直接返回数据,否则会尝试从MySQL中查询数据并返回。

有些人盯上了论坛的内容,便搞了个爬虫程序,模拟帖子ID的生成规则,调用查询详情接口并传入自己生成的ID去遍历挖取系统内的帖子数据,这样导致很多传入的ID是无效的、系统内并不存在对应ID的帖子数据。
在这里插入图片描述
所以,上面大量无效的ID请求到系统内,因为无法命中缓存而被转到MySQL中查询,而MySQL中其实也无法查询到对应的数据(因为这些ID是恶意生成的、压根不存在)。

大量此类请求频繁的传入,就会导致请求一直依赖MySQL进行处理,极易冲垮下游模块。这个便是经典的缓存穿透问题(缓存穿透与缓存击穿非常相似,区别点在于缓存穿透的实际请求数据在数据库中也没有,而缓存击穿是仅仅在缓存中没命中,但是在数据库中其实是存在对应数据的)。

缓存穿透的情况往往出现在一些外部干扰或者攻击情景中,比如外部爬虫、比如黑客攻击等等。为了解决缓存穿透的问题,可以考虑基于一些类似白名单的机制(比如基于布隆过滤器的策略,后面系列文章中会详细探讨),当然,有条件的情况下,也可以构建一些反爬策略,比如添加请求签名校验机制、比如添加IP访问限制策略等等。

缓存的数据一致性

数据库更新+缓存删除

在旁路型缓存的读操作分支中,从缓存中没有读取到数据而改为从DB中获取到数据之后,通常都会选择将记录写入到缓存中。所以我们也可以在写操作的时候选择将缓存直接删除,等待后续读取的时候重新加载到缓存中。

这样也会有两种组合情况:

  • 先删除缓存,再更新数据库
  • 先更新数据库,再删除缓存
    在这里插入图片描述
    这种也会出现前面说的先操作成功,后操作失败的问题。

我们先看下先删除缓存再更新数据库的操作策略。如果先删除缓存成功,然后更新数据库失败,这种情况下,再次读取的时候,会从DB里面将旧数据重新加载回缓存中,数据是可以保持一致的。

虽然更新数据库失败这种场景下不会出现问题,但是在数据库更新成功这种正常情况下,却可能会在并发场景中出现问题。因为常见的缓存(如Redis)是没有事务的,所以可能会因为并发处理顺序的问题导致最终数据不一致。如下图所示:
在这里插入图片描述
一番操作完成后,实际上缓存中存储的是A修改前的内容,而DB中存储的是A修改后的数据,两者因此出现了不一致的问题。这样导致后面的查询请求依旧是从缓存中获取到旧数据,而更新后的新数据无法生效。

那么,如果采用先更新数据库,再删除缓存的策略,又会有何种表现呢?假设数据库更新成功,但是缓存删除失败,我们也可以通过数据库事务回滚的方式将数据库更新操作回滚掉,这样在非并发状态下,可以确保数据库与缓存中数据是一致的。
在这里插入图片描述当然,因为基于数据库事务机制来控制,需要注意下事务的粒度不能过大,避免事务成为阻塞系统性能的瓶颈。在对并发性能要求极高的情况下,可以考虑非事物类的其余方式来实现,如重试机制、或异步补偿机制、或多者结合方式等。

比如下图所示的这种策略:

在这里插入图片描述

原文出处 https://mp.weixin.qq.com/s/jHzblPTB-HT2OHL6wkJZ4Q

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值