用Redis实现布隆过滤器的实践方案

背景

业务方须要实现一个曝光去重的功能,决定采用布隆过滤器,又由于是多节点应用,为保证数据一致性,经过Redis实现。本文记录下开发时的思路,以及优化过程。html

初次实现

Redis4.0以上对布隆进行了插件支持,能够用特定的指令进行元素添加和判重,但考虑到不是全部环境的Redis都支持插件安装,以及违背死磕精神,决定自行实现。redis

初版的实现使用Guava的BloomFilter进行hash操做,redis经过String类型存放bit数组。算法

估算空间

在实现业务前,估算大体须要插入的元素以及能接受的误判率,来计算预计须要的空间(引用Guava中的方法)。

/**
   * @param n 预计插入的元素
   * @param p 误判率(0 < p < 1)
   */
  long optimalNumOfBits(long n, double p) {
    if (p == 0) {
      p = Double.MIN_VALUE;
    }
    return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
  }

例如咱们预计插入500个元素,误判率取千分之三,输入到函数中获得 6045 ,即6045 bit = 755.625 B = 0.73 KB , 固然在Redis中数据结构还有额外存储,因此结果仅供参考。数据结构

Setbit & Getbit

布隆的Hash算法有不少,例如MURMUR128_MITZ_32,算法实现此处不赘述,能够google一下,通过数次hash后获得下标数组,储存着元素映射到数组的下标。函数

判重:

for (int i : offset) {
        if (!redisTemplate.opsForValue().getBit(key, i)){
            return false;
        }
    }
    return true;

添加:

 for (int i : offset) {
        redisTemplate.opsForValue().setBit(key, i, true);
    }

至此,布隆就实现完毕了。google

Pipeline

虽然getbit和setbit都是O(1)操做,然而每一个元素的 添加/判重 都须要进行数次setbit,其次数与插入量和布隆过滤器长度相关:

/**
     * @param n 预估插入量
     * @param m 布隆过滤器长度
     */
    int optimalNumOfHashFunctions(long n, long m) {
        return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
    }

取上文中的6045bit以及500个预估插入,进行代入获得操做次数 = 5。

每 添加/判重 1个元素就须要进行 5 次bit操做,这期间创建了5次TCP链接,显然对通道形成浪费,咱们用redis pipeline优化一下~

添加:

redisTemplate.executePipelined((RedisCallback) connection -> {
        for (int i : offset) {
            connection.setBit(redisTemplate.getKeySerializer().serialize(key), i, true);
        }
        return null;
    });

判重:

List<Boolean> list = redisTemplate.executePipelined((RedisCallback) connection -> {
        for (int i : offset) {
            connection.getBit(redisTemplate.getKeySerializer().serialize(key), i);
        }
        return null;
    });
    List<List<Boolean>> valuePairs = Lists.partition(list, numHashFunctions);
    Map<R, Boolean> result = Maps.newHashMapWithExpectedSize(values.size());
    for (int i = 0; i < values.size(); i++) {
        R v = values.get(i);
        result.put(v, valuePairs.get(i).stream().reduce(true, Boolean::logicalAnd));
    }
    return result;

同时笔者将方法改形成可批量判重元素的形式,将结果集按操做次数拆分红数个子集(pipeline返回的结果集是有序的,这点很重要),每一个子集各自累加,最终获得一张[元素:是否存在]的Map。

实测pipeline化后速度提高了很多,不过还没完。

bitfield

bit操做快,但请求次数也多,在上述pipeline上线后,redis在业务高峰时qps有明显的上升。

set/get bit每次只能操做单个bit位。是否能够一条命令操做完成多个bit位的操做?

BITFIELD

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

BITFIELD 命令能够将一个 Redis 字符串看做是一个由二进制位组成的数组, 并对这个数组中任意偏移进行访问。

BITFIELD能够指定多个子命令,有 get/set/incr 三种操做类型,能够在一条命令中完成复合操做,并返回结果集,固然命令的执行速度取决于由多少个子命令组成。

Redis官方解释开发bitfield的动机是为了方便操做bitmap,但不妨碍咱们在布隆过滤器中使用它。

添加:

BitFieldSubCommands commands = BitFieldSubCommands.create();
    for (int i : offset) {
        commands.set(BitFieldSubCommands.BitFieldType.unsigned(1))
                    .valueAt(i)
                    .to(1);
    }
    redisTemplate.opsForValue().bitField(key, commands);

注意在定义子命令时要声明操做数的长度,指定为无符号1位便可。

判重:

BitFieldSubCommands commands = BitFieldSubCommands.create();
    for (int i : offset) {
        commands.get(BitFieldSubCommands.BitFieldType.unsigned(1))
                .valueAt(i);
    }
    List<Long> result = redisTemplate.opsForValue().bitField(key, commands);

判重时对结果集的处理同pipeline。

使用bitfield后,经测试高qps现象有明显改善,但对cpu改善不大,由于redis内部执行的bit操做并无减小。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值