Heroku部署的NodeJS应用从Google Cloud Storage批量下载图片时内容重复问题求助
问题分析与解决方案
这个问题的核心在于全局变量的异步竞态和异步操作未正确等待,导致批量下载时所有文件都被最后一个请求的内容覆盖。我们一步步拆解问题并修复:
1. 全局变量引发的竞态问题
你定义了全局变量downloadFileName,当批量调用downloadFromGCS时,循环会快速覆盖这个变量,而.download()是异步操作——等异步任务真正执行时,downloadFileName已经被更新为循环的最后一个文件名,所以所有下载最终都指向了最后一个文件的内容。
2. 低效的文件存在性检查
你通过getFiles()遍历存储桶所有文件来判断目标文件是否存在,这在文件多的时候会非常慢。GCS提供了直接检查单个文件是否存在的方法,完全不需要遍历全量文件。
3. 异步操作未正确等待
user.recipe.forEach()里调用downloadImage是异步的,但你没有等待所有下载完成就直接返回了响应。不仅会导致文件还没下载完就返回,也无法保证异步操作的顺序和正确性。
修复后的代码
下面是修改后的核心代码,解决了上述所有问题:
const fs = require('fs'); const path = require('path'); const { Storage } = require('@google-cloud/storage'); const mongoose = require('mongoose'); const User = mongoose.model('Users'); const filePath = 'uploads'; // 确保uploads目录存在,避免下载时出错 if (!fs.existsSync(filePath)) { fs.mkdirSync(filePath); } const recipesReadList = async (req, res) => { getUser(req, res, async (req, res) => { try { const user = await User.findById(req.params.userid).select('recipe').exec(); if (user.recipe && user.recipe.length > 0) { // 用for...of循环等待所有下载完成,避免竞态 for (const recipe of user.recipe) { const localPath = path.join(filePath, recipe.imageName); if (!fs.existsSync(localPath)) { await downloadImage(recipe.imageName); } } return res.status(200).json(user.recipe); } else { return res.status(200).json([]); } } catch (err) { return res.status(500).json({ error: err.message }); } }); }; const getUser = (req, res, callback) => { if (req.payload && req.payload.email) { User.findOne({ email: req.payload.email }).exec((err, user) => { if (err || !user) { return res.status(401).json({ error: 'User not found' }); } callback(req, res, user.userName); }); } else { return res.status(401).json({ error: 'Invalid credentials' }); } }; const downloadImage = async (file) => { await downloadFromGCS(file); // 这里要await确保下载完成 }; const GCS_Storage = new Storage({ projectId: process.env.GCLOUD_PROJECT, keyFilename: path.join(__dirname, '../../', `${process.env.GCS_KEYFILE}`), }); async function downloadFromGCS(fileName) { const bucket = GCS_Storage.bucket(process.env.GCS_BUCKET); const remoteFile = bucket.file(fileName); // 直接检查文件是否存在,无需遍历全桶 const [exists] = await remoteFile.exists(); if (!exists) { console.log(`File not found on cloud storage: ${fileName}`); return; } const localPath = path.join(filePath, fileName); // 去掉全局变量,直接使用局部变量,避免竞态 await remoteFile.download({ destination: localPath }); console.log(`Successfully downloaded ${fileName} to ${localPath}`); } module.exports = { recipesReadList, };
关键修改点说明
- 移除全局变量:用局部变量
localPath和remoteFile代替全局的downloadFileName,彻底避免异步竞态。 - 高效检查文件存在:使用
remoteFile.exists()直接验证GCS上的文件,性能大幅提升。 - 正确等待异步操作:
- 将
recipesReadList改为async函数,用for...of循环遍历食谱列表,确保每一张图片下载完成后再处理下一张。 downloadImage里await downloadFromGCS(file),确保下载完成后再继续。
- 将
- 增加错误处理:添加
try/catch捕获异步操作中的错误,避免服务器崩溃。
验证下载完成的方法
- 用
await包裹所有异步下载操作,确保代码执行到返回响应时,所有文件都已经下载完成。 - 可以在
downloadFromGCS里添加日志(如示例中的console.log),观察每一个文件的下载完成状态。 - 也可以在返回响应前,检查本地文件的大小和修改时间,确认文件已正确写入。
内容的提问来源于stack exchange,提问作者JonW
相关产品推荐
相关产品推荐

