十億QQ號(hào)如何去重?你知道嗎?
前言
最近在網(wǎng)上看到一個(gè)問題:10億QQ號(hào)如何去重?
我覺得挺有意思的。
今天這篇文章跟大家一起分享一些常見的解決方案,希望對(duì)你會(huì)有所幫助。
一、技術(shù)難點(diǎn)
1.數(shù)據(jù)規(guī)模分析
- 原始數(shù)據(jù):10億×8字節(jié) = 8GB
- HashSet去重:至少16GB內(nèi)存(Java對(duì)象開銷)
- 理想方案:<1GB內(nèi)存
2. 核心挑戰(zhàn)
二、單機(jī)解決方案:位圖法
1.算法原理
利用位數(shù)組表示數(shù)字存在性:
public class BitMap {
privatefinalbyte[] bits;
public BitMap(int maxNum) {
this.bits = newbyte[(maxNum >> 3) + 1]; // 每byte存儲(chǔ)8個(gè)數(shù)字
}
public void add(int num) {
int arrayIndex = num >> 3; // num/8
int position = num & 0x07; // num%8
bits[arrayIndex] |= 1 << position;
}
public boolean contains(int num) {
int arrayIndex = num >> 3;
int position = num & 0x07;
return (bits[arrayIndex] & (1 << position)) != 0;
}
}
2.QQ號(hào)范圍優(yōu)化
QQ號(hào)范圍:10000(5位) - 9999999999(10位)位圖內(nèi)存計(jì)算:
(10^10 - 10^4) / 8 / 1024/1024 ≈ 1.16GB
優(yōu)化方案:
// 偏移量?jī)?yōu)化:存儲(chǔ)(qq - 10000)
public void add(long qq) {
long num = qq - 10000;
int arrayIndex = (int)(num >> 3);
int position = (int)(num & 7);
bits[arrayIndex] |= 1 << position;
}
三、進(jìn)階方案:布隆過(guò)濾器
1.應(yīng)對(duì)內(nèi)存限制
2.參數(shù)設(shè)計(jì)與實(shí)現(xiàn)
public class BloomFilter {
privatefinal BitSet bitset;
privatefinalint size;
privatefinalint[] seeds;
public BloomFilter(int size, int hashCount) {
this.bitset = new BitSet(size);
this.size = size;
this.seeds = newint[hashCount];
for (int i = 0; i < hashCount; i++) {
seeds[i] = i * 31;
}
}
public void add(String qq) {
for (int seed : seeds) {
int hash = hash(qq, seed);
bitset.set(Math.abs(hash % size), true);
}
}
public boolean contains(String qq) {
for (int seed : seeds) {
int hash = hash(qq, seed);
if (!bitset.get(Math.abs(hash % size))) {
returnfalse;
}
}
returntrue;
}
private int hash(String value, int seed) {
// MurmurHash 實(shí)現(xiàn)
int result = 0;
for (char c : value.toCharArray()) {
result = seed * result + c;
}
return result;
}
}
3.內(nèi)存優(yōu)化效果
方案 | 內(nèi)存消耗 | 誤差率 |
原始存儲(chǔ) | 8 GB | 0% |
位圖法 | 1.16 GB | 0% |
布隆過(guò)濾器(0.1%) | 171 MB | 0.001 |
四、磁盤方案:外部排序與多路歸并
1.處理流程
2.關(guān)鍵代碼實(shí)現(xiàn)
// 外部排序
public void externalSort(String input, String output) throws IOException {
List<File> chunks = splitAndSort(input, 100_000_000); // 每個(gè)文件1千萬(wàn)
mergeFiles(chunks, output);
}
// 多路歸并
void mergeFiles(List<File> files, String output) {
PriorityQueue<MergeEntry> queue = new PriorityQueue<>();
List<BufferedReader> readers = new ArrayList<>();
// 初始化堆
for (File file : files) {
BufferedReader reader = new BufferedReader(new FileReader(file));
readers.add(reader);
String line = reader.readLine();
if (line != null) {
queue.add(new MergeEntry(line, reader));
}
}
try (BufferedWriter writer = new BufferedWriter(new FileWriter(output))) {
long last = -1;
while (!queue.isEmpty()) {
MergeEntry entry = queue.poll();
long qq = Long.parseLong(entry.value);
// 去重:只寫入不重復(fù)的QQ號(hào)
if (qq != last) {
writer.write(entry.value);
writer.newLine();
last = qq;
}
// 讀取下一行
String next = entry.reader.readLine();
if (next != null) {
queue.add(new MergeEntry(next, entry.reader));
}
}
} finally {
readers.forEach(r -> { try { r.close(); } catch (IOException e) {}});
}
}
class MergeEntry implements Comparable<MergeEntry> {
String value;
BufferedReader reader;
public MergeEntry(String value, BufferedReader reader) {
this.value = value;
this.reader = reader;
}
@Override
public int compareTo(MergeEntry o) {
returnthis.value.compareTo(o.value);
}
}
五、分布式解決方案
1.分片策略設(shè)計(jì)
2.Spark實(shí)現(xiàn)方案
val qqRDD = spark.read.textFile("hdfs://qq_data/*.txt")
.map(_.toLong)
.repartition(1000) // 分為1000個(gè)分區(qū)
// 每個(gè)分區(qū)內(nèi)部去重
val distinctRDD = qqRDD.mapPartitions { iter =>
val bitmap = newRoaringBitmap()
iter.foreach(qq => bitmap.add(qq.toInt))
bitmap.iterator.asScala.map(_.toLong)
}
// 全局去重(可選)
val globalDistinct = distinctRDD.distinct()
globalDistinct.saveAsTextFile("hdfs://result/")
3.內(nèi)存優(yōu)化:RoaringBitmap
存儲(chǔ)優(yōu)勢(shì)對(duì)比:
普通位圖:10^10 / 8 / 1024/1024 ≈ 1.16 GB
RoaringBitmap:稀疏數(shù)據(jù)下可壓縮至100-300 MB
六、生產(chǎn)級(jí)架構(gòu):Lambda架構(gòu)
1.系統(tǒng)架構(gòu)圖
2.各層技術(shù)選型
架構(gòu)層 | 技術(shù)棧 | 處理目標(biāo) |
批處理層 | Spark + HDFS | 全量數(shù)據(jù)去重 |
速度層 | Flink + Redis | 實(shí)時(shí)增量去重 |
服務(wù)層 | Spring Boot + HBase | 統(tǒng)一查詢接口 |
3.實(shí)時(shí)去重實(shí)現(xiàn)
public class QQDeduplication {
privatestaticfinal String REDIS_KEY = "qq_set";
public boolean isDuplicate(String qq) {
try (Jedis jedis = jedisPool.getResource()) {
// 使用HyperLogLog進(jìn)行基數(shù)估計(jì)
if (jedis.pfcount(REDIS_KEY) > 1_000_000_000L) {
returntrue; // 已超過(guò)10億,直接返回重復(fù)
}
return jedis.sadd(REDIS_KEY, qq) == 0;
}
}
}
七、終極方案:分層位圖索引
1.架構(gòu)設(shè)計(jì)
2.存儲(chǔ)計(jì)算
QQ號(hào)范圍:10000 - 9999999999(約100億)分層設(shè)計(jì):
- 第一層分片:100個(gè)區(qū)間(每區(qū)間1億)
- 第二層分片:100個(gè)子區(qū)間(每區(qū)間100萬(wàn))
- 第三層存儲(chǔ):RoaringBitmap(每分區(qū)1.2MB)
總內(nèi)存需求:
100 × 100 × 1.2MB = 12GB(分布式存儲(chǔ)可行)
3.Java實(shí)現(xiàn)
public class LayeredBitmap {
privatefinal RoaringBitmap[][][] bitmaps;
privatestaticfinalint L1 = 100; // 一級(jí)分片
privatestaticfinalint L2 = 100; // 二級(jí)分片
public LayeredBitmap() {
bitmaps = new RoaringBitmap[L1][L2][];
}
public void add(long qq) {
int l1Index = (int)((qq - 10000) / 100_000_000);
long remainder = (qq - 10000) % 100_000_000;
int l2Index = (int)(remainder / 1_000_000);
int value = (int)(remainder % 1_000_000);
if (bitmaps[l1Index][l2Index] == null) {
bitmaps[l1Index][l2Index] = new RoaringBitmap();
}
bitmaps[l1Index][l2Index].add(value);
}
public boolean contains(long qq) {
// 類似add的分片計(jì)算
RoaringBitmap bitmap = bitmaps[l1Index][l2Index];
return bitmap != null && bitmap.contains(value);
}
}
八、方案對(duì)比與選型建議
方案 | 適用場(chǎng)景 | 內(nèi)存/存儲(chǔ) | 時(shí)間復(fù)雜度 | 精度 |
單機(jī)位圖 | <1億數(shù)據(jù) | O(n) | O(1) | 100% |
布隆過(guò)濾器 | 百億級(jí)容忍誤差 | O(1) | O(k) | <99.9% |
外部排序 | 單機(jī)磁盤處理 | 磁盤空間 | O(n log n) | 100% |
Spark分布式 | 海量數(shù)據(jù)批量處理 | 集群存儲(chǔ) | O(n) | 100% |
Redis實(shí)時(shí)去重 | 增量數(shù)據(jù)實(shí)時(shí)處理 | O(n) | O(1) | 100% |
分層位圖索引 | 超大規(guī)模精準(zhǔn)去重 | O(n)壓縮存儲(chǔ) | O(1) | 100% |
九、實(shí)戰(zhàn)經(jīng)驗(yàn)與避坑指南
1.數(shù)據(jù)傾斜解決方案
問題場(chǎng)景:部分QQ號(hào)段過(guò)于集中(如100000-199999)
解決策略:
// 動(dòng)態(tài)分片函數(shù)
int getShardId(long qq, int shardCount) {
String str = String.valueOf(qq);
// 取后6位作為分片依據(jù)
int suffix = Integer.parseInt(str.substring(Math.max(0, str.length() - 6)));
return suffix % shardCount;
}
2.去重精度保障
3.成本優(yōu)化建議
- 冷熱分離:熱數(shù)據(jù)用內(nèi)存位圖,冷數(shù)據(jù)存磁盤
- 壓縮存儲(chǔ):使用RoaringBitmap替代普通位圖
- 分級(jí)存儲(chǔ):
a.最近3個(gè)月數(shù)據(jù):內(nèi)存存儲(chǔ)
b.歷史數(shù)據(jù):HBase+壓縮
總結(jié)
- 分治思想:10億問題拆解為1000個(gè)100萬(wàn)問題
- 空間換時(shí)間:位圖法用存儲(chǔ)空間換取O(1)時(shí)間復(fù)雜度
- 概率智慧:布隆過(guò)濾器用可控誤差換取千倍空間壓縮
- 分層設(shè)計(jì):億級(jí)→百萬(wàn)級(jí)→萬(wàn)級(jí)分層處理
- 動(dòng)靜分離:批處理處理歷史數(shù)據(jù),實(shí)時(shí)處理增量數(shù)據(jù)
10億QQ號(hào)去重的本質(zhì),是將問題拆解到每個(gè)計(jì)算單元都能高效處理的粒度。