咆哮的 BitMaps
Roaring Bitmaps是什么?
Roaring Bitmaps(Roaring Bitmaps)是一种高效的数据结构,用于压缩表示大规模数据集合的位图,它主要用于存储和检索键值对,并提供对键的导航和范围查询的功能。它在处理稀疏数据集合时表现出色,并且能够显著减少内存占用。传统的位图(Bitmap)使用一个位数组来表示数据集合,其中每个位表示一个元素的存在或缺失。然而,当数据集合稀疏时,即大部分位为0时,传统的位图会浪费大量的内存空间。
Roaring Bitmaps 通过使用多种压缩技术来解决这个问题,以提高内存利用率。它的主要思想是将数据集合分解为多个小的位图块,并选择最适合每个块的压缩方法。Roaring Bitmaps 的关键特性包括:
- 分段压缩:Roaring Bitmaps 将数据集合分解为多个不相交的段(chunks),每个段使用不同的压缩方法。这样,对于稀疏的段,可以采用更适合的压缩算法,而对于密集的段则可以使用更紧凑的表示方法。
- Run-Length Encoding(RLE):Roaring Bitmaps 使用 RLE 来压缩连续的位段,其中相同位状态(0或1)的连续位被压缩为一个区间。这种压缩方法对于表示长连续序列的位图非常有效。
- 排序:Roaring Bitmaps 在每个段内对位的位置进行排序,以加速位操作的执行。这种有序性有助于快速的位运算和范围查询。
使用 Roaring Bitmaps,你可以高效地存储和操作大规模的稀疏位集合,Roaring Bitmaps 提供了多个实现版本,包括RoaringBitmap(32位整数)、Roaring64Bitmap(64位整数),本文介绍Roaring64Bitmap的快速入门。
Roaring64NavigableMap
导入依赖
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.9.23</version>
</dependency>
基本使用
import org.roaringbitmap.longlong.Roaring64NavigableMap;
public class Roaring64NavigableMapExample {
public static void main(String[] args) {
// 创建 Roaring64NavigableMap 实例
Roaring64NavigableMap map = new Roaring64NavigableMap();
// 添加键值对
map.put(10L, "Value 10");
map.put(5L, "Value 5");
map.put(20L, "Value 20");
// 获取值
String value1 = map.get(10L);
System.out.println("Value of key 10: " + value1);
// 遍历映射
System.out.println("All key-value pairs in the map:");
map.forEach((key, value) -> {
System.out.println("Key: " + key + ", Value: " + value);
});
// 导航操作:查找比给定键(5L)大的下一个键
System.out.println("Next key after 5: " + map.higherKey(5L));
// 导航操作:查找比给定键(20L)小的前一个键
System.out.println("Previous key before 20: " + map.lowerKey(20L));
// 删除键值对
map.remove(5L);
// 检查键是否存在
boolean containsKey = map.containsKey(5L);
System.out.println("Contains key 5: " + containsKey);
}
}
常用API
put(long key, String value)
:将指定的键与值关联,并将其添加到映射中。get(long key)
:根据给定的键获取相应的值。remove(long key)
:从映射中删除指定键的键值对。containsKey(long key)
:检查映射中是否存在指定的键。isEmpty()
:检查映射是否为空。size()
:返回映射中键值对的数量。getLongCardinality():
获取Map中不重复键的数量keySet()
:返回映射中所有键的集合。values()
:返回映射中所有值的集合。forEach(BiConsumer<? super Long, ? super String> action)
:迭代映射中的所有键值对,并执行指定的操作。higherKey(long key)
:返回映射中比给定键大的下一个键。lowerKey(long key)
:返回映射中比给定键小的前一个键。subMap(long fromKey, long toKey)
:返回映射中键的子映射,其中键的范围从 fromKey(包括)到 toKey(不包括)。rank(long key)
:返回映射中小于或等于给定键的键的数量。select(int rank)
:返回映射中指定排名的键。firstKey()
:返回映射中的第一个键。lastKey()
:返回映射中的最后一个键。clear()
:清空映射,删除所有键值对
需求案例
使用Stream懒加载的方式查3000w用户数据的phoneNumber。
oaring64NavigableMap map = new Roaring64NavigableMap();
//查A User表中所有用户手机号
try (Stream<String> aUserStream = AUserDao.findPhoneNumber()) {
crbtUserStream.forEach(phoneNumber -> {
try {
map.addLong(Long.parseLong(phoneNumber.trim()));
} catch (Exception e) {
log.info("phonenumber format exception: {}", phoneNumber);
}
});
}
//查B User表中所有用户手机号
try (Stream<String> bUserStream = BUserDao.findPhoneNumber()) {
crbtUserStream.forEach(phoneNumber -> {
try {
map.addLong(Long.parseLong(phoneNumber.trim()));
} catch (Exception e) {
log.info("phonenumber format exception: {}", phoneNumber);
}
});
}
//获取 去重后所有用户数
map.getLongCardinality()
通过将 HINT_FETCH_SIZE 设置为 Integer.MIN_VALUE,JPA 将使用 MySQL 驱动程序的默认批量大小,这意味着每个批次能够获取更多的数据。
需要注意的是,数据库驱动程序的默认批量大小是为了在性能和资源消耗之间取得平衡。较大的批量大小可以减少数据库往返次数,提高查询效率,但也会增加内存消耗。因此,在处理大量数据时,需要根据系统的内存和性能限制来评估和调整批量大小。
@QueryHints(value = @QueryHint(name = HINT_FETCH_SIZE, value = "" + Integer.MIN_VALUE))
@Query("select phoneNumber from xxUser")
Stream<String> findPhoneNumber();
懒加载 or 分页查询
使用懒加载的方式查询和采用分页查询,哪种方式的效率更高,取决于具体的使用场景和需求。
- 懒加载方式查询(使用
Stream
):- 优点:懒加载的方式逐个获取数据,可以减少内存消耗,特别是在处理大量数据时。它适用于需要逐个处理结果或处理结果集较大的情况。
- 缺点:每次访问数据库都会进行查询,可能会引入一定的延迟和额外的数据库访问开销。如果需要多次遍历结果集,可能会导致多次数据库查询。
- 分页查询:
- 优点:分页查询一次性获取指定页的数据,减少了多次数据库查询的开销。它适用于需要一次性获取较小结果集的情况。
- 缺点:分页查询需要提前确定分页大小和页数,可能需要多次查询才能获取完整结果集。对于大结果集,可能会占用较多的内存。
因此,选择使用懒加载的方式还是分页查询取决于以下因素:
- 数据量:如果结果集较大,懒加载的方式可以减少内存消耗。
- 访问模式:如果需要逐个处理结果或处理结果集较大,懒加载的方式更适合。如果只需要一次性获取特定页的数据,分页查询合适。
- 性能要求:如果对响应时间和数据库访问次数有严格要求,分页查询可能更有效率。