通过Redis讲缓存实战经验

技术21-12-2023

缓存的特征

本文通过Redis讲解缓存实战经验,但是我们在进入篇章之前,需要细致的了解一些缓存的大体特征。
image.png
在计算机系统中,默认有两种缓存:

可以发现,他们的量级都比前者更加快与高效。那么它便有如下的特征:

随之而来的问题是,缓存的工作原理、替换策略,异常处理和扩展机制。
因为有限的空间,我们不可能把所有的数据都放入缓存中,那么对于Redis而言,我们要解决的问题如下:


缓存类型

对于缓存的结果来说,只有两种情况

我们使用Redis来做旁路缓存,因为他是独立于原有系统的软件系统,我们与此交互的方式,只能通过在原来的基础上,增加代码去进行交互。
image.png
按照释放接受写请求,把缓存分为了只读缓存和读写缓存。

只读缓存

我们对Redis的操作只有读的操作,我们不会对其进行更新,当我们数据库有修改的时候,通过数据库更新,将Redis中的数据删除,通过缓存缺失再次拉取缓存。
image.png
只读缓存的好处很明显:

所有最新的数据都在数据库中,而数据库是提供数据可靠性保障的,这些数据不会有丢失的风险。当我们需要缓存图片、短视频这些用户只读的数据时,就可以使用只读缓存这个类型了。

读写缓存

读写缓存,无非就是把所有的写请求,页发送到缓存中,在缓存中更新。但是风险也很明显,你无法保证Redis集群的可靠性,一旦宕机,缓存丢失,可能导致内存数据丢失,给应用业务带来风险。
根据业务应用对数据可靠性和缓存性能的不同要求,我们会有同步直写和异步写回两种策略。

image.png

如何选择?

关于是选择只读缓存,还是读写缓存,主要看我们对写请求是否有加速的需求。

例子:商品大促的场景中,商品库存信息会被一直修改,这个时候每次修改都需要到数据库中处理,会拖慢整个应用,这个时候适合选择读写缓存比较好。但是,在短视频场景中,属性虽然多,但一般不会再修改,这个时候会读缓存比较好。
最后思考一个问题:使用只读缓存和直写策略的读写缓存对比?
image.png


替换策略

讲完了缓存类型,我们知道缓存是有限的,这就意味着,我们的数据不可能无限制的存储,必须包含着淘汰。

如何处理淘汰数据?

答:看是否干净!(我们可以通过对比来进行判断是否为干净数据)
如果这个数据是干净数据,那么我们就直接删除;
如果这个数据是脏数据,我们需要把它写回数据库 ?
image.png
那这种策略,是属于哪种类型与策略?

缓存多大合适?

上面也讲到了不可能缓存全部,那么缓存设置多大合适,此外缓存哪些数据呢?
这里需要提到“二八原理“,80%的请求实际只访问了20%的数据。所以,用1TB 的内存做缓存,并没有必要。
所以建议:** 把缓存容量设置为总数据量的15%到30%,兼顾访问性能和内存空间开销。 **
当然,要结合应用实际访问特性和成本开销,来进行一个综合考虑。

写满了淘汰策略

写满了是不可避免的,所以就有了淘汰策略:
image.png
这里讲讲LRU, 在实际实现时,需要用链表管理所有的缓存数据,这会带来额外的空间开 销。而且,当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
所以**Redis对其进行了简化,以减轻数据淘汰对缓存性能的影响。**Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。

使用建议


缓存异常解决方案

缓存异常主要有4个方面:

如何解决数据不一致?

在探讨这个问题,需要明白何为一致?

那么何时会不一致?

在实际场景中,无论是先删除缓存再更新数据库看,还是先更新数据库再删缓存。都存在数据不一致的情况!

image.png

重试机制

重试机制是一种常见的解决缓存不一致的方案。将需要缓存的数据放入消息队列了,从而避免数据删除失败导致脏数据的存在。
image.png

并发下的数据一致

上面将到的方案,是针对于一个操作失败的情况,但是有大量并发请求,还是有可能读到不一致的数据。

先删除缓存,再更新数据库

image.png
两个线程的不同步,导致了读取到旧值。其本质原因是因为,缓存更新操作和数据库更新操作的频率不一致,所以要使得他们的频率一致,我们可以使用延迟双删的操作。通过给另外一个腾出时间,让自己休息后又一次把旧数据替换掉。
**在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。 **
image.png

先更新数据库值,再删除缓存值

这种情况,业务影响比较小,因为如果不是有很多并发请求的话,线程A是能很快完成操作并且更新缓存,其他数据库是不会读到旧信息的。
image.png

这种情况,很难发生,因为这个时候写回缓存通常在更新数据库之前!所以实际的方案中,我们通常会如此选择!

方案总结

image.png
最后, 在大多数业务场景下,我们会把 Redis 作为只读缓存使用, 建议是优先使用先更新数据库再删除缓存的方法。

最后,如果真的要求强一致性,可以在客户和缓存并发读请求,等待数据库更新完,缓存值删除后,再读取数据,从而保证数据一致性。

缓存雪崩

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发 送到数据库层,导致数据库层的压力激增。

事前

那它发生的原因有哪些?

对于大量数据同时过期我们有两种方案:

对于大量Redis实例宕机的情况:
Redis通常支持数万级别的请求,而数据库只能支持数千级别的请求。故我们可以采取如下策略避免数据块压力过大:

事后

前面都是事情发生之后采取的策略,那有什么策略可以在事情尽可能做好?

缓存击穿

缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增。
这种情况的发生,通过在热点数据过期失效时,解决方法也很简单:

当然这种压力的影响,是要比缓存雪崩带来的压力小的。

缓存穿透

缓存穿透是指要访问的数据既不在Redis缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。此时,应用也无法从数据库中读取数据再写入缓存,来服务后续请求,这样一来,缓存也就成了“摆设”。
何时会发生:

解决方案:

注意,缓存穿透的影响是非常大的,要特别注意!

本质

扯到这里,来一个思考题吧!** 服务熔断、服务降级、请求限流的方法,可以用来应对缓存穿透吗? **

在缓存穿透的场景下,业务应用是要从 Redis 和数据库中读取不存在的数据,此时,如果没有人工介入,Redis 是无法发挥缓存作用的。

另外,这里,有个地方需要注意下,对于缓存雪崩和击穿问题来说,服务熔断、服务降级和请求限流这三种方法属于有损方法,会降低业务吞吐量、拖慢系统响应、降低用户体验。不过,采用这些方法后,随着数据慢慢地重新填充回 Redis,Redis 还是可以逐步恢复缓存层作用的


缓存污染

访问少的数据留在内存中,白白浪费了空间!

这一部分主要是对比缓存污染的淘汰的策略。

在刚才,我们了解到了有如下几种策略:

所以结合上面的缺点,有了LRU,结合次数+时间判断!

LFU

有一个很妙,它只是把原来 24bit 大小的 lru 字段,又进一 步拆分成了两部分。

这样用8个bit,即255可以吗?
注意,Redis在计数上,有了一个优化策略。
image.png
image.png
这种非线性递增的计数方法,及时数据量很大,也能有很好的数据筛选表现。
当然,还有一些不好的点,比如按次数筛选,可能会有部分数据本身访问频次就一直很高,它一直留在内存中。所以Redis也对此有了一个优化:


总结

在这篇文章中,我们介绍了:

对于类型,要明白读缓存和读写缓存,他们之间存在的问题!

对于替换策略:

对于异常方案,要明白其下对应的策略:

对于缓存污染

Author's photo

HuanXin-Chen

A tech enthusiast and avid sharer, this dream chaser firmly believes that great things will happen!

See other articles: