通俗的理解Bitmap(位圖)和RoaringBitmap(壓縮位圖)

一厚棵、使用的場景

日常業(yè)務中需要大量存儲一些重復的字符串烤宙,例如每日簽到用戶、朋友圈點贊的好友婿崭、計算每日登錄用戶等拨拓。字符串無論長短不僅會浪費大量的存儲資源,而且讀取查詢也耗時耗資源氓栈,那有沒有一種存儲方式對這一類場景進行優(yōu)化呢渣磷。

二、什么原理

1授瘦、Bitmap如何解決這個問題

拿每日簽到用戶的場景舉例醋界,首先對用戶id進行編號(offset)。


圖1.png

這樣我們只需要每天創(chuàng)建一個只存儲位的數(shù)組提完,比如2023-12-8日只有zhangsan和zhaoliu進行了登錄形纺,我們只需要一個4位的字節(jié)數(shù)組來進行存儲:


圖2.png

這樣就達到了減少存儲的目的。

2徒欣、壓縮Bitmap是如何做到壓縮的

然而現(xiàn)實的場景中逐样,存儲的數(shù)據(jù)并不是連續(xù)的,而是稀疏的打肝,而且創(chuàng)建Bitmap的時候脂新,長度必須是最大的offset的長度,例如我們有64個用戶粗梭,那么創(chuàng)建的位數(shù)組就是64位争便。
假如我們有1萬個用戶,但今天只有編號9999的用戶登錄了系統(tǒng)断医,那我們還是創(chuàng)建一個1萬個Byte的數(shù)組始花,反而增加了內存的消耗。
為了更容易理解這個問題孩锡,我們還是以64位Bitmap為例,也就是最大支持存儲64個用戶亥贸,當我們只存儲63時:


圖3.png

先創(chuàng)建了長度為64位的數(shù)組躬窜,只在63這個位置上的存儲是有意義的,其他位置都是0炕置,那這種情況是不是可以進行優(yōu)化呢荣挨?
我們把數(shù)組拆分成2組,1到32的用戶存儲到第1組朴摊,把33到64的用戶存儲到第二組。這個時候我們發(fā)現(xiàn)第一組的數(shù)組里全是0,那第一組是不是可以不創(chuàng)建弃鸦?這樣我們就用一個32位大小的數(shù)組存儲了64位的數(shù)據(jù)曲楚。
接下來更進一步,把64位的數(shù)組拆成每16個一組,那么我們只需要創(chuàng)建最后一個分組的數(shù)組鹃操,也就是16位的數(shù)組來達到進一步壓縮的目的韭寸。
第4組


圖4.png
未命名流程圖.jpg

三、怎么模擬實現(xiàn)

代碼來簡單的模擬實現(xiàn)壓縮Bitmap

1荆隘、先創(chuàng)建一個類累存儲單個的Bitmap

public class ArrayContainer {
    public char[] values;

    public ArrayContainer(int initialCapacity) {
        this.values = new char[initialCapacity];
    }
}

2恩伺、實現(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、測試使用

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實現(xiàn)壓縮Bitmap存儲

Redis本身是支持Bitmap存儲的晶渠,但是壓縮的Bitmap是不支持的。根據(jù)上面的原理燃观,同樣可以把一個大的Bitmap轉成一個個小的Bitmap來達到壓縮的目的褒脯。

1、包裝實體保存壓縮位圖的信息

public class CompressedBitInfo implements Serializable {

    /**
     * 真實offset
     */
    private long sourceOffset;
    /**
     * 分桶的編號
     */
    private long bucketIndex;
    /**
     * 桶內的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仪壮、工具類計算每個小桶的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憨颠、讀寫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();
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市积锅,隨后出現(xiàn)的幾起案子爽彤,更是在濱河造成了極大的恐慌,老刑警劉巖缚陷,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件适篙,死亡現(xiàn)場離奇詭異,居然都是意外死亡箫爷,警方通過查閱死者的電腦和手機嚷节,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虎锚,“玉大人硫痰,你說我怎么就攤上這事〈芑ぃ” “怎么了效斑?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柱徙。 經常有香客問我缓屠,道長,這世上最難降的妖魔是什么护侮? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任敌完,我火速辦了婚禮,結果婚禮上羊初,老公的妹妹穿的比我還像新娘滨溉。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布业踏。 她就那樣靜靜地躺著禽炬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勤家。 梳的紋絲不亂的頭發(fā)上腹尖,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音伐脖,去河邊找鬼热幔。 笑死,一個胖子當著我的面吹牛讼庇,可吹牛的內容都是我干的绎巨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蠕啄,長吁一口氣:“原來是場噩夢啊……” “哼场勤!你這毒婦竟也來了?” 一聲冷哼從身側響起歼跟,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤和媳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后哈街,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體留瞳,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年骚秦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片作箍。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡硬梁,死狀恐怖,靈堂內的尸體忽然破棺而出胞得,到底是詐尸還是另有隱情荧止,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布懒震,位于F島的核電站,受9級特大地震影響嗤详,放射性物質發(fā)生泄漏个扰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一葱色、第九天 我趴在偏房一處隱蔽的房頂上張望递宅。 院中可真熱鬧,春花似錦、人聲如沸办龄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俐填。三九已至安接,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間英融,已是汗流浹背盏檐。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驶悟,地道東北人胡野。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像痕鳍,于是被迫代替她去往敵國和親硫豆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容