1. 缓存雪崩、缓存击穿、缓存穿透
1.1 缓存雪崩
定义:
缓存雪崩是指在同一时间大量缓存同时失效,导致大量请求直接到达数据库,造成数据库负载过高,甚至可能导致崩溃。例如,在一个电商网站上,某些商品信息的缓存同时设定了相同的过期时间,当这些缓存同时失效时,大量的用户请求将直接打向数据库,从而引发雪崩效应。
解决方法:
- 随机打散缓存失效时间:
- 为每个缓存设置一个基础失效时间的同时,加入一个随机范围的时间。比如商品信息的缓存基础失效时间为 10 分钟,那么可以再加上 1-3 分钟的随机时间,使得缓存失效时间更分散,避免大量缓存同时失效。
- 设置缓存不过期:
- 对一些重要的、频繁访问的数据,可以考虑设置为永久缓存,不设过期时间。虽然这样会占用一些内存,但避免了缓存集中失效带来的雪崩风险。可以定期手动刷新这些缓存,保证数据及时性。
- 双层缓存机制:
- 在一级缓存(比如 Redis)之外加一个二级缓存(比如本地缓存),当一级缓存失效时,可以暂时从二级缓存中获取数据,减少直接打向数据库的请求数量,减轻压力。
1.2 缓存击穿
定义:
缓存击穿是指一个非常热门的数据在缓存失效后,短时间内有大量请求集中访问该数据,导致在缓存重建前大量请求涌入数据库,给数据库带来很大压力。比如一个热门商品在秒杀活动中失效时,很多用户在同一时间访问,导致大量请求涌向数据库。
解决方法:
-
互斥锁机制:
-
当一个缓存失效后,第一个请求尝试从数据库中获取数据的同时,加上互斥锁,其他请求将会等待锁释放后再获取数据。这种方法可以有效防止大量请求同时去访问数据库。
-
在实现上可以使用分布式锁,比如使用 Redis 自带的
SETNX
和EXPIRE
命令,给数据设置锁和锁的有效期。这样只会有一个线程查询数据库并重建缓存,其他线程等待缓存重建完毕即可。
-
-
热点数据缓存永不过期:
- 对于一些访问量特别高、且内容变化不频繁的热点数据,可以直接设置为不过期缓存。这样即使数据需要更新,也可以手动刷新缓存,不让数据过期。
-
提前缓存重建:
- 对于预估会有大量访问的热点数据,提前在缓存即将过期前主动刷新缓存数据,保证热点数据一直有效,防止失效后大量请求进入数据库。
1.3 缓存穿透
定义:
缓存穿透是指用户请求的数据在缓存和数据库中都不存在,导致每次请求都会直接访问数据库,进而可能对数据库造成高压力。缓存穿透通常是由错误的请求或者恶意的攻击引发的,比如频繁请求不存在的用户信息。
解决方法:
- 限制非法请求:
- 可以通过限制 API 的访问频率或者对可疑 IP 进行屏蔽,防止恶意攻击。比如使用限流算法,对同一时间段内的频繁访问进行拦截。
- 设置空值缓存:
- 对于经常被访问但实际不存在的数据,可以将其在缓存中设置为空值或者默认值。这样在短时间内相同的请求就会直接返回空值,而不会多次查询数据库。设置空值缓存时需要注意过期时间,以免过多占用缓存空间。
- 使用布隆过滤器:
- 布隆过滤器是一种空间效率非常高的数据结构,可以用于快速判断数据是否存在。在缓存前端添加一个布隆过滤器,将所有可能存在的数据存入其中,用户请求时先通过布隆过滤器判断,如果数据不在过滤器内,直接返回"无数据",避免无意义的数据库查询。
2. 热点数据的缓存
我们以电商平台为例,设计一个动态缓存策略,通过数据的最新访问时间来缓存热门商品。在这里,我们会用 Redis 的 zadd
和 zrange
方法来实现排序和筛选,确保热点数据始终有效,避免不常访问的商品占用缓存空间。
2.1 设计步骤
我们可以使用 Redis 的 有序集合 (Sorted Set) 作为核心工具,通过访问时间更新队列,实现排名和定期清理的策略。
1. 构建排序队列并记录访问时间
- 使用 Redis 的
zadd
命令,将商品 ID 和访问时间添加到 Redis 有序集合中,例如zadd product_rank <访问时间戳> <商品ID>
。 - 每次用户访问某个商品时,更新该商品的访问时间(访问时间戳越大,商品排名越靠前)。
2. 定期淘汰冷门商品
-
可以设置一个定时任务(例如每隔一小时运行一次),定期检查
product_rank
队列中的商品排名。 -
使用
zrange
或zrevrange
命令按访问时间排序,获取商品的排名信息,例如获取访问排名靠后的商品(可以通过zrange product_rank 0 <N>
来获取排名最后的 N 个商品ID)。 -
对于这些低频访问的商品,我们可以直接将其从缓存中移除,从而为更热门的商品腾出空间。例如:
javaredis.zremrangebyrank("product_rank", 0, <N>); // 移除排名最靠后的商品
3. 随机补充商品数据
-
为保持缓存的多样性,可以随机从数据库中抽取一些商品数据补充到缓存中。随机补充数据有助于增加缓存的丰富性,避免用户的偏爱商品总是占据缓存空间。
-
将随机抽取到的商品信息添加到 Redis 的缓存队列
product_rank
,并分配一个合适的时间戳(可以是当前时间),这样这些商品会位于较低的排名,不会与热点商品争夺缓存空间。
4. 示例代码说明
以下是伪代码示例,帮助直观理解:
java
// 每次商品被访问时,更新商品的访问时间
String productId = "商品ID";
long currentTimestamp = System.currentTimeMillis();
redis.zadd("product_rank", currentTimestamp, productId); // 更新访问时间
// 定期清理冷门商品
int N = 100; // 假设需要移除访问量排名靠后的100个商品
List<String> lowRankProducts = redis.zrange("product_rank", 0, N - 1); // 获取排名靠后的商品ID
for (String id : lowRankProducts) {
redis.del("product_data:" + id); // 从缓存中移除冷门商品数据
}
redis.zremrangebyrank("product_rank", 0, N - 1); // 从队列中移除这些商品的记录
// 随机从数据库中补充商品
List<String> randomProducts = db.query("SELECT id FROM products ORDER BY RAND() LIMIT 20"); // 随机取出20个商品
for (String id : randomProducts) {
String productData = db.query("SELECT * FROM products WHERE id = ?", id); // 获取商品详细信息
redis.set("product_data:" + id, productData); // 将商品数据添加到缓存
redis.zadd("product_rank", currentTimestamp, id); // 为新商品添加当前时间戳
}
2.2 总结
- 访问时间更新与排名: 通过 Redis 有序集合的时间戳记录商品访问时间,每次访问更新排名。
- 定期移除冷门商品: 定期清理访问排名低的商品,从缓存中移除,节省空间。
- 随机补充数据: 从数据库中随机选取部分商品补充缓存,保持缓存数据多样性。
这样设计的缓存策略能动态跟踪用户偏好,确保热点商品优先留在缓存中,同时自动移除低频访问的商品,以更合理地使用缓存资源。
3. 常见的缓存更新策略
3.1 Cache Aside(旁路缓存)策略
Cache Aside 策略是目前应用最广泛的缓存策略,常用于需要手动控制缓存和数据库数据同步的场景。它的核心是"旁路"缓存,即在访问时通过代码逻辑主动更新缓存。
操作流程
-
读取数据:
- 请求数据时,先检查缓存中是否存在该数据。
- 如果缓存命中(即数据存在),直接返回缓存中的数据。
- 如果缓存未命中(即数据不存在),则从数据库中读取数据。
- 将读取到的数据放入缓存中,设置过期时间(通常是几分钟到几个小时,视业务需求而定)。
- 返回数据给客户端。
-
写入数据:
- 当需要更新数据时,首先直接更新数据库中的数据。
- 数据库更新成功后,删除或更新缓存中的该数据。
- 这一步避免了缓存中的旧数据影响新请求的数据读取。
关键点:为什么要先更新数据库,再删除缓存?
在更新数据库和缓存的顺序上,先更新数据库再删除缓存是为了确保数据的一致性和避免并发数据问题,具体原因如下:
-
防止数据不一致:如果先删除缓存再更新数据库,在缓存被删除和数据库更新完成的时间间隙里,其他请求可能会查询缓存。由于缓存被删除,这些请求会直接去数据库读取到旧数据,最终导致缓存数据和数据库不一致。
-
避免并发问题:如果先删除缓存,然后还没有来得及更新数据库,此时其他请求进入数据库获取旧数据,并将旧数据写入缓存。这种情况下,即使最终更新了数据库,缓存仍然可能保留旧数据,导致数据不一致。
因此,通过先更新数据库再删除缓存可以确保数据一致性,减少并发带来的数据错乱问题。
优缺点
- 优点:代码逻辑灵活,适合读多写少的场景;可以精确控制缓存和数据库之间的数据同步。
- 缺点:读写代码会变得更复杂,尤其在高并发或对数据一致性要求较高的场景中,需要非常谨慎地控制缓存和数据库同步。
适用场景
Cache Aside 策略适用于需要较高缓存命中率的场景,比如社交媒体、商品详情展示等读取频率远高于更新频率的业务。
3.2 Read/Write Through(读穿 / 写穿)策略
Read/Write Through 策略采用了"读穿"或"写穿"缓存机制,即直接把数据的读写操作交由缓存来管理。这样可以减少开发者管理缓存和数据库一致性的负担,缓存系统会自动处理数据的读取和同步。
操作流程
- Read Through(读穿):
- 读取数据时,先访问缓存。
- 如果缓存中不存在该数据,缓存系统会自动从数据库中加载数据并返回给客户端,同时将数据存入缓存中。
- Write Through(写穿):
- 更新数据时,直接将数据写入缓存。
- 缓存系统会自动将缓存中的数据同步写入数据库,保证数据库数据与缓存一致。
优缺点
- 优点:
- 数据一致性高:因为缓存系统自动更新数据库和缓存中的数据。
- 实现简单:不需要在代码中管理缓存和数据库的同步,由缓存系统自动完成。
- 缺点:
- 写操作性能较低:每次写入数据时,都会更新缓存和数据库,因此在写操作频繁的场景中可能会造成数据库压力。
- 延迟问题:由于每次写都直接写入数据库,延迟会相对较高,不适合对实时写入性能要求很高的场景。
适用场景
Read/Write Through 策略适合一致性要求高,且写操作频率较低的场景,例如配置管理、用户偏好设置等不频繁更改的数据。
3.3 Write Back(写回)策略
Write Back 策略是一种延迟写策略,即在更新数据时,先将数据写入缓存,而不立即更新数据库,而是等到特定时间点或条件触发时,将缓存中的数据批量写入数据库。这种策略能显著提高写操作性能,但一致性保障较弱。
操作流程
- 写入数据:
- 写操作先将数据更新到缓存中,而不立即写入数据库。
- 将缓存中的数据标记为"已修改"或放入待写回队列中。
- 批量更新数据库:
- 定期或在达到一定条件(如缓存写操作次数达到上限)后,缓存系统会将所有已修改的数据批量写入数据库。
- 写入完成后,重置缓存数据的标记或清空队列。
优缺点
- 优点:
- 写性能极高:只需直接更新缓存,不会频繁操作数据库,写操作响应速度非常快。
- 降低数据库压力:写操作集中在缓存中,延迟到特定时机批量写入数据库,减少了数据库的写入频率。
- 缺点:
- 数据一致性较低:缓存与数据库可能有一段时间不同步,存在数据不一致的风险。
- 可靠性要求高:如果缓存宕机或出现数据丢失,则未写回数据库的数据可能会全部丢失。
适用场景
Write Back 策略适用于对一致性要求较低、但写操作频繁的场景,例如日志系统、用户行为记录等。尤其适合需要较高写入吞吐量的系统。
3.3 总结
策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Cache Aside | 灵活性高,控制精确 | 代码复杂度较高 | 读多写少的场景,如商品详情展示 |
Read/Write Through | 数据一致性高,缓存系统自动同步 | 写操作性能低,延迟较高 | 配置管理、用户设置 |
Write Back | 写性能高,数据库压力小 | 数据一致性低,缓存丢失风险高 | 日志系统、用户行为记录 |
这些缓存策略的选择取决于业务的具体需求。例如,社交平台和电商的商品展示页面可以优先考虑 Cache Aside 以减少数据库压力;而用户偏好的设置和其他一致性要求高的场景适合 Read/Write Through ;而日志系统等写频繁、实时性不高的场景则可以采用 Write Back 策略以保证写操作的高效性。