一、问题引入:
当我们在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,确定这个值应该放到桶中的哪一个位置,如下图所示。
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 字节。
三、使用步骤:
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");