用Redis快速实现BloomFilter!

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 背景最近工作上有个类似需求是: 现有约3亿条数据词典存在于一个csv文件A中,作为数据源。对于 用户输入的任意单词M,需要快速的在A中匹配M单词是否存在。(A文件约3G大小左右,总行数三亿)拿到这个需求,你的第一想法怎么做呢?正常思路可能是:将csv文件A导入某关系型数据库。sql查询按M匹配。上面的方式有个明显的缺点是:慢!

背景
最近工作上有个类似需求是: 现有约3亿条数据词典存在于一个csv文件A中,作为数据源。对于 用户输入的任意单词M,需要快速的在A中匹配M单词是否存在。(A文件约3G大小左右,总行数三亿)

拿到这个需求,你的第一想法怎么做呢?

正常思路可能是:

将csv文件A导入某关系型数据库。
sql查询按M匹配。
上面的方式有个明显的缺点是:慢!

3亿多行的数据,即便是建好索引进行检索,匹配到也得话不少时间(笔者没亲自试过,感兴趣的朋友可以自行测试测试,理论上快不起来的)。

目前能 在时间复杂度和空间复杂度上达到最佳的方案,恐怕就是Bloom Filter了, 维基地址:Bloom Filter

此处给不太了解Bloom Filter的读者看,熟悉的朋友直接看下一节。

本文场景Bloom Filter 使用思路解释:
假设申请了一段bit位大数组(即数组中的元素只能是一个bit位,1或0,默认元素值都为0)
将csv文件A中的每个单词,经过多个hash函数进行hash运算之后得到在大数组中对应的多个下标位置
将步骤2中得到的多个下标位置的bit位都置为1.
对于用户输入的任意单词M,按照2的步骤得到多个下标位置,其对应大数组中的值全部为1则存在,否则不存在。

方案选型
实现Bloom Filter的方法很多,有各种语言版本的,这里为了真切感受一下算法的魅力,笔者这里决定用java 代码徒手撸了!

另一方面,考虑到分布式应用的需要,显然在单机内存上构建Bloom Filter存储是不太合适的。 这里选择redis。

redis有以下为操作,可以用于实现bloomfilter:

redis> SETBIT bit 10086 1
(integer) 0
redis> GETBIT bit 10086
(integer) 1
redis> GETBIT bit 100 # bit 默认被初始化为 0
(integer) 0

实现细节
实现bloom filter的关键是hash函数,一般为了降低误报率、减少hash碰撞的影响,会选择多个hash函数。

那么,怎么写一个hash函数呢?

不要方,我们要的hash是 input: String --> output: int , jdk里面的String类不是恰好也有一个hashCode 方法吗? 翻出来看一看!

/**

 * Returns a hash code for this string. The hash code for a
 * {@code String} object is computed as
 * <blockquote><pre>
 * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
 * </pre></blockquote>
 * using {@code int} arithmetic, where {@code s[i]} is the
 * <i>i</i>th character of the string, {@code n} is the length of
 * the string, and {@code ^} indicates exponentiation.
 * (The hash value of the empty string is zero.)
 *
 * @return  a hash code value for this object.
 */
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

看到这一行h = 31 * h + val[i];,貌似原理其实也很简单,每个游戏卖号字符对应的ascii码,经过一个公式计算依次加起来。这里有个系数31, 稍微变一下, 不就可以有多个hash函数了吗。

以下是稍加修改后的hash函数:

//总的bitmap大小  64M
private static final int cap = 1 << 29;
/*
 * 不同哈希函数的种子,一般取质数
 * seeds数组共有8个值,则代表采用8种不同的哈希函数
 */
private int[] seeds = new int[]{3, 5, 7, 11, 13, 31, 37, 61};
private int hash(String value, int seed) {
    int result = 0;
    int length = value.length();
    for (int i = 0; i < length; i++) {
        result = seed * result + value.charAt(i);
    }
    return (cap - 1) & result;
}

剩下的事情便很简单了,对每个词典A中的单词,依次调seeds 中对应的hash函数(这里一共是8个),用redis的setbit操作,将下标值置为1.

redis代码 (这里用pipeline 包装了下。)

@Service
public class RedisService {

@Autowired
private StringRedisTemplate template;
public void multiSetBit(String name, boolean value, long... offsets) {
    template.executePipelined((RedisCallback<Object>) connection -> {
        for (long offset : offsets) {
            connection.setBit(name.getBytes(), offset, value);
        }
        return null;
    });
}
public List<Boolean> multiGetBit(String name, long... offsets) {
    List<Object> results = template.executePipelined((RedisCallback<Object>) connection -> {
        for (long offset : offsets) {
            connection.getBit(name.getBytes(), offset);
        }
        return null;
    });
    List<Boolean> list = new ArrayList<>();
    results.forEach(obj -> {
        list.add((Boolean) obj);
    });
    return list;
}

}

最后,代码串起来大概长这个样子:

FileInputStream inputStream = new FileInputStream("/XXXX.csv");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
HashSet totalSet=new HashSet<>();
String word=null;
while((word = bufferedReader.readLine()) != null){

for (int seed : seeds) {
    int hash = hash(word, seed);
    totalSet.add((long) hash);
}
long[] offsets = new long[totalSet.size()];
int i=0;
for(Long l:totalSet){
    offsets[i++]=l;
}
redisService.multiSetBit("BLOOM_FILTER_WORDS_DICTIONARY", true, offsets);

}

查的时候也类似:

String word = "XXXX"; //实际输入
long[] offsets = new long[seeds.length];
for (int i = 0; i < seeds.length; i++) {

int hash = hash(mobile, seeds[i]);
offsets[i] = hash;

}
List results = redisService.multiGetBit("BLOOM_FILTER_WORDS_DICTIONARY", offsets);
//判断是否都为true (则存在)
boolean isExisted=true;
for(Boolean result:results){

if(!result){
    isExisted=false;
    break;
}

}

注意事项
setbit的offset是用大小限制的,在0到 232(最大使用512M内存)之间,即0~4294967296之前,超过这个数会自动将offset转化为0,因此使用的时候一定要注意。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6月前
|
缓存 NoSQL 算法
【redis】布隆过滤器(Bloom Filter)原理解析与应用
【redis】布隆过滤器(Bloom Filter)原理解析与应用
99 1
|
6月前
|
存储 NoSQL 算法
深入浅出Redis(十一):Redis四种高级数据结构:Geosptial、Hypeloglog、Bitmap、Bloom Filter布隆过滤器
深入浅出Redis(十一):Redis四种高级数据结构:Geosptial、Hypeloglog、Bitmap、Bloom Filter布隆过滤器
|
6月前
|
缓存 NoSQL Java
硬核 | Redis 布隆(Bloom Filter)过滤器原理与实战
硬核 | Redis 布隆(Bloom Filter)过滤器原理与实战
769 0
|
6月前
|
缓存 NoSQL Redis
Redis系列-9.Redis布隆过滤器BloomFilter
Redis系列-9.Redis布隆过滤器BloomFilter
99 1
|
存储 缓存 监控
【Redis源码】bloomfilter布隆过滤器
【Redis源码】bloomfilter布隆过滤器
113 1
|
存储 缓存 NoSQL
Redis之布隆过滤器(Bloom Filter)解读
Redis之布隆过滤器(Bloom Filter)解读
|
缓存 NoSQL Redis
【Redis原理机制 五】BloomFilter的实现原理及优化
【Redis原理机制 五】BloomFilter的实现原理及优化
159 0
|
缓存 移动开发 NoSQL
php结合redis实现高并发下的抢购、秒杀功能的实例
php结合redis实现高并发下的抢购、秒杀功能的实例
254 0
|
NoSQL Redis
Redis学习4:List数据类型、拓展操作、实现日志等
注意点:对存储空间的顺序进行分析!
Redis学习4:List数据类型、拓展操作、实现日志等
|
存储 NoSQL Redis
Redis学习3:hash类型操作、拓展操作、实现购物等
首先可以理解成一个redis里面有一个小的redis。同时要注意引入了一个field的名字。
Redis学习3:hash类型操作、拓展操作、实现购物等
下一篇
无影云桌面