对需要导出的数据进行定量拆分(拆分成多个报表文件),然后使用zip进行实时压缩。达到降低网络消耗,加快下载(让甲方爸爸开心)。
如果不想看过程的话,可以直接跳到 最终版本
文中所有指标,针对的是正式业务数据。测试代码中的数据量太小,想疯狂压测的话,可以把数据搞复杂一点。

客户的需求迭代

V1.0

可以导出数据

  • 前端基于Vue构建,有对应的插件可以生成Excel。直接由前端导出

客户反馈:报表数据量太小

V2.0

不仅要导出当前页的数据,而且要(当前报表的)所有数据

  • 解除请求中,分页大小的限制

客户反馈:导出太慢。内存飙升。很容易卡死

V2.1

  • 台导出excel,并随 response 流返回

    这里没有采用异步生成文件,然后通过http的方式下载。是基于工期和数据安全性的考虑
    1、在短期内做一个下载校验服务器,不太容易(下载一次后自动删除文件)
    2、http的形式基本上就可以任意用户访问/下载。没有安全性

运维反馈:K8s上面的项目,不定期的重启。对比服务器负载,及日志得出结论:导出Excel的过程太占内存
客户反馈:太慢,文件太大,中断了需要重新下载

V2.2

本地进行了压力测试,在200W条数据的情况下,数据占用内存大概1GB。当生成Excel的时候,内存占用上升到14GB左右。
此时我们就不得不考虑一个上古方案了CSV
压测后发现,200W条数据,在写csv文件时,大概占用3GB内存。但是在1048576行之后的数据就消失

客户撒泼:我就要全部数据

V3.0

异步操作:每100W行,生成一个新的csv文件,然后使用zip打包

最终版本(V3.0)

使用如下代码测试下来,在200W条数据量的前提下,内存并无大幅波动(不到100MB)。压缩比有50(压缩前的csv大概2GB,打包后的zip大概40MB)。效果还算是不错的。

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
package main

import (
"archive/zip"
"fmt"
"io"
"os"
)

func main() {
zipFile, _ := os.Create("a.zip")
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()

const maxLine = 100000 // 设置单文件最大行数
var csvName string
var csvWriter io.Writer
var err error

// 模拟从数据库中获取到的数据(这里压测数据为200W)
for i := int64(0); i < 2000000; i++ {
// 每达到maxLine(10W)条数据的时候,生成一个新的文件
if i%maxLine == 0 {
csvName = fmt.Sprintf("%d.csv", i/maxLine)
fmt.Println("zipWriter.Create(" + csvName + ")")
csvWriter, err = zipWriter.Create(csvName)
if err != nil {
fmt.Println("-----------", err)
}
csvWriter.Write([]byte{0xEF, 0xBB, 0xBF}) // UTF-8编码
csvWriter.Write([]byte("姓名,年龄,班级,分数\r\n"))
}
// 这里使用简单的数据,实际场景中的数据比这个复杂几倍
csvWriter.Write([]byte("张三,10,五年级,100\r\n")) // 写入每一行数据
}
}