一文带你了解缓存设计的大坑点|青训营笔记

108 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记

以下内容是我在项目设计过程中,在缓存设计方面遇到的一个问题的总结以及解决方案。

一 : 确定缓存的设计模式 缓存的设计模式

  1. Cache Aside 建议: 采用【先更新数据库,再删除缓存】方案,并配合【消息队列】或【订阅变更日志】的方式来做。

假设缓存刚好到期失效时,读请求从db中读取数据,写请求更新完数据后再失效缓存后,读请求将旧数据存入到缓存中,这种情况也会导致脏数据的问题。

实际上这种情况发生的概率很低,要发生这种情况的前提条件是写数据库要先于读数据库完成,一般而言读数据库相比于写数据库要耗时更短,这种前提条件成立的概率很低。针对这种情况,也可以采用异步双删策略以及过期失效的方式来避免。(可以考虑通过异步双删(通过两次删除来解决并发读造成的脏数据)或者给数据设置过期时间来解决。)当发生缓存未命中的情况时响应较慢。实现逻辑都在应用程序中,如果后端拆分微服务,会造成代码冗余

  1. Read/Write Through 读数据策略:当数据发生更新时,查询缓存时更新缓存,然后由缓存层同步的更新数据库

写数据策略:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己同步更新数据库。

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

特点:该模式将缓存作为主要的数据源,而数据库对于应用程序是透明的,更新数据库的任务交给缓存来处理,应用程序直接和缓存服务打交道即可。

  1. ReadThrough/Write Behind 读数据策略:当数据发生更新时,查询缓存时更新缓存,然后由缓存层同步的更新数据库

写数据策略:当数据更新的时候直接更新缓存数据,然后建立异步任务去更新数据库。

这种异步方式请求响应会很快,系统的吞吐量会明显提升。

但是因为是异步更新数据库,数据一致性的保障就会变弱,如果更新数据库失败则会永远的造成系统脏数据,需要很精细设计系统重试的策略,另外如果异步服务宕机的话,还要考虑更新的数据如何持久化,服务重启后能够迅速恢复。在更新数据库时,由于并发多任务的存在,还需要考虑并发写是否会造成脏数据的问题。

总结:

  1. Cache Aside: 优点:缓存仅仅保存被请求的数据,属于懒加载模式(Lazy Loading),避免了任何数据都被写入缓存造成缓存频繁更新

缺点:

(1)当发生缓存未命中的情况时响应较慢。

(2)实现逻辑都在应用程序中,如果后端拆分微服务,会造成代码冗余

  1. Read/Write Through: 优点:

(1)缓存不存在脏数据

(2)读取效率更高,因为写操作每次都会更新缓存,所以提高了读操作命中缓存的概率

(3)缓存单独抽成服务,应用程序的逻辑相对简单

缺点:对于写多读少的应用不是很合适,数据可能更改了很多次,每次都写入缓存却没有被读取,造成了大量写入延迟时间的浪费。

3.ReadThrough/Write Behind:

优点:读操作和写操作效率很高,因为都是直接从缓存中读取和写入。

缺点:有数据丢失的风险,如果缓存挂掉而数据没有及时写到数据库中,那么缓存中的有些数据将永久丢失。

二:缓存的清除策略:

  1. 过期

只要是缓存, 都应该设置过期时间, 设置有效期的优点:

(1)节省空间
(2)做到数据弱一致性,有效期失效后,可以保证数据的一致性
(一致是指redis和mysql数据一致,弱:通过数据过期然后数据回填保证数据一致,强:直接修改保证数据一致)

1.1 定时过期

每个设置过期时间的key都创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;

但是会占用大量的CPU资源进行计时和处理过期数据,从而影响缓存的响应时间和吞吐量。

1.2 惰性过期

只有当访问一个key时,才会判断该key是否已过期,过期则清除(返回nil)。

该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

1.3 定期过期

每隔一定的时间,扫描数据库中一部分设置了有效期的key,并清除其中已过期的key。

该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

redis 一般同时使用了惰性过期和定期过期两种过期策略。

定期过期: 默认是每100ms检测一次,遇到过期的key则进行删除,这里的检测并不是顺序检测,而是随机检测。 惰性过期: 当我们去读/写一个key时,会触发Redis的惰性过期策略,直接删除过期的key

三:缓存的淘汰策略

假定某个key逃过了定期过期, 且长期没有使用(即逃过惰性过期), 那么redis的内存会越来越高。当redis占用的内存达到系统上限时, 就会触发 内存淘汰机制。

  1. LRU
  2. LFU
  3. TinyLFU

四:可能会存在穿透问题:

缓存只是为了缓解数据库压力而添加的一层保护层,当从缓存中查询不到我们需要的数据就要去数据库中查询了。 如果被黑客利用,频繁去访问缓存中没有的数据,那么缓存就失去了存在的意义,瞬间所有请求的压力都落在了数据库上,这样会导致数据库连接异常

解决方案:

  1. 对于数据库中不存在的数据, 也对其在缓存中设置默认值Null(为避免占用资源, 一般过期时间会比较短)

2.使用布隆过滤器,用于判断数据是否包含在集合中,但是有误杀的风险

五:可能存在的 雪崩 问题:

如果大量缓存数据都在同一个时间过期, 那么很可能出现 缓存集体失效, 会导致所有的请求都直接访问数据库, 导致数据库压力过大

解决方案:

  1. 设置过期时间时添加随机值, 让过期时间进行一定程度分散,避免同一时间集体失效。

  2. 采用多级缓存,不同级别缓存设置的超时时间不同,即使某个级别缓存都过期,也有其他级别缓存兜底。