当数据量起来之后:一个老程序员的认知升级
前置:这篇写于 2020 年 3 月。我在某健康集团做健康管理平台,第一次面对"每天 80 万条健康数据、几百万条 Excel 导入"这种规模。
为什么写这篇
2019 年底,我从某跨境支付公司跳到某健康集团。前一家做的是日均几万订单的跨境支付,后一家做的是日均几十万次健康检查、几百万条报告数据的健康平台。
我天真地以为,量变到质变只是性能调优。
来了 3 个月,我被打脸了。
数据量级一旦变了,思维方式必须变。 写单机代码 10 年的经验,在百万级数据面前几乎要全部推翻重来。这篇随笔记录了我被打脸后,悟到的 4 个真实业务场景 + 5 条方法论 + 4 个常见坑。
4 个经典业务场景
场景 1:40 亿个 QQ 号去重,1GB 内存
这是面试题里的"老八股",但真要落地,思路和工程取舍完全不一样。
直接上 HashSet<Long>? 40 亿 × 8 字节 = 32 GB,直接 OOM。
核心思路:
- Bitmap(位图):把"存数值"换成"存 1 个比特位"。43 亿个 bit = 512 MB,刚好装进 1GB 内存。
- 布隆过滤器(Bloom Filter):如果数据是字符串、不能直接转 long,用多个 hash 函数映射到固定大小的 bit 数组,有微小误判率但内存极省。
- 分桶 + 外排序:如果内存连 512MB 都没有,按
hash % 100把数据分到 100 个小文件,单机顺序处理。
| |
512 MB 内存,无误差,O(1) 查询。 我当时看到这个解法的第一反应是——“我以前写的代码,全是 O(N) 的笨办法”。
场景 2:百万级 Excel 导入数据库
这个场景我踩过最大的坑——上来就用 WorkbookFactory.create(file),结果 100 万行 xlsx 内存直接爆掉。
核心思路:
- 流式解析(EasyExcel / POI SAX 模式):内存里只保留当前行,永远不一次性加载。
- 批处理 + 多线程:每 2000 行一批,主线程解析,子线程批量写库。MySQL JDBC 一定要加
rewriteBatchedStatements=true,否则不是真批处理。 - 二阶段校验:本地校验(格式、长度) + DB 校验(外键是否存在)。不要在循环里查 DB,先批量拉一份到内存里比对。
- 失败策略:别用大事务,用 TaskID 异步 + 错误行回写
error.xlsx。用户能下载、自己改、二次上传。
生产环境小贴士:导入是 IO 密集型,线程数可以开大(
2 × CPU核数),但拒绝策略必须用CallerRunsPolicy——队列满了让主线程亲自写,自动反压。
场景 3:2GB 文件找出高频 Top 100
一开始我用 Files.readAllLines()——直接把 2GB 文件读成字符串列表,内存放大 3-5 倍直接 OOM。
两个标准解法:
解法 A:流式读取 + 最小堆(单机 4GB 内存场景)
| |
解法 B:分治法(百 GB 文件也能用)
- Hash 分流:按
hash(word) % 64写 64 个小文件 - 局部 Top 100:每个小文件独立算 Top 100
- 全局归并:64 × 100 = 6400 个候选,再筛一次
解法 C:Linux 命令行"降维打击"
| |
5 个 tr/sort/uniq 搞定,30 秒出结果。 Linux 工具链是 C 写的,性能比我写的 Java 还快。
场景 4:给第三方提供接口
我以前做内部系统,接口爱怎么写怎么写。做对外接口才发现,光"防刷"就是个大工程。
4 个必做项:
- RESTful + 统一响应格式:
{code, message, data}三段式,错误码统一,优雅降级。 - 限流:Redis + Lua 脚本实现原子化的分布式令牌桶。
- 设备指纹 + 请求签名:防脚本 + 防伪造。
- 结构化日志:所有外部接口调用记日志,敏感信息过滤(手机号、身份证打码)。
最关键的:接口契约先行(YApi / OpenAPI),不要后端写完了再补文档。前后端联调周期能从一周压到一天。
5 条方法论
方法论 1:内存估算先行
写任何大数据代码前,先算一下:
| 场景 | 数据量 | 朴素方案 | 优化方案 |
|---|---|---|---|
| 40 亿 QQ 号去重 | 40 亿 long | HashSet → 32 GB ❌ | Bitmap → 512 MB ✅ |
| 2GB 文本 Top 100 | 200 万独立词 | readAllLines → 8GB+ ❌ | 流式 + HashMap → 几百 MB ✅ |
| 百万行 Excel | 100 万行 | WorkbookFactory → OOM ❌ | EasyExcel SAX → 几 MB ✅ |
不算这一笔,后面所有优化都是瞎调。
方法论 2:流式 > 一次性加载
这是大数据处理的"第一性原理"。文件大到内存装不下,唯一的解法是分块流式处理。EasyExcel、BufferedReader、Kafka Consumer、Log Tail,底层都是这个思想。
方法论 3:分治是万金油
“凡是单机装不下的,就分桶;凡是单线程跑太慢的,就并行;凡是单点会挂的,就冗余。”
MapReduce 是工业级模板。40 亿 QQ 号去重能分桶,2GB 文件 Top 100 能分桶,百亿条日志分析也能分桶。
方法论 4:业务约束 > 技术最优
我曾经追求过"性能 100% 压榨",后来发现业务允许 1% 误判率时,内存能省 10 倍。比如布隆过滤器。
先问业务"我能不能接受 1% 误差?",比问"用 Bloom 还是 Roaring"重要 10 倍。
方法论 5:批处理 + 异步 + 拒绝策略
百万级导入、批量发通知、批量同步数据——三件套必须配套:
- 批处理:1000~5000 条一批
- 异步:返回 TaskID,不阻塞用户
- 拒绝策略:队列满了让上游等(
CallerRunsPolicy)或丢弃非关键任务
4 个常见坑
坑 1:误用分布式事务
百万级导入用分布式事务 → 长事务锁表 → 整个库卡死。
解法:最终一致性 + 补偿 + 错误行回写。99% 的场景不需要分布式事务。
坑 2:MySQL 伪批处理
JDBC 写 addBatch() 但没加 rewriteBatchedStatements=true——还是一条条 insert,性能比单条还慢。
坑 3:HashMap 装下"全部数据"
我早期写的代码里,到处都是 Map<userId, User> 全量缓存。几万用户没事,百万用户 OOM。
解法:本地缓存(Caffeine)+ 过期时间 + 最大条目数;分布式缓存(Redis)+ 淘汰策略。
坑 4:忽略 GC 停顿
高频写入场景,JVM Full GC 一次几秒——前面的调优全废了。
解法:堆外内存(DirectBuffer)/ 零 GC 语言(Rust)/ 异步批写(攒一批再 flush)。
个人反思
回头看 2020 年那个"被打脸"的我,最大的认知升级是:
“单机思维"和"分布式思维"之间,隔着一道叫"数据量级"的墙。
以前我以为:
- 性能 = 算法 + 数据结构
- 调优 = JVM 参数 + SQL 索引
现在我明白:
- 性能 = 内存估算 + 流式处理 + 分治并行
- 调优 = 业务约束 + 异步批处理 + 拒绝策略
数据量级改变了思维范式。 任何人没被"大数"打过一次脸,都不会真正理解这句话。
后记
2020 年下半年,我离开了某健康集团,去了一家更硬核的物联网公司(代号 某物联网项目)。那家公司每天处理 3 万台设备、12,000 QPS 的实时数据。
但如果没在 2020 年初被"百万级 Excel"和"40 亿 QQ 号"打脸过,我大概率接不住后面的挑战。
数据量级这道墙,早撞比晚撞好。
关联阅读:
- 面试随笔:写给 2013 年那个第一次当面试官的我
