手动轮询MUX API重复获取asset致Firestore重复写入问题
问题根因
重复写入Firestore的核心原因是异步轮询、数据库写入的副作用逻辑直接写在了组件渲染函数体中,没有做幂等控制,两个触发点会导致逻辑重复执行:
- Next.js开发环境默认开启React严格模式,组件首次挂载时会执行两次渲染
- 你配置的SWR设置了5秒自动轮询刷新upload数据,每次重渲染只要满足
isUploading && upload && upload.asset_id的判断条件,就会新建一个轮询任务,等asset状态变为ready时就会重复触发Firestore写入。你代码中预留的console.count("counter")打印的计数值,会和最终重复写入的数据条数完全对应。
排查思路
- 查看控制台
counter的输出值,确认组件重渲染次数和重复写入条数是否匹配 - 在Firestore写入逻辑前加唯一标识日志,确认多次触发时对应的
asset_id、upload_id完全一致,排除MUX接口返回重复数据的可能 - 检查状态更新时机:原代码在调用
addDoc后立即执行setIsUploading(false),没有等待写入完成,状态更新生效前的重渲染会再次进入判断分支启动新的轮询任务。
解决方案
1. 加幂等锁避免重复执行
用ref存储已处理的uploadID标记,同一个上传任务的轮询、写入逻辑只允许执行一次:
首先在组件其他useRef声明的同级位置,新增一个标记ref:
const processedUploadRef = useRef(null);
将原来写在渲染体中的轮询、写入逻辑增加判断,已处理过的任务直接跳过,同时调整状态更新时机,等Firestore写入完成后再修改上传状态:
if (isUploading && upload && upload.asset_id && processedUploadRef.current !== upload.id) { // 提前打上处理标记,阻断后续重渲染重复进入 processedUploadRef.current = upload.id; const assetPromise = getReadyAssetPromise(upload); const colRef = collection(db, "Videos"); assetPromise.then((asset) => { addDoc(colRef, { description: description, tags: tags.split(","), title: title, duration: asset.duration, thumb_url: `https://image.mux.com/${asset.playback_ids[0].id}/thumbnail.png`, vid_asset_id: upload.asset_id, vid_playback_id: asset.playback_ids[0].id, vid_url: `https://stream.mux.com/${asset.playback_ids[0].id}.m3u8`, vid_tier: "free", created_at: serverTimestamp() }).then(() => { // 写入成功后再更新状态,避免提前更新导致的逻辑异常 setIsUploading(false); setIsPreparing(false); showToast(); // 重置标记位,支持下次上传 processedUploadRef.current = null; }); }); }
2. 将副作用逻辑迁移到useEffect
不要把异步副作用直接写在渲染函数体中,用useEffect包裹相关逻辑,明确依赖项,仅当依赖变化时才执行逻辑,从根源上避免每次渲染都触发任务:
useEffect(() => { // 不满足条件直接退出 if (!isUploading || !upload || !upload.asset_id || processedUploadRef.current === upload.id) { return; } processedUploadRef.current = upload.id; const colRef = collection(db, "Videos"); getReadyAssetPromise(upload).then((asset) => { addDoc(colRef, { description: description, tags: tags.split(","), title: title, duration: asset.duration, thumb_url: `https://image.mux.com/${asset.playback_ids[0].id}/thumbnail.png`, vid_asset_id: upload.asset_id, vid_playback_id: asset.playback_ids[0].id, vid_url: `https://stream.mux.com/${asset.playback_ids[0].id}.m3u8`, vid_tier: "free", created_at: serverTimestamp() }).then(() => { setIsUploading(false); setIsPreparing(false); showToast(); processedUploadRef.current = null; }); }); // 将逻辑中用到的所有状态加入依赖数组 }, [isUploading, upload, title, description, tags]);
修改完成后可以删掉原来渲染体中直接写的那段轮询写入逻辑。
3. 长期可靠性优化
- 前端轮询方案可靠性差,页面关闭、网络异常都可能导致漏写,建议替换为MUX的Webhook回调机制,在后端服务接收
asset.ready事件后再写入数据库,从架构层面避免前端状态导致的异常 - 给Firestore的
vid_asset_id字段添加唯一索引,从数据库层面做兜底,就算前端逻辑出现异常,也不会写入重复的视频记录。
内容的提问来源于stack exchange,提问作者Yihan Duan
相关产品推荐
相关产品推荐

