新手开发小游戏后端:Firebase RDS/Firestore购物品实现与并发安全咨询
Hey there! Let's break down your questions one by one since you're building a small game backend and new to Firebase databases—super exciting project, by the way!
For Firestore (NoSQL Document Database)
Firestore’s document-based structure fits game user data really well. Here’s how to structure the flow:
- Store each user’s data in a single document at
users/{userId}. The document should include:balance: A number field tracking the user’s current currencyinventory: A map (object) where keys are item IDs and values are the quantity of that item the user owns (e.g.,{"health_potion": 3, "sword": 1})
- Use Firebase Transactions to execute the purchase atomically. Transactions ensure that all reads and writes in the sequence happen as a single, indivisible operation—no partial updates if something goes wrong.
For RDS (Relational Database, e.g., Firebase Cloud SQL)
If you’re using a relational database, structure your tables to model the relationships clearly:
- A
userstable with columns:user_id(primary key),balance - An
inventorytable with columns:user_id(foreign key tousers),item_id,quantity(primary key onuser_id + item_id) - Use database transactions combined with conditional updates to handle the purchase. Instead of querying first then updating, you can bake the balance check directly into your UPDATE statement to avoid race conditions.
It depends on your long-term needs, but for a small game backend, Firestore is generally the more fitting choice—here’s why:
- Low operational overhead: Firestore is fully managed, so you don’t have to worry about server maintenance, scaling, or backups. Perfect for small teams or solo devs focused on building the game, not infrastructure.
- Flexible data model: The document structure makes it easy to evolve your user inventory (e.g., adding new item attributes later) without altering table schemas.
- Built-in concurrency handling: Transactions and optimistic locking are baked into Firestore, so you don’t have to reinvent the wheel for common game backend flows.
That said, RDS might make sense if you:
- Already have deep SQL experience and prefer working with relational models
- Plan to build complex analytics or reporting that relies on joins across multiple tables (e.g., tracking global item purchase trends)
The key here is to avoid splitting the "check balance → update balance" steps into separate operations—because concurrent requests could read the same balance before either has finished updating. Here’s how to fix it:
For Firestore
Use transactions to wrap the entire purchase logic. Firestore will automatically retry the transaction if it detects that the user’s document was modified during execution, ensuring the balance stays consistent. Example code (JavaScript):
const db = getFirestore(); const userRef = doc(db, 'users', userId); const itemPrice = 150; // Replace with your item's actual price const itemId = 'health_potion'; try { await runTransaction(db, async (transaction) => { const userDoc = await transaction.get(userRef); if (!userDoc.exists()) { throw new Error('User does not exist'); } const currentBalance = userDoc.data().balance; if (currentBalance < itemPrice) { throw new Error('Insufficient balance'); } // Update balance and increment item quantity in inventory const newInventory = { ...userDoc.data().inventory }; newInventory[itemId] = (newInventory[itemId] || 0) + 1; transaction.update(userRef, { balance: currentBalance - itemPrice, inventory: newInventory }); }); console.log('Purchase successful!'); } catch (e) { console.error('Purchase failed:', e.message); }
For RDS
Use a database transaction with a conditional UPDATE to ensure the balance is only deducted if it’s sufficient. This leverages the database’s row-level locking to prevent concurrent modifications. Example SQL (MySQL):
BEGIN TRANSACTION; -- Deduct balance only if user has enough UPDATE users SET balance = balance - ? WHERE user_id = ? AND balance >= ?; -- Check if the update was successful (should affect 1 row) SELECT ROW_COUNT() INTO @affected_rows; IF @affected_rows = 1 THEN -- Add item to inventory (or increment quantity if it already exists) INSERT INTO inventory (user_id, item_id, quantity) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE quantity = quantity + 1; COMMIT; ELSE -- Insufficient balance or user not found ROLLBACK; END IF;
By using these atomic operations, you eliminate the risk of users spending more than they have—even if multiple purchase requests hit your /BuyItem API at the exact same time.
内容的提问来源于stack exchange,提问作者Vladyslav Melnychenko




