海量qq去重
40亿个qq号如何去重?仅1GB内存
解析:
QQ号(数字)最大通常不会超过10位,在Java中通常用 long 或 String 表示。如果是40亿个 long 型QQ号,原始大小大约是 $40 \times 10^8 \times 8 \text{ 字节} \approx 32\text{ GB}$,直接加载到内存肯定会 OOM(内存溢出)。
在1GB内存的严格限制下,Java中最优的解法是使用 BitMap(位图算法) ,或者在允许微小误差的情况下使用 Bloom Filter(布隆过滤器) 。
使用位图(bitmap)去解决,如果出现过就标记为1,1个字节8位,4个字节32位,空间缩小32倍,14.9/32约等于0.46GB
方案一:BitMap(无误差,最推荐)
原理:
我们不用存储QQ号本身,而是用内存中的“一个比特位(bit)”来标记某个QQ号是否存在。比特位的下标(Index)直接对应QQ号。
如果QQ号 12345 存在,就把第 12345 个 bit 置为 1。
QQ号目前最大约43亿($2^{32} \approx 42.9亿$)。如果我们要表示完所有的QQ号,需要约43亿个bit。
内存计算:$43 \times 10^8 \text{ bits} \approx 5.37 \times 10^8 \text{ 字节} \approx 512\text{ MB}$。
512MB 刚好远小于 1GB 的限制 ,完全可以一次性在内存中无误差地完成去重。
Java 代码实现
在Java中,标准库的 java.util.BitSet 内部使用的是 long[] 数组,它的逻辑大小最大支持 $2^{31}-1$ 个 bit(约21亿),无法直接装下43亿。因此我们需要自己用 long[] 数组简单封装一个支持无符号整型(43亿)的 ExtendedBitSet 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| import java.io.*;
public class QQDuplicateRemover {
// 43亿个连续数字需要约 512MB 内存
private static final long MAX_QQ_NUMBER = 4300000000L;
// 一个long有64位
private final long[] bits;
public QQDuplicateRemover() {
// 计算需要多少个long才能装下43亿个bit
int size = (int) (MAX_QQ_NUMBER / 64) + 1;
this.bits = new long[size];
}
/**
* 标记一个QQ号存在
* @return 如果之前已经存在,返回 true;如果是新数据,返回 false
*/
public boolean add(long qq) {
int index = (int) (qq / 64); // 找到在哪个 long 元素里
int position = (int) (qq % 64); // 找到在该 long 的哪一位 (0-63)
long mask = 1L << position;
// 检查这一位是否已经是 1
if ((bits[index] & mask) != 0) {
return true; // 已存在,即重复了
}
// 将该位置为 1
bits[index] |= mask;
return false; // 新数据
}
public static void main(String[] args) {
QQDuplicateRemover remover = new QQDuplicateRemover();
// 模拟大数据流式读取(假设从一个巨大的文件中一行行读取,不占用内存)
String filePath = "huge_qq_list.txt";
String outputPath = "unique_qq_list.txt";
try (BufferedReader br = new BufferedReader(new FileReader(filePath));
BufferedWriter bw = new BufferedWriter(new FileWriter(outputPath))) {
String line;
long duplicateCount = 0;
while ((line = br.readLine()) != null) {
if (line.trim().isEmpty()) continue;
long qq = Long.parseLong(line.trim());
// 去重核心:如果add返回false,说明是第一次见,写入新文件
if (!remover.add(qq)) {
bw.write(String.valueOf(qq));
bw.newLine();
} else {
duplicateCount++;
}
}
System.out.println("去重完成!检测到重复QQ号数量: " + duplicateCount);
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
Go 语言实现
Go 语言在处理大块内存分配时非常高效。我们可以直接申请一个 []uint64 切片。
- 一个
uint64 占用 8 字节(64 bits)。 - 43 亿 / 64 ≈ 67,187,500 个元素。
go代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
| package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
const maxQQNumber = 4300000000
type QQBitMap struct {
bits []uint64
}
func NewQQBitMap() *QQBitMap {
// 计算需要多少个 uint64 元素
size := (maxQQNumber / 64) + 1
return &QQBitMap{
bits: make([]uint64, size),
}
}
// Add 标记一个QQ号存在
// 如果之前已经存在,返回 true;如果是新数据,返回 false
func (bm *QQBitMap) Add(qq uint64) bool {
index := qq / 64
position := qq % 64
mask := uint64(1) << position
// 检查该位是否已经是 1
if (bm.bits[index] & mask) != 0 {
return true // 已存在,重复
}
// 置为 1
bm.bits[index] |= mask
return false // 新数据
}
func main() {
bitmap := NewQQBitMap()
// 模拟流式读取大文件(不一次性加载到内存)
inputFile, err := os.Open("huge_qq_list.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer inputFile.Close()
outputFile, err := os.Create("unique_qq_list.txt")
if err != nil {
fmt.Println("创建输出文件失败:", err)
return
}
defer outputFile.Close()
scanner := bufio.NewScanner(inputFile)
writer := bufio.NewWriter(outputFile)
defer writer.Flush()
duplicateCount := 0
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
qq, err := strconv.ParseUint(line, 10, 64)
if err != nil {
continue
}
// 去重判断
if !bitmap.Add(qq) {
_, _ = writer.WriteString(strconv.FormatUint(qq, 10) + "\n")
} else {
duplicateCount++
}
}
if err := scanner.Err(); err != nil {
fmt.Println("读取过程中发生错误:", err)
}
fmt.Printf("去重完成!检测到重复QQ号数量: %d\n", duplicateCount)
}
|
Rust 语言实现
Rust 凭借精细的内存控制和零成本抽象(Zero-cost Abstractions),是解决此类低内存大数据场景的王牌。我们可以直接用 Vec<u64>,也可以使用标准库衍生出的第三方高性能库(如 bit-vec 或高效压缩位图 roaring)。
这里为了对标底层逻辑,先给出 纯手工原生实现 ,随后附上生产环境更推荐的 Roaring Bitmap 方案。
1. Rust 原生实现(无第三方依赖)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
const MAX_QQ_NUMBER: u64 = 4_300_000_000;
struct QQBitMap {
bits: Vec<u64>,
}
impl QQBitMap {
fn new() -> Self {
let size = (MAX_QQ_NUMBER / 64) as usize + 1;
// 分配 512MB 的连续堆内存,初始化为 0
Self { bits: vec![0; size] }
}
/// 标记一个QQ号存在
/// 如果之前已经存在,返回 true;如果是新数据,返回 false
fn add(&mut self, qq: u64) -> bool {
let index = (qq / 64) as usize;
let position = (qq % 64) as u32;
let mask = 1u64 << position;
if (self.bits[index] & mask) != 0 {
true // 已存在
} else {
self.bits[index] |= mask;
false // 新数据
}
}
}
fn main() -> io::Result<()> {
let mut bitmap = QQBitMap::new();
let input_file = File::open("huge_qq_list.txt")?;
let reader = BufReader::new(input_file);
let output_file = File::create("unique_qq_list.txt")?;
let mut writer = BufWriter::new(output_file);
let mut duplicate_count = 0;
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(qq) = trimmed.parse::<u64>() {
if !bitmap.add(qq) {
writeln!(writer, "{}", qq)?;
} else {
duplicate_count += 1;
}
}
}
// 显式刷新缓冲区
writer.flush()?;
println!("去重完成!检测到重复QQ号数量: {}", duplicate_count);
Ok(())
}
|
2. Rust 工业级标准:Roaring Bitmap 方案
在 Rust 实际工程中,更推荐使用 roaring 库。它不仅高度优化了位图计算性能,而且具备自动压缩功能。如果 40 亿 QQ 号分布相对稠密或连续,它的内存占用甚至可以压缩到 几十MB 到 一两百MB 。
添加依赖 (Cargo.toml):
1
2
| [dependencies]
roaring = "0.10"
|
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| use roaring::RoaringTreemap; // 支持 64 位整数的 Roaring Bitmap
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
fn main() -> io::Result<()> {
// 实例化一个支持 u64 范围的压缩位图
let mut bitmap = RoaringTreemap::new();
let reader = BufReader::new(File::open("huge_qq_list.txt")?);
let mut writer = BufWriter::new(File::create("unique_qq_list.txt")?);
let mut duplicate_count = 0;
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(qq) = trimmed.parse::<u64>() {
// bitmap.insert 如果原本不存在会返回 true,存在则返回 false
if bitmap.insert(qq) {
writeln!(writer, "{}", qq)?;
} else {
duplicate_count += 1;
}
}
}
writer.flush()?;
println!("去重完成!检测到重复QQ号数量: {}", duplicate_count);
Ok(())
}
|
.
方案二:Bloom Filter(如果QQ号是不规则字符串)
注意: 如果你的QQ号不是纯数字,或者范围远超43亿(比如包含了字母、或者未来升级到了更长的位数),BitMap 的数组就会变得极大,导致内存不够。
这时候就要用 布隆过滤器(Bloom Filter) 。
它通过多个哈希函数将一个QQ号映射到同一个位图的多个点上。
- 优点: 极其节省内存。哪怕QQ号是复杂的字符串,40亿数据通常也只需要几百MB。
- 缺点: 存在微小的 误判率(False Positive) 。也就是说,如果它说一个QQ号“不存在”,那它 绝对不存在 ;但如果它说“存在”,有极小的概率这个QQ号其实没出现过(哈希碰撞)。
Java 实现建议
在实际工程中,不需要手写,直接使用 Google 的 Guava 库,它对内存控制做到了极致。
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 需要引入 Guava 依赖
// com.google.common.hash.BloomFilter;
long expectedInsertions = 4000000000L; // 40亿
double fpp = 0.01; // 允许 1% 的误判率
// Guava 会根据数据量和误判率自动计算并分配最省内存的 Bit 数组
BloomFilter<Long> filter = BloomFilter.create(
Funnels.longFunnel(),
expectedInsertions,
fpp
);
// 判断与插入
if (filter.mightContain(qq)) {
// 可能是重复数据(有1%概率误判)
} else {
filter.put(qq); // 绝对是新数据,留下来
}
|
两个极端情况:
1. 如果QQ号断层非常严重怎么办?
比如40亿个QQ号里,有几个号码是 99999999999(11位甚至12位超大数字)。
- 问题: 如果用基础 BitMap,为了开辟到
99999999999 的索引,你的数组要开得极大,内存直接爆掉。 - 解法: 改用 Roaring Bitmap(高效压缩位图) 。Java 中有非常成熟的开源实现
RoaringBitmap。它将 32 位无符号整数分块(每块 $2^{16}=65536$ 个数字)。如果某一块全空,它不占内存;如果很稀疏,它用数组存;如果很稠密,它才用位图存。在非连续的大数据去重中,它是工业界的标准解法。
2. 如果只有几MB内存,连 512MB 都没有呢?
- 解法:外排序 + 分治法(MapReduce思想) 。
- 分桶(Hash Sharding): 把大文件读出来,用
qq % 100 的结果,把40亿数据分流到 100 个小文件中(file_0 到 file_99)。相同的QQ号必定落入同一个文件。 - 小文件去重: 每次只把一个小文件加载到内存中(此时一个小文件只有 4000万数据,用
HashSet 都可以轻松搞定),去重后输出。 - 合并: 把 100 个去重后的小文件拼起来,就是最终结果。
百万级excel快速导入数据库
要考虑的问题:
1.数据正确性问题
2.各类异常情况,格式等
3.失败后如何处理
4.出现重复的情况
5.内存溢出问题,
6.效率问题
内存溢出(OOM)的终结者:流式解析
- 绝对不要使用: Apache POI 的
WorkbookFactory.create()。它会将整个 Excel 的 DOM 树一次性加载到内存中。一个 100 万行的 .xlsx 文件,在内存中展开可能会放大 50~100 倍,瞬间吃光数 GB 内存。 - 工业级解法: 使用 EasyExcel (阿里开源)或 Apache POI 的
SXSSF(SAX 解析模式)。- 原理: 它们采用 流式(Event-driven)逐行解析 。内存中永远只保留当前解析的几行数据,内存占用是恒定的(通常只需几M或几十M),不管文件多大都绝不会 OOM。
效率问题:四轮驱动加速
- 批处理(Batch Insert): 绝不能一条条插入。设置一个缓冲区(例如每 1000~5000 条为一批),使用
mybatis-plus 的 saveBatch 或原生 JDBC 的 addBatch()。 - 多线程并行(Producer-Consumer):
- 主线程(生产者): 负责单线程流式读取 Excel(Excel 格式决定了它很难多线程并发读取)。读满一批(如 2000 条),就打包成一个 Task 扔进线程池。
- 线程池(消费者): 多个异步线程并发将数据写入数据库。
- 数据库连接池与驱动优化:
- 连接池(如 HikariCP)的最大连接数要喂饱和。
- 关键配置: 如果使用 MySQL,JDBC 连接串必须加上
&rewriteBatchedStatements=true,否则它的 Batch 只是伪批处理,加上后才会真正合并成一条 insert into ... values (),(),()。
1 & 2. 数据的防线:正确性与各类异常
百万级数据中,必然充斥着各种“奇葩”格式和脏数据。
正确性与格式校验
- JSR-303 异步校验: 在 EasyExcel 的
ReadListener 监听器中,接收到对象后立即使用 Validator 进行注解校验(如 @NotBlank, @Email, @Size)。 - 类型转换器(Converter):
- 时间日期: 明确指定格式(如
yyyy-MM-dd HH:mm:ss),防止由于 Excel 本身单元格格式转换导致的五花八门的时间戳或文本。 - 大数字(如手机号、身份证): Excel 容易将其变成科学计数法(
1.38E+10)。代码中必须强制按 String 转换并做正则清洗。
业务级“二阶段校验”
- 一阶段(本地校验): 纯内存校验(非空、长度、格式、枚举值)。不通过的直接打标签归为“失败行”。
- 二阶段(DB 校验): 比如“外键是否存在”。不要在循环里查 DB!应该分批批量拉取 DB 的对应数据到本地 Cache(或者用 Redis),在内存中做比对。
3 & 4. 容错与防重:失败处理与重复数据
处理百万数据时,因为第 90 万条报错而导致前 90 万条全部回滚,是不可接受的体验。
失败后如何处理?(两套可选策略)
根据业务严苛程度,通常选择 方案 B :
- 策略 A:强一致性(全成功或全失败)
- 实现: 利用分布式事务或大事务。但百万级数据放在一个事务里会导致 长事务 ,锁死整张表,数据库直接挂掉。通常不建议。
- 策略 B:最终一致性(断点续传 + 错误报告)—— 推荐
- 异步处理,返回 TaskID: 用户上传后,后端立即生成一个导入任务记录(状态:导入中),让用户去“任务中心”查看进度。
- 错误记录表/文件: 线程池在执行批插入时,若某一批次失败(如某一行违反唯一索引), 只回滚当前批次 。随后将这一批改为“逐条插入”,找出具体哪几行报错,把错误行和原因记入“错误日志表”。
- 最终产物: 导入完成后,状态改为“部分成功”。用户可以下载一个由后端生成的
error.xlsx,里面只有失败的行,并且最后一列写明了“失败原因”(如:第 85 万行:手机号格式错误)。用户修改后可二次上传。
出现重复的情况(去重策略)
重复分为两种: Excel 内部自带重复 ,以及 Excel 与 DB 原有数据重复 。
1. Excel 内部去重
- 如果业务不允许内部重复,利用 布隆过滤器(Bloom Filter) 或 RoaringBitmap (如果去重 Key 可以转为 Long 整数)。
- 在解析流中,每读一行,先过一遍布隆过滤器,存在则直接判定为“重复行”,踢入错误日志。
2. 与 DB 原有数据重复(核心看业务语义)
| 业务语义 | SQL 解决方案 | 适用场景 |
|---|
| 直接覆盖(以新为准) | INSERT INTO ... ON DUPLICATE KEY UPDATE | 幂等更新,如商品最新价格同步。 |
| 跳过不导(以旧为准) | INSERT IGNORE INTO ... | 历史流水归档,不可更改。 |
| 先查再断(提示错误) | 批量拉取 DB 的 Key,在内存中对比,重复的归入 error.xlsx。 | 注册账号、工号导入等严格防重场景。 |
🏗 企业级百万导入经典架构拓扑
整个流程通过生产者-消费者模型解耦,既保护了内存,又压榨了多核 CPU 的性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| [ 客户端 ] --( 上传 Excel )--> [ Controller 接收并落盘临时文件 ]
|
( 异步触发,返回 TaskID )
|
v
[ EasyExcel 流式解析 (单线程) ]
|
( 每解析 2000 条,执行本地校验 )
|
v
[ 提交给自定义线程池 (多线程并行) ]
/ | \
[Thread-1] [Thread-2] [Thread-3]
| | |
(批量DB校验 + writeBatchedStatements 批量写入)
\ | /
v
[ 数据库 (MySQL/PgSQL) ]
|
(如果有失败行 -> 写入错误日志)
|
v
[ 导出 error.xlsx 供用户下载 ]
|
🎯 落地核心避坑总结
- 文件落盘: 收到前端的大文件后, 先写到服务器本地临时目录(或 OSS) ,再去读取这个文件。不要直接用
MultipartFile.getInputStream() 一直拖着 HTTP 连接,否则大文件解析太久会导致 HTTP 连接超时断开。 - 合理配置线程池: 导入是 IO 密集型任务 ,线程数可以适当开大(例如 $2 \times \text{CPU核心数}$)。但必须要配置
CallerRunsPolicy(调用者运行策略)作为拒绝策略。当数据库写得慢、线程池队列满了的时候,让主线程(EasyExcel 解析线程)亲自去同步写库,从而自动实现 限流反压 ,防止队列塞满导致 OOM。
2GB的文件找出高频top100的单词
处理 2GB 文件并找出高频 Top 100 单词,最核心的挑战在于 内存管理 。2GB 的文本文件如果直接整块读入内存(特别是转换为 Java 对象、Python 字典或分割成字符串数组时),由于对象头开销和内存放大效应,往往会暴增到 6GB~10GB 以上,极易触发 OOM(内存溢出)。
作为开发者,处理这种大文件通常有两种思路:一是 单机流式处理(Time-Memory Trade-off) ,二是 分治法(更通用、可扩展到百 GB 文件) 。
下面为你提供两种最高效、最实用的落地方案:
方案一:单机流式读取 + 最小堆(推荐,代码最简单)
如果你的机器有 4GB 以上的可用内存,我们可以通过流式逐行/逐块读取(Stream / Buffer)来严格控制内存。在内存中,我们只维护一个哈希表(Map/Dict)来存 单词 -> 频次,以及一个大小为 100 的最小堆(Min-Heap / PriorityQueue)来筛选 Top 100。
内存粗算 :2GB 文本中去重后的独立单词(Lexicon)通常不会超过几百万个。假设去重后有 200 万个非重复单词,每个单词平均 10 字节,哈希表占用内存大约几百 MB,完全可以单机一把过。
1. 核心步骤
1.流式分块读取: **控制内存。**使用缓冲区(Buffer)流式读取文件,每次读入几兆数据,利用正则或状态机匹配出单词,切忌使用 readAllLines。
2.内存哈希计数: **增量更新。**在内存中维护一个高效的 HashMap,读取到单词后直接在 Map 中累加频次:map.put(word, map.getOrDefault(word, 0) + 1)。
3.构建大小为 100 的最小堆: **动态筛选。**遍历处理完的 HashMap。先往堆里放 100 个元素。随后的每个单词,若其频次大于堆顶元素的频次,则弹出堆顶,将新单词压入堆。
4.逆序输出: **得到 Top 100。**最终堆里剩下的 100 个元素就是全局频次最高的单词。将它们依次弹出并逆序排列,即可得到 Top 100 的结果。
2. 现代 Java 21+ 实现示例
利用现代 Java 的流式处理和并发特性,可以写出非常优雅且内存安全的代码:
Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TopWords {
public static void main(String[] args) {
String filePath = "large_file.txt";
Map<String, Long> wordCounts = new HashMap<>(1024 * 1024); // 预估初始容量减少扩容
Pattern pattern = Pattern.compile("[a-zA-Z]+"); // 仅匹配英文字母
// 1. 流式读取与计数
try (BufferedReader reader = new BufferedReader(new FileReader(filePath), 8 * 1024 * 1024)) { // 8MB Buffer
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
String word = matcher.group().toLowerCase(); // 统一转小写
wordCounts.put(word, wordCounts.getOrDefault(word, 0L) + 1);
}
}
} catch (IOException e) {
e.printStackTrace();
}
// 2. 最小堆获取 Top 100
PriorityQueue<Map.Entry<String, Long>> minHeap = new PriorityQueue<>(
100, Comparator.comparingLong(Map.Entry::getValue)
);
for (Map.Entry<String, Long> entry : wordCounts.entrySet()) {
if (minHeap.size() < 100) {
minHeap.add(entry);
} else if (entry.getValue() > minHeap.peek().getValue()) {
minHeap.poll();
minHeap.add(entry);
}
}
// 3. 输出结果
List<Map.Entry<String, Long>> result = new ArrayList<>(minHeap);
result.sort((e1, e2) -> Long.compare(e2.getValue(), e1.getValue())); // 降序排列
result.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
}
}
|
方案二:分治法 / 外部排序(在大内存受限或面对 200GB 文件时必用)
如果你的运行环境内存 极其严苛 (例如只有 256MB),或者文件后续会升级到 200GB 导致内存根本装不下独立的哈希表,这时候必须采用 分治法(MapReduce 思想) 。
核心架构设计
1
2
3
4
5
6
7
8
9
10
11
12
13
| [ 2GB 大文件 ]
|
+-------------+-------------+ (步骤 1: 内存不放大的 Hash 分流)
| | |
[小文件 0] [小文件 1] ... [小文件 63]
| | |
(内存 Map) (内存 Map) (内存 Map) (步骤 2: 分别统计各小文件的 Top 100)
| | |
[Top 100] [Top 100] ... [Top 100]
\ | /
+------------+------------+ (步骤 3: 汇总到总的最小堆,滤出全局 Top 100)
|
[ 全局 Top 100 ]
|
详细落地步骤
- Hash 拆分(分流) :
- 流式读取大文件,对获取到的每个单词,计算其哈希值(如
Math.abs(word.hashCode()) % 64)。 - 根据哈希值,将单词流式写入到对应的 64 个小文件中(每个小文件大约几M到几十M)。
- 关键点 :相同的单词必然会被分到同一个小文件中。
- 局部统计 :
- 依次把这 64 个小文件读入内存。因为相同的单词都在同一个文件里,我们此时可以放心地用一个小
HashMap 统计当前小文件的单词频次,并用最小堆挑出该文件的 Top 100。
- 全局归并 :
- 把 64 个小文件各自产生的 Top 100(总共 $64 \times 100 = 6400$ 个单词)汇总到一个最终的最小堆中,筛选出最后的全局 Top 100。
方案三:生产环境的“降维打击”(命令行极速流)
在实际 Linux 服务器上,如果你不需要写进业务代码,只是为了排查日志或紧急分析, 永远不要手写代码 。利用 Linux 管道符组合原生命令,它们是由 C 语言编写、经过几十年极致优化的流式工具,速度通常比自己写的代码快得多:
Bash
1
| cat large_file.txt | tr -cs 'a-zA-Z' '[\n*]' | tr 'A-Z' 'a-z' | sort | uniq -c | sort -nr | head -n 100
|
命令拆解:
tr -cs 'a-zA-Z' '[\n*]':把所有非字母的字符(空格、标点)全部替换为换行符,让单词一行一个。tr 'A-Z' 'a-z':大写转小写。sort:将单词排序(为 uniq 做准备)。uniq -c:去重并统计每个单词出现的频次。sort -nr:按照频次(-n 代表数值,-r 代表倒序)从大到小排序。head -n 100:只取前 100 行。
💡 生产环境小贴士 :如果单机 2GB, 方案一 (直接上内存 Map 配合 Buffer)最快最省事,整体开发成本最低。
给第三方提供接口要注意哪些?
1.遵循restful规范,数据格式统一,可以优雅降级,完善文档和sdk简化集成
2.安全性,敏感信息,校验和防护,限流策略
3.性能,缓存和异步
4.日志记录,结构化的日志,外部集成,敏感信息过滤
接口防刷
1.限流,分布式限流:redis+lua脚本实现原子化计数器,令牌桶算法
2.设备指纹识别
3.请求签名
4.黑名单机制
工作流引擎flowable+drools规则引擎
- 触发节点: 当提交流程至“审批路由”节点时,工作流引擎将流程变量(如申请人、金额)封装为 Drools 的 Fact 对象。
- 规则匹配: Drools 执行预设的 DRL 规则文件或决策表(Decision Table),动态计算出目标审批人列表或审批节点。
- 流程流转: Drools 将计算结果(如审批人 ID:
approverId=123)返回给工作流引擎。 - 分配任务: 工作流引擎根据返回的结果,精准将任务推送给指定审批人。
“在实际的 Web 项目中,如果把业务规则(如审批金额门槛、风控评级)直接写在 Flowable 流程图的网关表达式里,会存在严重的架构缺陷:
- 规则一变,流程必变 :改一个审批金额就要重新发布流程定义,导致历史正在运行的流程实例出现版本兼容问题。
- 职责不分离 :Flowable 的核心职责是 组织架构的横向流转 (谁来审批),而 Drools 的核心职责是 业务指标的纵向判定 (该不该审、去哪审)。
我们公司的做法是: Flowable 负责骨架,Drools 负责灵魂 。
用 Flowable 统一管理审批链路(Service Task -> Exclusive Gateway),而在 Service Task 节点中调用 Drools。一旦财务把‘总监审批门槛’从 1 万改成了 5 万,我们 只需要动态更新数据库里的 Drools .drl 文本 ,流程图和 Java 代码完全不需要动,做到了真正的‘零重启、零重新发布流程’,极大提升了业务的敏捷度。”