通常在后端开发中遇到Excel文件导出是一件常见的需求,在数据量小的情况下,这个功能的开发是很简单的,直接读取数据到内存中一次性写入excel中导出即可,但是通常导出的数据量会比较大,可能会有几十万生成上百万条,这时你就要考虑很多场景,不然你可能会遇到以下问题:
- 接口超时;
- 一次性加载数据到内存中,导致内存溢出;
- EXCEL文件过大,无法打开等问题;
那要如何规避这些问题呢,我们接着往下聊。
1. 超时
引起超时的原因有多种,比如说数据查询慢、写入excel慢、数据库连接慢、资源占用高服务响应慢等等,我们主要关注下查询和写入两个场景。
1.1 数据查询慢
通常对于大批量的数据导出,我们会使用分页的方式,比如每次查询1000条数据,然后写入到excel中。这里就会遇到一个分页查询的问题,没有正确的使用索引和覆盖索引(避免回表)都会导致查询效率降低,进而影响到导出的速度。
使用覆盖索引的示例:
sql
SELECT * FROM table
JOIN (
SELECT id FROM table
ORDER BY create_time
LIMIT {offset}, {size}
) AS tmp USING(id);
1.2 写入excel慢
首先要避免单行写入,尽量用流式写入,对于大数据导出的场景要分sheet页或者多文件处理,我这里比较推荐多文件,然后压缩写入zip包中。
2. 内存溢出
通常情况下内存溢出,一般是由于一次性将数据加载到内存,或多次加载到内存后,再进行写入excel操作导致的。那要避免这种问题就要将每次查询到的内容先流式写入的excel中,然后进行下一次查询,直到全部查询完。
3. EXCEL文件过大,无法打开
首先减少一个excel文件中的数据量,对于大批量数据的导出,可视情况将数据拆分到多个sheet页或多个excel文件中;还可以考虑其它数据格式的替代,比如csv文件(通常文件大小只有excel文件的1/10)。
4. excelize库实现多excel文件写入zip导出
以下是一个使用 excelize
结合 zip
写入的通用代码模板,满足以下要求:
- 分excel文件,写入到zip压缩包中导出;
- 使用
gin
框架支持 Web 同步下载; - 每个 Excel 文件使用流式写入;
- 每写完一个 Excel 就立即写入到 ZIP 中,避免占用过多内存;
- 表头通过反射自动生成;
- 支持任意结构体类型的导出,便于复用与扩展。
go
package main
import (
"archive/zip"
"bytes"
"fmt"
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
)
type ComponentDetail struct {
ID string `excel:"ID"`
Name string `excel:"名称"`
Version string `excel:"版本"`
Time string `excel:"创建时间"`
}
func main() {
r := gin.Default()
r.GET("/export", exportHandler)
r.Run(":8080")
}
func exportHandler(c *gin.Context) {
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", `attachment; filename="export.zip"`)
// 创建 zip writer
zipWriter := zip.NewWriter(c.Writer)
defer zipWriter.Close()
// 模拟多个组件导出
for i := 0; i < 3; i++ {
componentID := fmt.Sprintf("component_%d", i+1)
// 模拟数据
var data []ComponentDetail
for j := 0; j < 10; j++ {
data = append(data, ComponentDetail{
ID: fmt.Sprintf("%s_item_%d", componentID, j+1),
Name: fmt.Sprintf("组件名-%d", j+1),
Version: "v1.0",
Time: time.Now().Format("2006-01-02 15:04:05"),
})
}
// 每写完一个 Excel 就立刻写入 ZIP
if err := WriteExcelToZip(zipWriter, fmt.Sprintf("%s.xlsx", componentID), data); err != nil {
c.String(http.StatusInternalServerError, "导出失败: %v", err)
return
}
}
}
go
// WriteExcelToZip 将任意结构体切片数据导出为 Excel,并写入 zip 文件中
func WriteExcelToZip[T any](zipWriter *zip.Writer, filename string, data []T) error {
// 创建 zip 条目
entry, err := zipWriter.Create(filename)
if err != nil {
return err
}
// 创建 excel 文件
f := excelize.NewFile()
defer f.Close()
sheet := "Sheet1"
streamWriter, err := f.NewStreamWriter(sheet)
if err != nil {
return err
}
// 获取结构体字段作为表头
t := reflect.TypeOf(data)
if t.Kind() == reflect.Slice {
t = t.Elem()
}
headers := []interface{}{}
fields := []int{}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("excel")
if tag == "" {
tag = field.Name
}
headers = append(headers, tag)
fields = append(fields, i)
}
if err := streamWriter.SetRow("A1", headers); err != nil {
return err
}
// 写入数据行
for i, v := range data {
val := reflect.ValueOf(v)
row := []interface{}{}
for _, fi := range fields {
row = append(row, val.Field(fi).Interface())
}
cell, _ := excelize.CoordinatesToCellName(1, i+2)
if err := streamWriter.SetRow(cell, row); err != nil {
return err
}
}
// 刷新
if err := streamWriter.Flush(); err != nil {
return err
}
// 写入 zip entry
_, err = f.WriteTo(entry)
return err
}
5. 总结
以上就是excel web端导出的优化方法,其实还有一些优化空间,比如增加进度显示,支持http客户端中断等;但是总体来说,对于大数据量的导出,我们主要关注避免一次性将数据加载到内存,尽量使用分页查询和流式写入,同时要避免内存溢出和文件过大的问题。