缓存是什么?
缓存技术是一种用于加速数据访问的优化策略。它通过将频繁访问的数据存储在高速存储介质(如内存)中,减少对慢速存储设备(如硬盘或远程服务器)的访问次数,从而提升系统的响应速度和性能。
缓存的基本原理是:当某个数据被请求时,系统首先检查缓存中是否已存储该数据。如果缓存中存在,则直接返回缓存中的数据,称为"缓存命中";如果缓存中没有该数据,则从源数据存储(如数据库或远程服务器)中获取数据,并将其存入缓存,以便下次请求时使用。
缓存的常见类型:
-
内存缓存 :将数据存储在内存中,常见的缓存系统有 Redis 和 Memcached。内存的访问速度远远高于硬盘,因此可以大幅提升性能。
-
磁盘缓存:将数据存储在硬盘或 SSD 中,相比内存访问较慢,但可以存储更大量的数据。适用于需要较大存储空间但对速度要求不如内存严格的场景。
-
浏览器缓存:网页浏览器会缓存静态资源(如图片、JavaScript 文件等)以减少重复请求,从而提高用户体验和加载速度。
-
CDN缓存:内容分发网络(CDN)将静态内容缓存到靠近用户的边缘节点,以降低延迟,提高访问速度。
-
服务缓存:如Go的本地缓存
-
MySQL缓存:change buffer 机制,查询缓存机制(已废弃)
性能比较:内存类大于磁盘类,客户端类大于服务端类,近的节点大于远的节点(存储介质&距离)
常见的缓存策略:
-
LRU(Least Recently Used):当缓存空间不足时,优先淘汰最近最少使用的数据。(冷热数据)
-
FIFO(First In, First Out):先进先出,最早加入缓存的数据会被优先移除。(新旧数据)
-
TTL(Time To Live):每个缓存数据都有一个有效期,到期后会被自动清除或重新加载。(定时器机制)
-
Write-through:每次对缓存进行修改时,同时将数据写入原始数据存储。(双写,保证数据一致性)
-
Lazy Loading(懒加载):仅当需要时才加载数据到缓存中。(节省资源)
-
Cache Invalidation(缓存失效):当数据源发生变化时,缓存中的数据需要被标记为无效或更新,以保持数据的一致性。(驱动更新)
示例:
Redis 提供了多种淘汰策略来处理内存达到上限时的情况。以下是 Redis 常见的淘汰策略:
-
noeviction
当内存不足时,Redis 不会淘汰任何数据,并且会返回错误。适用于不允许丢失数据的场景。
-
allkeys-lru
基于最少最近使用(LRU,Least Recently Used)算法来淘汰最不常用的键。当 Redis 达到内存限制时,会从所有的键中淘汰最少使用的键。
-
volatile-lru
只对设置了过期时间的键进行 LRU 淘汰。如果内存不足,Redis 会选择最久未使用的、设置了过期时间的键来删除。
-
allkeys-random
随机删除键,适用于在内存不足时,删除任意一个键。
-
volatile-random
随机删除设置了过期时间的键。它只删除有过期时间的键,不会影响没有过期时间的键。
-
volatile-ttl
根据过期时间(TTL,Time To Live)来淘汰键,选择即将过期的键进行删除。这种策略适用于缓存场景,删除那些快过期的键。
常见的缓存问题:
1. 缓存穿透
缓存穿透是指客户端请求的数据既不在缓存中,也不在数据库中,导致每次请求都直接访问数据库,绕过了缓存。
解决方案:
- 空缓存策略 :对于不存在的数据,可以将其在缓存中设置一个空值(如
null
或特殊标记),并设置一个较短的过期时间。这样下次请求相同数据时,可以直接返回空缓存,避免每次都去数据库查询。 - 缓存过滤:在请求数据前,进行数据验证和过滤(如验证用户输入),确保请求的数据是有效的,减少无效请求。
2. 缓存雪崩
缓存雪崩指的是缓存中的大量数据在同一时间过期,导致大量请求同时访问数据库,造成数据库压力剧增,甚至崩溃。
解决方案:
- 随机过期时间:设置缓存的过期时间时,加上随机偏差,避免多个缓存项在同一时刻过期。
- 分布式缓存:将缓存分布到多个服务器上,避免单个缓存服务器故障导致整个缓存系统崩溃。
- 提前加载缓存:根据访问量和数据的热点情况,提前将关键数据加载到缓存中,避免全量加载导致的瞬时压力。
3. 缓存击穿
缓存击穿是指缓存中某个热点数据过期或被删除后,有大量请求同时访问该数据,导致直接访问数据库,从而引发数据库压力过大。
解决方案:
- 加锁或队列机制:在缓存失效时,对该数据加锁或使用队列来保证同一时间只有一个请求去数据库查询并更新缓存,其他请求等待查询结果。
- 缓存预热:可以在缓存失效前,提前加载或更新缓存,避免缓存失效后直接访问数据库。
4. 缓存一致性问题
缓存与数据库之间的数据可能会出现不一致的情况。例如,数据在数据库中更新了,但缓存没有及时更新,导致用户看到的数据是过期的。
解决方案:
- 缓存失效策略:在数据更新后,立即删除缓存中相应的条目。可以通过应用程序逻辑主动更新缓存,或者设置合适的过期时间。
- 双写一致性:在对数据库进行修改时,同时修改缓存中的数据,确保缓存和数据库的数据一致性。
- 延迟一致性:如果严格一致性要求不高,可以允许一定时间内缓存与数据库不一致,稍后通过后台任务同步缓存。
5. 缓存污染
缓存污染是指缓存中存入了错误、无效或不再使用的数据,这些数据会导致缓存效率下降,甚至对应用性能造成影响。
解决方案:
- 数据校验:在将数据写入缓存之前,进行有效性检查,确保缓存的数据是正确的。
- 定期清理:定期扫描和清理缓存中的无效或过期数据,保持缓存的健康状态。
- 合理设置缓存过期时间:缓存过期时间应根据数据的变化频率来合理设置,避免缓存存储过多过期或不常用的数据。
6. 缓存并发问题
当多个请求同时访问缓存中的同一数据时,可能会导致缓存更新的冲突,尤其是在高并发场景下,可能会造成缓存不一致或资源竞争。
解决方案:
- 使用锁机制:可以为缓存数据增加锁(如互斥锁),保证同一时间只有一个请求可以更新缓存,避免并发更新冲突。
- 异步更新:通过异步方式更新缓存,避免长时间的同步阻塞。
7. 缓存击穿和过期
数据缓存的生命周期管理是一个复杂的问题,如果没有合理的策略,缓存中的数据可能在一段时间后过期,导致大量请求直接访问数据库。
解决方案:
- 永不过期的缓存:对于一些不容易发生变化的数据,可以设置为永不过期,避免频繁更新缓存。
- 使用合适的缓存策略:比如定期刷新缓存、主动更新缓存等,确保缓存始终保持有效。
8. 内存溢出
当缓存数据过多,超出了缓存系统的内存限制时,可能会导致缓存系统发生内存溢出,影响系统的稳定性。
解决方案:
- 限制缓存大小:根据应用的实际需求,设置合适的缓存大小限制,避免缓存过多的数据。
- LRU(最少最近使用)淘汰策略:对于不常使用的数据,可以采用 LRU 或其他淘汰策略来清理缓存,避免内存溢出。
9. 缓存穿透/渗透与数据泄露
在一些恶意攻击或错误请求的情况下,缓存可能会泄露敏感数据,特别是在没有加密或限制访问控制的情况下。
解决方案:
- 数据加密:对于缓存中的敏感数据进行加密存储,防止数据泄露。
- 访问控制:根据不同用户权限设置缓存访问策略,防止不合法请求访问敏感数据。
10. 缓存更新延迟
在某些情况下,缓存更新的延迟可能会导致系统反应迟缓,特别是在高并发环境中。
解决方案:
- 异步更新缓存:对于非实时性要求非常高的场景,可以通过异步任务更新缓存,避免阻塞主流程。
- 批量更新缓存:在数据有大批量变化时,可以采用批量更新策略,而不是频繁更新单条缓存数据。
缓存的优点:
- 提高性能:通过减少对慢速存储的访问,缓存可以显著加快数据的读取速度。
- 减轻服务器负担:减少对数据库或其他后端系统的请求,降低它们的负载。
- 节省带宽:通过缓存远程内容(如网站资源或API响应),减少带宽消耗。
缓存的挑战:
- 一致性问题:缓存中的数据可能与原始数据不同步,导致数据不一致。通常需要通过策略(如缓存失效、刷新机制)来解决这一问题。
- 内存占用:缓存使用内存或磁盘存储,会占用一定的资源,需要合理规划和管理。
- 缓存穿透:恶意用户可能直接绕过缓存,频繁访问源数据,从而导致源系统的压力增加。
为什么要设计多级缓存?什么是CPU的多级缓存?
CPU 多级缓存(Multi-level Cache ,简称 L1/L2/L3 Cache)是为了提高 CPU 访问内存的速度而设计的一种缓存层次结构。由于直接从主内存读取数据的速度相对较慢,因此,现代 CPU 采用了多级缓存来加速数据访问,减少访问内存的延迟。CPU 多级缓存通常包括以下几层:
1. L1 缓存(一级缓存)
- 位置:L1 缓存在 CPU 核心内,距离处理器最接近。
- 速度:L1 缓存是所有缓存层中速度最快的,通常在几纳秒(ns)级别。
- 容量:L1 缓存的容量较小,一般在 16KB 到 128KB 之间。
- 用途 :L1 缓存通常分为两个部分:
- L1 数据缓存:存储数据。
- L1 指令缓存:存储即将执行的指令。
- 特点:L1 缓存是最小的,但也最快,主要存储当前正在处理的数据和指令。由于其速度快,L1 缓存的命中率对整体性能影响较大。
2. L2 缓存(二级缓存)
- 位置:L2 缓存位于 CPU 核心内或与多个核心共享(这取决于 CPU 架构)。
- 速度:L2 缓存的速度比 L1 缓存稍慢,但仍比主内存快,通常在 10 纳秒左右。
- 容量:L2 缓存的容量比 L1 大,通常在 128KB 到几 MB 之间。
- 用途:L2 缓存用于存储从 L1 缓存中淘汰的数据,或者那些 L1 缓存没有命中的数据。它在 L1 缓存和 L3 缓存之间起到了桥梁作用,缓解了 CPU 核心访问主内存时的瓶颈。
- 特点:L2 缓存要比 L1 大,速度稍慢,但通常每个核心都拥有独立的 L2 缓存。
3. L3 缓存(三级缓存)
- 位置:L3 缓存通常是多个 CPU 核心共享的缓存层级,它位于 L2 缓存之上,甚至可能在多核处理器中共享给所有核心。
- 速度:L3 缓存的速度比 L1 和 L2 缓存慢,但仍比访问主内存快,通常在几十纳秒(ns)级别。
- 容量:L3 缓存的容量最大,一般从几 MB 到几十 MB 不等,具体取决于处理器架构。
- 用途:L3 缓存用于存储那些在 L1 和 L2 缓存中都未命中的数据,它起到了对多个核心的共享存储作用,帮助缓解访问主内存的压力。
- 特点:L3 缓存是多个核心共享的,容量大但速度较慢。其设计目的是减少多个 CPU 核心之间的访问冲突和数据不一致。
4. 主内存(RAM)
当缓存(L1、L2、L3)都没有命中时,CPU 会访问主内存(RAM)。虽然主内存的存取速度比缓存慢得多(通常在几十到几百纳秒之间),但它的容量较大,能够存储更多的数据。
5. 缓存命中率和访问延迟
- 缓存命中率:缓存命中率指的是 CPU 请求的数据在缓存中找到的概率。缓存命中率越高,CPU 能更快地从缓存中获取数据,性能表现越好。
- 访问延迟:访问延迟是指从 CPU 发出请求到数据返回的时间。L1 缓存的访问延迟最短,而主内存的访问延迟最长。
多级缓存的设计目的
- 提高性能:通过层级化缓存,尽量减少 CPU 访问较慢的主内存的次数,从而提高整体计算性能。
- 平衡速度和容量:L1 缓存速度最快,但容量小;L3 缓存容量大,但速度较慢。通过层级缓存的设计,可以在速度和容量之间找到平衡。
- 减少内存带宽压力:多个级别的缓存减少了 CPU 对主内存的访问,降低了内存带宽的压力,提高了 CPU 处理能力。
多级缓存的业务实现
L1 :客户端缓存(SDK实现-Redis/服务缓存)
L2 : 服务端缓存(本地缓存)
L3: 第三方缓存(Redis)
1、在管理功能进行埋点,持久化DB数据到Redis,保证数据一致性
- 数据双写 ,先写Redis,再写数据库,数据库失败,则回滚缓存数据(单条记录要加锁)
- 数据检测脚本,监控redis 数据和数据库的一致性,晚上跑,自动更新修复数据
2、服务缓存- Go使用本地缓存,作为临时缓存挡住一部分请求
- 缓存时间设置为 1 min ,配置到 Apollo,主要挡住突发流量以及热点数据流量
- 降级处理,本地缓存失效,走Redis获取,再存储到本地缓存
3、SDK 获取时,在当前服务写缓存,缓存时间5秒
好处:
1、多级缓存机制,保证服务高可用
坏处:
1、增加维护成本
2、数据实时性变差(要看业务能否接受)