1.å¦ä½è®¾è®¡å¹¶å®ç°ä¸ä¸ªçº¿ç¨å®å
¨ç Map
2.详解布隆过滤器的源码原理和实现
3.布隆过滤器(Bloom Filter)详解
å¦ä½è®¾è®¡å¹¶å®ç°ä¸ä¸ªçº¿ç¨å®å ¨ç Map
Map æ¯ä¸ä¸ªé常常ç¨çæ°æ®ç»æï¼ä¸ä¸ªæ åºç key/value 对çéåï¼å ¶ä¸ Map ææç key é½æ¯ä¸åçï¼ç¶åéè¿ç»å®ç key å¯ä»¥å¨å¸¸æ°æ¶é´ O(1) å¤æ度å æ¥æ¾ãæ´æ°æå é¤å¯¹åºç valueã
è¦æ³å®ç°å¸¸æ°çº§çæ¥æ¾ï¼åºè¯¥ç¨ä»ä¹æ¥å®ç°å¢ï¼è¯»è åºè¯¥å¾å¿«ä¼æ³å°åå¸è¡¨ãç¡®å®ï¼Map åºå±ä¸è¬é½æ¯ä½¿ç¨æ°ç»æ¥å®ç°ï¼ä¼åç¨åå¸ç®æ³è¾ å©ã对äºç»å®ç keyï¼ä¸è¬å è¿è¡ hash æä½ï¼ç¶åç¸å¯¹åå¸è¡¨çé¿åº¦å模ï¼å° key æ å°å°æå®çå°æ¹ã
åå¸ç®æ³æå¾å¤ç§ï¼éåªä¸ç§æ´å é«æå¢ï¼
1. åå¸å½æ°
MD5 å SHA1 å¯ä»¥è¯´æ¯ç®ååºç¨æ广æ³ç Hash ç®æ³ï¼èå®ä»¬é½æ¯ä»¥ MD4 为åºç¡è®¾è®¡çã
MD4(RFC ) æ¯ MIT çRonald L. Rivest å¨ å¹´è®¾è®¡çï¼MD æ¯ Message Digestï¼æ¶æ¯æè¦ï¼ ç缩åãå®éç¨å¨ä½åé¿çå¤çå¨ä¸ç¨é«é软件å®ç°ââå®æ¯åºäº ä½æä½æ°çä½æä½æ¥å®ç°çã
MD5(RFC ) æ¯ Rivest äºå¹´å¯¹ MD4 çæ¹è¿çæ¬ãå®å¯¹è¾å ¥ä»ä»¥ä½åç»ï¼å ¶è¾åºæ¯4个ä½åç级èï¼ä¸ MD4 ç¸åãMD5 æ¯ MD4 æ¥å¾å¤æï¼å¹¶ä¸é度è¾ä¹è¦æ ¢ä¸ç¹ï¼ä½æ´å®å ¨ï¼å¨æåæåæå·®åæ¹é¢è¡¨ç°æ´å¥½ã
SHA1 æ¯ç± NIST NSA 设计为å DSA ä¸èµ·ä½¿ç¨çï¼å®å¯¹é¿åº¦å°äºçè¾å ¥ï¼äº§çé¿åº¦ä¸ºbit çæ£åå¼ï¼å æ¤æ穷举 (brute-force)
æ§æ´å¥½ãSHA-1 设计æ¶åºäºå MD4 ç¸ååç,并ä¸æ¨¡ä»¿äºè¯¥ç®æ³ã
常ç¨ç hash å½æ°æ SHA-1ï¼SHA-ï¼SHA-ï¼MD5 ãè¿äºé½æ¯ç»å ¸ç hash ç®æ³ãå¨ç°ä»£åç产ä¸ï¼è¿ä¼ç¨å°ç°ä»£ç hash ç®æ³ãä¸é¢å举å 个ï¼è¿è¡æ§è½å¯¹æ¯ï¼æååéå ¶ä¸ä¸ä¸ªæºç åæä¸ä¸å®ç°è¿ç¨ã
ï¼1ï¼ Jenkins Hash å SpookyHash
å¹´ Bob Jenkins å¨ã Dr. Dobbs Journalãæå¿ä¸å表äºä¸çå ³äºæ£åå½æ°çæç« ãA hash function for hash Table lookupããè¿ç¯æç« ä¸ï¼Bob 广æ³æ¶å½äºå¾å¤å·²æçæ£åå½æ°ï¼è¿å ¶ä¸ä¹å æ¬äºä»èªå·±æè°çâlookup2âãéåå¨å¹´ï¼Bob åå¸äº lookup3ãlookup3 å³ä¸º Jenkins Hashãæ´å¤æå ³ Bobâs æ£åå½æ°çå 容请åé ç»´åºç¾ç§ï¼Jenkins hash functionãmemcachedç hash ç®æ³ï¼æ¯æ两ç§ç®æ³ï¼jenkins, murmur3ï¼é»è®¤æ¯ jenkinsã
å¹´ Bob Jenkins åå¸äºä»èªå·±çä¸ä¸ªæ°æ£åå½æ°
SpookyHashï¼è¿æ ·å½åæ¯å 为å®æ¯å¨ä¸å£èåå¸çï¼ãå®ä»¬é½æ¥æ2åäº MurmurHash çé度ï¼ä½ä»ä»¬é½åªä½¿ç¨äºä½æ°å¦å½æ°è没æä½çæ¬ï¼SpookyHash ç»åºä½è¾åºã
ï¼2ï¼ MurmurHash
MurmurHash æ¯ä¸ç§éå å¯ååå¸å½æ°ï¼éç¨äºä¸è¬çåå¸æ£ç´¢æä½ã
Austin Appleby å¨å¹´åå¸äºä¸ä¸ªæ°çæ£åå½æ°ââMurmurHashãå ¶ææ°çæ¬å¤§çº¦æ¯ lookup3 é度ç2åï¼å¤§çº¦ä¸º1 byte/cycleï¼ï¼å®æä½åä½ä¸¤ä¸ªçæ¬ãä½çæ¬åªä½¿ç¨ä½æ°å¦å½æ°å¹¶ç»åºä¸ä¸ªä½çåå¸å¼ï¼èä½çæ¬ä½¿ç¨äºä½çæ°å¦å½æ°ï¼å¹¶ç»åºä½åå¸å¼ãæ ¹æ®Austinçåæï¼MurmurHashå ·æä¼å¼çæ§è½ï¼è½ç¶ Bob Jenkins å¨ãDr. Dobbs articleãæå¿ä¸å£°ç§°âæé¢æµ MurmurHash æ¯èµ·lookup3è¦å¼±ï¼ä½æ¯æä¸ç¥éå ·ä½å¼ï¼å 为æè¿æ²¡æµè¯è¿å®âãMurmurHashè½å¤è¿ é走红å¾çäºå ¶åºè²çé度åç»è®¡ç¹æ§ãå½åççæ¬æ¯MurmurHash3ï¼RedisãMemcachedãCassandraãHBaseãLuceneé½å¨ä½¿ç¨å®ã
ä½è ï¼ä¸ç¼æ®æµåéåè¾¹å°é
详解布隆过滤器的原理和实现
为什么需要布隆过滤器
想象一下遇到下面的场景你会如何处理:
手机号是否重复注册
用户是否参与过某秒杀活动
伪造请求大量 id 查询不存在的记录,此时缓存未命中,源码如何避免缓存穿透
针对以上问题常规做法是:查询数据库,数据库硬扛,源码如果压力并不大可以使用此方法,源码保持简单即可。源码
改进做法:用 list/set/tree 维护一个元素集合,源码源码医疗 启信判断元素是源码否在集合内,时间复杂度或空间复杂度会比较高。源码如果是源码微服务的话可以用 redis 中的 list/set 数据结构, 数据规模非常大此方案的内存容量要求可能会非常高。
这些场景有个共同点,源码可以将问题抽象为:如何高效判断一个元素不在集合中? 那么有没有一种更好方案能达到时间复杂度和空间复杂双优呢?
有!源码布隆过滤器。源码listview分页源码
什么是源码布隆过滤器布隆过滤器(英语:Bloom Filter)是 年由布隆提出的。它实际上是源码一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是源码否在一个集合中,它的优点是空间效率和查询时间都远远超过一般的算法。
工作原理
布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点(offset),把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是点点网源码 1,则被检元素很可能在。这就是布隆过滤器的基本思想。
简单来说就是准备一个长度为 m 的位数组并初始化所有元素为 0,用 k 个散列函数对元素进行 k 次散列运算跟 len(m)取余得到 k 个位置并将 m 中对应位置设置为 1。
布隆过滤器优缺点优点:
空间占用极小,因为本身不存储数据而是用比特位表示数据是否存在,某种程度有保密的效果。
插入与查询时间复杂度均为 O(k),常数级别,k 表示散列函数执行次数。
散列函数之间可以相互独立,互动源码安装可以在硬件指令层加速计算。
缺点:
误差(假阳性率)。
无法删除。
误差(假阳性率)
布隆过滤器可以 % 判断元素不在集合中,但是当元素在集合中时可能存在误判,因为当元素非常多时散列函数产生的 k 位点可能会重复。 维基百科有关于假阳性率的数学推导(见文末链接)这里我们直接给结论(实际上是我没看懂...),假设:
位数组长度 m
散列函数个数 k
预期元素数量 n
期望误差ε
在创建布隆过滤器时我们为了找到合适的 m 和 k ,可以根据预期元素数量 n 与 ε 来推导出最合适的 m 与 k 。
java 中 Guava, Redisson 实现布隆过滤器估算最优 m 和 k 采用的就是此算法:
//计算哈希次数@VisibleForTestingstaticintoptimalNumOfHashFunctions(longn,longm){ //(m/n)*log(2),butavoidtruncationduetodivision!returnMath.max(1,(int)Math.round((double)m/n*Math.log(2)));}//计算位数组长度@VisibleForTestingstaticlongoptimalNumOfBits(longn,doublep){ if(p==0){ p=Double.MIN_VALUE;}return(long)(-n*Math.log(p)/(Math.log(2)*Math.log(2)));}无法删除
位数组中的某些 k 点是多个元素重复使用的,假如我们将其中一个元素的ccoa源码下载 k 点全部置为 0 则直接就会影响其他元素。 这导致我们在使用布隆过滤器时无法处理元素被删除的场景。
可以通过定时重建的方式清除脏数据。假如是通过 redis 来实现的话重建时不要直接删除原有的 key,而是先生成好新的再通过 rename 命令即可,再删除旧数据即可。
go-zero 中的 bloom filter 源码分析core/bloom/bloom.go 一个布隆过滤器具备两个核心属性:
位数组:
散列函数
go-zero实现的bloom filter中位数组采用的是Redis.bitmap,既然采用的是 redis 自然就支持分布式场景,散列函数采用的是MurmurHash3
Redis.bitmap 为什么可以作为位数组呢?
Redis 中的并没有单独的 bitmap 数据结构,底层使用的是动态字符串(SDS)实现,而 Redis 中的字符串实际都是以二进制存储的。 a 的ASCII码是 ,转换为二进制是:,如果我们要将其转换为b只需要进一位即可:。下面通过Redis.setbit实现这个操作:
set foo a \ OK \ get foo \ "a" \ setbit foo 6 1 \ 0 \ setbit foo 7 0 \ 1 \ get foo \ "b"
bitmap 底层使用的动态字符串可以实现动态扩容,当 offset 到高位时其他位置 bitmap 将会自动补 0,最大支持 2^-1 长度的位数组(占用内存 M),需要注意的是分配大内存会阻塞Redis进程。 根据上面的算法原理可以知道实现布隆过滤器主要做三件事情:
k 次散列函数计算出 k 个位点。
插入时将位数组中 k 个位点的值设置为 1。
查询时根据 1 的计算结果判断 k 位点是否全部为 1,否则表示该元素一定不存在。
下面来看看go-zero 是如何实现的:
对象定义
//表示经过多少散列函数计算//固定次maps=type(//定义布隆过滤器结构体Filterstruct{ bitsuintbitSetbitSetProvider}//位数组操作接口定义bitSetProviderinterface{ check([]uint)(bool,error)set([]uint)error})位数组操作接口实现
首先需要理解两段 lua 脚本:
//ARGV:偏移量offset数组//KYES[1]:setbit操作的key//全部设置为1setScript=`for_,offsetinipairs(ARGV)doredis.call("setbit",KEYS[1],offset,1)end`//ARGV:偏移量offset数组//KYES[1]:setbit操作的key//检查是否全部为1testScript=`for_,offsetinipairs(ARGV)doiftonumber(redis.call("getbit",KEYS[1],offset))==0thenreturnfalseendendreturntrue`为什么一定要用 lua 脚本呢? 因为需要保证整个操作是原子性执行的。
//redis位数组typeredisBitSetstruct{ store*redis.Clientkeystringbitsuint}//检查偏移量offset数组是否全部为1//是:元素可能存在//否:元素一定不存在func(r*redisBitSet)check(offsets[]uint)(bool,error){ args,err:=r.buildOffsetArgs(offsets)iferr!=nil{ returnfalse,err}//执行脚本resp,err:=r.store.Eval(testScript,[]string{ r.key},args)//这里需要注意一下,底层使用的go-redis//redis.Nil表示key不存在的情况需特殊判断iferr==redis.Nil{ returnfalse,nil}elseiferr!=nil{ returnfalse,err}exists,ok:=resp.(int)if!ok{ returnfalse,nil}returnexists==1,nil}//将k位点全部设置为1func(r*redisBitSet)set(offsets[]uint)error{ args,err:=r.buildOffsetArgs(offsets)iferr!=nil{ returnerr}_,err=r.store.Eval(setScript,[]string{ r.key},args)//底层使用的是go-redis,redis.Nil表示操作的key不存在//需要针对key不存在的情况特殊判断iferr==redis.Nil{ returnnil}elseiferr!=nil{ returnerr}returnnil}//构建偏移量offset字符串数组,因为go-redis执行lua脚本时参数定义为[]stringy//因此需要转换一下func(r*redisBitSet)buildOffsetArgs(offsets[]uint)([]string,error){ varargs[]stringfor_,offset:=rangeoffsets{ ifoffset>=r.bits{ returnnil,ErrTooLargeOffset}args=append(args,strconv.FormatUint(uint(offset),))}returnargs,nil}//删除func(r*redisBitSet)del()error{ _,err:=r.store.Del(r.key)returnerr}//自动过期func(r*redisBitSet)expire(secondsint)error{ returnr.store.Expire(r.key,seconds)}funcnewRedisBitSet(store*redis.Client,keystring,bitsuint)*redisBitSet{ return&redisBitSet{ store:store,key:key,bits:bits,}}到这里位数组操作就全部实现了,接下来看下如何通过 k 个散列函数计算出 k 个位点
k 次散列计算出 k 个位点
//k次散列计算出k个offsetfunc(f*Filter)getLocations(data[]byte)[]uint{ //创建指定容量的切片locations:=make([]uint,maps)//maps表示k值,作者定义为了常量:fori:=uint(0);i<maps;i++{ //哈希计算,使用的是"MurmurHash3"算法,并每次追加一个固定的i字节进行计算hashValue:=hash.Hash(append(data,byte(i)))//取下标offsetlocations[i]=uint(hashValue%uint(f.bits))}returnlocations}插入与查询
添加与查询实现就非常简单了,组合一下上面的函数就行。
//添加元素func(f*Filter)Add(data[]byte)error{ locations:=f.getLocations(data)returnf.bitSet.set(locations)}//检查是否存在func(f*Filter)Exists(data[]byte)(bool,error){ locations:=f.getLocations(data)isSet,err:=f.bitSet.check(locations)iferr!=nil{ returnfalse,err}if!isSet{ returnfalse,nil}returntrue,nil}改进建议整体实现非常简洁高效,那么有没有改进的空间呢?
个人认为还是有的,上面提到过自动计算最优 m 与 k 的数学公式,如果创建参数改为:
预期总数量expectedInsertions
期望误差falseProbability
就更好了,虽然作者注释里特别提到了误差说明,但是实际上作为很多开发者对位数组长度并不敏感,无法直观知道 bits 传多少预期误差会是多少。
//NewcreateaFilter,storeisthebackedredis,keyisthekeyforthebloomfilter,//bitsishowmanybitswillbeused,mapsishowmanyhashesforeachaddition.//bestpractices://elements-meanshowmanyactualelements//whenmaps=,formula:0.7*(bits/maps),bits=*elements,theerrorrateis0.<1e-4//fordetailederrorratetable,see/zeromicro/go-zero欢迎使用 go-zero 并 star 支持我们!
微信交流群关注『微服务实践』公众号并点击 交流群 获取社区群二维码。
布隆过滤器(Bloom Filter)详解
布隆过滤器(Bloom Filter),一种年由布隆提出的高效数据结构,用于判断元素是否在集合中。其优势在于空间效率和查询速度,但存在误判率和删除难题。布隆过滤器由长二进制数组和多个哈希函数构成,新元素映射位置置1。判断时,若所有映射位置均为1,则认为在集合;有0则判断不在。尽管可能产生误报,但通过位数组节省空间,比如MB内存可处理亿长度数组。常用MurmurHash哈希算法,如mmh3库,它的随机分布特性使其在Redis等系统中广泛使用。
在Scrapy-Redis中,可以将布隆过滤器与redis的bitmap结合,设置位长度为2的次方,通过setbit和getbit操作实现。将自定义的bloomfilter.py文件添加到scrapy_redis源码目录,并在dupefilter.py中进行相应修改。需要注意的是,爬虫结束后可通过redis_conn.delete(key名称)释放空间。使用时,只需将scrapy_redis替换到项目中,遵循常规的Scrapy-Redis设置即可。