Next.js
قراءة في 9 دقائقمنع حالات السباق في متجر Next.js باستخدام Prisma وPostgreSQL
استكشف تحديث المخزون الذري وقيود قاعدة البيانات والمعاملات ومقاومة تكرار الطلبات عبر عملية شراء واقعية.

قد تبدو عملية الدفع صحيحة في اختبار مستخدم واحد، ومع ذلك تبيع مخزونًا زائدًا عند وصول طلبات متزامنة. الحل هو نقل الثوابت المهمة إلى عمليات ذرية وقيود قاعدة البيانات بدل الاعتماد على تسلسل قراءة ثم كتابة في كود التطبيق.
قد تبدو عملية الدفع صحيحة في اختبار مستخدم واحد، ومع ذلك تبيع مخزونًا زائدًا عند وصول طلبات متزامنة. الحل هو نقل الثوابت المهمة إلى عمليات ذرية وقيود قاعدة البيانات بدل الاعتماد على تسلسل قراءة ثم كتابة في كود التطبيق.
فهم حالة السباق
لنفترض أن آخر قطعة متاحة ومخزونها 1. تقرأ عمليتان القيمة نفسها، وتعتقد كلتاهما أن القطعة متاحة، ثم تنشئان طلبين. يحدث السباق لأن القراءة والإنقاص عمليتان منفصلتان دون ضمان تزامن مشترك.
اجعل التحديث ذريًا
import { Prisma } from '@prisma/client';
export async function reserveStock(productId: string, quantity: number) {
if (!Number.isInteger(quantity) || quantity < 1) throw new Error('Invalid quantity');
return prisma.$transaction(async (tx) => {
const result = await tx.product.updateMany({
where: { id: productId, stock: { gte: quantity } },
data: { stock: { decrement: quantity } },
});
if (result.count !== 1) throw new Error('Insufficient stock');
return tx.order.create({
data: { status: 'PENDING', items: { create: { productId, quantity } } },
});
}, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
}استخدم تحديثًا ذريًا مشروطًا: لا تُنقص المخزون إلا إذا كان لا يزال أكبر من أو يساوي الكمية المطلوبة. افحص عدد الصفوف المتأثرة؛ الصفر يعني فشل الحجز. ضع حجز المخزون وإنشاء الطلب داخل معاملة واحدة حتى يتم التراجع عنهما معًا عند الخطأ.
العزل وإعادة المحاولة
يمكن لمستوى Serializable اكتشاف المعاملات المتعارضة لكنه قد يلغي إحداها. يجب أن يعالج كود الإنتاج أخطاء التسلسل بإعادة محاولة محدودة. يعتمد مستوى العزل المناسب على نموذج البيانات وقاعدة البيانات؛ وقد يكفي التحديث الذري المشروط والقيود لقواعد المخزون البسيطة.
أبقِ الدفع خارج المعاملات الطويلة
لا تترك معاملة قاعدة البيانات مفتوحة أثناء انتظار مزود دفع بعيد. أنشئ طلبًا بحالة PENDING ثم ابدأ الدفع، وبعدها تحقق من رد المزود على الخادم. لا تثق بالرد قبل التحقق من التوقيع والمبلغ والعملة ومرجع الطلب.
اجعل الإشعارات المتكررة آمنة
export async function confirmPayment(providerEventId: string, orderId: string) {
return prisma.$transaction(async (tx) => {
// providerEventId should have a UNIQUE constraint in the database.
const event = await tx.paymentEvent.create({
data: { providerEventId, orderId },
});
await tx.order.update({
where: { id: event.orderId },
data: { status: 'PAID' },
});
});
}قد يعيد مزود الدفع إرسال الإشعارات. خزّن معرف حدث فريدًا أو مفتاح idempotency واجعل المعالجة المتكررة آمنة. القيد الفريد أقوى من أسلوب «تحقق ثم أدرج» لأن طلبين متزامنين قد ينجحان في التحقق معًا.
التخطيط للأعطال الجزئية
حدد ما يحدث إذا نجح الدفع ثم انتهت مهلة الخادم قبل تسجيل النتيجة، أو إذا فشل الدفع بعد حجز المخزون. قد يحتاج التصميم المتين إلى انتهاء صلاحية الحجز ومهمة مطابقة وانتقالات حالة واضحة مثل PENDING وPAID وFAILED وCANCELLED.
اختبار الثوابت
اختبر عمليتي شراء متزامنتين لآخر قطعة والإشعارات المكررة وانتهاء مهلة المزود وإعادة محاولة المعاملة وفشل إنشاء الطلب. تحقق من ثوابت قاعدة البيانات بعد كل اختبار، لا من رمز HTTP فقط.