这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记
以下内容是我在项目设计过程中,在缓存设计方面遇到的一个问题的总结以及解决方案。
一 : 确定缓存的设计模式 缓存的设计模式
- Cache Aside 建议: 采用【先更新数据库,再删除缓存】方案,并配合【消息队列】或【订阅变更日志】的方式来做。
假设缓存刚好到期失效时,读请求从db中读取数据,写请求更新完数据后再失效缓存后,读请求将旧数据存入到缓存中,这种情况也会导致脏数据的问题。
实际上这种情况发生的概率很低,要发生这种情况的前提条件是写数据库要先于读数据库完成,一般而言读数据库相比于写数据库要耗时更短,这种前提条件成立的概率很低。针对这种情况,也可以采用异步双删策略以及过期失效的方式来避免。(可以考虑通过异步双删(通过两次删除来解决并发读造成的脏数据)或者给数据设置过期时间来解决。)当发生缓存未命中的情况时响应较慢。实现逻辑都在应用程序中,如果后端拆分微服务,会造成代码冗余
- Read/Write Through 读数据策略:当数据发生更新时,查询缓存时更新缓存,然后由缓存层同步的更新数据库
写数据策略:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己同步更新数据库。
先更新缓存,再更新数据库
特点:该模式将缓存作为主要的数据源,而数据库对于应用程序是透明的,更新数据库的任务交给缓存来处理,应用程序直接和缓存服务打交道即可。
- ReadThrough/Write Behind 读数据策略:当数据发生更新时,查询缓存时更新缓存,然后由缓存层同步的更新数据库
写数据策略:当数据更新的时候直接更新缓存数据,然后建立异步任务去更新数据库。
这种异步方式请求响应会很快,系统的吞吐量会明显提升。
但是因为是异步更新数据库,数据一致性的保障就会变弱,如果更新数据库失败则会永远的造成系统脏数据,需要很精细设计系统重试的策略,另外如果异步服务宕机的话,还要考虑更新的数据如何持久化,服务重启后能够迅速恢复。在更新数据库时,由于并发多任务的存在,还需要考虑并发写是否会造成脏数据的问题。
总结:
- Cache Aside: 优点:缓存仅仅保存被请求的数据,属于懒加载模式(Lazy Loading),避免了任何数据都被写入缓存造成缓存频繁更新
缺点:
(1)当发生缓存未命中的情况时响应较慢。
(2)实现逻辑都在应用程序中,如果后端拆分微服务,会造成代码冗余
- Read/Write Through: 优点:
(1)缓存不存在脏数据
(2)读取效率更高,因为写操作每次都会更新缓存,所以提高了读操作命中缓存的概率
(3)缓存单独抽成服务,应用程序的逻辑相对简单
缺点:对于写多读少的应用不是很合适,数据可能更改了很多次,每次都写入缓存却没有被读取,造成了大量写入延迟时间的浪费。
3.ReadThrough/Write Behind:
优点:读操作和写操作效率很高,因为都是直接从缓存中读取和写入。
缺点:有数据丢失的风险,如果缓存挂掉而数据没有及时写到数据库中,那么缓存中的有些数据将永久丢失。
二:缓存的清除策略:
- 过期
只要是缓存, 都应该设置过期时间, 设置有效期的优点:
(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占用的内存达到系统上限时, 就会触发 内存淘汰机制。
- LRU
- LFU
- TinyLFU
四:可能会存在穿透问题:
缓存只是为了缓解数据库压力而添加的一层保护层,当从缓存中查询不到我们需要的数据就要去数据库中查询了。 如果被黑客利用,频繁去访问缓存中没有的数据,那么缓存就失去了存在的意义,瞬间所有请求的压力都落在了数据库上,这样会导致数据库连接异常
解决方案:
- 对于数据库中不存在的数据, 也对其在缓存中设置默认值Null(为避免占用资源, 一般过期时间会比较短)
2.使用布隆过滤器,用于判断数据是否包含在集合中,但是有误杀的风险
五:可能存在的 雪崩 问题:
如果大量缓存数据都在同一个时间过期, 那么很可能出现 缓存集体失效, 会导致所有的请求都直接访问数据库, 导致数据库压力过大
解决方案:
-
设置过期时间时添加随机值, 让过期时间进行一定程度分散,避免同一时间集体失效。
-
采用多级缓存,不同级别缓存设置的超时时间不同,即使某个级别缓存都过期,也有其他级别缓存兜底。