简述缓存穿透

缓存穿透

什么是缓存穿透?

缓存穿透说简单点就是 客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,但是这些请求都会打到数据库。举个例⼦:某个⿊客故意制造我们缓存中不存在的 key 发起⼤量请求,导致⼤量请求落到数据库。

有哪些解决方法?

最基本的就是⾸先做好参数校验,⼀些不合法的参数请求直接抛出异常信息返回给客户端。⽐如查询的数据库 id 不能⼩于 0、传⼊的邮箱格式不对的时候直接返回错误消息给客户端等等。

1.缓存无效key

如果缓存和数据库都查不到某个 key 的数据就写⼀个到 Redis 中去并设置过期时间,具体命令如下:SET key value EX 10086 。这种⽅式可以解决请求的 key 变化不频繁的情况,如果⿊客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存⼤量⽆效的 key 。很明显,这种⽅案并不能从根本上解决此问题。如果⾮要⽤这种⽅式来解决穿透问题的话,尽量将⽆效的 key 的过期时间设置短⼀点⽐如 1 分钟。

另外,这⾥多说⼀嘴,⼀般情况下我们是这样设计 key 的: 表名:列名:主键名:主键值

如果用Java代码展示的话,差不多是下面这样的:

public Object getObjectInclNullById(Integer id) {

    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    // 缓存为空
    if (cacheValue == null) {
        // 从数据库中获取
        Object storageValue = storage.get(key);
        // 缓存空对象
        cache.set(key, storageValue);
        // 如果存储数据为空,需要设置⼀个过期时间(300秒)
        if (storageValue == null) {
            // 必须设置过期时间,否则有被攻击的⻛险
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    }
    return cacheValue;
}
  • 优点:实现简单,维护方便。
  • 缺点:额外的内存消耗;可能造成短期的不一致

2. 布隆过滤器

布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否可能在一个集合中的数据结构。它可以用于在大规模数据集中快速查找是否存在某个元素,而且具有较高的空间效率和查询效率。

布隆过滤器的底层原理基于位向量(Bit Array)和多个哈希函数。它通常由以下几个组成部分:

  1. 位向量(Bit Array):布隆过滤器使用一个由二进制位组成的位向量来表示数据集合。初始时,所有位都被设置为0。
  2. 多个哈希函数:布隆过滤器需要使用多个独立的哈希函数。哈希函数的作用是将输入的元素映射成位向量中的多个位置,每个哈希函数对应一个位向量的位置。通常,哈希函数的输出值是一个整数,可以将其对位向量的长度取模,从而得到一个在合法范围内的位向量位置。
  3. 添加元素(Insertion):当要将一个元素添加到布隆过滤器中时,首先将该元素输入到多个哈希函数中,得到多个位向量位置,并将这些位置的位设置为1。
  4. 查询元素(Query):当要查询一个元素是否在布隆过滤器中时,同样需要将该元素输入到多个哈希函数中,得到多个位向量位置。如果发现其中任何一个位置的位为0,则可以确定该元素一定不在布隆过滤器中。如果所有位置的位都为1,则不能确定该元素一定在布隆过滤器中,因为可能存在冲突。

布隆过滤器的查询效率非常高,因为它只需进行位运算和内存访问,不需要实际比较元素的值。但是,布隆过滤器有一定的误判率。由于多个元素可能映射到同一个位向量位置,导致冲突,所以查询结果可能会有误判。误判率随着位向量的长度和哈希函数的数量增加而降低,但会增加布隆过滤器的空间占用。

因此,布隆过滤器适用于那些对一定的误判率能够容忍的场景,比如大规模数据集合中的快速查找和去重。但对于不能容忍误判的情况,布隆过滤器并不适合使用。

加入布隆过滤器之后的缓存处理流程如下:


但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是:布隆过滤器说某个元素存在,⼩概率会误判。布隆过滤器说某个元素不在,那么这个元素⼀定不在。

2.1 布隆过滤器如何配置

一般来说在开始使用布隆过滤器之前,需要先将集合中的所有数据依次添加进布隆过滤器中。这个过程称为布隆过滤器的建立(Build) 或者填充(Populate)。只有在这些数据被添加进布隆过滤器后,布隆过滤器才具备判断功能,能够在后续的查询中快速判断一个元素是否可能在集合中存在。

布隆过滤器在建立之后是不支持动态扩容的,即不能在建立后再添加新的位数组位置,因此在使用时,需要根据预估的元素数量和误判率来设置合适的位数组长度和哈希函数个数。

在Java项目中使用布隆过滤器通常需要借助第三方库来实现。有许多开源的布隆过滤器库可供选择,其中比较常用的包括Guava的BloomFilter、Apache Commons Collections的BloomFilter等。你可以根据自己的需求选择合适的库。

下面我们将使用一个简单的示例代码,演示了如何使用Guava库中的布隆过滤器:

首先,需要在项目的pom.xml(如果是使用Maven管理项目)中添加Guava库的依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version> <!-- 最新版本可能会有变化,可根据实际情况修改 -->
</dependency>

然后在Java代码中创建和使用布隆过滤器:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class BloomFilterExample {
    public static void main(String[] args) {
        // 假设需要过滤的数据为字符串类型
        int expectedInsertions = 10000; // 预期插入的元素数量
        double fpp = 0.01; // 期望的误判率(False Positive Probability),这里设置为0.01表示1%
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), expectedInsertions, fpp);

        // 添加元素到布隆过滤器中
        bloomFilter.put("element1");
        bloomFilter.put("element2");
        // ...

        // 查询元素是否可能存在于布隆过滤器中
        boolean exists = bloomFilter.mightContain("element1");
        System.out.println("element1可能存在于布隆过滤器中:" + exists);
    }
}

在上面的示例代码中,我们首先使用BloomFilter.create方法创建了一个布隆过滤器。需要指定预期插入的元素数量(expectedInsertions)和期望的误判率(fpp)。然后使用put方法将元素添加到布隆过滤器中。最后,使用mightContain方法查询元素是否可能存在于布隆过滤器中。

当然,我们也可以实现定期从数据库中读取数据并填充到布隆过滤器,一般来说我们使用定时任务来完成。在Java项目中,我们可以使用ScheduledExecutorService来实现定时任务,下面我们给出一个示例代码方便理解使用ScheduledExecutorService来定时更新布隆过滤器:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class BloomFilterUpdater {
    private BloomFilter<String> bloomFilter;

    public BloomFilterUpdater(int expectedInsertions, double fpp) {
        bloomFilter = BloomFilter.create(Funnels.unencodedCharsFunnel(), expectedInsertions, fpp);
    }

    // 定期更新布隆过滤器的任务
    private void updateBloomFilter() {
        // TODO: 从数据库中读取数据并填充到布隆过滤器中
        // 示例:假设从数据库中读取数据到dataList中
        // for (String data : dataList) {
        //     bloomFilter.put(data);
        // }
    }

    public static void main(String[] args) {
        int expectedInsertions = 10000; // 预期插入的元素数量
        double fpp = 0.01; // 期望的误判率(False Positive Probability),这里设置为0.01表示1%

        // 创建布隆过滤器更新器
        BloomFilterUpdater updater = new BloomFilterUpdater(expectedInsertions, fpp);

        // 创建定时任务执行器
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        // 每隔一定时间执行一次布隆过滤器的更新任务
        long initialDelay = 0; // 初始延迟时间,单位为毫秒
        long period = 1; // 间隔时间,单位为天,这里设置为1天
        scheduler.scheduleAtFixedRate(updater::updateBloomFilter, initialDelay, period, TimeUnit.DAYS);
    }
}

在上面的示例代码中,我们首先创建了一个BloomFilterUpdater类来管理布隆过滤器,并在其中实现了定期更新布隆过滤器的方法updateBloomFilter。该方法中可以根据实际需求从数据库中读取数据,并将数据填充到布隆过滤器中。在main方法中,我们创建了一个定时任务执行器ScheduledExecutorService,然后使用scheduleAtFixedRate方法来设置定时任务,指定了初始延迟时间和间隔时间。定时任务会周期性地执行updateBloomFilter方法,从而定期更新布隆过滤器。

注意:在实际应用中,需要根据具体的业务需求来确定定时任务的执行间隔和从数据库中读取数据的逻辑。另外,定时任务执行的频率不宜过高,以免给数据库和系统带来过大的压力。适当地设置执行间隔可以平衡数据同步和性能。

总结起来,布隆过滤器的配置通常涉及以下几个方面:

  1. 选择合适的布隆过滤器库,例如Guava、Apache Commons Collections等。
  2. 根据预期插入的元素数量和期望的误判率来创建布隆过滤器。
  3. 在系统启动时或定期地从数据库中读取数据并填充到布隆过滤器中,保持布隆过滤器与数据库数据的同步。
  4. 使用布隆过滤器进行查询,判断元素是否可能存在于集合中。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容