golang批量下载图片并打包zip的性能优化

所以你应该知道,在zip文件里面创建文件是串行的,这是由它的特性决定的。

// Create adds a file to the zip file using the provided name.
// It returns a Writer to which the file contents should be written.
// The file contents will be compressed using the Deflate method.
// The name must be a relative path: it must not start with a drive
// letter (e.g. C:) or leading slash, and only forward slashes are
// allowed. To create a directory instead of a file, add a trailing
// slash to the name.
// The file's contents must be written to the io.Writer before the next
// call to Create, CreateHeader, or Close.
func (w *Writer) Create(name string) (io.Writer, error) {
	header := &FileHeader{
		Name:   name,
		Method: Deflate,
	}
	return w.CreateHeader(header)
}

那么可以优化的空间就在于获取网络图片的时候了,通过多协程的方式来提高效率。

常规的方式是:

// 创建zip文件
zipwriter := zip.NewWriter(file)

// 遍历图片数组
for _, f := range fs {
	// 创建文件
	// 获取图片内容
	// 将内容写入文件
}

70张图片,耗时在 31.62s

优化方案,采用任务流式处理:
遍历图片 --> TaskA --> TaskB

TaskA:多协程,获取图片内容。
TaskB:单协程,写入到zip。

示例代码:

func download() {
	type photo struct {
		Url     string
		Name    string
		Content []byte
	}

	photoInChan := make(chan photo, 100) // 太小了会有一定的阻塞
	photoOutChan := make(chan photo, 100) // 太小了会有一定的阻塞
	photoNum := 0 // 总的图片数
	photoOver := 0 // 总消费数
	isOver := false // 图片是否已遍历完,流式模式下不能只判断 photoNum == photoOver
	mu := &sync.Mutex{} // 自增操作存在并发问题,需要加锁

	// 获取图片内容,做好协程退出机制
	for i := 0; i < 20; i++ {
		go func(no int) {
			over := false
			for {
				if over {
					break
				}
				select {
				case p := <-photoInChan:
					p.Content = util.GetRemoteContent(p.Url)
					photoOutChan <- p
					mu.Lock()
					photoOver++
					mu.Unlock()
				case <-time.After(time.Millisecond * 500):
					// 最后一个任务只会被一个协程消费,其他协程仍然处于阻塞状态,因此需要一个超时处理来实现主动退出。
					// 本来想使用context来做,发现并不适合这个场景。
					if photoOver == photoNum && isOver {
						fmt.Println("download goroutine is returned: ", no)
						over = true
					}
				}
			}
		}(i)
	}

	zipfile := "xxx.zip"
	f, err := os.Create(zipfile)
	if err != nil {
		return
	}
	defer f.Close()

	zipwriter := zip.NewWriter(f)
	defer zipwriter.Close()

	var wg sync.WaitGroup
	wg.Add(1)

	// 写入zip操作,串行处理
	go func() {
		defer func() {
			wg.Done()
		}()
		over := 0
		for p := range photoOutChan {
			iowriter, err := zipwriter.Create(p.Name)
			if err != nil {
				continue
			}
			iowriter.Write(p.Content)
			over++
			if over == photoNum && isOver {
				break
			}
		}
	}()

	// 图片数组
	for _, val := range photos {
		arr := strings.Split(val, ".")
		ext := arr[len(arr)-1]

		photoInChan<-photo{
			Url: val,
			Name: util.CreatePhotoName() + "." + ext,
		}

		photoNum++
	}

	fmt.Println("总图片数:", photoNum)
	isOver = true

	wg.Wait()

	return
}

耗时 20.93s

总体来看耗时降低了 30%,还可以吧。