问题背景与挑战
-
数据规模:40亿QQ号,假设每个QQ号为32位无符号整数(最大值2³²-1 ≈ 42.9亿)
-
内存限制:1GB ≈ 1,073,741,824字节(1024³)
-
传统方法缺陷:
HashSet<Long>
存储40亿数据需要约 64GB内存(每个Long对象约16字节)- 直接加载到内存会导致OOM(OutOfMemoryError)
核心技术:位图法(Bitmap)
核心思想:每个bit位表示一个数字是否存在
优势:
- 内存极致压缩:40亿数据仅需 512MB内存(40亿bit ÷ 8 ÷ 1024³ ≈ 476MB)
- 时间复杂度O(1) :判断和插入均为位运算
Java实现详细设计
1. 位图结构设计
-
为什么不用Java内置BitSet?
Java的BitSet
内部使用long[]
存储,但最大理论容量为Integer.MAX_VALUE
位(约20亿),无法覆盖40亿数据 -
自定义位图实现:
public class Bitmap { private final long[] words; // 使用long数组存储位信息 private static final int ADDRESS_BITS_PER_WORD = 6; // 2^6 = 64 public Bitmap(long maxNum) { // 计算需要多少个long元素:ceil((maxNum + 1) / 64) int numWords = (int) ((maxNum >>> ADDRESS_BITS_PER_WORD) + 1); this.words = new long[numWords]; } /** * 检查并设置位(原子操作) * @return true表示已存在,false表示首次插入 */ public boolean containsAndSet(long num) { int wordIndex = (int) (num >>> ADDRESS_BITS_PER_WORD); int bitIndex = (int) (num & 0x3F); // 等价于 num % 64 long mask = 1L << bitIndex; // 关键操作:原子性检查并设置位 long oldValue = words[wordIndex]; if ((oldValue & mask) != 0) { return true; // 已存在 } words[wordIndex] = oldValue | mask; return false; // 新插入 } }
2. 内存占用精确计算
-
位图容量公式:
内存大小(字节)= ceil(最大QQ号 / 8)
假设QQ号最大为4,294,967,295(2³²-1):4,294,967,295 bits ≈ 4,294,967,295 / 8 / 1024 / 1024 ≈ 512MB
-
Java对象内存开销:
long[]
数组对象头:12字节(Mark Word) + 4字节(数组长度) = 16字节- 每个long元素占8字节
- 总内存 = 16 + (numWords * 8)
示例:
numWords = 4,294,967,295 / 64 + 1 = 67,108,864 总内存 = 16 + (67,108,864 * 8) ≈ 536,870,928字节 ≈ 512MB
3. 数据读取与处理
public class Deduplicator {
public static void main(String[] args) {
// 初始化位图(最大QQ号假设为2^32-1)
Bitmap bitmap = new Bitmap(4294967295L);
try (BufferedReader reader = new BufferedReader(
new FileReader("qq.txt"), 8192 * 1024)) { // 8MB缓冲提升IO性能
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
try {
long qq = Long.parseLong(line);
if (qq < 0 || qq > 4294967295L) {
System.err.println("非法QQ号: " + qq);
continue;
}
if (bitmap.containsAndSet(qq)) {
// 重复处理逻辑(如写入日志或文件)
logDuplicate(qq);
}
} catch (NumberFormatException e) {
System.err.println("格式错误: " + line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void logDuplicate(long qq) {
// 可改为写入文件或其他持久化操作
System.out.println("Duplicate QQ: " + qq);
}
}
关键优化技术
1. 分块处理(处理超大数据文件)
-
场景:若QQ号文件超过内存可用空间(如100GB)
-
实现方案:
- 使用
BufferedReader
逐行读取(避免全量加载) - 通过缓冲设置提升IO效率(例如8MB缓冲区)
- 对重复数据分批写入磁盘,避免内存堆积
- 使用
2. 并发处理优化
// 多线程版本(示例片段)
ExecutorService executor = Executors.newFixedThreadPool(8);
List<Future<?>> futures = new ArrayList<>();
try (BufferedReader reader = ...) {
String line;
while ((line = reader.readLine()) != null) {
final long qq = parseQQ(line);
futures.add(executor.submit(() -> {
if (bitmap.containsAndSet(qq)) {
logDuplicate(qq);
}
}));
}
// 等待所有任务完成
for (Future<?> future : futures) {
future.get();
}
}
注意事项:
containsAndSet
需保证线程安全(可改用AtomicLongArray
或分段锁)- IO密集型与CPU密集型任务分离
3. 边界条件处理
-
非法QQ号检测:
if (qq < 0 || qq > 4294967295L) { // 处理超出位图范围的QQ号 handleInvalidQQ(qq); }
-
超大QQ号解决方案:
- 若QQ号超过32位,可采用哈希映射(如取模运算)
- 分片位图(例如按高位划分多个512MB位图)
进阶方案对比
方案 | 内存占用 | 时间复杂度 | 特点 |
---|---|---|---|
位图法 | 512MB | O(1) | 精确去重,仅限整数类型 |
布隆过滤器 | 100MB | O(k) | 存在误判率,适合允许误差场景 |
外部排序+去重 | 低 | O(n log n) | 磁盘IO高,适合无法全内存的场景 |
生产环境扩展建议
-
分布式位图:
- 使用Redis的
BITFIELD
命令实现分布式位图 - 分片规则:
shardKey = qq % 1024
(将数据分散到多个节点)
- 使用Redis的
-
混合存储方案:
- 热数据用内存位图
- 冷数据持久化到数据库(如HBase)
-
监控与告警:
- 监控位图内存使用率(避免超过阈值)
- 记录重复率统计报表
FAQ常见问题
Q1:如果QQ号不是连续的怎么办?
- 不影响位图法,只要最大值不超过位图容量,稀疏分布不影响内存占用
Q2:如何处理超过32位的QQ号?
- 方案1:使用哈希函数(如MurmurHash)映射到32位空间(可能冲突)
- 方案2:分层位图(高位做分片索引,低位做局部位图)
Q3:位图初始化时间太长怎么办?
- 使用
Arrays.fill(words, 0L)
快速初始化 - 并行初始化(多线程分段处理数组)
总结
通过位图法,Java可以在 512MB内存内完成40亿QQ号的精确去重,核心关键在于:
- 利用位操作实现内存极致压缩
- 原子性操作保证线程安全
- 分块处理解决大数据IO瓶颈
实际应用中需根据数据特性选择分片策略、哈希函数等扩展方案。