Java与Redis中的位图(BitSet)
什么是位图?
位图(bitmap)是一种非常常用的结构,在索引,数据压缩等方面有广泛应用
位图使用字节来存储数据。在java中一个Long占8个字节,一个字节(Byte)占用8个bit,因此一个Long型数据占用64个bit长度。如果让每一个0/1比特位都作为代表一个数据是否存在的boolean值,那么一个long整形数据就可以存储64个数据,存储的效率非常高。
首先来看一下Java源码中位图实现(bitset)对于位图数据结构的描述
This class implements a vector of bits that grows as needed. Each component of the bit set has a boolean value. The bits of a BitSet are indexed by nonnegative integers. Individual indexed bits can be examined, set, or cleared. One BitSet may be used to modify the contents of another BitSet through logical AND, logical inclusive OR, and logical exclusive OR operations.
By default, all bits in the set initially have the value false.
Every bit set has a current size, which is the number of bits of space currently in use by the bit set. Note that the size is related to the implementation of a bit set, so it may change with implementation. The length of a bit set relates to logical length of a bit set and is defined independently of implementation.这个类实现了一个按需增长的字节数组。每一个bit set中的每个字节都作为一个布尔值,同时拥有一个非负值索引。每一位索引值可以背检验、设置或清楚。两个位图之间可以进行与、或以及逻辑与操作。在位图中每个比特位值被设置为负值。
每个位图都有一个size表示存储已经被使用的位,但是size可能会根据位图的具体实现而有所不同,但位图的长度(length)则与具体实现无关。
private final static int ADDRESS_BITS_PER_WORD = 6;
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
//一个long型数组,每个long存储64bit位
private long[] words;
public BitSet() {
initWords(BITS_PER_WORD);
sizeIsSticky = false;
}
public BitSet(int nbits) {
// nbits can't be negative; size 0 is OK
if (nbits < 0)
throw new NegativeArraySizeException("nbits < 0: " + nbits);
initWords(nbits);
sizeIsSticky = true;
}
private static int wordIndex(int bitIndex) {
//计算存储位所在word索引
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
private void initWords(int nbits) {
//计算所需要long数组大小
words = new long[wordIndex(nbits-1) + 1];
}
位图在被初始化时,如果调用默认构造函数会被初始化成一个只有一个大小为1的Long型数组。
主要操作主要有三个, examined、set、cleared
首先看到set
public void set(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
expandTo(wordIndex);
words[wordIndex] |= (1L << bitIndex); // Restores invariants
checkInvariants();
}
目的就是使用位移以及或操作,将long类型的index位置1
public boolean get(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
checkInvariants();
int wordIndex = wordIndex(bitIndex);
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}
get操作就是判断某一位是否为一的操作
public void clear(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
//获取long索引
int wordIndex = wordIndex(bitIndex);
if (wordIndex >= wordsInUse)
return;
//该位与0,相当于清零
words[wordIndex] &= ~(1L << bitIndex);
recalculateWordsInUse();
checkInvariants();
}
使用位图有什么好处、有什么应用场景?
不管是在java还是在以c为底层的redis中,使用bitmap数据结构显而易见的好处就是可以极大的提高数据的存储效率。举例来说如果有一个用户签到的场景,需要判断某个用户在一个月签到了几天,或者今天是否已经签到了,显而易见我们可以使用Hashmap来进行存储,key为用户的id+日期,但是这样意味着每签到一天都需要一个key/value对象进行存储,因此我们可以考虑使用bitmap。每个用户一年的签到数需要365个比特来存储,即为365bit 也就是45bytes,10000个用户也只需要445kb的数据来存储一 年的签到数据。
还有一个场景是用户登陆场景,如果平均一个人一天登录1次,那么1亿个用户一个星期就会产生1 * 1 * 7 = 7亿条数据,一个月就会产生30亿条数据,没必要花费这么多的资源。
用户是否登录可以用0/1来表示,0代表用户不登陆,1表示登录,那么1bit 就可以表示用户是否登录。
1亿个用户一天的数据量也就 1 0000 0000bit = 11.92m,也就是说用户一天的登录信息也就产生11.92m的数据量。一个月也就357.63m的数据量。这样的存储是十分高效的
参考资料: