如何在1G内存下对40亿QQ号去重?Java高效解决方案

666 阅读5分钟

问题背景与挑战

  • 数据规模:40亿QQ号,假设每个QQ号为32位无符号整数(最大值2³²-1 ≈ 42.9亿)

  • 内存限制:1GB ≈ 1,073,741,824字节(1024³)

  • 传统方法缺陷

    • HashSet<Long>存储40亿数据需要约 64GB内存(每个Long对象约16字节)
    • 直接加载到内存会导致OOM(OutOfMemoryError)

核心技术:位图法(Bitmap)

核心思想:每个bit位表示一个数字是否存在
优势

  1. 内存极致压缩:40亿数据仅需 512MB内存(40亿bit ÷ 8 ÷ 1024³ ≈ 476MB)
  2. 时间复杂度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)

  • 实现方案

    1. 使用BufferedReader逐行读取(避免全量加载)
    2. 通过缓冲设置提升IO效率(例如8MB缓冲区)
    3. 对重复数据分批写入磁盘,避免内存堆积
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位图)

进阶方案对比

方案内存占用时间复杂度特点
位图法512MBO(1)精确去重,仅限整数类型
布隆过滤器100MBO(k)存在误判率,适合允许误差场景
外部排序+去重O(n log n)磁盘IO高,适合无法全内存的场景

生产环境扩展建议

  1. 分布式位图

    • 使用Redis的BITFIELD命令实现分布式位图
    • 分片规则:shardKey = qq % 1024(将数据分散到多个节点)
  2. 混合存储方案

    • 热数据用内存位图
    • 冷数据持久化到数据库(如HBase)
  3. 监控与告警

    • 监控位图内存使用率(避免超过阈值)
    • 记录重复率统计报表

FAQ常见问题

Q1:如果QQ号不是连续的怎么办?

  • 不影响位图法,只要最大值不超过位图容量,稀疏分布不影响内存占用

Q2:如何处理超过32位的QQ号?

  • 方案1:使用哈希函数(如MurmurHash)映射到32位空间(可能冲突)
  • 方案2:分层位图(高位做分片索引,低位做局部位图)

Q3:位图初始化时间太长怎么办?

  • 使用Arrays.fill(words, 0L)快速初始化
  • 并行初始化(多线程分段处理数组)

总结

通过位图法,Java可以在 512MB内存内完成40亿QQ号的精确去重,核心关键在于:

  1. 利用位操作实现内存极致压缩
  2. 原子性操作保证线程安全
  3. 分块处理解决大数据IO瓶颈
    实际应用中需根据数据特性选择分片策略、哈希函数等扩展方案。