目录

一、引言
缓存是应对高并发、提升系统性能的关键技术,而 Redis 凭借其卓越性能,成为缓存场景中的核心选择。本文将从缓存的基本概念入手,理解 Redis 缓存的工作原理,同时拆解实际应用中常见的缓存问题及应对思路。
二、什么是缓存
我们可以回想坐高铁的经历:从进站检票、过安检,到最后出站,前前后后要刷好几次身份证。这时候,没人会把身份证塞进行李箱里 —— 虽说箱子空间大,但每次拿取都要翻找,特别麻烦。大家更习惯把它放在口袋或随手能摸到的地方,方便拿。这里的 “口袋”,其实就相当于行李箱的 “缓存”。
所以,什么是缓存呢?事实上,缓存是相对来讲的。比如在计算机中,CPU 从硬盘读取数据其实很慢。要是每次用到数据都去访问硬盘的话,CPU 将要花很大一部分时间等数据传过来,根本就没法充分发挥它的处理能力。为了解决这个问题,内存就派上了用场。CPU 访问内存的速度比访问硬盘快得多,把平时常用的数据从硬盘加载到内存里,CPU 要用时直接从内存拿,这样系统处理数据的效率一下子就提上来了。所以,内存就是硬盘的缓存。
Redis是基于内存的,相比于MySQL等需要访问磁盘的数据库来说快不少,所以Redis就可以做为MySQL的缓存。
总的来说,高速介质可以作为低速介质的缓存。
三、缓存的更新策略
缓存更新的策略可以分为两大类,定期更新和实时更新。
3.1 定期更新
3.2 实时更新
对于每一次访问,如果Redis缓存命中,则直接返回数据给用户。如果未命中,那么就访问数据库,然后将数据返回给用户,同时将这个数据写入Redis缓存。
四、Redis缓存的工作原理
4.1 工作流程
对于读请求:先查询Redis缓存。若缓存命中,则直接返回。如果缓存未命中,那么就将请求打到数据库,然后将数据库的查询结果写入缓存,并返回给客户端。
对于写请求:先更新数据库,然后在从缓存中删除相关数据块。
为什么要删除Redis缓存中的相关数据块?
为了确保数据一致。如果采取的是更新Redis相关数据块的策略,那么在更新的过程中,可能会有客户端获取这个数据,因为此时这个数据还存在于缓存中,所以不会访问数据库,这样就有可能还没完成数据的更新,这个旧数据就被客户端读走了,就导致了数据的不一致。直接删除,让请求直接打到数据库,然后在将这个数据写入缓存是更稳妥的做法,访问数据库虽然比缓存慢,但是它可以保证数据是最新的。
4.2 缓存过期策略
因为内存有限,所以不能只一味的往里写,还要考虑如何清理不需要的数据。
当我们往Redis中插入某个键,并且给这个键设置了过期时间。那么Redis 就会将该键放入一个过期字典中(类似哈希表结构),记录键与其过期时间的映射关系。
Redis的过期策略主要有两种:
1)惰性删除。只有当访问某个键时,才检查它的过期时间,如果过期,就删除。因为不用频繁的去检测这些键的过期时间,所以CPU开销就小,这是它的优点。但是,可能会导致大量的过期键堆积,浪费内存,这是它的缺点。
2)定期删除。每隔一段时间,随机抽取部分过期键检查并删除。如果时间间隔比较小,那么内存中过期的键将会得到及时的清除。但是,太频繁的检查也会导致CPU开销大。
Redis则是将以上两种方式混合,平衡CPU消耗和内存占用。
4.3 内存淘汰策略
内存淘汰策略,是Redis进行内存管理的一个兜底策略。只有内存达到 maxmemory 阈值时才会触发。触发内存淘汰策略,说明过期键都删完了,内存还是满的,这时候,只能对还未过期的键下手了。Redis内置的淘汰策略如下:
| 策略 | 说明 |
|---|---|
volatile-lru | 从设置了过期时间的键中,删除 “最近最少使用”(LRU)的键。 |
allkeys-lru | 从所有键中,删除 “最近最少使用” 的键(适用于缓存场景,所有键都是缓存)。 |
volatile-random | 从设置了过期时间的键中,随机删除。 |
allkeys-random | 从所有键中,随机删除。 |
volatile-ttl | 从设置了过期时间的键中,删除 “剩余过期时间最短” 的键。 |
noeviction | 不删除任何键,直接返回错误(默认,不推荐缓存场景)。 |
五、缓存常见问题及解决方案
5.1 缓存预热
缓存预热简单说,就是在高访问量来临前,主动把热点数据加载到缓存中的操作,避免用户请求直接冲击数据库。这一份热点数据可以那么准确,只要能帮助数据库抵挡大部分请求即可,然后随着程序的运行,原来缓存的热点数据会被自动调整,适应当前情况。
5.2 缓存穿透
缓存穿透(Cache Penetration)和下面要讲的缓存击穿(Cache Breakdown)很容易混淆。
客户端请求的数据,在缓存(Cache)中不存在,在数据库(DB)中也不存在。这就叫做缓存穿透。
导致缓存穿透的原因有以下几种:
1)业务设计不合理,缺少必要的参数校验环节,对非法的key也进行查询。
2)误删了数据库中的部分数据。
3)黑客攻击。
比如,客户端发起请求,查询一个不存在的 ID(如用户 ID=-1)。缓存中未命中该数据。请求穿透到数据库,数据库查询后发现也没有该数据。黑客就可以对大量不存在的ID进行请求,然后这些请求全被打到数据库,就会导致数据压力过大,甚至扛不住。
对应的解决方案就是:对于不存在的key,将它的value设为空,然后写入到Redis缓存中,并且设置合理的过期时间,当再次访问这个不存在的key时,直接缓存命中返回空。
还有另一种解决方案:布隆过滤器。
在缓存前面,加上一层布隆过滤器。把数据库中所有的key都存入布隆过滤器中,然后客户端的请求向到布隆过滤器这一层,判断对应的key是否存在,不存在就直接返回。如果存在,就继续向下访问。
虽然说布隆过滤器存在一定的误判率,但是对于不存在的数据,它的判断是准确的。这个误判率的意思就是,原本就不存在的数据,被布隆过滤器误判为存在。不过问题不大,误判之后该请求直接打到数据库,此时数据库在二次判断即可。
5.3 缓存击穿
客户端请求的数据,在缓存中不存在,但在数据库中存在,通常是因为缓存刚好过期失效。这个就叫做缓存击穿。
所以,缓存穿透和缓存击穿的区别在哪?
缓存穿透请求的是原本就不存在的数据,而缓存击穿是请求存在的数据,但是它缓存失效了,需要到数据库读取该数据。
比如,某热门数据(如热门商品详情)的缓存突然过期。大量用户同时请求该数据,缓存全部未命中。所有请求同时打到数据库,查询同一数据。数据库返回数据后,将数据重新写入缓存,后续请求从缓存获取。这就是一个典型的缓存击穿。
解决方案有以下几种:
1)使用互斥锁。当缓存未命中时,只能有一个请求获取互斥锁进入数据库查询,其他请求等待,然后查询结果写入缓存后,等待的请求从缓存中获取数据,避免并发查库。
2)热点数据永不过期,不对热点数据设置过期时间。
3)同一类热点数据,将它们的过期时间错开(可以是一个基础时间加上随机时间),避免集中过期。
5.4 缓存雪崩
大量不同 Key的缓存同时失效,或者缓存服务直接宕机,导致海量不同类型的请求同时穿透到数据库。
这里注意和缓存击穿区分开来,缓存击穿是同一类key集中过期,而缓存雪崩是不同类型的key缓存失效。
解决方案:
1)所有的key要错开过期时间,避免同一时间大量不同类型的key过期。
2)搭建缓存服务集群(如 Redis Cluster),避免单点故障导致整个缓存不可用。
3)服务降级,当请求量过高时,直接拒绝部分请求,保护数据库。
5.5 缓存与数据库一致性
对于写请求:先更新数据库,然后在从缓存中删除相关数据块。具体原因已经在4.1节中详细解释。
六、结语
“路虽远,行则将至。事虽难,作则可成。”
完~
1169

被折叠的 条评论
为什么被折叠?



