一、使用的場(chǎng)景
日常業(yè)務(wù)中需要大量存儲(chǔ)一些重復(fù)的字符串,例如每日簽到用戶、朋友圈點(diǎn)贊的好友、計(jì)算每日登錄用戶等。字符串無(wú)論長(zhǎng)短不僅會(huì)浪費(fèi)大量的存儲(chǔ)資源,而且讀取查詢也耗時(shí)耗資源,那有沒(méi)有一種存儲(chǔ)方式對(duì)這一類(lèi)場(chǎng)景進(jìn)行優(yōu)化呢。
二、什么原理
1、Bitmap如何解決這個(gè)問(wèn)題
拿每日簽到用戶的場(chǎng)景舉例,首先對(duì)用戶id進(jìn)行編號(hào)(offset)。

這樣我們只需要每天創(chuàng)建一個(gè)只存儲(chǔ)位的數(shù)組,比如2023-12-8日只有zhangsan和zhaoliu進(jìn)行了登錄,我們只需要一個(gè)4位的字節(jié)數(shù)組來(lái)進(jìn)行存儲(chǔ):

這樣就達(dá)到了減少存儲(chǔ)的目的。
2、壓縮Bitmap是如何做到壓縮的
然而現(xiàn)實(shí)的場(chǎng)景中,存儲(chǔ)的數(shù)據(jù)并不是連續(xù)的,而是稀疏的,而且創(chuàng)建Bitmap的時(shí)候,長(zhǎng)度必須是最大的offset的長(zhǎng)度,例如我們有64個(gè)用戶,那么創(chuàng)建的位數(shù)組就是64位。
假如我們有1萬(wàn)個(gè)用戶,但今天只有編號(hào)9999的用戶登錄了系統(tǒng),那我們還是創(chuàng)建一個(gè)1萬(wàn)個(gè)Byte的數(shù)組,反而增加了內(nèi)存的消耗。
為了更容易理解這個(gè)問(wèn)題,我們還是以64位Bitmap為例,也就是最大支持存儲(chǔ)64個(gè)用戶,當(dāng)我們只存儲(chǔ)63時(shí):

先創(chuàng)建了長(zhǎng)度為64位的數(shù)組,只在63這個(gè)位置上的存儲(chǔ)是有意義的,其他位置都是0,那這種情況是不是可以進(jìn)行優(yōu)化呢?
我們把數(shù)組拆分成2組,1到32的用戶存儲(chǔ)到第1組,把33到64的用戶存儲(chǔ)到第二組。這個(gè)時(shí)候我們發(fā)現(xiàn)第一組的數(shù)組里全是0,那第一組是不是可以不創(chuàng)建?這樣我們就用一個(gè)32位大小的數(shù)組存儲(chǔ)了64位的數(shù)據(jù)。
接下來(lái)更進(jìn)一步,把64位的數(shù)組拆成每16個(gè)一組,那么我們只需要?jiǎng)?chuàng)建最后一個(gè)分組的數(shù)組,也就是16位的數(shù)組來(lái)達(dá)到進(jìn)一步壓縮的目的。
第4組


三、怎么模擬實(shí)現(xiàn)
代碼來(lái)簡(jiǎn)單的模擬實(shí)現(xiàn)壓縮Bitmap
1、先創(chuàng)建一個(gè)類(lèi)累存儲(chǔ)單個(gè)的Bitmap
public class ArrayContainer {
public char[] values;
public ArrayContainer(int initialCapacity) {
this.values = new char[initialCapacity];
}
}
2、實(shí)現(xiàn)壓縮Bitmap的邏輯
public class CompressedBitmap {
public static final char Y = 1;
public static final char N = 0;
public static final int ARRAY_CONTAINER_SIZE = 16;
ArrayContainer[] containers;
public CompressedBitmap(long initialCapacity) {
int containersSize = (int)(initialCapacity / ARRAY_CONTAINER_SIZE);
this.containers = new ArrayContainer[containersSize];
}
public void add(long offset){
int shardIndex = getShardIndex(offset);
ArrayContainer container = containers[shardIndex];
if(container == null){
container = new ArrayContainer(ARRAY_CONTAINER_SIZE);
}
int shardOffset = getShardOffset(offset);
container.values[shardOffset] = Y;
containers[shardIndex] = container;
}
public int get(long offset){
int shardIndex = getShardIndex(offset);
ArrayContainer container = containers[shardIndex];
if(container == null){
return N;
}
int shardOffset = getShardOffset(offset);
if(Y == container.values[shardOffset]){
return Y;
}
return N;
}
public int getShardIndex(long offset){
return (int)((offset - 1) / ARRAY_CONTAINER_SIZE);
}
public int getShardOffset(long offset){
return (int)(offset % ARRAY_CONTAINER_SIZE);
}
public static void main(String[] args) {
System.out.println(2 / ARRAY_CONTAINER_SIZE);
}
}
3、測(cè)試使用
public static void main(String[] args) {
CompressedBitmap bitmap = new CompressedBitmap(64);
bitmap.add(63);
System.out.println(bitmap.get(63));
System.out.println(bitmap.get(64));
System.out.println(bitmap.get(2));
}
四、Redis實(shí)現(xiàn)壓縮Bitmap存儲(chǔ)
Redis本身是支持Bitmap存儲(chǔ)的,但是壓縮的Bitmap是不支持的。根據(jù)上面的原理,同樣可以把一個(gè)大的Bitmap轉(zhuǎn)成一個(gè)個(gè)小的Bitmap來(lái)達(dá)到壓縮的目的。
1、包裝實(shí)體保存壓縮位圖的信息
public class CompressedBitInfo implements Serializable {
/**
* 真實(shí)offset
*/
private long sourceOffset;
/**
* 分桶的編號(hào)
*/
private long bucketIndex;
/**
* 桶內(nèi)的offset
*/
private long bucketOffset;
public long getSourceOffset() {
return sourceOffset;
}
public void setSourceOffset(long sourceOffset) {
this.sourceOffset = sourceOffset;
}
public long getBucketIndex() {
return bucketIndex;
}
public void setBucketIndex(long bucketIndex) {
this.bucketIndex = bucketIndex;
}
public long getBucketOffset() {
return bucketOffset;
}
public void setBucketOffset(long bucketOffset) {
this.bucketOffset = bucketOffset;
}
@Override
public String toString() {
return "CompressedBitInfo{" + "sourceOffset=" + sourceOffset + ", bucketIndex=" + bucketIndex + ", bucketOffset=" + bucketOffset + '}';
}
}
2、工具類(lèi)計(jì)算每個(gè)小桶的Bitmap
public class CompressedBitTookit {
public static final long DEFAULT_BUCKET_SIZE = 65536L;
public static CompressedBitInfo getBitInfo(long sourceOffset, long bucketSize) {
CompressedBitInfo bucketInfo = new CompressedBitInfo();
bucketInfo.setSourceOffset(sourceOffset);
long bucketIndex = sourceOffset / bucketSize;
long bucketOffset = sourceOffset % bucketSize;
bucketInfo.setBucketIndex(bucketIndex);
bucketInfo.setBucketOffset(bucketOffset);
return bucketInfo;
}
public static CompressedBitInfo getBitInfo(long sourceOffset) {
CompressedBitInfo bucketInfo = getBitInfo(sourceOffset, DEFAULT_BUCKET_SIZE);
return bucketInfo;
}
public static long getSourceOffset(long bucketIndex, long bucketSize, long bucketOffset) {
return bucketIndex * bucketSize + bucketOffset;
}
public static long getSourceOffset(long bucketIndex, long bucketOffset) {
return getSourceOffset(bucketIndex, DEFAULT_BUCKET_SIZE, bucketOffset);
}
3、讀寫(xiě)B(tài)itmap
public void setCompressedBit(String key, long offset, boolean value, int expireSeconds) {
CompressedBitInfo bitInfo = CompressedBitTookit.getBitInfo(offset);
String bitKey = getBitKey(key, bitInfo.getBucketIndex());
redis.setBit(bitKey, bitInfo.getBucketOffset(),value);
redis.expire(bitKey, expireSeconds);
}
public void setCompressedBit(String key, long offset, boolean value) {
CompressedBitInfo bitInfo = CompressedBitTookit.getBitInfo(offset);
String bitKey = getBitKey(key, bitInfo.getBucketIndex());
redis.setBit(bitKey, bitInfo.getBucketOffset(),value);
}
public void setCompressedBit(String key, long offset) {
this.setCompressedBit(key, offset, true);
}
public void remCompressedBit(String key, long offset) {
this.setCompressedBit(key, offset, false);
}
public Boolean getCompressedBit(String key, long offset) {
CompressedBitInfo bitInfo = CompressedBitTookit.getBitInfo(offset);
String bitKey = getBitKey(key, bitInfo.getBucketIndex());
log.debug("getCompressedBit with key:{}, offset:{}",bitKey, bitInfo.getBucketOffset());
return redis.getBit(bitKey, bitInfo.getBucketOffset());
}
public void deleteAllCompressedBit(String key, long maxOffset) {
CompressedBitInfo bitInfo = CompressedBitTookit.getBitInfo(maxOffset);
for (int i = 0; i < bitInfo.getBucketIndex(); i++) {
String bitKey = getBitKey(key, i);
redis.expire(bitKey, 0);
}
}
private String getBitKey(String key, long bucketIndex) {
return new StringBuffer(key).append("_").append(bucketIndex).toString();
}