场景面试

海量qq去重

40亿个qq号如何去重?仅1GB内存

解析:

QQ号(数字)最大通常不会超过10位,在Java中通常用 longString 表示。如果是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思想)
  1. 分桶(Hash Sharding): 把大文件读出来,用 qq % 100 的结果,把40亿数据分流到 100 个小文件中(file_0file_99)。相同的QQ号必定落入同一个文件。
  2. 小文件去重: 每次只把一个小文件加载到内存中(此时一个小文件只有 4000万数据,用 HashSet 都可以轻松搞定),去重后输出。
  3. 合并: 把 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。

效率问题:四轮驱动加速

  1. 批处理(Batch Insert): 绝不能一条条插入。设置一个缓冲区(例如每 1000~5000 条为一批),使用 mybatis-plussaveBatch 或原生 JDBC 的 addBatch()
  2. 多线程并行(Producer-Consumer):
    • 主线程(生产者): 负责单线程流式读取 Excel(Excel 格式决定了它很难多线程并发读取)。读满一批(如 2000 条),就打包成一个 Task 扔进线程池。
    • 线程池(消费者): 多个异步线程并发将数据写入数据库。
  3. 数据库连接池与驱动优化:
    • 连接池(如 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 供用户下载 ]

🎯 落地核心避坑总结

  1. 文件落盘: 收到前端的大文件后, 先写到服务器本地临时目录(或 OSS) ,再去读取这个文件。不要直接用 MultipartFile.getInputStream() 一直拖着 HTTP 连接,否则大文件解析太久会导致 HTTP 连接超时断开。
  2. 合理配置线程池: 导入是 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 ]

详细落地步骤

  1. Hash 拆分(分流)
  • 流式读取大文件,对获取到的每个单词,计算其哈希值(如 Math.abs(word.hashCode()) % 64)。
  • 根据哈希值,将单词流式写入到对应的 64 个小文件中(每个小文件大约几M到几十M)。
  • 关键点 :相同的单词必然会被分到同一个小文件中。
  1. 局部统计
  • 依次把这 64 个小文件读入内存。因为相同的单词都在同一个文件里,我们此时可以放心地用一个小 HashMap 统计当前小文件的单词频次,并用最小堆挑出该文件的 Top 100。
  1. 全局归并
  • 把 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 流程图的网关表达式里,会存在严重的架构缺陷:

  1. 规则一变,流程必变 :改一个审批金额就要重新发布流程定义,导致历史正在运行的流程实例出现版本兼容问题。
  2. 职责不分离 :Flowable 的核心职责是 组织架构的横向流转 (谁来审批),而 Drools 的核心职责是 业务指标的纵向判定 (该不该审、去哪审)。

我们公司的做法是: Flowable 负责骨架,Drools 负责灵魂 。 用 Flowable 统一管理审批链路(Service Task -> Exclusive Gateway),而在 Service Task 节点中调用 Drools。一旦财务把‘总监审批门槛’从 1 万改成了 5 万,我们 只需要动态更新数据库里的 Drools .drl 文本 ,流程图和 Java 代码完全不需要动,做到了真正的‘零重启、零重新发布流程’,极大提升了业务的敏捷度。”

本作品采用 CC BY-NC-SA 4.0 协议进行许可
使用 Hugo 构建
主题 StackJimmy 设计