首页 > 其他分享 >彻底理解布隆过滤器怎么解决缓存穿透问题

彻底理解布隆过滤器怎么解决缓存穿透问题

时间:2024-12-11 23:58:10浏览次数:13  
标签:缓存 String 布隆 key 过滤器 public

一.业务背景

实际业务中使用Redis,都是先通过用户插入数据到Mysql中,然后更新缓存到Redis,下一次用户再查询该数据的时候就可以通过Redis来进行查询。

先看下图,是假设的一个用户查询的场景:

首先用户查询的时候会去缓存里面查询,查看是否有该数据,如果不存在,就会去Mysql中查询,然后将数据返回给用户和缓存到Redis;

然后一些非法请求过来, 查询的一些Key在Redis里面不存在,并且Mysql也不存在,这些数据就不会被缓存到Redis中;

此时,这中频繁的发起伪造的请求,就相当于导致Redis的缓存不存在了,所有请求都会穿透Redis的缓存去直接访问Mysql,就会把Mysql搞宕机。

这就是我们所说的缓存穿透问题,解决这个问题可以通过布隆过滤器。

二.什么是BitMaps

在谈布隆过滤器之前,需要先了解Bitmaps,它是Redis的一种高级数据结构。

定义:Bitmaps(位图)是一种数据结构,用于表示一个由二进制位(bit)组成的集合,每个二进制位的值可以是 0 或 1,通常用于表示某些状态或者集合中元素的存在与否,位图在内存中高效地存储和操作大量的布尔值(即 0 或 1),因此在许多场景中,特别是在高效存储和查找时,具有非常重要的应用。

比如现在要去存一个1 3 5 7,在BitMap里面怎么表示?

从上图来看,每个整数映射到它的值(例如,1 映射到第 1 位,3 映射到第 3 位,等等),1存入的时候,存在bit[1],3存在bit[3],5存在bit[5],7存在bit[7],那么就只需要一个bit类型的8位数组,它就只有一个bit的大小。

在BitMap里面又诞生了一个叫做布隆过滤器,在大致了解了布隆过滤器后底层实现后,再来谈谈布隆过滤器。

三.布隆过滤器

1970 年布隆提出了一种布隆过滤器的算法(概率判断算法),用来判断一个元素是否在一个集合中,这种算法由一个二进制数组和一个 Hash 算法组成。

比如上面的查询逻辑,当不正常的用户查查询数据的时候,先判断该数据存不存在,不存在直接返回就行了,这样就不会去查询Mysql了。

接下里看一下布隆过滤器的实现:

数据库中存在的数据有A B C,通过Hash算法存储在二进制数组中,并且把对应位置写成1,然后此时过来一个D F数据,接下来要去判断F在不在集合,就需要通过Hash算法计算F出来的位置,此时位置如果是0,代表F不存在,因为不存在的时候默认值就是0;

但是这样可能会出现一个问题,就是Hash算法不能保证存储的数据一定分散在不同的数组位置,所以是会出现Hash冲突的问题,所以说D数据经过计算它所在的位置依然是1,这就是布隆过滤器的误判问题。

1.为什么误判问题不是很重要?

通过Hash计算在数组上不一定是数据库中存在的数据,但是通过Hash计算不在数组的一定不在集合,这种误判的概率是很小的,所以及时出现误判,也是可以运行的。

2.优化方案:

(1)可以通话增大数组来减少Hash冲突的概率;

(2)采用两个Hash算法,只要一Hash算法匹配不存在就不存在:

在 Guava 中,你可以通过实现 Funnel 接口和自定义 Hasher 来使用多个哈希算法,也就是说,可以在 Hasher 中使用两个不同的哈希函数来计算同一个元素的多个哈希值,测试代码如下:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.base.Charsets;

import java.nio.charset.StandardCharsets;

public class CustomBloomFilter {
    public static void main(String[] args) {
        // 创建一个自定义的Funnel,使用两个哈希算法
        Funnel<String> funnel = new Funnel<String>() {
            @Override
            public void funnel(String from, Hasher into) {
                // 使用第一个哈希算法(MurmurHash)
                into.putString(from, StandardCharsets.UTF_8);
                int murmurHash = Hashing.murmur3_128().hashString(from, StandardCharsets.UTF_8).asInt();
                into.putInt(murmurHash);

                // 使用第二个哈希算法(SHA-256)
                byte[] sha256Hash = Hashing.sha256().hashString(from, StandardCharsets.UTF_8).asBytes();
                for (byte b : sha256Hash) {
                    into.putByte(b);
                }
            }
        };

        // 创建布隆过滤器
        BloomFilter<String> bloomFilter = BloomFilter.create(funnel, 100000, 0.01);

        // 测试布隆过滤器
        String value = "hello";
        bloomFilter.put(value);

        System.out.println("Contains 'hello'? " + bloomFilter.mightContain(value));
    }
}

四.缓存击穿的解决

流程如下:

1.创建一个布隆过滤器,用来存储需要缓存的数据的键;

2.如果用户查询的数据不存在布隆过滤器中,直接返回;

3.如果用户查询的数据存在布隆过滤器,通过Redis去获取数据;

4.如果Redis没有获取到数据,再查询Mysql,并将该数据的加入布隆过滤器中。

通过上诉几步,就可以解决缓存击穿问题。

五.代码实现

下面写一段简单代码,举例一下实现步骤:

1.引入布隆过滤器库

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.0.1-jre</version>
</dependency>

2. 布隆过滤器初始化和配置

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

import java.nio.charset.StandardCharsets;

public class BloomFilterExample {

    // 创建一个布隆过滤器
    private static BloomFilter<String> bloomFilter;

    public static void initBloomFilter() {
        // 初始化布隆过滤器,预计存储100000个元素,错误率设为0.01
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), 100000, 0.01);
        // 添加初始的元素到布隆过滤器
        addToBloomFilter("user123");
        addToBloomFilter("user456");
    }

    // 向布隆过滤器添加元素
    public static void addToBloomFilter(String data) {
        bloomFilter.put(data);
    }

    // 检查元素是否存在布隆过滤器中
    public static boolean mightContain(String data) {
        return bloomFilter.mightContain(data);
    }
}

3.模拟缓存穿透防护

import java.util.HashMap;
import java.util.Map;

public class CacheService {

    // 模拟数据库(简单的 HashMap)
    private static final Map<String, String> database = new HashMap<>();

    static {
        // 模拟数据库中存在的数据
        database.put("user123", "User 123 Data");
        database.put("user456", "User 456 Data");
    }

    // 模拟缓存
    private static final Map<String, String> cache = new HashMap<>();

    public static String getFromCache(String key) {
        return cache.get(key);
    }

    public static void setCache(String key, String value) {
        cache.put(key, value);
    }

    // 模拟从数据库查询数据
    public static String getFromDatabase(String key) {
        return database.get(key);
    }

    // 使用布隆过滤器防止缓存穿透
    public static String getData(String key) {
        // 先检查布隆过滤器,防止缓存穿透
        if (!BloomFilterExample.mightContain(key)) {
            return "Data does not exist";  // 直接返回数据不存在
        }

        // 查缓存
        String cachedData = getFromCache(key);
        if (cachedData != null) {
            return cachedData;  // 缓存命中
        }

        // 如果缓存没有,查数据库
        String dbData = getFromDatabase(key);
        if (dbData != null) {
            // 如果数据库中有,设置到缓存
            setCache(key, dbData);
            // 更新布隆过滤器
            BloomFilterExample.addToBloomFilter(key);
            return dbData;
        } else {       
            return "Data does not exist";  // 数据不存在
        }
    }
}

 4.测试代码

public class Main {
    public static void main(String[] args) {
        // 初始化布隆过滤器
        BloomFilterExample.initBloomFilter();

        // 模拟请求的数据
        String[] keys = {"user123", "user456", "user789"};

        for (String key : keys) {
            System.out.println("Fetching data for: " + key);
            String result = CacheService.getData(key);
            System.out.println(result);
        }
    }
}

六.总结

1.通过使用布隆过滤器,可以显著减少不必要的数据库查询,防止缓存穿透,提升系统性能;

2.布隆过滤器虽然有误判的可能(会错误地判断一个元素存在),但不会漏判,这保证了数据一致性和系统的稳定性。

标签:缓存,String,布隆,key,过滤器,public
From: https://blog.csdn.net/BestandW1shEs_lsy/article/details/144384219

相关文章

  • 微信H5页面如何更新缓存?
    微信H5页面缓存问题一直比较头疼,因为微信内置浏览器内核的更新策略和缓存机制比较复杂,不容易控制。以下是一些常用的更新缓存的策略,前端开发中可以根据实际情况选择使用:1.文件名添加版本号或哈希值:这是最常用的方法,也是最有效的方法之一。通过在文件名后面添加版本号(例如inde......
  • 巧用缓存:高效实现基于 read4 的文件读取方法
    文章目录摘要描述题目描述要求read4方法定义read方法定义题解答案题解代码题解代码分析示例测试及结果示例测试代码示例运行结果时间复杂度空间复杂度总结关于我们摘要本篇文章将探讨一道经典的编程题:通过read4方法实现读取n个字符的功能。我们将详细介......
  • web缓存需要后台或者运维怎么配合呢?
    Web缓存需要后端或运维的配合,主要体现在以下几个方面:1.设置合适的HTTP缓存头:这是最重要的一环。后端需要在响应中设置正确的缓存控制头,例如Cache-Control、Expires、ETag、Last-Modified等。这些头信息告诉浏览器和代理服务器如何缓存以及何时缓存失效。前端开发人员......
  • Java Web 开发学习中:过滤器与 Ajax 异步请求
    一、过滤器Filter:过滤器的概念与用途在一个庞大的Web应用中,有许多资源需要受到保护或进行特定的预处理。过滤器就像是一位智能的守卫,站在资源的入口处,根据预先设定的规则,决定哪些请求可以顺利访问资源,哪些请求需要被拦截或进行特殊处理。比如,在众多页面中,判断用户是否登录......
  • safari有一个快捷键非常好用对于前端开发人员 (Command + Option + R)重新加载页面并忽
    SyntaxError:Unexpectedtoken'}',运行前端项目,safari浏览器控制台报如上错误,检查代码没有多大括号,最后发现是浏览器缓存问题。重新加载页面并忽略缓存:按Command+Option+R,这将强制Safari重新加载页面并忽略缓存。这对于开发人员非常有用,尤其是在调试CSS或......
  • Redis与缓存
    目录缓存缓存优缺点 缓存更新策略超时剔除先删缓存再更新数据库旁路缓存(先更新数据库,再删缓存) 先更新数据库,再更新缓存读写穿透​编辑异步缓存写入模式缓存常见问题缓存穿透 缓存雪崩缓存击穿缓存在业务开发中,必然会存在需要频繁访问的数据即热点数据,如果......
  • ASP.NET Core Web API中使用缓存加速响应
    https://www.bilibili.com/video/BV1kpzSYUEES不适用Redis方案,在响应报文中添加Cache-Control:no-cache配合ETag实现缓存加速核心思路,使用https://github.com/SimonCropp/Delta库,监视数据库变更,如果无变更,直接返回304状态码,跳过数据库业务查询,实现相应加速实现变更的原理,M......
  • 【Leetcode Top 100】146. LRU 缓存
    问题背景请你设计并实现一个满足LRU(最近最少使用)缓存约束的数据结构。实现LRUCache类:LRUCache(intcapacity)以正整数作为容量cap......
  • 关于java中的过滤器(Filter)
    在学习SpringBoot中了解到了过滤器和拦截器,浅谈一下对这两个的认识,若有不足,烦请斧正!首先,过滤器是javaWeb中提供的,而拦截器是spring提供的,这篇主要讲解Filter。用法:过滤器允许开发者在请求到达Servlet之前或响应返回客户端之前执行一些预处理或后处理操作(例如登录校验、敏感......
  • 小程序内嵌web-view缓存
    现状:项目是通过web-view内嵌在小程序里的H5页面,然而发布新版本之后,在小程序入口进去看到的还是旧页面原因:通用缓存原理浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存web-view组......