المستوى: مبتدئ
لو موقع حجز التذاكر بتاعك سمح لاتنين عملاء يدفعوا فلوس على نفس المقعد، انت ما عملتش حاجة غلط في الـ payment gateway. المشكلة في 3 أسطر كود بسيط بيشغّل اسمها Race Condition، ولو ما اتعرّفتش عليها من بدري، هتلاقي نفسك بترجّع فلوس بعد كل launch.
Race Condition: لما الكود يكون صح والسلوك غلط
Race Condition هي حالة بتحصل لما اتنين أو أكتر من الطلبات بيشتغلوا على نفس البيانات في نفس اللحظة، والنتيجة النهائية بتعتمد على ترتيب التنفيذ — ترتيب انت أصلاً مش متحكم فيه. الاسم جاي من فكرة إن الطلبات بتتسابق على نفس المورد، واللي يوصل الأول بيكسب.
المشكلة باختصار — مثال السينما
تخيّل سينما فيها مقعد واحد فاضي في الصف الأول. وحيد فتح الموقع، شاف المقعد متاح، وضغط زرار "احجز". في نفس الميلي ثانية، سارة على لاب توب تاني فتحت نفس الصفحة، شافت نفس المقعد متاح، وضغطت "احجز". الـ backend بتاعك استلم الطلبين بفارق 2 ميلي ثانية بينهم.
الكود بتاعك بيشيك المقعد، بيلاقيه متاح في الطلبين، فيوافق على الاتنين. وحيد دفع، سارة دفعت، السينما عندها مقعد واحد بحجزين، وانت عندك مكالمة دعم فني صعبة الصبح.
ده مش bug في الـ DB، وده مش مشكلة في الـ payment gateway. ده bug في طريقة تفكيرك في الزمن لمّا تكتب الكود.
الكود اللي بيخلق المشكلة
الـ pattern الشائع وله اسم رسمي: Check-Then-Act. انت بتشيك أولاً، وبعدين بتعمل action على أساس الشيك. المشكلة إن في وقت ما بين الـ check والـ act، حد تاني ممكن يكون غيّر الواقع من تحت رجلك.
// Node.js + PostgreSQL — كود فيه bug خفي
async function bookSeat(seatId, userId) {
// 1) Check
const result = await db.query(
'SELECT is_booked FROM seats WHERE id = $1',
[seatId]
);
if (result.rows[0].is_booked) {
return { error: 'المقعد محجوز' };
}
// 2) Act — في الفترة بين الـ SELECT والـ UPDATE،
// ممكن يكون حد تاني خلّص الـ UPDATE بتاعه!
await db.query(
'UPDATE seats SET is_booked = true, user_id = $1 WHERE id = $2',
[userId, seatId]
);
return { success: true };
}الكود ده هيشتغل ١٠٠٪ صح في الاختبار المحلي. هتفتح Postman وتبعت ١٠٠ طلب واحد ورا التاني، وهتلاقي السلوك سليم. لكن أول ما اتنين users يضغطوا في نفس اللحظة الفعلية، الـ DB بترد على الاتنين بـ is_booked = false، الاتنين بيدخلوا في الـ UPDATE، والمقعد يتحجز مرتين.
ليه ده بيحصل أصلاً؟
فيه فهم خاطئ شائع بين المبتدئين: ناس بتفتكر إن الـ DB بينفّذ طلب واحد بعد التاني، الواحد بيخلص ثم التاني يبدأ. الحقيقة إن PostgreSQL أو MySQL بيشغّلوا عشرات أو مئات الـ queries بالتوازي على connections مختلفة، كل واحد على thread لوحده. الـ DB لازم يكون كده عشان يخدم 1,000 طلب/ثانية بدل 50.