这次我们来看一个在互联网大厂和数据分析领域被高频使用的底层数据结构:Roaring Bitmap。它不是某个新发布的软件,而是一种高效压缩位图算法,核心要解决的问题是:如何在海量用户标签、实时推荐、广告定向、数据仓库等场景下,对亿级甚至十亿级的用户ID集合进行快速、低内存的集合运算(如交集、并集、差集)。
如果你正在处理用户画像、人群圈选、AB测试分组这类需要频繁进行集合操作的任务,并且被传统BitMap的内存消耗或HashSet的查询速度所困扰,那么Roaring Bitmap就是你必须要了解的技术。它的设计目标非常直接:在保持接近位图(BitMap)的快速位运算性能的同时,将内存占用压缩到极致,从而让单机处理超大整数集合成为可能。
本文不会停留在概念层面,而是聚焦于实战。我们将拆解Roaring Bitmap的核心原理,对比其与传统方案的性能差异,并通过具体的代码示例,展示如何在Java等环境中使用它来管理亿级用户标签,完成人群的快速圈选和计算。你会看到它的硬件门槛极低(主要依赖内存),启动即用(通常是引入一个Jar包),并且能无缝集成到现有的数据管道和API服务中,支持高并发的批量查询任务。
1. 核心能力速览
Roaring Bitmap 不是一个独立运行的服务,而是一个嵌入式的数据结构库。因此,它的“规格”主要体现在性能、容量和集成方式上。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 高效压缩位图算法 / 内存数据结构库 |
| 核心功能 | 超大整数集合的存储、压缩与高速集合运算(交、并、差、非) |
| 内存占用 | 远低于传统 BitMap。对于稀疏分布的大整数集合(如用户ID),内存占用可压缩至原始位图的百分之几甚至千分之一。 |
| 查询性能 | 接近传统 BitMap。支持快速的成员检查(contains)和基数计算(cardinality)。集合运算速度极快,是HashSet的数十倍甚至上百倍。 |
| 启动/集成方式 | 作为库引入项目。Java中通常通过Maven/Gradle添加依赖,直接调用API。 |
| 是否支持API | 本身无网络API,但其封装的数据结构可轻松集成到Spring Boot等服务的REST API中,对外提供人群计算服务。 |
| 是否支持批量任务 | 原生支持。是其核心设计目标,非常适合批处理人群包计算、离线用户画像分析等场景。 |
| 适合场景 | 用户标签系统、实时推荐引擎、广告定向投放、数据分析OLAP、数据库倒排索引。 |
| 不适合场景 | 非整数类型数据、需要频繁单条增删的场景(批量操作更优)、对延迟有纳秒级要求的极端场景。 |
2. 适用场景与使用边界
适合谁用?
- 后端开发工程师:需要构建高性能用户分群、权限系统的开发者。
- 数据平台工程师:负责用户画像、标签系统、AB测试平台搭建的工程师。
- 算法工程师:在推荐、广告场景中需要快速计算用户交集的人群。
- 大数据工程师:在Spark、Flink等计算引擎中处理大规模集合运算。
能解决什么问题?
- 人群圈选:“找出同时拥有
标签A和标签B,但没有标签C的所有用户”。 - 实时推荐:用户上线时,快速计算其所属的所有标签人群,进行实时匹配。
- 广告定向:根据广告主设定的复杂人群条件(地域、兴趣、行为),在毫秒级内筛选出目标用户。
- 数据统计:快速计算某个标签下的用户总数(基数),或多个标签间的重叠用户数。
- 倒排索引:替代传统的倒排列表,用压缩位图存储文档ID列表,加速查询。
使用边界与注意事项
- 数据类型:仅适用于整数(通常是用户ID、商品ID等)集合。字符串等类型需要先映射为整数。
- 更新频率:虽然支持单点增删,但其最大优势在于批量构建和批量查询。对于超高频率的单点更新,可能需要结合其他数据结构。
- 内存与磁盘:Roaring Bitmap是内存数据结构。虽然序列化后可以持久化到磁盘,但核心操作都在内存中进行,需要保证足够的内存容量。
- 合规与隐私:当用于存储用户标签时,需严格遵守数据安全与隐私保护法规。确保用户数据脱敏、授权使用,并在传输、存储过程中进行加密。
3. 环境准备与前置条件
Roaring Bitmap 本身是算法和数据结构,不依赖特定的操作系统或GPU。其运行环境主要取决于你使用的编程语言和项目架构。
通用环境清单:
- 开发语言:Roaring Bitmap 有多个语言实现,最成熟的是Java版本。此外还有 C/C++、Go、Python 等版本。本文以 Java 为例。
- Java 环境:JDK 8 或更高版本。建议使用 JDK 11 或 17 等 LTS 版本以获得更好的性能。
- 构建工具:Maven 或 Gradle,用于管理项目依赖。
- 内存:这是最关键的资源。你需要根据预估的用户基数(用户ID最大值)和标签稀疏度来评估内存需求。一个粗略的估计是:存储数千万到亿级用户ID的稀疏集合,内存占用可能在几十MB到几百MB之间。务必在实际数据上进行测试。
- 集成环境:计划将 Roaring Bitmap 集成到的系统,如 Spring Boot 微服务、Spark 作业、Flink 任务等。
4. 安装部署与启动方式
在 Java 项目中使用 Roaring Bitmap 非常简单,本质上就是添加一个依赖库。
Maven 项目,在pom.xml中添加依赖:
<dependency> <groupId>org.roaringbitmap</groupId> <artifactId>RoaringBitmap</artifactId> <version>0.9.47</version> <!-- 请检查并使用最新版本 --> </dependency>Gradle 项目,在build.gradle中添加依赖:
dependencies { implementation 'org.roaringbitmap:RoaringBitmap:0.9.47' }添加依赖后,无需任何“启动”操作。直接在代码中导入类,即可开始使用:
import org.roaringbitmap.RoaringBitmap; public class RoaringBitmapDemo { public static void main(String[] args) { // 1. 创建 RoaringBitmap 对象 RoaringBitmap rb1 = new RoaringBitmap(); // 2. 添加数据(用户ID) rb1.add(1, 3, 5, 100000, 2000000); // 添加多个ID rb1.add(5000000); // 添加单个ID // 3. 此时 rb1 就已经在内存中运行起来了 System.out.println("rb1 的基数(元素个数): " + rb1.getCardinality()); } }这就是全部的“部署”过程。它的“服务”就是内存中的对象,通过 API 调用来完成所有功能。
5. 功能测试与效果验证
我们来模拟一个真实的用户标签管理场景,验证 Roaring Bitmap 的核心功能。
5.1 场景构建:初始化标签位图
假设我们有三个用户标签:
tag_tech_lover: 科技爱好者,用户ID: 1, 3, 5, 100001, 2000001tag_movie_fan: 电影迷,用户ID: 3, 5, 7, 2000001, 3000000tag_shopper: 购物达人,用户ID: 5, 100001, 3000000, 4000000
import org.roaringbitmap.RoaringBitmap; import java.io.*; public class UserTagDemo { public static void main(String[] args) { // 初始化三个标签对应的用户集合 RoaringBitmap techLovers = new RoaringBitmap(); techLovers.add(1, 3, 5, 100001, 2000001); RoaringBitmap movieFans = new RoaringBitmap(); movieFans.add(3, 5, 7, 2000001, 3000000); RoaringBitmap shoppers = new RoaringBitmap(); shoppers.add(5, 100001, 3000000, 4000000); System.out.println("=== 初始标签人群 ==="); System.out.println("科技爱好者人数: " + techLovers.getCardinality()); System.out.println("电影迷人数: " + movieFans.getCardinality()); System.out.println("购物达人人人数: " + shoppers.getCardinality()); } }5.2 功能测试1:集合运算(人群圈选)
这是最核心的功能。我们通过集合运算来回答业务问题。
// 接上面的代码 System.out.println("\n=== 复杂人群圈选 ==="); // 问题1:既是科技爱好者又是电影迷的用户(交集) RoaringBitmap techAndMovie = RoaringBitmap.and(techLovers, movieFans); System.out.println("科技爱好者 & 电影迷: " + techAndMovie.toArray()); // 输出 [3, 5, 2000001] // 问题2:是科技爱好者但不是购物达人的用户(差集) RoaringBitmap techNotShopper = RoaringBitmap.andNot(techLovers, shoppers); System.out.println("科技爱好者 & !购物达人: " + techNotShopper.toArray()); // 输出 [1, 3, 2000001] // 问题3:所有至少有一个标签的用户(并集) RoaringBitmap anyTag = RoaringBitmap.or(techLovers, movieFans, shoppers); System.out.println("至少有一个标签的总人数: " + anyTag.getCardinality()); // 输出 9 // 问题4:是电影迷或购物达人,但不是科技爱好者的用户 // (movieFans ∪ shoppers) - techLovers RoaringBitmap movieOrShopper = RoaringBitmap.or(movieFans, shoppers); RoaringBitmap result = RoaringBitmap.andNot(movieOrShopper, techLovers); System.out.println("(电影迷|购物达人) & !科技爱好者: " + result.toArray()); // 输出 [7, 3000000, 4000000]预期结果与判断:代码运行后,应能正确输出符合各逻辑条件的用户ID数组。这验证了 Roaring Bitmap 在复杂逻辑组合下的计算正确性。
5.3 功能测试2:序列化与持久化
标签数据需要持久化存储(如Redis、数据库、HDFS)。Roaring Bitmap 提供了高效的序列化方法。
// 接上面的代码 System.out.println("\n=== 序列化与反序列化 ==="); try { // 将 techLovers 位图序列化到字节数组 ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); techLovers.serialize(dos); byte[] serializedBytes = bos.toByteArray(); System.out.println("序列化后字节大小: " + serializedBytes.length + " bytes"); // 从字节数组反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(serializedBytes); DataInputStream dis = new DataInputStream(bis); RoaringBitmap deserializedRb = new RoaringBitmap(); deserializedRb.deserialize(dis); // 验证反序列化后的数据是否一致 System.out.println("反序列化后基数是否一致: " + deserializedRb.equals(techLovers)); // 应为 true } catch (IOException e) { e.printStackTrace(); }判断成功标准:序列化与反序列化过程不报错,且反序列化后的位图与原位图通过equals方法比较为true。序列化后的字节数组大小远小于存储原始整数列表的大小。
5.4 功能测试3:性能对比(与HashSet)
我们通过一个简单的测试,感受性能差距。注意:此测试仅为演示,严谨性能测试需用JMH等工具。
import java.util.HashSet; import java.util.Random; public class PerformanceDemo { public static void main(String[] args) { int dataSize = 1_000_000; // 100万用户ID int maxUserId = 50_000_000; // 用户ID范围到5000万 Random rand = new Random(42); // 固定种子保证可重复 RoaringBitmap rb = new RoaringBitmap(); HashSet<Integer> hashSet = new HashSet<>(); // 1. 构建数据 long start = System.currentTimeMillis(); for (int i = 0; i < dataSize; i++) { int id = rand.nextInt(maxUserId); rb.add(id); } long timeRbBuild = System.currentTimeMillis() - start; start = System.currentTimeMillis(); for (int i = 0; i < dataSize; i++) { int id = rand.nextInt(maxUserId); // 注意:重新生成相同序列的ID rand.setSeed(42 + i); // 重置随机种子,确保两组数据完全一致 id = rand.nextInt(maxUserId); hashSet.add(id); } long timeHsBuild = System.currentTimeMillis() - start; // 2. 计算交集(模拟人群圈选) // 创建另一个有部分重叠的集合 RoaringBitmap rb2 = new RoaringBitmap(); HashSet<Integer> hashSet2 = new HashSet<>(); rand.setSeed(12345); for (int i = 0; i < dataSize; i++) { int id = rand.nextInt(maxUserId); rb2.add(id); hashSet2.add(id); } start = System.currentTimeMillis(); RoaringBitmap.and(rb, rb2); long timeRbIntersect = System.currentTimeMillis() - start; start = System.currentTimeMillis(); HashSet<Integer> intersectSet = new HashSet<>(hashSet); intersectSet.retainAll(hashSet2); long timeHsIntersect = System.currentTimeMillis() - start; System.out.println("=== 性能对比 (数据量: " + dataSize + ") ==="); System.out.println("构建时间 - RoaringBitmap: " + timeRbBuild + " ms, HashSet: " + timeHsBuild + " ms"); System.out.println("交集计算 - RoaringBitmap: " + timeRbIntersect + " ms, HashSet: " + timeHsIntersect + " ms"); // 3. 内存占用对比(近似) // RoaringBitmap 序列化后大小可作为内存占用的近似参考 // HashSet 的内存占用通常远大于其元素本身的大小 System.out.println("\n提示:RoaringBitmap 在稀疏大数据集上的内存优势远超 HashSet,交集计算速度通常快一个数量级以上。"); } }运行这段代码,你会直观看到在百万级数据量上,RoaringBitmap 在集合运算速度上的巨大优势。内存优势则需要通过更专业的工具(如JProfiler)或观察序列化大小来间接评估。
6. 接口 API 与批量任务
Roaring Bitmap 本身没有网络接口,但我们可以轻松地将其封装成服务。以下是一个简单的 Spring Boot 控制器示例,提供人群圈选的 HTTP API。
1. 服务层封装核心逻辑
import org.roaringbitmap.RoaringBitmap; import org.springframework.stereotype.Service; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service public class UserTagService { // 模拟存储:标签名 -> 对应的用户RoaringBitmap private Map<String, RoaringBitmap> tagBitmapStore = new ConcurrentHashMap<>(); /** * 根据标签名获取位图 */ public RoaringBitmap getBitmapByTag(String tagName) { return tagBitmapStore.getOrDefault(tagName, new RoaringBitmap()); } /** * 核心人群计算:AND, OR, ANDNOT * @param operation "AND", "OR", "ANDNOT" * @param tagNames 操作的标签数组 * @return 计算结果的用户ID数组 */ public int[] computeUserGroup(String operation, String[] tagNames) { if (tagNames == null || tagNames.length == 0) { return new int[0]; } RoaringBitmap result = getBitmapByTag(tagNames[0]); for (int i = 1; i < tagNames.length; i++) { RoaringBitmap current = getBitmapByTag(tagNames[i]); switch (operation.toUpperCase()) { case "AND": result = RoaringBitmap.and(result, current); break; case "OR": result = RoaringBitmap.or(result, current); break; case "ANDNOT": // ANDNOT 通常只作用于两个位图,这里简化处理为 result ANDNOT current result = RoaringBitmap.andNot(result, current); break; default: throw new IllegalArgumentException("Unsupported operation: " + operation); } } return result.toArray(); } // ... 其他方法,如添加标签数据等 }2. 提供 REST API
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Arrays; @RestController @RequestMapping("/api/user-tags") public class UserTagController { @Autowired private UserTagService userTagService; /** * 人群圈选接口 * GET /api/user-tags/segment?operation=AND&tags=tech,movie */ @GetMapping("/segment") public SegmentResponse segmentUsers( @RequestParam String operation, @RequestParam String tags) { String[] tagArray = tags.split(","); int[] userIds = userTagService.computeUserGroup(operation, tagArray); SegmentResponse response = new SegmentResponse(); response.setOperation(operation); response.setTags(Arrays.asList(tagArray)); response.setUserCount(userIds.length); response.setUserIds(userIds); // 注意:实际生产环境可能只返回数量或分页ID return response; } // 响应对象 static class SegmentResponse { private String operation; private List<String> tags; private int userCount; private int[] userIds; // getters and setters ... } }3. 批量任务集成
在数据平台中,批量更新标签是常态。我们可以用 Spark 来演示如何批量生成 Roaring Bitmap。
// Spark Scala 示例:从用户行为日志批量计算每个标签的用户位图 import org.roaringbitmap.RoaringBitmap import org.apache.spark.sql.{SparkSession, functions => F} val spark = SparkSession.builder().appName("TagBitmapBuilder").getOrCreate() import spark.implicits._ // 1. 读取用户打标日志 (userId, tag) val logDF = spark.read.parquet("hdfs://path/to/user_tag_logs/*.parquet") .select($"userId".cast("int"), $"tag") // 2. 按tag分组,聚合userId,构建RoaringBitmap val tagBitmapRDD = logDF.rdd .map(row => (row.getAs[String]("tag"), row.getAs[Int]("userId"))) .aggregateByKey(new RoaringBitmap())( // 分区内聚合:将userId添加到bitmap (bitmap: RoaringBitmap, userId: Int) => { bitmap.add(userId); bitmap }, // 分区间合并:合并两个bitmap (bitmap1: RoaringBitmap, bitmap2: RoaringBitmap) => RoaringBitmap.or(bitmap1, bitmap2) ) // 3. 将结果(tag -> 序列化的bitmap字节数组)保存到存储系统 tagBitmapRDD.map { case (tag, bitmap) => val bos = new java.io.ByteArrayOutputStream() val dos = new java.io.DataOutputStream(bos) bitmap.serialize(dos) (tag, bos.toByteArray) }.toDF("tag", "bitmapBytes") .write .mode("overwrite") .parquet("hdfs://path/to/tag_bitmap_store/")这个 Spark 作业会高效地处理海量日志,为每个标签生成一个压缩的 Roaring Bitmap 文件,供后续的实时查询服务加载使用。
7. 资源占用与性能观察
如何观察内存占用?
- 直接观察序列化大小:如前面测试所示,
serialize()后得到的字节数组长度是衡量其在内存和磁盘中占用空间的良好指标。 - 使用 JVM 工具:
- 在启动命令中添加
-XX:+PrintCompressionOops等参数观察对象指针压缩情况(对位图这类大量小对象的数据结构有益)。 - 使用
jmap -histo:live <pid>查看 RoaringBitmap 对象的实例数量和总大小。 - 使用 VisualVM、JProfiler 或 async-profiler 进行堆内存分析,查看
org.roaringbitmap包下的对象内存。
- 在启动命令中添加
- 经验公式(估算):内存占用主要取决于:
- 基数(Cardinality):集合中实际有多少个整数。
- 数据分布:整数是密集连续(如1-100万)还是稀疏分散(如随机分布在0-10亿)。
- Roaring Bitmap 会将32位整数空间(0~2^32-1)分成若干桶(Container)。对于稀疏数据,它使用紧凑的数组存储,内存效率极高。
性能影响因素:
- 操作类型:
contains(检查是否存在)是O(1)级别,极快。集合运算(and,or,andNot,xor)的速度与涉及的位图大小和数据分布有关,但通常远快于基于HashSet的同类操作。 - 数据分布:对完全随机的稀疏数据压缩效果最好。对完全连续的数据,它会退化成高效的位集(Bitset),内存占用稍大但运算依然很快。
- JVM 优化:确保为JVM分配足够堆内存(
-Xmx),并考虑使用G1等垃圾回收器以减少大内存工作集下的GC停顿。
降低资源占用的建议:
- 使用
runOptimize():在批量添加完数据后,调用bitmap.runOptimize()方法,可以尝试进一步压缩内存(以轻微增加后续修改操作为代价)。 - 选择合适的整数范围:如果用户ID范围可以控制,尽量使用连续的、较小的整数,能提升性能并减少内存。
- 离线构建,在线查询:在Spark/Flink中离线计算好标签位图,序列化存储。在线服务只加载和查询,避免在线服务进行大量的单点更新操作。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 添加大量数据后内存占用依然很高 | 数据可能过于密集,导致Roaring Bitmap内部使用了未压缩的BitmapContainer。 | 1. 检查数据的基数(getCardinality())和最大值。2. 调用 bitmap.getSizeInBytes()估算内存。 | 1. 如果数据确实密集,高内存是正常的,但运算速度依然快。 2. 考虑是否能用更小的整数范围。 |
| 序列化/反序列化时报错或数据不一致 | 1. 序列化与反序列化的API使用不正确。 2. 字节流在传输或存储过程中损坏。 | 1. 检查代码,确保使用DataOutput/DataInput流。2. 对比序列化前后位图的 equals()结果。 | 1. 严格遵循官方示例代码。 2. 对于网络传输,确保使用可靠的协议并校验数据完整性。 |
| 集合运算结果不符合预期 | 1. 参与运算的位图数据有误。 2. 运算逻辑(AND, OR, ANDNOT)理解有误。 | 1. 打印出参与运算的各个位图的toArray()或getCardinality()进行验证。2. 用少量测试数据手动验算。 | 1. 检查数据源,确保位图被正确构建。 2. 复习集合运算的数学定义, ANDNOT(A,B)表示在A中但不在B中的元素。 |
| 在Spark等分布式环境中使用时报序列化错误 | RoaringBitmap 未实现java.io.Serializable接口,但实现了更高效的Externalizable。 | 检查在RDD操作中是否错误地尝试了Java原生序列化。 | 在Spark中,应使用mapPartitions或aggregateByKey配合本地操作,避免将RoaringBitmap作为需要网络传输的复杂对象。或者使用Kryo序列化并注册RoaringBitmap的序列化器。 |
| GC(垃圾回收)压力大 | 频繁创建和丢弃大量的、较大的RoaringBitmap对象。 | 使用JVM监控工具观察GC频率和Old Gen使用情况。 | 1. 对象复用:考虑使用对象池。 2. 减少中间对象:使用 RoaringBitmap.and/lor的原地操作版本(如rb1.and(rb2))来避免创建新对象。3. 调整JVM堆大小和GC策略。 |
| 与数据库查询性能对比不明显 | 数据量太小,或者数据库索引已经非常高效。 | 1. 增大测试数据量到百万、千万级。 2. 确保对比的是相同的复杂集合运算逻辑。 | Roaring Bitmap 的优势在于内存中的大规模集合运算。对于小数据量或简单的单点查询,数据库可能更有优势。正确场景下才能发挥其威力。 |
9. 最佳实践与使用建议
- 预热与缓存:在线服务中,将常用的标签位图反序列化后缓存在内存(如Guava Cache或Caffeine)中,避免每次请求都进行IO读取。
- 分片策略:如果用户ID总量巨大(如百亿),单个位图可能过大。可以考虑按用户ID范围(如按亿分片)或业务维度(如按渠道分片)进行水平分片,每个分片使用独立的RoaringBitmap。
- 组合查询优化:对于固定的、频繁使用的复杂人群组合(如“(标签A AND 标签B) OR (标签C ANDNOT 标签D)”),可以预先计算好结果位图并缓存,实现O(1)复杂度的查询。
- 监控与告警:监控标签位图服务的内存使用量、QPS、计算延迟。为关键人群圈选接口设置慢查询告警。
- 数据更新策略:
- 增量更新:维护一个“增量位图”,记录新增和移除的用户。定期(如每小时)将增量合并到全量位图中。查询时,需要同时查询全量和增量位图并进行逻辑合并。
- 全量重建:在离线数仓中每天用最新的全量数据重建所有标签位图,然后整体切换上线。策略更简单,数据一致性高。
- 合规与安全:
- 脱敏:存储和计算的是用户ID,而非直接的个人身份信息(PII)。
- 权限:对人群圈选API进行严格的权限控制,确保只有授权业务方可以查询特定标签。
- 审计:记录所有的人群查询日志,用于溯源和合规审计。
10. 总结与下一步
Roaring Bitmap 是处理海量整数集合运算的“利器”。它的价值不在于概念新颖,而在于工程上的极致优化:用巧妙的分桶和压缩算法,在速度与空间之间取得了近乎完美的平衡。
最值得尝试的点:如果你的系统中存在“从亿级用户中快速找出符合多个条件的用户”这类需求,并且当前方案(如数据库联表、HashSet内存计算)遇到性能或资源瓶颈,那么引入 Roaring Bitmap 很可能带来数量级的提升。
最先应该验证的功能:不要一上来就处理全量数据。先用一个小的、代表性的数据集(例如10万个用户ID)测试核心流程:位图构建、序列化持久化、反序列化加载、交集/并集计算。验证整个数据链路是通的。
最容易踩的坑:
- 数据一致性:确保离线构建的位图与在线查询服务加载的位图版本一致。
- 内存评估不足:虽然压缩率高,但处理十亿级用户、数万标签的全量数据,总内存需求仍需仔细评估和测试。
- API误用:注意
and和or等静态方法返回的是新对象,而rb1.and(rb2)是原地修改rb1。根据场景选择正确的方法。
后续扩展方向:
- 探索 Roaring64NavigableMap:如果你的用户ID是64位的(如Long类型),可以使用
Roaring64NavigableMap。 - 与现有生态集成:
- Doris/Pinot等OLAP数据库:它们内部已集成Roaring Bitmap用于加速查询,了解其使用方式。
- Redis:可以将序列化后的位图字节数组存入Redis,作为分布式缓存。
- Apache Druid:其在数据索引中大量使用Roaring Bitmap。
- 阅读论文与源码:要真正理解其强大之处,推荐阅读原始的论文《Better bitmap performance with Roaring bitmaps》以及其Java实现的源码,理解其三种Container(ArrayContainer, BitmapContainer, RunContainer)的设计与转换策略。
将 Roaring Bitmap 纳入你的技术工具箱,下次面对海量数据集合运算时,你会多一个高效而优雅的选择。建议收藏本文,在需要时参照步骤进行集成和验证。