布隆过滤器解决缓存穿透

目录

布隆过滤器介绍

结构分析

使用布隆过滤器

解决缓存穿透



布隆过滤器介绍


布隆过滤器主要是为了解决海量数据的存在性问题。它是一种非常节省空间的概率数据结构,运行速度快,占用内存小。它实际上是一个很长的二进制向量和一系列随机映射函数。

布隆过滤器可以用于检索一个元素是否在一个集合中。主要用于判断一个元素是否在一个集合中。主要是解决大规模数据下不需要精确过滤的场景,如检查垃圾邮件地址,爬虫URL地址去重,解决缓存穿透问题等。

它最大的优点就是支持海量数据场景下高效率判断元素是否存在,而缺点是无法删除且容易出现【多个元素通过哈希后,可能会产生hash碰撞导致映射同一位置】,随着元素逐渐增多且容量不变,那么hash碰撞概率增大。

总体来说,它++判断某个元素存在,由于存在误判,这个元素不一定是存在的;而判断如果某个元素不存在,那这个元素一定不存在++。


结构分析


布隆过滤器主要使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,在初始状态,所有位置的位都是0,并且每个元素只能是 0 或者 1(代表 false 或者 true)。

当一个元素加入布隆过滤器中的时候,会进行如下操作:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

假设有两个hash函数,那么就会在两个位置赋值为1:

当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

使用布隆过滤器


首先引入依赖:

XML 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>

然后再 application.yml 写入配置:

bash 复制代码
spring:
  data:
    redis:
      database: 0
      host: 127.0.0.1
      port: 6379
      password: ****
      timeout: 3000
bloom-filter:
  name: bloom-filter
  expectedInsertions: 1000
  falseProbability: 0.01

属性配置:

java 复制代码
@Data
@ConfigurationProperties(prefix = BloomFilterProperties.PREFIX)
public class BloomFilterProperties {

    public static final String PREFIX = "bloom-filter";
    /**
    * 布隆过滤器名字
    */
    private String name;
    /**
    * 布隆过滤器的容量,初始化容纳约 2 万个 key
    */
    private Long expectedInsertions = 20000L;
    /**
    * 布隆过滤器碰撞率
    */
    private Double falseProbability = 0.01D;
}

然后自动装配:

java 复制代码
@EnableConfigurationProperties(BloomFilterProperties.class)
public class BloomFilterAutoConfiguration {
    
    /**
     * 布隆过滤器
     */
    @Bean
    public BloomFilterHandler rBloomFilterUtil(RedissonClient redissonClient, BloomFilterProperties bloomFilterProperties) {
        return new BloomFilterHandler(redissonClient, bloomFilterProperties);
    }
}

然后写一个布隆过滤器工具组件:

java 复制代码
/**
 * 布隆过滤器处理器(用于解决缓存穿透问题)
 *
 * 作用:
 *  1. 在访问 Redis / 数据库之前,先判断某个 key 是否"可能存在"
 *  2. 对数据库中一定不存在的 key 进行拦截,避免缓存穿透
 *
 * 技术实现:
 *  - 基于 Redisson 提供的 RBloomFilter
 *  - 数据实际存储在 Redis 中
 *  - 支持分布式、多实例共享
 */
public class BloomFilterHandler {

    /**
     * 用于缓存穿透治理的布隆过滤器实例
     *
     * 特点:
     *  - 内部是位数组 + 多个 hash 函数
     *  - 判断结果只有两种:
     *      false:一定不存在(可直接拦截请求)
     *      true :可能存在(继续走缓存/数据库流程)
     */
    private final RBloomFilter<String> cachePenetrationBloomFilter;

    /**
     * 构造方法:初始化布隆过滤器
     *
     * @param redissonClient         Redisson 客户端,用于获取 Redis 中的 BloomFilter
     * @param bloomFilterProperties 布隆过滤器配置参数(容量、误判率等)
     */
    public BloomFilterHandler(RedissonClient redissonClient,
                              BloomFilterProperties bloomFilterProperties) {

        // 根据配置的名称,从 Redis 中获取(或创建)一个布隆过滤器
        // 多个服务实例拿到的是同一个 BloomFilter
        RBloomFilter<String> cachePenetrationBloomFilter =
                redissonClient.getBloomFilter(bloomFilterProperties.getName());

        // 初始化布隆过滤器:
        // expectedInsertions:预计插入的元素数量
        // falseProbability :允许的误判率
        // tryInit 语义:
        //  - 如果 BloomFilter 已存在,不会重复初始化
        //  - 避免服务重启导致布隆过滤器被重置
        cachePenetrationBloomFilter.tryInit(
                bloomFilterProperties.getExpectedInsertions(),
                bloomFilterProperties.getFalseProbability()
        );

        // 赋值给成员变量,供后续 add / contains 使用
        this.cachePenetrationBloomFilter = cachePenetrationBloomFilter;
    }

    /**
     * 向布隆过滤器中添加一个元素
     *
     * 使用场景:
     *  - 系统启动时,将数据库中的主键批量写入 Bloom
     *  - 新增数据成功后,同步写入 Bloom
     *
     * @param data 需要加入布隆过滤器的 key(如 userId / orderNo)
     * @return 是否添加成功
     */
    public boolean add(String data) {
        return cachePenetrationBloomFilter.add(data);
    }

    /**
     * 判断某个 key 是否可能存在
     *
     * 缓存穿透的核心方法:
     *  - false:该 key 在数据库中一定不存在,可直接拦截请求
     *  - true :该 key 可能存在,需要继续查询缓存或数据库
     *
     * @param data 查询的 key
     * @return 是否可能存在
     */
    public boolean contains(String data) {
        return cachePenetrationBloomFilter.contains(data);
    }

    /**
     * 获取预计插入元素数量(配置值)
     *
     * 一般用于:
     *  - 运维监控
     *  - 压测评估
     */
    public long getExpectedInsertions() {
        return cachePenetrationBloomFilter.getExpectedInsertions();
    }

    /**
     * 获取布隆过滤器的误判率
     *
     * 误判率越低:
     *  - 占用内存越大
     *  - 但被误放行的不存在 key 越少
     */
    public double getFalseProbability() {
        return cachePenetrationBloomFilter.getFalseProbability();
    }

    /**
     * 获取布隆过滤器底层位数组大小
     *
     * 可用于评估内存占用情况
     */
    public long getSize() {
        return cachePenetrationBloomFilter.getSize();
    }

    /**
     * 获取 hash 函数的个数
     *
     * hash 次数越多:
     *  - 判断越准确
     *  - 但性能开销略增
     */
    public int getHashIterations() {
        return cachePenetrationBloomFilter.getHashIterations();
    }

    /**
     * 获取当前布隆过滤器中已插入的元素数量
     *
     * 注意:
     *  - 该值为近似值(非精确统计)
     */
    public long count() {
        return cachePenetrationBloomFilter.count();
    }
}

我们以登录注册为例,可以在登录注册后添加它的手机号:

java 复制代码
    @Transactional(rollbackFor = Exception.class)
    public Boolean register(UserRegisterDto userRegisterDto) {
        // 业务逻辑...
        bloomFilterHandler.add(userMobile.getMobile());
        return true;
    }

然后判断用户手机号在布隆过滤器中是否存在,如果判断存在的话,由于布隆过滤器有碰撞率,则需要在数据库中再次判断:

java 复制代码
    public void doExist(String mobile){
        boolean contains = bloomFilterHandler.contains(mobile);
        if (contains) {
            LambdaQueryWrapper<UserMobile> queryWrapper = Wrappers.lambdaQuery(UserMobile.class)
                    .eq(UserMobile::getMobile, mobile);
            UserMobile userMobile = userMobileMapper.selectOne(queryWrapper);
            if (Objects.nonNull(userMobile)) {
                throw new DaMaiFrameException(BaseCode.USER_EXIST);
            }
        }
    }

解决缓存穿透


我们可以通过"在访问 Redis 和数据库之前,提前判断请求 key 是否可能存在",从根源上拦截了数据库中一定不存在的数据请求,从而解决缓存穿透问题。

缓存穿透场景:

java 复制代码
请求不存在的 key(如 userId=99999999)
 ↓
Redis miss
 ↓
MySQL 也不存在
 ↓
高并发 → DB 被打爆

由于系统不知道数据库不存在数据,所以只能查询数据库,那么我们可以将数据添加到布隆过滤器,在布隆过滤器中去判断是否拥有该数据,如果没有,则不需要继续查询:

java 复制代码
if (!bloomFilterHandler.contains(key)) {
    return null;
}

整体思路:

java 复制代码
请求
 ↓
① BloomFilter 判断 key 是否存在
    ├─ 不存在 → 直接返回(拦截穿透)
    └─ 可能存在
 ↓
② Redis 查询
    ├─ 命中数据 → 返回
    ├─ 命中空值 → 返回 null
    └─ 未命中
 ↓
③ 数据库查询
    ├─ 不存在 → 写入空值缓存(短 TTL)
    └─ 存在 → 回写 Redis

查询代码:

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {

    private final BloomFilterHandler bloomFilterHandler;
    private final RedisTemplate<String, Object> redisTemplate;
    private final UserMapper userMapper;

    private static final String USER_CACHE_KEY_PREFIX = "user:";
    private static final long CACHE_TTL = 1;      // 正常数据缓存 1 小时
    private static final long NULL_CACHE_TTL = 2; // 空值缓存 2 分钟

    public User getUserById(String userId) {

        // ==============================
        // ① 布隆过滤器:拦截一定不存在的请求(解决缓存穿透的核心)
        // ==============================
        if (!bloomFilterHandler.contains(userId)) {
            return null;
        }

        String redisKey = USER_CACHE_KEY_PREFIX + userId;

        // ==============================
        // ② 查询 Redis
        // ==============================
        Object cache = redisTemplate.opsForValue().get(redisKey);

        // ②-1 命中空值缓存
        if (cache instanceof NullValue) {
            return null;
        }

        // ②-2 命中正常数据
        if (cache instanceof User) {
            return (User) cache;
        }

        // ==============================
        // ③ 查询数据库
        // ==============================
        User user = userMapper.selectById(userId);

        // ③-1 数据库不存在 → 写入空值缓存(防止 Bloom 误判)
        if (user == null) {
            redisTemplate.opsForValue()
                    .set(redisKey, NullValue.INSTANCE, NULL_CACHE_TTL, TimeUnit.MINUTES);
            return null;
        }

        // ③-2 数据库存在 → 回写缓存
        redisTemplate.opsForValue()
                .set(redisKey, user, CACHE_TTL, TimeUnit.HOURS);

        return user;
    }
}
相关推荐
ChineHe25 分钟前
Redis入门篇001_Redis简介与特性
数据库·redis·缓存
困知勉行198541 分钟前
Redis数据结构及其底层实现
数据库·redis·缓存
哆啦code梦1 小时前
Redis Key命名规范实战指南
redis
uup2 小时前
SpringBoot 集成 Redis 分布式锁实战:从手动实现到注解式优雅落地
java·redis
大G的笔记本2 小时前
redis相关概念解释
redis
零度@2 小时前
Java-Redis 缓存「从入门到黑科技」2026 版
java·redis·缓存
optimistic_chen2 小时前
【Redis 系列】常用数据结构---ZSET类型
数据结构·数据库·redis·xshell·zset·redis命令
小股虫2 小时前
缓存攻防战:在增长中台设计一套高效且安全的缓存体系
java·分布式·安全·缓存·微服务·架构
DemonAvenger2 小时前
Redis与微服务:分布式系统中的缓存设计模式
数据库·redis·性能优化
不爱学英文的码字机器2 小时前
用 openJiuwen 构建 AI Agent:从 Hello World 到毒舌编辑器
人工智能·redis·编辑器