如何在React+Firestore中防止并发选课并处理名额可用性
选课系统并发名额控制问题解决
问题背景
开发支持用户选修课程的Web应用,每门选修课设有固定名额限制,但遇到并发场景下的名额管控失效问题:
- 仅剩1个名额时,多名用户可同时完成选课
- 无剩余名额时,用户仍能成功选课
当前选课处理代码:
const handleApprove = async () => { if (isEnrolled || isConfirming) { return; } setIsConfirming(true); const shouldEnroll = window.confirm("Are you sure you want to enroll?"); setIsConfirming(false); if (shouldEnroll) { try { const firestore = firebase.firestore(); const electiveRef = firestore.collection("electives").doc(project.id); const userRef = firestore.collection("users").doc(user.uid); await firestore.runTransaction(async (transaction) => { const electiveDoc = await transaction.get(electiveRef); if (!electiveDoc.exists) { throw new Error("Elective document does not exist!"); } const electiveData = electiveDoc.data(); const updatedSlots = electiveData.slots - 1; if (updatedSlots >= 0) { const updatedElectiveDoc = await transaction.get(electiveRef); const updatedElectiveData = updatedElectiveDoc.data(); const remainingSlots = updatedElectiveData.slots; if (remainingSlots >= 0) { transaction.update(electiveRef, { slots: updatedSlots }); await userRef.update({ isEnroll: true, elective: project.name, subjectCode: project.details, }); logout(); setIsEnrolled(true); } else { alert("No available slots for enrollment in this elective."); setIsEnrolled(false); // Reset isEnrolled if enrollment fails } } else { alert("No available slots for enrollment."); setIsEnrolled(false); // Reset isEnrolled if there are no available slots } }); } catch (error) { console.error("Error enrolling:", error); alert("An error occurred while enrolling. Please try again."); setIsEnrolled(false); // Reset isEnrolled if enrollment fails } } };
核心需求
- 并发操作场景下,仅剩1个名额时仅允许一名用户完成选课
- 无剩余名额时彻底阻止用户选课操作
问题分析
原代码虽使用Firestore事务,但存在多个逻辑错误导致失效:
- 事务内重复获取课程文档,属于冗余操作且干扰判断逻辑
- 名额判断顺序错误:先扣减名额再检查是否合法,应先确认剩余名额>0再执行扣减
- 用户数据更新未通过事务管控,会出现事务回滚但用户状态已修改的不一致情况
- 事务内部执行
alert、setState等UI操作,事务重试时会重复触发这些行为
解决方案
修正后的代码如下:
const handleApprove = async () => { if (isEnrolled || isConfirming) { return; } setIsConfirming(true); const shouldEnroll = window.confirm("Are you sure you want to enroll?"); setIsConfirming(false); if (!shouldEnroll) return; try { const firestore = firebase.firestore(); const electiveRef = firestore.collection("electives").doc(project.id); const userRef = firestore.collection("users").doc(user.uid); await firestore.runTransaction(async (transaction) => { // 获取课程最新数据 const electiveDoc = await transaction.get(electiveRef); if (!electiveDoc.exists) { throw new Error("Elective document does not exist!"); } const electiveData = electiveDoc.data(); const currentSlots = electiveData.slots; // 先检查是否还有可用名额 if (currentSlots <= 0) { throw new Error("No available slots for enrollment."); } // 原子性更新课程名额 transaction.update(electiveRef, { slots: currentSlots - 1 }); // 通过事务更新用户选课状态,确保数据一致性 transaction.update(userRef, { isEnroll: true, elective: project.name, subjectCode: project.details, }); }); // 事务成功后再执行UI更新和后续操作 logout(); setIsEnrolled(true); alert("Enrollment successful!"); } catch (error) { console.error("Error enrolling:", error); // 根据错误类型给出精准提示 const errorMsg = error.message.includes("No available slots") ? "No available slots for enrollment." : "An error occurred while enrolling. Please try again."; alert(errorMsg); setIsEnrolled(false); } };
关键优化点
- 简化事务逻辑:移除重复的文档获取操作,仅在事务初始化时获取一次课程数据
- 修正名额判断逻辑:先验证当前剩余名额是否合法,再执行扣减操作,避免出现负数名额
- 统一事务管控:用户数据更新通过
transaction.update执行,确保课程名额与用户状态修改的原子性 - 分离事务与UI操作:将弹窗、状态更新、退出登录等操作移至事务成功后执行,避免事务重试时重复触发
- 精准错误处理:根据错误信息分类给出提示,提升用户体验
内容的提问来源于stack exchange,提问作者Gokz
相关产品推荐
相关产品推荐

