我见过太多网上帖子文章,百万上来就是千万各种方案,要换什么API,导出要加异步,场景要调内存等各种手段。样实优化
看到这些我都有种小时候感冒乡村医生上来就是现样一堆药的无奈感;至今没见过谁是从真正项目场景出发去分析因果关系。
一切手段都是百万为了服务场景,服务用户。千万
图片
这题明面上问的导出是 Excel 导出优化,实际上是场景在扒你对数据链路全流程(查询 - 处理 - 生成 - 传输)的分析感知&处理能力 —— 大部分人一上来就盯着 Excel 生成本身死磕,根本不理解对症下药以及优化过程。样实优化
我的现样风格是总分,从问题场景由浅入深,百万先花2分钟看下决策图,千万再跟着我的导出思路来分析:
图片
先别急着说你有什么牛逼的方案,我们先从原始问题出发,你最先接手原始的代码可能是这样:前端点导出,站群服务器后端接口直接查数据库(可能还没加索引),把所有数据一次性捞到内存里,用 POI 同步生成 Excel 文件,最后通过 HTTP 响应直接返回。
你是不是觉得这流程特顺?如果是这样,那你问题大了,我们逐步道来。
如果导出的数据只有几百条,这方案凑活用。但如果业务方说 “我要导出近一年的订单,大概 100 万条,甚至更多”,你这代码会出啥问题?
首先,查库就卡壳了 ——100 万条数据一次性 select *,就算有索引,MySQL 把结果集拼装好返回给应用,这网络传输和内存占用就够你喝一壶的。
我早年做报表系统时就踩过这坑,一次导出把应用服务的 JVM 堆直接干到 GC 频繁,最后 OOM 挂了一个节点,现在想起来都脸红。
你真的以为把查库优化了(比如加索引、用 limit 分页)就没事了?好,企商汇就算你分页查库,每次查 1 万条,100 次查完。
但接下来同步生成 Excel 的过程,还是在占用着当前的 HTTP 连接 ——Tomcat 的连接池就那么大,要是同时有 10 个用户这么导出,单机容量有限,连接池直接满了,后面的正常请求全排队,这服务不就等于半瘫了?
说白了,同步模式下,导出任务就是个 “连接池杀手”,把请求线程全占着干 “慢活”。
一堆资源都在浪费没有最大化利用,线上风险极高,随时可能打爆你的服务,用户体验也极差。
就算你扛过了查库和线程占用,生成的 Excel 文件要是有几十 MB,问题又来了 ——HTTP 传输慢不说,前端等待超时怎么办?
用户点了导出,等了 5 分钟没反应,以为没成功,WordPress模板又点了好几次,结果后端重复生成好几个大文件,资源直接 double 浪费。
此时你开始大概意识到问题是什么了,这个场景面对的是些什么问题,把你的思考过程抬上来再谈解决方案,才是合格的RD,也是面试官喜欢的候选人。
那咱开始优化。首先要解决的不是 Excel,是 **“同步阻塞” 这个巨坑 **。怎么解?异步化。
把 “用户触发导出” 和 “Excel 生成” 拆成两回事 —— 用户点导出时,后端不直接处理,而是生成一个 “导出任务 ID”,扔到 MQ 里,然后立刻返回给前端 “任务已受理,请用 ID 查结果”。
前端拿着 ID 轮询(或者后端直接生成完后通过消息助手通知用户),等 MQ 消费者把 Excel 生成完,再通知用户下载。
图片
当然最好能利用mq分布式多机消费的特性,将数据量进行分批拆分,每台机器处理一批或者一段,这样就不会导致单机oom
这一步就把请求线程解放了,连接池再也不会被堵死。但这里要注意,异步不是一劳永逸的 —— 你得处理任务状态(等待中 / 生成中 / 成功 / 失败);
还得考虑失败重试(比如生成到一半 MQ 挂了怎么办?),甚至要做任务限流,不能让 1000 个用户同时扔导出任务,把 MQ 和生成服务压垮。
解决了异步,再看数据查询。分页查库是基础,但有个坑:当偏移量很大时(比如 limit 100000, 1000),MySQL 会扫描前面 10 万条数据再跳过,这时候索引效率会骤降。
怎么办?用 “游标分页”—— 比如按订单 ID 排序,每次查的时候带上上次的最大 ID(where id > last_id limit 1000),这样索引一直有效。
另外,要是查库涉及多表关联,或者计算逻辑复杂(比如统计每个用户的订单总额);
直接查业务库会影响线上业务,如果实时性不是那么高,完全可以考虑离线数仓 —— 把导出需要的明细或汇总数据,提前用定时任务同步到 ClickHouse、Hive 这类 OLAP 数据库里,导出时查数仓,不碰业务库。
或者直接扔异步队列里面(注意,最好不要扔线程池,单机容易oom,最好分布式多机消费处理)
大厂稍微大一点的非实时场景基本都是这样干!
接下来才到 Excel 生成本身。直接在应用服务里用 POI 生成,还是有问题 ——100 万条数据生成 Excel,就算用 SXSSF(流式生成,避免 OOM),也会占用不少 CPU 和内存。
能不能把这步也拆出去?(不到万不得已一般不拆,工作量反而增大,但是你得有这个意识)搞个专门的 “Excel 生成服务”,只负责从 MQ 接任务、从数仓查数据、生成文件。这样业务服务和生成服务解耦,各自扩容 —— 业务服务扛并发请求,生成服务扛 CPU 密集型的文件生成。
另外,生成格式也能做文章:如果业务对 Excel 格式要求不高(比如只是看数据,不用公式、图表),可以先生成 CSV 文件(生成速度比 Excel 快 10 倍不止),再转成 Excel;
或者直接让用户下载 CSV(前端也能打开);如果必须要 Excel,除了 POI,还可以试试 Alibaba 的 EasyExcel,它对内存的优化比原生 POI 更到位,还支持注解配置,少写不少破代码。
最后是大文件传输的问题。生成好的 Excel 文件,要是几十 MB 甚至上百 MB,让应用服务直接通过 HTTP 返给用户,还是会占用带宽。
这时候就得用对象存储(比如 OSS、S3)—— 刚才上面也提过,可以生成服务把 Excel 文件写完后,直接上传到 OSS,然后把 OSS 的下载链接(带签名、设过期时间,避免泄露)存到数据库里。
用户查结果时,后端直接返回这个链接,用户去 OSS 下载,应用服务彻底不用扛传输压力。也可以直接用消息助手通知用户通过链接下载,省时省力(不过小公司可能成本略高)
这里要注意签名的安全性,比如链接过期时间设 1 小时,避免用户把链接分享出去,泄露数据。
到这你是不是觉得差不多了?别急,还有个容易被忽略的点:缓存重复请求。
比如同一个用户,一天内重复导出 “近 7 天的订单”,数据没变化,没必要重复生成。这时候可以加个缓存,key 是 “用户 ID + 导出条件(时间范围、字段)”,value 是 OSS 链接,缓存过期时间设成数据更新的周期(比如订单数据实时更新,缓存设 1 小时)。
这样重复请求直接命中缓存,省了查库和生成的成本。但要注意缓存失效策略 —— 如果底层数据更新了(比如用户改了订单状态),得及时清掉对应的缓存,不然用户下载到的是旧数据。
最后总结下,没有什么 “牛逼的方案” 能解决所有导出慢的问题,核心思路是 “拆解链路、分治优化”:把 “查数据 - 生成文件 - 传输文件” 拆成三个独立环节,每个环节用最适合的技术去扛(数仓扛查询、异步服务扛生成、对象存储扛传输)
同时用缓存减少重复劳动,通过打点监控(比如任务成功率、生成耗时)盯着链路中的坑。
任何脱离场景谈优化都是耍流氓 —— 如果业务方导出的数据永远不超过 1 万条,那搞异步和对象存储就是过度设计;
但如果是 To B 业务,用户动不动导出百万级数据,那上述的链路改造就是必须的。