目录
一、业务需求
你负责的项目使用布隆过滤器进行缓存穿透的防御,但是随着数据量的增加,布隆过滤器桶的数量已经不足以容纳新的数据,请你提供一种解决方案。
二、分析
不足这问题解决方案其实也只有扩容两字,无非就是扩容的方式不同,比如说如果一个布隆过滤器不够的话我们就再多创建几个,然后通过分片将其合并;又或是直接换个大点的地方存储,我们的优化方案实际上也就是这两个方向。
三、优化方案
(一)分片布隆过滤器
直接哈希取模即可,简单高效。
public class ShardedBloomFilter { private final List<BloomFilter<String>> shards; private final int shardCount;
// 初始化分片 public ShardedBloomFilter(int shardCount, long totalExpectedInsertions, double falsePositiveRate) { this.shardCount = shardCount; this.shards = new ArrayList<>(shardCount);
// 计算每个分片的预期插入量 long insertionsPerShard = (totalExpectedInsertions + shardCount - 1) / shardCount; for (int i = 0; i < shardCount; i++) { shards.add(BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), insertionsPerShard, falsePositiveRate )); } }
// 根据元素哈希选择分片 private int getShardIndex(String element) { return Math.abs(element.hashCode()) % shardCount; }
// 插入 public void put(String element) { int shardIndex = getShardIndex(element); shards.get(shardIndex).put(element); }
// 判断是否存在 public boolean mightContain(String element) { int shardIndex = getShardIndex(element); return shards.get(shardIndex).mightContain(element); }}但是同时缺点也很明显,一是只能部署在单机上,无法在上应用;二是其存储在JVM的堆内存空间中,会挤压其他对象的内存空间,而且其以位数组的方式存储,直接晋升老年代,从而增多Minor GC甚至Full GC的次数,拖慢程序性能;三是位数组为大对象,容易引发OOM。
(二)分布式布隆过滤器
分布式布隆过滤器一般指的是在Redis上的布隆过滤器,由于摆脱了单机JVM的内存存储限制,所以被广泛应用在分布式架构上。其跟普通布隆过滤器的区别就是存储的地方不一样。
其有封装的包装类RBloomFilter,直接使用即可,非常的方便。
但是其也会挤占普通缓存数据的空间,导致触发缓存淘汰的次数增加,为了解决这个问题,我们也可以给其进行分片,分散到集群当中。
public class DistributedShardedBloomFilter { private final List<RedisShard> shards; private final double falsePositiveRate; private final long expectedInsertions; private static final String BLOOM_PREFIX = "bf:shard:";
/** * Redis分片封装类 * * 包含分片标识和对应的Redis客户端连接 */ private static class RedisShard { // 分片唯一标识 private final String name; // Redis客户端连接 private final StringRedisTemplate redisTemplate;
public RedisShard(String name, StringRedisTemplate redisTemplate) { this.name = name; this.redisTemplate = redisTemplate; } }
public DistributedShardedBloomFilter(List<RedisNode> nodes, long expectedInsertions, double falsePositiveRate) { this.falsePositiveRate = falsePositiveRate; this.expectedInsertions = expectedInsertions; this.shards = new ArrayList<>(nodes.size());
// 初始化每个Redis分片的连接 for (RedisNode node : nodes) { // 创建Redis客户端连接 StringRedisTemplate template = createRedisTemplate(node.host(), node.port()); // 封装分片信息 shards.add(new RedisShard(node.id(), template)); }
// 在所有分片上初始化布隆过滤器 initializeFilters(); }
/** * 创建Redis客户端连接 */ private StringRedisTemplate createRedisTemplate(String host, int port) { // 配置Redis连接参数 RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
// 创建连接工厂(使用Jedis客户端) JedisConnectionFactory factory = new JedisConnectionFactory(config); // 初始化连接池 factory.afterPropertiesSet();
// 创建Redis模板 StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(factory); template.afterPropertiesSet();
return template; }
/** * 初始化所有分片的布隆过滤器 * 计算每个分片应处理的数据量,并在各分片上创建布隆过滤器 */ private void initializeFilters() { // 计算每个分片的预期容量(向上取整) long perShardCapacity = (expectedInsertions + shards.size() - 1) / shards.size();
// 在每个分片上初始化布隆过滤器 for (RedisShard shard : shards) { // 执行命令创建布隆过滤器 executeBloomCommand( shard.redisTemplate, BloomCommand.RESERVE, BLOOM_PREFIX + shard.name, String.valueOf(falsePositiveRate), String.valueOf(perShardCapacity) ); } }
/** * 使用一致性哈希定位元素所在分片 */ private RedisShard getShard(String element) { // 计算元素的128位MurmurHash long hash = Hashing.murmur3_128().hashString(element, StandardCharsets.UTF_8).asLong();
// 使用一致性哈希算法确定分片位置 int shardIndex = Hashing.consistentHash(hash, shards.size());
return shards.get(shardIndex); }
// 添加 public void add(String element) { RedisShard shard = getShard(element); executeBloomCommand( shard.redisTemplate, BloomCommand.ADD, BLOOM_PREFIX + shard.name, element ); }
// 检查是否存在 public boolean mightContain(String element) { RedisShard shard = getShard(element); return Boolean.TRUE.equals(shard.redisTemplate.execute((RedisCallback<Boolean>) connection -> { String key = BLOOM_PREFIX + shard.name;
// 发送Redis命令并解析响应 Long result = (Long) connection.execute( BloomCommand.EXISTS.name(), key.getBytes(StandardCharsets.UTF_8), element.getBytes(StandardCharsets.UTF_8) );
// 结果1表示存在,0表示不存在 return result != null && result == 1; })); }
// 添加元素的执行命令 private void executeBloomCommand(StringRedisTemplate redisTemplate, BloomCommand command, String key, String... args) { redisTemplate.execute((RedisCallback<Void>) connection -> { // 构建命令参数数组 byte[][] commandArgs = new byte[args.length + 1][]; commandArgs[0] = key.getBytes(StandardCharsets.UTF_8);
// 填充剩余参数 for (int i = 0; i < args.length; i++) { commandArgs[i + 1] = args[i].getBytes(StandardCharsets.UTF_8); }
// 执行原始命令 connection.execute(command.name(), commandArgs); return null; }); }
private enum BloomCommand { // 创建布隆过滤器 RESERVE, // 添加元素 ADD, // 检查元素 EXISTS }
/** * Redis节点信息记录 */ public record RedisNode(String id, String host, int port) {}}(三)可扩展布隆过滤器
这个方案采取的思想就是直接扩容,当原本的布隆过滤器内存占用率达九成时,就再创建一个空间为原来两倍大的布隆过滤器作为下一层,原来那层的权限变为只读,新写入的元素都会插入在第二层的布隆过滤器上,如果第二次的内存占用率也达九成就继续创建下一层……
public class ScalableBloomFilter { // 目标总误判率 private final double targetFalsePositiveRate; // 衰减因子 private final double decayFactor; // 初始层容量 private final int initialCapacity; // 扩容填充阈值 private final double fillRatioThreshold;
private final List<BloomFilter<String>> filters = new ArrayList<>(); private int currentLayerIndex = -1;
public ScalableBloomFilter(double targetFalsePositiveRate, double decayFactor, int initialCapacity) { this.targetFalsePositiveRate = targetFalsePositiveRate; this.decayFactor = decayFactor; this.initialCapacity = initialCapacity; this.fillRatioThreshold = 0.75; // 初始化第一层 addNewLayer(); }
// 添加新层 private void addNewLayer() { double layerFalsePositiveRate = targetFalsePositiveRate * Math.pow(decayFactor, filters.size()); int layerCapacity = (currentLayerIndex == -1) ? initialCapacity : filters.get(currentLayerIndex).approximateElementCount() * 2;
BloomFilter<String> newFilter = BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), layerCapacity, layerFalsePositiveRate );
filters.add(newFilter); currentLayerIndex++; }
// 插入元素 public void add(String element) { BloomFilter<String> currentLayer = filters.get(currentLayerIndex); if (currentLayer.approximateElementCount() >= fillRatioThreshold * currentLayer.expectedFpp()) { // 扩容 addNewLayer(); // 更新当前层 currentLayer = filters.get(currentLayerIndex); } currentLayer.put(element); }
// 检查元素是否存在 public boolean mightContain(String element) { // 从旧层到新层遍历 for (BloomFilter<String> filter : filters) { if (filter.mightContain(element)) { // 任一层命中即返回 return true; } } return false; }}但是很容易想到,分片布隆过滤器有的缺点它都有,但是其动态扩展的特点也能够腾出多余的内存空间供其他对象使用,但依旧无法掩盖其劣势。
除此之外还有一个缺点就是使用越久,层数越多,查询最新元素的效率就越慢,因为得逐层寻找。
四、总结
三种方案,单机项目优先使用分片方案,分布式则采取分布式分片方案即可,但可扩展方案并不推荐。
码文不易,留个赞再走吧
原文链接: 布隆过滤器因内存上限无法处理超量数据的优化方案 作者: Yilena
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时










