之前写的一篇文章,nginx转发数据流文件,解释了为什么用数据流返回大文件。用数据流返回大文件(文本类的),数据在所有的节点都不缓存,在服务端读取一部分就返回一部分,也就是先返回响应,然后文件慢慢下载,这样就不会有超时,因为响应已经返回了。而且文件不会服务端先生成,代理再缓存,最后再返回客户端。这样的思路非常有意思。今天写两个具体的demo来看下。
普通思路是从数据库查询所有数据,再写到文件,然后静态文件返回。这样容易因为文件太大导致下载时间超过几分钟,超时的问题。所以现在的思路是,接到请求,边查询边返回,返回的响应体不是文件而是数据流。
Go的gin框架demo如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"io"
)
type OperationLog struct {
Id int `gorm:"primaryKey"`
UserId string `gorm:"user_id"`
UserName string `gorm:"user_name"`
}
func main() {
router := gin.Default()
router.GET("/streamData", streamData)
router.Run(":8080")
}
func getDB() (db *gorm.DB, err error) {
dsn := "root:123456(172.0.0.1:3306)/test?charset=utf8mb4"
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{SingularTable: true}})
if err != nil {
print("%v", err)
//panic("无法连接到数据库")
}
return db, err
}
func streamData(c *gin.Context) {
/*
先获取数据
*/
db, err := getDB()
if err != nil {
print("%v", err)
panic("无法连接到数据库")
}
pageSize := 5
pageNumber := 1
var logs []OperationLog
var total int64
offset := (pageNumber - 1) * pageSize
// 总数
db.Offset(offset).Limit(pageSize).Find(&logs).Count(&total)
// 总页数
pages := int(total) / pageSize
rem := int(total) % pageSize
if rem != 0 {
pages += 1
}
// 分段下载数据
c.Header("Transfer-Encoding", "chunked")
// 强制下载
c.Header("ContentType", "\"application/octet-stream; charset=utf-8-sig\"")
// 文件名
c.Header("Content-Disposition", "attachment;filename=test.csv")
// 流式响应
c.Stream(func(w io.Writer) bool {
// 每次只查询一页的数据,每次只返回一页的数据
for i := 0; i <= pages; i++ {
pageData := getPageData(db, i, pageSize)
idStr := ""
for _, v := range pageData {
idStr += fmt.Sprintf("%d", v.Id)
}
n, errWrite := w.Write([]byte(idStr + "\n"))
if errWrite != nil {
print(err)
}
print("%v,%d", idStr, n)
}
// 返回false结束
return false
})
}
func getPageData(db *gorm.DB, pageNumber int, pageSize int) []OperationLog {
var logs []OperationLog
offset := (pageNumber - 1) * pageSize
db.Offset(offset).Limit(pageSize).Find(&logs)
return logs
}
返回响应非常快
还可以看下Stream(),接收入参为io.Writer的函数,每次把上下文的写入器传入step函数,刷新写入器,最后监听通道关闭,结束。
python 的bottle如下(思路,不能跑),使用游标,每次只查几个,查完就返回
def stream_data(self, request_body, db):
engine = get_engine()
conn = engine.raw_connection()
name_suffix = str(uuid.uuid4())[:6]
file_name = resource_type + name_suffix + '.csv'
sql = "select * from user"
response = HTTPResponse(body=self.streaming_res(conn, sql))
response.streaming = True
# 告诉代理或浏览器,这是分段下载的数据
response.set_header("Transfer-Encoding", "chunked")
# 未知类型的数据流文件 utf-8-sig编码让excel默认打开不会乱码
response.content_type = "application/octet-stream; charset=utf-8-sig"
# 强制浏览器下载文件
response.set_header("Content-Disposition", "attachment;filename=" + file_name)
return response
def streaming_res(self,db, sql_query: str):
"""
生成器函数,将sql查询的数据,一行一行返回并格式化
"""
try:
cs = db.cursor()
cs.execute(sql_query)
while True:
result = cs.fetchmany(100)
if result:
yield ','.join(result)
else:
break
except Exception as e:
log.error("error:" + str(e))
finally:
if cs: cs.close()
if db: db.close()
参考博客: