位图
业务开发过程中,需要对某个对象进行标记或记录对象某个状态时,只要插入和读取两个操作,常用散列表、红黑树等数据结构记录。
当记录数据量非常庞大时,消耗的内存会非常大,存储效率和查询效率也会非常低。如使用散列表记录1亿用户今日是否访问过,使用int存储用户id,需要381MB内存空间(4bytes*100000000/1024/1024),加上链表的指针,消耗内存很可能会大于500M。
没个状态值使用4个字节来存储浪费了很多内存,用户是否访问过,最少使用一个bit就可以表示。使用一个bit代表一个用户,1亿用户最小需要1亿bit,合记为11MB,大大减少了内存占用。
虽然散列表与位图的插入、读取时间复杂度为O(1),但是位图不需要进行扩容、Hash计算,并且使用位运算,连续内存利于CPU缓存等特性,理论上时间效率也大于散列表。
位图也有致命的缺点,当数据密度非常小时,位图的空间效率反而会低于散列表。如上例子中1亿用户的id分布在1到100亿之间,使用的空间1.16GB。
优点:
- 节省内存空间
- 插入和查询时间复杂度都为O(1)
缺点:
-
存储数据不能重复
-
只能处理正整数的数据
-
对数据要求比较高,需要数据密度高时才能体现出高的空间效率
布隆过滤器(Bloom Filter)
布隆过滤器解决了位图在存储数据密度低时低效的问题。
如上例中,1亿用户分布在1-100亿之间,我们使用10亿个bit存储这1亿个用户。通过一个Hash算法计算将用户分布在1-10亿之间,很大概率会出现Hash冲突的问题。但如果使用多个不同的Hash算法计算,不同用户最终的计算结果都不同的概率几乎可以忽略不计。
布隆过滤器就是通过将元素进行多个Hash算法计算,都存入位图中,查询时使用同样的Hash算法计算,对应当所有值都为true时,表示存在。这样就可以极大的提升位图的存储效率。
布隆过滤器也有致命的缺陷,即存在误判率,也称为假阳性率。当数据量不断增大,位图中非true位置越来越少,很可能会出现未插入的数据,查询结果为true。所以布隆过滤器的特点就是 False is always false. True is maybe true。因为误判率的存在,布隆过滤器多用于黑名单校验。
可以通过Hash函数个数与预期插入数据个数,预测误判率,详见Guava实现的BloomFilter。如果插入数据过多,超出了预期的误判率。可以选择新建一个位图,将新数据插入到新的位图,但是在查询时可能需要多查询一个位图。
优点:
- 提高了位图的存储效率
- 与散列表相比没有链表遍历,主要耗时为Hash计算,理论上时间效率更高
缺点:
- 存在误判率
- 无法删除数据
具体实现
- JAVA BitSet
- Guava BloomFilter
- Redis BitMap
应用
- 黑名单校验
- 位图排序(桶排序)
- 快速去重
- ConsistencyCheck
- 爬虫URL校验
- 解决缓存穿透问题
注意事项
-
位图高效的空间利用率只有在数据密集的情况下才能体现出来。最近遇到的场景:用户每日首次进入时弹窗广告,用户ID从10亿至20亿,用Integer存储。假设每日访问用户为n,存储共消耗32n个bit;用位图存储需要10亿个bit。所以当每日访问量在3125万人时,位图的存储效率才会超过列表存储。
-
位图可以做到精准校验,布隆过滤器存在误判。