什么是布隆过滤器?

核心功能:快速判断元素是否存在

特点

  • ✅ 空间效率极高(百万数据仅需1MB)
  • ✅ 查询速度快(O(k),k为hash函数个数)
  • ❌ 存在误判(可能把不存在判断为存在)
  • ❌ 无法删除元素

判断逻辑

if (bloomFilter.contains(key)) {
    // 可能存在(需要进一步查询确认)
} else {
    // 一定不存在(可以直接返回)
}

原理

数据结构:位数组 + 多个hash函数

1. 添加元素"apple"
   hash1("apple") = 3 → bit[3] = 1
   hash2("apple") = 7 → bit[7] = 1
   hash3("apple") = 10 → bit[10] = 1

2. 判断元素是否存在
   if (bit[3] == 1 && bit[7] == 1 && bit[10] == 1) {
       return "可能存在";
   } else {
       return "一定不存在";
   }

误判原因:多个元素的hash值可能碰撞

hash1("apple") = 3, hash2("apple") = 7, hash3("apple") = 10
hash1("banana") = 3, hash2("banana") = 11, hash3("banana") = 15
hash1("cherry") = 7, hash2("cherry") = 10, hash3("cherry") = 20

查询"unknown":hash1=3, hash2=7, hash3=10
→ 都是1 → 误判为存在(实际是apple的hash值)

使用Redisson实现

1. 添加依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.20.0</version>
</dependency>

2. 配置Redisson

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://127.0.0.1:6379")
            .setPassword("yourpassword");
        return Redisson.create(config);
    }
}

3. 基础使用

@Service
public class BloomFilterService {
    @Autowired
    private RedissonClient redisson;

    public void init() {
        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user:ids");

        // 初始化:预期元素100万,误判率0.01(1%)
        bloomFilter.tryInit(1000000L, 0.01);

        // 添加元素
        bloomFilter.add("user:1001");
        bloomFilter.add("user:1002");
    }

    public boolean mightExist(String userId) {
        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user:ids");
        return bloomFilter.contains(userId);
    }
}

实战案例

案例1:防止缓存穿透

@Service
public class UserService {
    @Autowired
    private RedissonClient redisson;
    @Autowired
    private RedisTemplate<String, Object> redis;
    @Autowired
    private UserMapper userMapper;

    private RBloomFilter<Long> userBloomFilter;

    @PostConstruct
    public void init() {
        userBloomFilter = redisson.getBloomFilter("user:bloom");
        userBloomFilter.tryInit(10000000L, 0.01);  // 1000万用户,1%误判率

        // 初始化:加载所有userId到布隆过滤器
        List<Long> userIds = userMapper.selectAllUserIds();
        userIds.forEach(userBloomFilter::add);
        log.info("布隆过滤器初始化完成,用户数: {}", userIds.size());
    }

    public User getUser(Long userId) {
        // 1. 布隆过滤器判断
        if (!userBloomFilter.contains(userId)) {
            log.info("用户不存在: {}", userId);
            return null;  // 一定不存在,直接返回
        }

        // 2. 查询Redis缓存
        String key = "user:" + userId;
        User user = (User) redis.opsForValue().get(key);
        if (user != null) {
            return user;
        }

        // 3. 查询数据库
        user = userMapper.selectById(userId);
        if (user != null) {
            redis.opsForValue().set(key, user, 3600, TimeUnit.SECONDS);
        } else {
            // 设置空值缓存(防止误判导致的穿透)
            redis.opsForValue().set(key, new User(), 60, TimeUnit.SECONDS);
        }

        return user;
    }

    // 新增用户时,同步到布隆过滤器
    public void createUser(User user) {
        userMapper.insert(user);
        userBloomFilter.add(user.getId());
    }
}

案例2:爬虫去重

@Service
public class CrawlerService {
    @Autowired
    private RedissonClient redisson;

    private RBloomFilter<String> urlBloomFilter;

    @PostConstruct
    public void init() {
        urlBloomFilter = redisson.getBloomFilter("crawler:urls");
        urlBloomFilter.tryInit(100000000L, 0.001);  // 1亿URL,0.1%误判率
    }

    public boolean shouldCrawl(String url) {
        if (urlBloomFilter.contains(url)) {
            log.debug("URL已爬取: {}", url);
            return false;  // 可能已爬取
        }

        // 标记为已爬取
        urlBloomFilter.add(url);
        return true;
    }

    public void crawl(String url) {
        if (!shouldCrawl(url)) {
            return;
        }

        // 爬取逻辑
        log.info("开始爬取: {}", url);
        // ... 爬取网页内容
    }
}

案例3:邮件去重

@Service
public class EmailService {
    @Autowired
    private RedissonClient redisson;

    public boolean sendEmail(String email, String content) {
        // 每日邮件去重
        String bloomKey = "email:sent:" + LocalDate.now();
        RBloomFilter<String> bloomFilter = redisson.getBloomFilter(bloomKey);

        if (!bloomFilter.isExists()) {
            bloomFilter.tryInit(1000000L, 0.01);
            bloomFilter.expire(2, TimeUnit.DAYS);
        }

        // 生成邮件指纹(email + content hash)
        String fingerprint = email + ":" + content.hashCode();

        if (bloomFilter.contains(fingerprint)) {
            log.warn("重复邮件,跳过发送: {}", email);
            return false;
        }

        // 发送邮件
        doSendEmail(email, content);

        // 记录已发送
        bloomFilter.add(fingerprint);
        return true;
    }

    private void doSendEmail(String email, String content) {
        // 实际发送逻辑
    }
}

案例4:推荐系统去重

@Service
public class RecommendService {
    @Autowired
    private RedissonClient redisson;

    public List<Article> getRecommendations(Long userId) {
        String bloomKey = "recommend:history:" + userId;
        RBloomFilter<Long> historyBloom = redisson.getBloomFilter(bloomKey);

        if (!historyBloom.isExists()) {
            historyBloom.tryInit(10000L, 0.01);
            historyBloom.expire(30, TimeUnit.DAYS);
        }

        // 获取推荐列表
        List<Article> candidates = fetchCandidates(userId);

        // 过滤已推荐过的文章
        List<Article> result = candidates.stream()
            .filter(article -> !historyBloom.contains(article.getId()))
            .limit(20)
            .collect(Collectors.toList());

        // 记录已推荐
        result.forEach(article -> historyBloom.add(article.getId()));

        return result;
    }

    private List<Article> fetchCandidates(Long userId) {
        // 获取推荐候选
        return new ArrayList<>();
    }
}

误判率与内存

公式

位数组大小(m) = -n * ln(p) / (ln(2)^2)
hash函数个数(k) = m / n * ln(2)

n: 预期元素数量
p: 误判率

示例计算

n=100万, p=0.01(1%)
m = -1000000 * ln(0.01) / (ln(2)^2) ≈ 9585058 bits ≈ 1.14 MB
k = 9585058 / 1000000 * ln(2) ≈ 7 个hash函数

对照表

元素数量误判率内存占用hash函数
100万0.01 (1%)1.14 MB7
100万0.001 (0.1%)1.71 MB10
1000万0.01 (1%)11.4 MB7
1亿0.01 (1%)114 MB7

最佳实践

1. 合理设置容量和误判率

// 根据业务需求设置
// 用户数据:误判率可以高一点(0.01-0.05),节省内存
// 爬虫去重:误判率要低一点(0.001-0.01),避免重复爬取
bloomFilter.tryInit(expectedSize, 0.01);

2. 定期重建

@Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点
public void rebuildBloomFilter() {
    RBloomFilter<Long> oldFilter = redisson.getBloomFilter("user:bloom");
    RBloomFilter<Long> newFilter = redisson.getBloomFilter("user:bloom:new");

    // 初始化新过滤器
    newFilter.tryInit(10000000L, 0.01);

    // 加载所有userId
    List<Long> userIds = userMapper.selectAllUserIds();
    userIds.forEach(newFilter::add);

    // 原子替换
    oldFilter.delete();
    newFilter.rename("user:bloom");

    log.info("布隆过滤器重建完成");
}

3. 监控误判率

@Component
public class BloomFilterMonitor {
    @Autowired
    private RedissonClient redisson;

    @Scheduled(fixedRate = 60000)  // 每分钟
    public void monitor() {
        RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("user:bloom");

        long expectedInsertions = bloomFilter.getExpectedInsertions();
        long size = bloomFilter.count();
        double falseProbability = bloomFilter.getFalseProbability();

        log.info("布隆过滤器状态: 预期={}, 实际={}, 误判率={}",
            expectedInsertions, size, falseProbability);

        // 告警:实际大小超过预期
        if (size > expectedInsertions * 1.1) {
            log.warn("布隆过滤器容量不足,建议扩容");
        }
    }
}

4. 降级方案

public User getUser(Long userId) {
    try {
        // 优先使用布隆过滤器
        if (!userBloomFilter.contains(userId)) {
            return null;
        }
    } catch (Exception e) {
        log.error("布隆过滤器查询失败,降级为直接查询", e);
        // 降级:跳过布隆过滤器
    }

    // 继续查询Redis和数据库
    return queryFromCacheOrDB(userId);
}

布隆过滤器 vs 其他方案

方案内存查询速度精确性删除
布隆过滤器极低极快有误判
Set精确
数据库精确

选择建议

  • 数据量大(百万级)→ 布隆过滤器
  • 需要精确判断 → Set或数据库
  • 需要删除元素 → Set或数据库

总结

核心价值

  • 极低内存(100万数据1MB)
  • 极快查询(O(k))
  • 防止缓存穿透

典型场景

  • 缓存穿透防护
  • 爬虫URL去重
  • 邮件/推送去重
  • 推荐系统去重

注意事项

  • 存在误判(需要二次确认)
  • 无法删除(考虑定期重建)
  • 容量规划(预估好数据量)
  • 降级方案(布隆过滤器不可用时)