BitMap 主要用来处理大文件场景下的数据搜索、去重、排序。
1、MySQL中的应用
那一个简单的例子来说吧,假设有一个用户表,和一堆的属性:
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
`user_name` varchar(20) NOT NULL DEFAULT '' COMMENT '姓名',
`phone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
`status` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '状态',
`create_time` int(10) NOT NULL DEFAULT '0' COMMENT '创建时间',
`update_time` int(10) NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`),
key user_id (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';
有如下用户属性:
程序员、司机、90后、秃头大叔、爱打篮球、爱打游戏、爱吃辣...我们如果将这些属性都关联到用户UID上呢?比较常规的作答就是给每个属性分配一个ID
tagId = 1:程序员
tagId = 2:司机
tagId = 3:90后
tagId = 4:秃头大叔
tagId = 5:爱打篮球
tagId = 6:爱打游戏
tagId = 7:爱吃辣
......
方案1:在user表增加一个tagIds的字段,存储格式为 1,3,6,7 或者 [1,3,6,7]
如果要进行筛选包含tagId=6的数据,需要进行find_in_set等函数进行比较。
方案2:另外新建一个关联表userTags,有多少标签就insert多少条关联数据
以上两个方案还存在一个问题:如果是做图像识别等场景,每个用户可能有成千上万个标签,数据量会较大那么如何节约存储呢?这个时候可以考虑下BitMap:
假设预先开启N个 bit 位,如图,默认每个 bit 位的值都是0

用户拥有某个 tagId 的属性,就将对应 bit 的结果置为 1,假设某用户属性ID为1,3,6,7

原理很容易理解,如果转到MySQL字段中如何存储?
方案1:按照字符默认开辟 00000000000, 将对应位置1后,存储varchar为00001100101(前置0可省)
方案2:假设上面表格为二进制数据,如果选择了tagId:1,3,6,7。对应二进制如下:
00000000001
00000000100
00000100000
00001000000
或运算结果为: 00001100101
转换为10进制为: 2^6 + 2^5 + 2^2 + 2^0 = 101
(由于存在 2^0 = 1,在设计时也可直接舍弃该位从 2^1 开始计算)
对应数据库内存储的值为:101对于以上两个方案,其实都存在一个问题:如果需要筛选用户标签包含某属性的ID如果去做?
方案1:可能存储的字符会很多。
有64个标签就要开辟 varchat(64) 用来存储,如果用bigint也就能存储20个标签
而且筛选会很难处理,比如筛选爱打游戏的用户,即如何判断某位上的值是否为1?
方案2:转为整数后存储的字符会很少,但也不能超过64个标签,因为bigint最大为2^64
对于筛选,比如筛选爱打游戏的用户:先列举出爱打游戏标签的二进制 00000100101,
依次拿所有的数据进行异或运算,如果与运算之后的结果为:00000100000(十进制32)即为符合条件。
映射到SQL为以下两种写法:
where tagIds & 32 > 0; (32对应二进制 2^5)
where tagIds & 32 = 32;
但 BitMap 转10进制存储后仍有一个问题无法解决,where查询无法命中索引那么如何解决MySQL这种查询场景的弊端呢?
其实可以考虑用ES存储为json,然后通过ES自带的高效匹配进行处理,关于ES的介绍我们在另外一篇博客有介绍:juejin.cn/post/684490…
2、实际场景中的应用
简单列举一个场景:很多时候面试都会问的一个问题,已知某文件内包含一些电话号码,每个号码为8位数字,统计去重后号码的个数。
思考:一个字节(1B)可以存储8个bit位,对应一个long类型的整数。
常见的存储:一个电话号码需要 1B ,1M大约可存储 105W 个电话号码,如果数据量较少,我们直接开辟内存直接存储即可,假设文件内的号码是亿级别的如何处理?
考虑下BitMap:8位整数不超过99999999,需开辟 99999999 个bit 位,每个bit位只存储0/1。
即 99999999 bit = 99999999/8 = 12500000 B = 12500000 / 1024 = 12207 KB = 12 MB 这样我们只需要12MB就能完成亿+级别的所有电话号码的存储。
其实 Bloom Filter 也是基于 BitMap 算法的一种扩展,关于Bloom Filter 我们在另一篇博客中有介绍:juejin.cn/post/684490…