You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Firestore避免同一文档重复请求:React彩票应用并发抢票方案咨询

解决Firestore并发彩票分配的竞态问题

嘿,这个并发竞态问题我之前在类似的库存分配场景里碰过,给你梳理几个靠谱的解决方案,从基础到优化一步步来:

一、最可靠的基础方案:Firestore事务

首先明确:事务是解决这类“读取-修改”并发冲突的标准方案,Firestore的事务会自动处理冲突——如果事务执行期间目标文档被其他操作修改,它会自动重试,直到成功或达到重试上限,完全适配你的彩票分配场景。

具体实现思路很清晰:

  1. 在事务内查询一张isAvailable: true的彩票
  2. 找到后立即将其标记为isAvailable: false,同时记录分配信息
  3. 事务会确保这两步是原子性的,不会出现多个用户抢到同一张票的情况

给你一个React中用Firebase v9+的代码示例:

import { getFirestore, runTransaction, collection, query, where, limit } from "firebase/firestore";

const db = getFirestore();

async function assignLotteryTicket(userId) {
  try {
    const assignedTicket = await runTransaction(db, async (transaction) => {
      // 1. 查询可用彩票
      const availableTicketsQuery = query(
        collection(db, "lotteries"),
        where("isAvailable", "==", true),
        limit(1)
      );
      const querySnapshot = await transaction.get(availableTicketsQuery);

      if (querySnapshot.empty) {
        throw new Error("抱歉,当前没有可用彩票了");
      }

      const ticketDoc = querySnapshot.docs[0];
      // 2. 原子更新彩票状态
      transaction.update(ticketDoc.ref, {
        isAvailable: false,
        assignedTo: userId,
        assignedAt: new Date()
      });

      return { id: ticketDoc.id, ...ticketDoc.data() };
    });

    console.log("成功分配彩票:", assignedTicket);
    return assignedTicket;
  } catch (error) {
    console.error("分配失败:", error);
    throw error; // 抛给前端处理提示
  }
}

这个方案的优势是绝对保证数据一致性,不会出现重复分配。需要注意的点:

  • 默认重试次数是5次,如果并发极高导致多次重试失败,要在前端给用户提示“请稍后再试”
  • 事务会读取文档内容,几百级并发完全没问题,性能不会有明显瓶颈

二、更轻量的优化方案:条件式原子更新

如果你的业务逻辑不需要读取彩票的其他字段(只需要标记为已分配),可以用带条件的事务更新来减少不必要的读取开销。其实核心还是事务,但我们可以在更新时明确添加条件,确保只有当isAvailabletrue时才执行更新:

// 简化版的条件事务更新
async function assignLotteryTicket(userId) {
  let attemptCount = 0;
  const maxAttempts = 5;

  while (attemptCount < maxAttempts) {
    attemptCount++;
    try {
      const availableTicketsQuery = query(
        collection(db, "lotteries"),
        where("isAvailable", "==", true),
        limit(1)
      );
      const querySnapshot = await getDocs(availableTicketsQuery);

      if (querySnapshot.empty) throw new Error("无可用彩票");

      const ticketDoc = querySnapshot.docs[0];
      // 用事务执行带条件的更新
      await runTransaction(db, async (transaction) => {
        const doc = await transaction.get(ticketDoc.ref);
        if (!doc.data().isAvailable) {
          throw new Error("彩票已被分配"); // 触发事务重试
        }
        transaction.update(ticketDoc.ref, {
          isAvailable: false,
          assignedTo: userId,
          assignedAt: new Date()
        });
      });

      return { id: ticketDoc.id, ...ticketDoc.data() };
    } catch (error) {
      if (error.message !== "彩票已被分配") throw error;
      // 否则继续重试
    }
  }

  throw new Error("多次尝试分配失败,请稍后再试");
}

这种方式比全量读取的事务更高效,因为只有当文档状态符合条件时才会执行更新。

三、高并发场景进阶优化

如果你的并发量会突破上千级,或者想进一步降低Firestore的压力,可以考虑这些方案:

  • 预分配彩票池:提前把一批可用彩票(比如100张)标记为pending状态,存到一个单独的子集合或字段中。用户请求时直接从这个池子里取,取完后再异步补充新的可用彩票到池子里。这样能减少全局查询的冲突概率。
  • 云函数托管分配逻辑:把分配代码放到Firebase云函数中,前端只需要调用云函数接口。云函数的重试机制更稳定,还能做一些限流、熔断的处理,避免前端直接压Firestore。
  • 避免全局查询:如果彩票可以按批次划分,可以让用户随机请求某一批次的彩票,分散查询压力,减少冲突。

总结

  • 对于你当前几百级的并发,Firestore事务完全够用,代码简单可靠,是首选方案。
  • 如果想优化性能,用带条件的事务更新能减少不必要的开销。
  • 更高并发场景下,预分配或云函数能进一步提升系统稳定性。

内容的提问来源于stack exchange,提问作者Crowd Store

火山引擎 最新活动