缓存的设计
为什么使用缓存
为了提高数据读取或写入速度
缓存的类型
硬件缓存,如cpu的L1、L2、L3 cache
软件缓存,如内存缓存、数据库缓存、客户端缓存
本文只对内存缓存做介绍
内存缓存的形式
通常以 k-v 对的形式存在
key 是缓存的唯一标识,避免碰撞,对敏感数据要进行脱敏(SHA-2)
value 是缓存的数据
缓存的更新机制
时效性更新机制
为缓存的key设置过期时间,到达过期时间,被动删除;
写进程:只写数据提供方,不理会缓存
读进程:先读缓存,如果缓存中没有,到数据库读,再写入缓存
存在问题:放弃了缓存与数据库的实时一致性
适用场景:对实时性要求不高的场景,如点赞数、收藏人数
主动更新机制
双写机制
双写方案的比较
方案 | 操作 | 存在问题 |
---|---|---|
1 | 先更新缓存,再更新数据库 | 更新数据库失败,会导致数据不一致 |
2 | 先更新数据库,再更新缓存 | 并发写操作时,可能导致数据不一致 |
3 | 删除缓存,更新数据库 | 可能读到旧数据 |
4 | 更新数据库,再删除缓存 | 较低概率读到旧数据 |
对于方案3,4,可以采用延时双删,降低数据不一致的概率
双写策略的实现
方法一:在代码中实现
方法二:将删除缓存的功能抽离为单独的服务
Read/Write Through
调用方只与缓存交互
写进程:直接将数据写入缓存,由缓存服务写到数据库,这两个写操作在一个事务中完成
读进程:直接从缓存中读数据,如果没有数据,由缓存服务从数据库中读取,返回给调用方
优点:程序只与缓存交互,代码简单
缺点:不能忍受数据不一致
Write Behind
Write-Behind
和Write-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可以从缓存中查询空对象
存在的问题:
如果有大量缓存穿透,空对象会占用大量内存空间
- 可能有数据不一致的情况(key在数据库存在对象,缓存中是空对象)
构建key的白名单
将数据库中的key放到白名单中,如果请求的key不在白名单中,则不在数据库中,直接返回空对象;
但是这种方式占用的内存空间较大,当key数量很大时,查询效率会降低;可以通过布隆过滤器的思路进行优化,节省空间,提高查询效率
写缓存
上面介绍的是调用方来读数据时的缓存应用,当调用方大量并发的写,通过在调用方和数据库之间加入写缓存,防止直接将数据写入数据库,如消息队列