BitMap
场景一:(大部分开发面试都会遇到的一个问题)
有10亿个用户id (int类型),判断用户是否登录?(限制内存)
🤔 10亿个int类型,10亿 × 4 = 40亿byte = 4 GB
,如何既要保证速度,又要保证占用内存呢?
场景二:
短视频平台中有十亿+用户,广告主想要精准投放流量,如何进行人群的圈选呢?
BitMap介绍
bit(位)map(地图)被叫做位图
,其实理解其原理之后,可能叫位(bit)映射(map)更贴切一些。
BitMap比较适合通过比较小的内存解决大量数字的去重、排序和判存问题。
BitMap 结构
用场景一的问题举例:BitMap解决思路就是构建一个bit数组,bit每一位只能表示1或者0,其中数组的索引值映射到userId,当前索引上的数字是1的时候代表对应的userId已登录,是0的时候代表userId未登录。所以,逐行读取文件中的userid并给bit数组赋值,最终通过该bit数组就可以实现去重、排序和判存了。 (上图中的用户3
、4
就是已登录的)
假设每个位使用1比特(bit)来表示,那么十亿级别的位图将有10^9个位。由于每个字节(byte)包含8个位,我们可以将位数除以8来得到字节数。
总字节数 = 10^9 / 8 = 125,000,000 字节
将字节数转换为更常见的单位,可以得到以下结果:
- 125,000,000 字节 = 125 兆字节(MB)
位图也不是万能的,还是上述的题目,如果需要统计出每一个userId的出现次数,位图这种通过1和0标识有和无的方式就无法表达这个业务含义了。当位图比较稀疏的时候,也可能浪费空间,比如文件中就1和1000000000这两个userid,那么为为了使用位图而开辟的bit数组大小已经远超过两个int数字所使用的空间了。
RoaringBitmap
RoaringBitmap可以认为是BitMap的另外一种实现,该实现方式中考虑了数据量的多少与存储空间的平衡问题。将32的数字分为高16位和低16位,其中低16位根据数据量的多少,可以通过三种数据结构来实现:ArrayContainer
,BitMapContainer
和RunContainer
,其目的就是尽量节约存储空间。
- 数组容器(Array Container) :这种容器使用一个数组来存储整数,适用于稀疏数据的情况,即数据之间的间隔较大。例如,如果一个容器内的整数数量小于4096,就使用数组容器来存储,因为在这种情况下,使用数组容器比使用位图容器更节省空间。
- 位图容器(Bitmap Container) :位图容器使用一个长度为65536的位数组(bit array)来存储数据,适用于稠密数据的情况,即数据之间的间隔较小。例如,如果一个容器内的整数数量大于4096,就使用位图容器来存储,因为在这种情况下,使用位图容器比使用数组容器更节省空间。
- 运行长度编码容器(Run Container) :运行长度编码(Run-length Encoding,RLE)是一种简单的数据压缩算法,适用于连续重复数据的情况。在 Roaring Bitmap 中,如果一系列连续的整数都存在于位图中,那么可以使用一个运行长度编码容器来存储这些连续的整数的起始值和长度,从而节省空间。
RoaringBitmap 根据实际的数据情况,动态地选择最适合的容器类型,从而实现了既高效的数据存储,又快速的数据查询。在做位图计算(AND、OR、XOR)时,RoaringBitmap提供了相应的算法来高效地实现在多个容器之间的运算,使得RoaringBitmap无论在存储和计算性能上都表现优秀。
常见BitMap
Java中的BitSet
BitSet
是 Java 中的一个类,用于表示位集合(bit set)。它提供了一种有效地存储和操作位(布尔值)的方式。
下面是一个示意图,展示了 BitSet
的基本结构:
lua
+---------+---------+---------+---------+---------+---------+---------+---------+
| 位索引: | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
+---------+---------+---------+---------+---------+---------+---------+---------+
| 位值: | true | false | true | false | true | false | false |
+---------+---------+---------+---------+---------+---------+---------+---------+
在上述示意图中,每个方框表示一个位,从左到右依次编号。位索引从 0 开始,表示每个位在 BitSet
中的位置。位值表示每个位的状态,可以是 true
(被设置为 1)或 false
(被设置为 0)。
BitSet
提供了一系列方法来操作位集合,例如:
set(int index)
:将指定索引位置的位设置为true
。clear(int index)
:将指定索引位置的位设置为false
。get(int index)
:获取指定索引位置的位的值。size()
:获取位集合的大小(位的数量)。length()
:获取位集合的长度(最高位的索引加 1)。and(BitSet set)
:对当前位集合与另一个位集合执行逻辑与操作。or(BitSet set)
:对当前位集合与另一个位集合执行逻辑或操作。xor(BitSet set)
:对当前位集合与另一个位集合执行逻辑异或操作。
【例子】判断某个用户是否存在:
java
public class UserPresenceChecker {
private BitSet userBitSet;
public UserPresenceChecker() {
userBitSet = new BitSet();
}
public void addUser(long userId) {
userBitSet.set((int) userId);
}
public boolean isUserPresent(long userId) {
return userBitSet.get((int) userId);
}
public static boolean checkUserPresence(long userId) {
UserPresenceChecker presenceChecker = new UserPresenceChecker();
// 假设有一亿用户存在
for (int i = 0; i < 100000000; i++) {
presenceChecker.addUser(i);
}
//模拟123不存在
presenceChecker.userBitSet.set(123,false);
return presenceChecker.isUserPresent(userId);
}
public static void main(String[] args) {
long userId = 123;
boolean isPresent = checkUserPresence(userId);
if (isPresent) {
System.out.println("用户 " + userId + " 存在");
} else {
System.out.println("用户 " + userId + " 不存在");
}
}
}
RoaringBitmap 在Java中也有实现,还提供了更多的逻辑运算方式。
Redis中的BitMap
Redis 的位图是一种特殊的字符串类型,可以用于存储和操作位级别的数据。
Redis 中的位图使用字节数组来表示,每个字节可以存储 8 个位。位图可以非常高效地进行位级别的操作,如设置位、获取位、计数位、位图之间的逻辑运算等。
以下是一些 Redis 中位图相关的命令:
SETBIT key offset value
:设置指定偏移量上的位为指定值(0 或 1)。GETBIT key offset
:获取指定偏移量上的位的值。BITCOUNT key [start end]
:计算指定范围内的位为 1 的数量。BITOP operation destkey key [key ...]
:对多个位图进行逻辑运算,并将结果保存到目标位图中。BITPOS key bit [start] [end]
:查找指定位的第一个出现位置。
以下是一个示例,展示如何在 Redis 中使用位图:
bash
# 设置位图
SETBIT mybitmap 0 1
SETBIT mybitmap 2 1
SETBIT mybitmap 5 1
# 获取位图
GETBIT mybitmap 0 # 返回 1
GETBIT mybitmap 1 # 返回 0
# 计算位图中位为 1 的数量
BITCOUNT mybitmap # 返回 3
# 逻辑运算
SETBIT mybitmap2 1 1
SETBIT mybitmap2 3 1
BITOP AND mybitmap_and mybitmap mybitmap2 # 对两个位图进行 AND 运算,并将结果保存到 mybitmap_and 中
# 查找位为 1 的第一个出现位置
BITPOS mybitmap 1 0 # 返回 0
BITPOS mybitmap 1 1 # 返回 2
通过 Redis 的位图功能,你可以高效地存储和操作位级别的数据,例如记录用户的在线状态、统计用户活跃度、进行布隆过滤器等应用场景。
ClickHouse中的BitMap
😜3,2,1上链接!!!
BitMap应用案例
人群圈选
如果有一张表,存储着每一个userid对应的常驻省和性别,如何从这张表中查出所有北京市男性用户的userId,并存储成一个人群包。
最简单直接的思路是select userId from table where sex = '男' and province = '北京',但是当满足条件的userId达到千万+级别时,通过SQL语句直接获取userId就不太合适,一是结果集比较大,二是执行比较耗时。
此时可以转换一个角度解决这类问题,如果表结构如下:
可以将不同标签值对应的userId列表转换成BitMap,再次筛选北京市男性用户的时候,直接将男BitMap与北京BitMap做交集即可,快速高效且节约大量的存储空间。
如图所示: