通俗的理解Bitmap(位图)和RoaringBitmap(压缩位图)

一、使用的场景

日常业务中需要大量存储一些重复的字符串,例如每日签到用户、朋友圈点赞的好友、计算每日登录用户等。字符串无论长短不仅会浪费大量的存储资源,而且读取查询也耗时耗资源,那有没有一种存储方式对这一类场景进行优化呢。

二、什么原理

1、Bitmap如何解决这个问题

拿每日签到用户的场景举例,首先对用户id进行编号(offset)。


图1.png

这样我们只需要每天创建一个只存储位的数组,比如2023-12-8日只有zhangsan和zhaoliu进行了登录,我们只需要一个4位的字节数组来进行存储:


图2.png

这样就达到了减少存储的目的。

2、压缩Bitmap是如何做到压缩的

然而现实的场景中,存储的数据并不是连续的,而是稀疏的,而且创建Bitmap的时候,长度必须是最大的offset的长度,例如我们有64个用户,那么创建的位数组就是64位。
假如我们有1万个用户,但今天只有编号9999的用户登录了系统,那我们还是创建一个1万个Byte的数组,反而增加了内存的消耗。
为了更容易理解这个问题,我们还是以64位Bitmap为例,也就是最大支持存储64个用户,当我们只存储63时:


图3.png

先创建了长度为64位的数组,只在63这个位置上的存储是有意义的,其他位置都是0,那这种情况是不是可以进行优化呢?
我们把数组拆分成2组,1到32的用户存储到第1组,把33到64的用户存储到第二组。这个时候我们发现第一组的数组里全是0,那第一组是不是可以不创建?这样我们就用一个32位大小的数组存储了64位的数据。
接下来更进一步,把64位的数组拆成每16个一组,那么我们只需要创建最后一个分组的数组,也就是16位的数组来达到进一步压缩的目的。
第4组


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

三、怎么模拟实现

代码来简单的模拟实现压缩Bitmap

1、先创建一个类累存储单个的Bitmap

public class ArrayContainer {
    public char[] values;

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

2、实现压缩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实现压缩Bitmap存储

Redis本身是支持Bitmap存储的,但是压缩的Bitmap是不支持的。根据上面的原理,同样可以把一个大的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、读写Bitmap

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();
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,635评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,543评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,083评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,640评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,640评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,262评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,833评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,736评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,280评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,369评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,503评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,870评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,340评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,460评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,909评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,512评论 2 359

推荐阅读更多精彩内容