bitmap原理
bitmap是什么?在计算机中一个字节(byte)=8位(bit),这里的bit就是位,数据的最小表示单位,map一般是表示地图或者映射。
简单回顾一下二进制的一些知识:
1byte=8bit
1个bit有二种状态:0或1
所以1个byte可以表示00000000->11111111,也就是十进制0-255
其中十进制和二进制的对应关系如下:
0 ---------> 0000 0000 1 ---------> 0000 0001 2 ---------> 0000 0010 3 ---------> 0000 0011 4 ---------> 0000 0100 5 ---------> 0000 0101 6 ---------> 0000 0110 7 ---------> 0000 0111 8 ---------> 0000 1000 9 ---------> 0000 1001 10 ---------> 0000 1010 11 ---------> 0000 1011 12 ---------> 0000 1100 13 ---------> 0000 1101 14 ---------> 0000 1110 15 ---------> 0000 1111 ....................... ....................... 255...........1111 1111
在大部分变成语言里,int类型一般的都是占4个byte,也就是32位,甭管你这个数字是1或者是21亿你都得栈32位,所以如果你现在有10亿数字需要存在内存里面,需要分配多少内存呢:
1000000000 * 4 / 1024 / 1024 = 3800MB,大概需要3800MB内存,这里计算出的数值只适合C,在PHP里面,一个整形变量占用的实际空间远远大于4byte,是好几倍。
为了解决这个问题,bitmap采用一种映射机制,举个例子,假如有1、3、7、2、5这5个数字需要存放,正常情况下需要5*4bye=20byte,但是bitmap只需要1byte,它是如何做到的?
假设下面是1byte,首先将所有位置为0:00000000
从第一个0开始数数,把对应数字的位置置位1,比如说第一个1那就是第2个位置置位1,第二个三就是把第4个位置置位1,以此类推
1 => 0 1 0 0 0 0 0 0 3 => 0 0 0 1 0 0 0 0 7 => 0 0 0 0 0 0 0 1 2 => 0 0 1 0 0 0 0 0 5 => 0 0 0 0 0 1 0 0
累加起来最终的串就是:0 1 1 1 0 1 0 1
其实最终的数字和二进制没什么关系,纯粹是数数,这个串就代表最大到7的数字,然后我们就开始数数,从0开始:
比如第1个位置是1,那就记个1
比如第2个位置是1,那就记个2
比如第3个位置是1,那就记个3
比如第5个位置是1,那就记个5
比如第7个位置是1,那就记个7
结果就是1 2 3 5 7,不仅仅排序,而且还排重了,如果按照这种转换机制,1个int类型,32位的话,可以表示0-31之间的数字
如果要表示最大一万的数,就需要1万个位的串,但是编程语言并没有这样的数据类型,但是可以用数组去模拟。举个例子,一个整形是32位,也就说我们大概需要314个数组元素来表示这个串:
- 数组第1个元素 00 - 31
- 数组第2个元素 32 - 63
- 数组第3个元素 64 - 95
- 数组第4个元素 96 - 127
- ……
提到这个算法的好处,最大的好处就是节省内存,节省了好几十倍,适合处理大量数据,除了快速排序,还可以快速去重,快速查询是否存在,还有一个好听的应用Bloom Filter(布隆过滤器)
redis bitmap应用
setBit
说明:给一个指定的key的值的第offset位赋值位value
参数:key offset value:bool or int (1 or 0)
返回值:long:0或1
getBit
说明:返回一个指定key的二进制信息
参数:key offset
返回值:long
bitCount
说明:返回一个指定key中位的值为1的个数(是以byte为单位的bit)
参数:key start offset
返回值:long
bitOp
说明:对不同的二进制存储数据进行位运算(AND、OR、NOT、XOR)
参数:operation destkey key [key …]
返回值:long
bitmap优势:
- 基于最小的单位bit进行存储,所以非常省空间
- 设置时候时间复杂度O(1),读取时候时间复杂度O(n),操作时非常快的
- 二进制数据的存储,进行相关计算的时候非常快
- 方便扩容
bitmap限制:
redis中bit映射并限制在512MB之内,所以最大时2^32位。建议每个key的位数都控制下,因为读取时候时间复杂度O(n),越大的串读的时间花销越大。
bitmap使用场景:
1.视频属性的无限延伸:
需求分析:一个拥有亿级数据量的短视频app,视频存在各种属性(是否加锁、是否特效等等),需要做各种标记。
可能想到的解决方案:
- 存储在mysql中,肯定不行,一个是随着业务增长属性一直增加,并且存在有时间限制的属性,直接对数据库进行加减字段是非常不合理的做法。即使是存在一个字段中用json等压缩技术存储也存在读效率的问题,并且对于大几亿的数据来说,废弃的字段回收起来非常麻烦。
- 直接记录在redis中,根据业务属性+uid为key来存储。读写效率角度没毛病,但是存储的角度来说key的数据量都大于value了,太耗费空间了。即使是用json等压缩技术来存储。也存在问题,解压需要时间,并且大几亿的数据回收也是难题。
设计方案:
使用redis的bitmap进行存储。
key由属性id+视频分片id组成。value按照视频id对分片范围取模来决定偏移量offset。10亿视频一个属性约120m还是挺划算的。
伪代码:
2.用户在线状态
需求分析:需要对子项目提供一个接口,来提供某用户是否在线?
设计方案:使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户id为偏移量offset,如果在线就设置为1,不在线就设置为0,3亿用户只需要36MB的空间。
伪代码:
3.统计活跃用户
需求分析:需要计算活跃用户的数据情况。
设计方案:使用时间作为缓存的key,然后用户id为offset,如果当日活跃过就设置为1。之后通过bitOp进行二进制计算算出在某段时间内用户的活跃情况。
伪代码:
4.用户签到
需求分析:用户需要进行签到,对于签到的数据需要进行分析与相应的运运营策略。
设计方案:使用redis的bitmap,由于是长尾的记录,所以key主要由uid组成,设定一个初始时间,往后没加一天即对应value中的offset的位置。
伪代码:
原文出处: