RoaringBitMap高效压缩内存初探

858 阅读3分钟

一、问题引入:

当我们在redis中大量存储int或者long类型的数据时,会占用大量内存空间,如果数据量足够大,会严重影响系统的稳定运行。但是不存redis,那么势必就需要查数据库,会造成数据库的性能瓶颈。我们写一个测试Demo,往redis中存入一个set结构,内含100万个Long类型的数据,如下:

public void memTest1Func() {
    Long startTime = System.currentTimeMillis();
    Long start = 1734856521692028928L;
    for (int i = 0; i < 1000000; i++) {
        redisTemplate.opsForSet().add("test1:memtest", start + i);
    }
    Long endTime = System.currentTimeMillis();
    System.out.println("耗时:" + (endTime - startTime) / 1000);
}

经测试,执行时间42秒,占用内存50M。如果数据量过亿,那么这个时间和内存占用是非常可怕的。

二、RoaringBitmap简介:

RoaringBitmap是一个高效的压缩位图数据结构,用于处理大量的布尔型数据。它是由Facebook开发的一种数据结构,主要用于处理大规模的集合运算,如交集、并集、差集等。RoaringBitmap的主要优点是节省内存和计算资源,因为它使用了一种称为Run-Length Encoding(RLE)的压缩算法来存储数据。

1、实现思路为:

  • 将 32bit int(无符号的)类型数据 划分为 2^16 个桶,即最多可能有216=65536个桶,论文内称为container。用container来存放一个数值的低16位
  • 在存储和查询数值时,将数值 k 划分为高 16 位和低 16 位,取高 16 位值找到对应的桶,然后在将低 16 位值存放在相应的 Container 中(存储时如果找不到就会新建一个)

比如要将31这个数放进roarigbitmap中,它的16进制为:0000001F,前16位为0000,后16为001F。 所以先需要根据前16位的值:0,找到它对应的通的编号为0,然后根据后16位的值:31,确定这个值应该放到桶中的哪一个位置,如下图所示。

image.png

2、数据对比:

同样还是上面的demo,在roarigbitmap中存储100万个long数据,代码如下:

public void memTest2Func() throws IOException {
    Long startTime = System.currentTimeMillis();
    Roaring64Bitmap bitmap = new Roaring64Bitmap();
    Long start = 1734856521692028928L;
    for (int i = 0; i < 1000000; i++) {
        bitmap.add(start + i);
    }
    this.saveRoaringBitmap(bitmap, "test2:memtest");
    Long endTime = System.currentTimeMillis();
    System.out.println("压缩耗时:" + (endTime - startTime) / 1000);
}

经测试,执行时间只有几十毫秒,占用内存63 字节。

image.png

三、使用步骤:

1、引入POM依赖:

<dependency>
    <groupId>org.roaringbitmap</groupId>
    <artifactId>RoaringBitmap</artifactId>
    <version>1.0.5</version>	
</dependency>

2、注入bitmap的RedisTemplate:

@Bean
public RedisTemplate<String, byte[]> buildBitMapRedisTemplate(LettuceConnectionFactory connectionFactory) {
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

    RedisTemplate<String, byte[]> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);

    //重点在这四行代码
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    redisTemplate.afterPropertiesSet();

    return redisTemplate;
}

3、注入RedisTemplate:

@Autowired
private RedisTemplate<String,byte[]> bitRedisTemplate;

4、实现读写RoaringBitmap的方法:

public void saveRoaringBitmap(Roaring64Bitmap bitmap, String key) throws IOException {
    // 将RoaringBitmap序列化为字节数组
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
    bitmap.runOptimize();
    bitmap.serialize(dataOutputStream);
    byte[] serializedBitmap = byteArrayOutputStream.toByteArray();

    // 使用RedisTemplate将序列化的RoaringBitmap存入Redis
    bitRedisTemplate.opsForValue().set(key, serializedBitmap);
}

public Roaring64Bitmap loadRoaringBitmap(String key) throws IOException {
    // 从Redis中获取序列化的RoaringBitmap
    byte[] serializedBitmap = bitRedisTemplate.opsForValue().get(key);

    // 如果存在则反序列化为RoaringBitmap
    if (serializedBitmap != null) {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serializedBitmap);
        DataInputStream dataInputStream = new DataInputStream(byteArrayInputStream);
        Roaring64Bitmap bitmap = new Roaring64Bitmap();
        bitmap.deserialize(dataInputStream);

        return bitmap;
    } else {
        return null;
    }
}

5、实现RoaringBitmap的读写:

Roaring64Bitmap bitmap = new Roaring64Bitmap();
Long start = 1734856521692028928L;
for (int i = 0; i < 1000000; i++) {
    bitmap.add(System.currentTimeMillis());
}
this.saveRoaringBitmap(bitmap, "test2:memtest");