0%

架构设计-缓存

缓存的设计

为什么使用缓存

为了提高数据读取或写入速度

缓存的类型

硬件缓存,如cpu的L1、L2、L3 cache

软件缓存,如内存缓存、数据库缓存、客户端缓存

本文只对内存缓存做介绍

内存缓存的形式

通常以 k-v 对的形式存在

key 是缓存的唯一标识,避免碰撞,对敏感数据要进行脱敏(SHA-2)

value 是缓存的数据

缓存的更新机制

时效性更新机制

为缓存的key设置过期时间,到达过期时间,被动删除;

写进程:只写数据提供方,不理会缓存

读进程:先读缓存,如果缓存中没有,到数据库读,再写入缓存

存在问题:放弃了缓存与数据库的实时一致性

适用场景:对实时性要求不高的场景,如点赞数、收藏人数

主动更新机制

双写机制

双写方案的比较
方案 操作 存在问题
1 先更新缓存,再更新数据库 更新数据库失败,会导致数据不一致
2 先更新数据库,再更新缓存 并发写操作时,可能导致数据不一致
3 删除缓存,更新数据库 可能读到旧数据
4 更新数据库,再删除缓存 较低概率读到旧数据

对于方案3,4,可以采用延时双删,降低数据不一致的概率

双写策略的实现

方法一:在代码中实现

方法二:将删除缓存的功能抽离为单独的服务

Read/Write Through

调用方只与缓存交互

写进程:直接将数据写入缓存,由缓存服务写到数据库,这两个写操作在一个事务中完成

读进程:直接从缓存中读数据,如果没有数据,由缓存服务从数据库中读取,返回给调用方

优点:程序只与缓存交互,代码简单

缺点:不能忍受数据不一致

Write Behind

Write-BehindWrite-Through在“调用方只和缓存交互”这一点上很相似。不同点在于Write-Through会把数据立即写入数据库中,而Write-Behind会异步地把数据一起写入数据库,比如通过消息队列;

优点:提高了写操作的效率,追求最终一致性

缺点:牺牲了实时一致性

缓存的清理机制

由于内存空间有限,不会将数据库全部数据都放到缓存中,需要将一些不常用的数据从缓存中移除,以提高命中率

时效性清理

为key设置过期时间,自动清理

存在问题:不会区分缓存的重要性,一视同仁

数目阈值清理

思想:将缓存放入一个队列中,按照某种策略(分数)排序,当队列满时,将分数低的缓存出队

FIFO:First in First out,先进先出

LRU:Least Recently Used,最近最少使用

LFU:Least Frequently Used,最不经常使用

存在问题:当队列满后,每次都数据进入都需要清理

优先级清理

指定缓存的类型,比如对缓存设置重要级别,当内存不足时,不重要的缓存会被优先清理;如JVM中,强引用指向的缓存不会被清理,弱引用指向的缓存会被清理

实际生产中,一般采用时效性+数目阈值 结合的方式来清理缓存

缓存失效的问题

缓存击穿

一个非常热点的key,在大量并发集中访问,一旦该key由于过期被清理,大量并发会请求到数据库

解决方案:对热点key不设置过期时间,由后台主动更新key的value(做好互斥)

缓存雪崩

大量缓存同时失效,只在存在时效性清理机制的情况下存在缓存雪崩

解决方案:为缓存的过期时间设置为固定值 + 随机数,让缓存失效的时间分布尽量均匀,避免同时失效

缓存穿透

缓存与数据库中都没有的数据,每次都会请求数据库,如果有大量这样的请求同时涌入,对数据库造成巨大压力

解决方案

缓存空对象

当缓存和数据库中都查询不到key的数据时,将<key:空对象>写入缓存,下次请求该key可以从缓存中查询空对象

存在的问题:

  1. 如果有大量缓存穿透,空对象会占用大量内存空间

    1. 可能有数据不一致的情况(key在数据库存在对象,缓存中是空对象)
构建key的白名单

将数据库中的key放到白名单中,如果请求的key不在白名单中,则不在数据库中,直接返回空对象;

但是这种方式占用的内存空间较大,当key数量很大时,查询效率会降低;可以通过布隆过滤器的思路进行优化,节省空间,提高查询效率

写缓存

上面介绍的是调用方来读数据时的缓存应用,当调用方大量并发的写,通过在调用方和数据库之间加入写缓存,防止直接将数据写入数据库,如消息队列