Inventario sin vender de más
TeselaDB
Equipo de TeselaDB
30 minIntermedio
Descuenta el stock y crea el pedido en una sola transacción para que dos compras a la vez nunca vendan la misma unidad.
Cuando varios clientes compran el último producto a la vez, cada compra tiene que descontar el stock y crear su pedido sin pisar a las demás. Con una transacción multidocumento las dos escrituras ocurren juntas o no ocurre ninguna, tanto si el stock y los pedidos están en el mismo rango como si están en rangos de nodos distintos.
Lo que vas a construir
Una función placeOrder de Node.js que descuenta unidades solo si quedan y crea el pedido en la misma transacción.
Cinco compras a la vez de un producto con tres unidades dejan tres pedidos y el stock a cero.
Requisitos
- Node.js 18 o posterior con el paquete
mongodb. - Un clúster de TeselaDB y un usuario con
readWritesobre la baseshop. - La cadena de conexión en la variable de entorno
TESELADB_URI.
1. Descuenta con una condición
El filtro available: { $gte: quantity } hace que la actualización solo cambie el documento si quedan unidades
suficientes. Si no cambia nada, la función lanza un error y la transacción se deshace entera.
2. Escribe la compra en una transacción
withTransaction reintenta la transacción cuando choca con otra que tocó el mismo documento, así que no tienes que
escribir los reintentos. Guarda el programa como inventario.mjs:
import { MongoClient } from "mongodb";
const client = new MongoClient(process.env.TESELADB_URI);
const db = client.db("shop");
// Takes the units from the stock and writes the order in one transaction: both happen, or neither.
async function placeOrder(sku, quantity, customer) {
const session = client.startSession();
try {
return await session.withTransaction(async () => {
const taken = await db
.collection("stock")
.updateOne({ _id: sku, available: { $gte: quantity } }, { $inc: { available: -quantity } }, { session });
if (taken.modifiedCount === 0) throw new Error(`not enough stock for ${sku}`);
await db.collection("orders").insertOne({ sku, quantity, customer, createdAt: new Date() }, { session });
});
} finally {
await session.endSession();
}
}
await db.collection("stock").insertOne({ _id: "chair-red", available: 3 });
const customers = ["Ana", "Luis", "Marta", "Pablo", "Sara"];
const results = await Promise.allSettled(customers.map((customer) => placeOrder("chair-red", 1, customer)));
const placed = results.filter((result) => result.status === "fulfilled").length;
const { available } = await db.collection("stock").findOne({ _id: "chair-red" });
console.log(`${placed} orders placed, ${available} left`);
await client.close();Probarlo
Ejecuta node inventario.mjs contra una base vacía. Las cinco compras van a la vez, tres encuentran stock y dos fallan
con not enough stock:
3 orders placed, 0 leftLimpiar
db.getSiblingDB("shop").stock.deleteOne({ _id: "chair-red" });
db.getSiblingDB("shop").orders.deleteMany({ sku: "chair-red" });Desde mongosh, la primera orden borra el producto de prueba y la segunda borra sus pedidos.
Siguientes pasos
- Transacciones, con lo que garantiza el aislamiento por instantánea.
- Reintentos y errores transitorios, para escribir los reintentos a mano.