文章摘要
GPT 4
此内容根据文章生成,仅用于文章内容的解释与总结
投诉

缓存雪崩

通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。

img

那么,当大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。

对于缓存雪崩问题,我们可以采用两种方案解决。

  • 将缓存失效时间随机打散: 我们可以在原有的失效时间基础上增加一个随机值(比如 1 到 10 分钟)这样每个缓存的过期时间都不重复了,也就降低了缓存集体失效的概率。
  • 设置缓存不过期: 我们可以通过后台服务来更新缓存数据,从而避免因为缓存失效造成的缓存雪崩,也可以在一定程度上避免缓存并发问题。

缓存击穿

我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。 应对缓存击穿可以采取前面说到两种方案:

  • 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

缓存穿透

Redis的缓存穿透问题是一个在缓存应用中常见的性能问题,具体指的是用户请求的数据在缓存中不存在,且在数据库中也不存在。由于缓存未命中,这些请求会绕过缓存直接访问数据库,导致数据库压力骤增,甚至可能引发系统崩溃。

假设在一个电商系统中,用户通过商品ID来查询商品信息。这个系统使用了Redis作为缓存层,MySQL作为数据存储层。正常情况下,用户请求商品信息时,系统会首先在Redis中查找,如果找到则直接返回结果;如果Redis中没有找到,则去MySQL中查询,并将查询结果缓存到Redis中以便后续使用。

然而,如果攻击者知道某些商品ID在数据库中并不存在(比如通过扫描商品ID范围或使用非法参数),他们就可以大量发送这些不存在的商品ID的查询请求。由于这些请求在Redis中找不到对应的数据,它们会全部打到MySQL上。如果这类请求的数量非常庞大,就会给MySQL带来巨大的压力,甚至可能导致MySQL崩溃,从而影响整个系统的正常运行。

缓存穿透的发生一般有这两种情况:

  • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
  • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;

应对缓存穿透的方案,常见的方案有三种。

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

  • 设置空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在,即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

    布隆过滤器是一种概率型数据结构,用于判断一个元素是否在一个集合中。虽然它存在一定的误判率,但可以有效地减少不必要的数据库查询。在查询Redis之前,先使用布隆过滤器判断请求的商品ID是否存在,如果不存在则直接返回,不再查询数据库。

布隆过滤器

布隆(Bloom Filter)过滤器——全面讲解,建议收藏-CSDN博客

Q:假设已经将布隆过滤器注册为bean对象,为了防止缓存穿透的问题,我在其他service层使用布隆过滤器的时候的逻辑如何写?查询不到的话直接返回空对象吗?

A:在使用布隆过滤器来防止缓存穿透时,你的策略通常是在访问缓存之前先通过布隆过滤器快速检查请求的数据是否可能存在于你的数据库中或缓存中。如果布隆过滤器判断该数据“不存在”(即mightContain方法返回false),则可以直接返回一个空对象或错误信息,避免进一步查询数据库或缓存。如果布隆过滤器判断该数据“可能存在”(即mightContain方法返回true),则继续执行后续的缓存查询或数据库查询逻辑。

以下是一个简化的服务层方法示例,该方法展示了如何在使用Spring Boot和布隆过滤器时防止缓存穿透:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service  
public class YourService {

@Autowired
private RedisTemplate redisTemplate; // 假设这是你的缓存服务

@Autowired
private BloomFilterService bloomFilterService; // 假设这是你的布隆过滤器服务

@Autowired
private YourDatabaseService yourDatabaseService; // 假设这是你的数据库服务

// 业务做查询的方法
public YourDataObject getDataById(String id) {
// 首先,通过布隆过滤器检查该id是否可能存在
if (!bloomFilterService.mightContain(id)) {
// 如果布隆过滤器判断该id不存在,则直接返回一个空对象或错误信息
// 这里以返回null为例,但实际应用中可能需要更明确的响应
return null;
}

// 如果布隆过滤器判断该id可能存在,则继续查询缓存
YourDataObject cachedData = redisTemplate.get(id);
if (cachedData != null) {
// 如果缓存中有数据,则直接返回缓存数据
return cachedData;
}

// 如果缓存中没有数据,则查询数据库
YourDataObject dbData = yourDatabaseService.getById(id);
if (dbData != null) {
// 如果数据库中有数据,则更新缓存(如果需要)并返回数据
yourCacheService.put(id, dbData); // 假设这是你的缓存更新方法
return dbData;
}

// 如果数据库中也没有数据,则返回一个空对象或错误信息
// 这里以返回null为例
return null;
}
}

Spring Boot项目中使用布隆过滤器

在Spring Boot项目中使用布隆过滤器(Bloom Filter)可以有效地减少查询数据库或缓存的次数,特别是在处理大量数据并判断元素是否存在于某个集合时。布隆过滤器通过牺牲一定的错误率(即假正率)来换取空间效率和查询时间的显著提升。

步骤一:添加依赖

首先,你需要找到一个Java实现的布隆过滤器库。Google的Guava库提供了布隆过滤器的实现,你可以通过Maven或Gradle将其添加到你的项目中。

1
2
3
4
5
<dependency>  
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>你的Guava版本号</version>
</dependency>

请替换你的Guava版本号为最新的或者适合你项目的版本。

步骤二:配置布隆过滤器

在你的Spring Boot应用中,你可能需要配置一个布隆过滤器,并决定其预期的元素数量、错误率等参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.google.common.hash.Funnels;  
import com.google.common.hash.Hashing;
import com.google.common.base.Charsets;

import java.nio.charset.Charset;

public class BloomFilterConfig {

private static final int expectedInsertions = 1000000; // 预期插入的元素数量
private static final double fpp = 0.01; // 假正率

public static BloomFilter<CharSequence> createBloomFilter() {
// 使用Guava的BloomFilter.create函数创建布隆过滤器
// Funnels.stringFunnel(Charsets.UTF_8) 是将字符串转换为字节序列的函数
return BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),
expectedInsertions,
fpp
);
}
}

步骤三:使用布隆过滤器

在Spring Boot应用中,你可以在服务层或数据访问层使用布隆过滤器来减少不必要的数据库或缓存查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.google.common.hash.BloomFilter;  

@Service
public class SomeService {

private final BloomFilter<CharSequence> bloomFilter;

public SomeService() {
this.bloomFilter = BloomFilterConfig.createBloomFilter();
// 初始化时,可以添加一些元素到布隆过滤器中
// bloomFilter.put("someElement");
}

public boolean mightContain(String element) {
return bloomFilter.mightContain(element);
}

// 在需要的地方使用mightContain来减少查询
public void someMethod() {
String elementToCheck = "testElement";
if (bloomFilter.mightContain(elementToCheck)) {
// 假设这里执行数据库或缓存查询
// 注意:布隆过滤器可能有假正率,所以这里的查询是必要的
// ...
} else {
// 元素肯定不在集合中,无需进一步查询
}
}
}

注意事项

  • 布隆过滤器只能告诉你某个元素可能存在于集合中,但不能保证一定存在。
  • 当你从布隆过滤器得到true时,你需要进一步验证元素是否真的存在于你的数据源中(比如数据库或缓存)。
  • 当你从布隆过滤器得到false时,你可以确信元素不在集合中,因为布隆过滤器不会有假负率。
  • 布隆过滤器的性能和空间效率受其参数(如预期插入的元素数量和假正率)的影响,需要根据你的具体需求进行调整。

数据插入布隆过滤器的时机

将数据插入布隆过滤器(Bloom Filter)的时机主要取决于你的应用场景和需求。一般来说,以下情况是将数据插入布隆过滤器的常见时机:

  1. 新增数据时
    • 当有新数据需要被加入到你的系统中,并且这些数据未来可能需要被快速检查是否存在时,你应该在新增数据的同时将其插入到布隆过滤器中。这样可以利用布隆过滤器的空间效率和查询速度优势,快速判断新数据是否已存在,避免重复插入或执行不必要的操作。
  2. 数据更新时(视情况而定)
    • 数据更新时是否需要将数据重新插入布隆过滤器取决于你的具体需求。如果数据更新只是修改了某些字段的值,而这些字段与布隆过滤器中的检查逻辑无关,那么可能不需要重新插入。但是,如果数据更新改变了布隆过滤器中用于判断的关键信息(比如唯一标识符),那么你可能需要重新插入更新后的数据到布隆过滤器中,以确保查询的准确性。
  3. 缓存失效或数据库数据变动时
    • 在使用布隆过滤器作为缓存失效或数据库数据存在性检查的辅助手段时,如果缓存中的数据失效或数据库中的数据发生了变动(如新增、删除等),你需要同步更新布隆过滤器中的数据,以保持其与实际数据的一致性。
  4. 初始化或系统启动时
    • 在系统初始化或启动时,如果你需要将一些基础数据或热点数据预先加载到布隆过滤器中以提高查询效率,那么此时也是将数据插入布隆过滤器的合适时机。

需要注意的是,布隆过滤器存在误判率,即有可能将不存在的元素误判为存在。因此,在使用布隆过滤器进行存在性判断时,如果得到的结果为“存在”,你可能还需要通过其他方式(如直接查询数据库或缓存)来进一步验证。

此外,由于布隆过滤器不支持删除操作(或删除操作会导致误判率增加),因此在设计系统时需要考虑到这一点,避免在需要频繁删除元素的场景中使用布隆过滤器。如果确实需要删除元素,可以考虑使用其他数据结构或算法来辅助实现。