You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何无法同时将多个用户添加至Firebase Cloud Firestore队列?

问题分析与解决方法

你的核心问题是并发更新时队列被覆盖,原代码用setDoc直接替换整个队列,就算用了snapshot监听,客户端的users状态也可能不是最新的服务器数据,多个用户同时操作时就会互相覆盖。

原代码的问题点

  • 直接基于客户端本地的users状态拼接新队列,并发时不同客户端的本地状态可能都没拿到对方刚更新的数据,导致后提交的覆盖先提交的。
  • localStorage.setItem在更新Firestore之前执行,就算Firestore更新失败,客户端也会认为自己已经加入队列,状态不一致。

正确的实现方式(用事务保证原子性)

把addUser和队列更新逻辑改成事务操作,事务会确保读取的是最新的服务器数据,更新时如果数据已被修改会自动重试,彻底解决并发覆盖问题:

const [users, setUsers] = useState([]);
const [name, setName] = useState("");

useEffect(() => {
    const fetchQueue = async () => {
        const db = getFirestore();
        const usersRef = doc(db, "autocommitQueue", "users");

        const unsubscribe = onSnapshot(usersRef, (doc) => {
            if (doc.exists()) {
                setUsers(doc.data().queue || []);
            }
        });

        return () => unsubscribe();
    };

    fetchQueue();
}, []);

const addUser = async () => {
    if (!name.trim()) return;
    if (localStorage.getItem("id")) return;

    const db = getFirestore();
    const usersRef = doc(db, "autocommitQueue", "users");
    const userId = uuid4();
    const newUser = { name: name.trim(), id: userId };

    try {
        await runTransaction(db, async (transaction) => {
            const docSnapshot = await transaction.get(usersRef);
            let currentQueue = [];
            if (docSnapshot.exists()) {
                currentQueue = docSnapshot.data().queue || [];
            }
            // 检查是否已经存在相同id的用户(可选,防止重复)
            const userExists = currentQueue.some(user => user.id === userId);
            if (userExists) {
                throw new Error("用户已在队列中");
            }
            transaction.set(usersRef, { queue: [...currentQueue, newUser] }, { merge: true });
        });
        // 只有事务成功后,才存储id到localStorage
        localStorage.setItem("id", userId);
        alert("加入队列成功");
    } catch (error) {
        console.error("加入队列失败:", error);
        alert("加入队列失败,请重试");
    }
};

return(
    <Box
        sx={{
            display: localStorage.getItem("id") !== null ? "none" : "flex",
            alignItems: "center",
        }}
    >
        <TextField
            type="text"
            id="outlined-number"
            label="Name"
            onChange={(event) => setName(event.target.value)}
        />
        <Button
            onClick={addUser}
            variant="outlined"
            sx={{ "&.MuiButton-root": { height: "56px" } }}
        >
            Push
        </Button>
    </Box>
)

关键改进点

  • 用runTransaction包裹队列更新逻辑,事务中读取最新的服务器端队列数据,修改后提交,确保并发操作时不会互相覆盖。
  • 把localStorage.setItem放在事务成功之后执行,避免客户端状态和服务器不一致。
  • 添加了用户重复检查(可选),防止同一个用户重复加入。
  • 增加了错误处理,提示用户操作结果。

为什么之前的事务没生效?

大概率是你之前的事务实现逻辑有问题,比如没有在事务内部读取最新数据,或者用了setDoc而不是基于事务快照的更新,导致事务没有起到原子性保障的作用。上面的代码严格遵循了Firestore事务的使用规范,能解决并发更新冲突。

内容的提问来源于stack exchange,提问作者Игорь Кеся

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 04:20:56