
- بواسطة x32x01 ||
فاكرين مشهد السينما لما كل الناس بتسحب فلوس من نفس الحساب في نفس اللحظة؟
ده مش بس مضحك - ده مثال درامي على نوع من الأخطاء اللي بتظهر في تطبيقات الويب والتطبيقات التجارية اسمه Race Condition.
وأكيد في مواقع eCommerce أو أنظمة الدفع ده ممكن يسبب خسائر حقيقية.
في البوست ده هنعرف الثغرة بالتفصيل، هنعرض أمثلة واقعية، هنشوف كود توضيحي حساس (كيفية حصول المشكلة) وبعدها ندي حلول عملية وآمنة علشان تحمي مشروعك.
يعني إيه Race Condition ببساطة ؟
Race Condition ببساطة بتحصل لما اتنين أو أكتر من العمليات (requests) يحاولوا يوصلوا لنفس المورد أو نفس المتغير في نفس الوقت، والنتيجة بتعتمد على مين وصل الأول. في عالم الـ eCommerce ده ممكن يطلع عن طريق:
النتيجة: بيانات مضروبة (inconsistent) - يعني ممكن تبيع منتج أكثر من الكمية المتوفرة، أو يحصل سحب مزدوج من نفس الحساب.
مثال واقعي: مشكلة الـ Inventory في موقع بيع
تخيل صفحة المنتج فيها 1 قطعة بس متاحة. اتنين مستخدمين ضغطوا زر "اشترِ الآن" في نفس الوقت. لو الكود بيعمل خطوة بسيطة زي:
لو الاتنين نفذوا خطوة 1 في نفس الوقت، الاتنين هيرجعوا قيمة 1، وكل واحد هيحسب ويخزن 0 - والنتيجة إنه اتباع المنتج مرتين رغم وجود قطعة واحدة بس.
كود توضيحي (مثال ضعيف - فيه Race Condition)
ده مثال مبسط ببايثون + Flask مع قاعدة SQLite (فقط للتوضيح):
لو تعمل requests متزامنة جداً هتلاقي إن الاتنين ممكن يرجعوا نجاح - لأن مفيش قفل أو معاملة Atomic هنا.
إزاي نحل المشكلة؟ استراتيجيات عملية
فيه حلول متعددة - والاختيار منهم بيعتمد على البنية اللي عندك (قاعدة بيانات، لغة، حجم الترافيك):
في بايثون (psycopg2 مثال PostgreSQL):
الـ FOR UPDATE بيضمن إن أي عملية تانية تحاول تقرأ الصف ده هتنتظر لحد ما المعاملة تخلص - وده يمنع الـ Race Condition على مستوى الصف.
أو استخدم SETNX/GETSET مع منطق مناسب أو الـ RedLock للقفل الموزع.
لو المعيار نصيب، بتقدر تعمل retry بعد إعادة قراءة القيمة.
كود مصحح (بايثون + sqlalchemy مثال بسيط مع transaction)
اختيارات عملية حسب الحالة - إيه تختار؟
ازاي تختبر وتكشف Race Conditions؟
الخلاصة - الكلام المهم اللي لازم تفتكره
ده مش بس مضحك - ده مثال درامي على نوع من الأخطاء اللي بتظهر في تطبيقات الويب والتطبيقات التجارية اسمه Race Condition.
وأكيد في مواقع eCommerce أو أنظمة الدفع ده ممكن يسبب خسائر حقيقية.
في البوست ده هنعرف الثغرة بالتفصيل، هنعرض أمثلة واقعية، هنشوف كود توضيحي حساس (كيفية حصول المشكلة) وبعدها ندي حلول عملية وآمنة علشان تحمي مشروعك.
يعني إيه Race Condition ببساطة ؟
Race Condition ببساطة بتحصل لما اتنين أو أكتر من العمليات (requests) يحاولوا يوصلوا لنفس المورد أو نفس المتغير في نفس الوقت، والنتيجة بتعتمد على مين وصل الأول. في عالم الـ eCommerce ده ممكن يطلع عن طريق:- عمليتين شراء بتحاول تخصم نفس المنتج من الـ inventory في نفس اللحظة.
- عمليتين سحب فلوس على نفس الحساب في آن واحد.
- أو عملية زيادة أو نقصان في رصيد من غير قفل مناسب على العملية.
النتيجة: بيانات مضروبة (inconsistent) - يعني ممكن تبيع منتج أكثر من الكمية المتوفرة، أو يحصل سحب مزدوج من نفس الحساب.
مثال واقعي: مشكلة الـ Inventory في موقع بيع
تخيل صفحة المنتج فيها 1 قطعة بس متاحة. اتنين مستخدمين ضغطوا زر "اشترِ الآن" في نفس الوقت. لو الكود بيعمل خطوة بسيطة زي:
- يقرأ الكمية من قاعدة البيانات (quantity = 1)
- يقللها (quantity = quantity - 1)
- يحفظ القيمة الجديدة
لو الاتنين نفذوا خطوة 1 في نفس الوقت، الاتنين هيرجعوا قيمة 1، وكل واحد هيحسب ويخزن 0 - والنتيجة إنه اتباع المنتج مرتين رغم وجود قطعة واحدة بس.
كود توضيحي (مثال ضعيف - فيه Race Condition)
ده مثال مبسط ببايثون + Flask مع قاعدة SQLite (فقط للتوضيح):
Python:
# vulnerable_app.py (للتوضيح فقط)
from flask import Flask, request, jsonify
import sqlite3
app = Flask(__name__)
def get_db():
conn = sqlite3.connect('shop.db')
return conn
@app.route('/buy', methods=['POST'])
def buy():
product_id = request.json['product_id']
conn = get_db()
cur = conn.cursor()
# 1. قراءة الكمية
cur.execute("SELECT quantity FROM products WHERE id = ?", (product_id,))
row = cur.fetchone()
if not row or row[0] <= 0:
return jsonify({"status": "sold_out"}), 400
# 2. خصم الكمية
new_q = row[0] - 1
cur.execute("UPDATE products SET quantity = ? WHERE id = ?", (new_q, product_id))
conn.commit()
conn.close()
return jsonify({"status": "ok", "remaining": new_q})
إزاي نحل المشكلة؟ استراتيجيات عملية
فيه حلول متعددة - والاختيار منهم بيعتمد على البنية اللي عندك (قاعدة بيانات، لغة، حجم الترافيك):
1. استخدم Transactions + Row-level Locking (أفضل حل لقواعد بيانات SQL)
لو قاعدة البيانات بتدعم SELECT ... FOR UPDATE أو قفل الصف، استخدمه داخل معاملة: SQL:
BEGIN;
SELECT quantity FROM products WHERE id = 1 FOR UPDATE;
-- لو quantity > 0:
UPDATE products SET quantity = quantity - 1 WHERE id = 1;
COMMIT;
في بايثون (psycopg2 مثال PostgreSQL):
Python:
conn = psycopg2.connect(...)
cur = conn.cursor()
conn.autocommit = False
cur.execute("SELECT quantity FROM products WHERE id = %s FOR UPDATE", (product_id,))
q = cur.fetchone()[0]
if q > 0:
cur.execute("UPDATE products SET quantity = quantity - 1 WHERE id = %s", (product_id,))
conn.commit()
else:
conn.rollback()
2. استخدم Atomic Operations في قواعد NoSQL أو Redis
لو بتستخدم Redis، استعمل أوامر ذرية زي DECR أو INCRBY أو Lua scripts اللي بتتنفذ كعملية واحدة: Bash:
# مثال Redis atomic decrement
EVAL "local q = tonumber(redis.call('GET', KEYS[1])); if q > 0 then redis.call('DECR', KEYS[1]); return 1; else return 0; end" 1 product:1:qty
3. Optimistic Locking (بتستخدم نسخة/توقيع للسطر)
أضف حقل version أو row_version. كل عملية تقرأ النسخة الحالية وترسل التحديث مع شرط WHERE version = previously_read_version، لو عدد الأسطر اللي اتحدثت كان 0 يبقى حد تاني عمل update قبلنا - نكرر العملية أو نبلغ المستخدم. SQL:
UPDATE products SET quantity = quantity - 1, version = version + 1
WHERE id = 1 AND version = 5;
4. Idempotency Tokens لطلبات الدفع/سحب الأموال
للعمليات اللي ما ينفعش تتكرر (زي المدفوعات)، اطلبidempotency_key
من العميل. الخادم يحتفظ بسجل للـ key والنتيجة، لو اتكررت نفس الطلب بنفس الـ key يرجع نفس النتيجة بدل ما يعيد التنفيذ.5. Queueing: حول العمليات الحرجة لطابور (Worker Queue)
بدل ما تعالج الطلب فورا، اضفه لطابور (مثلاً RabbitMQ أو AWS SQS). Worker واحد أو مجموعة من الـ workers هتعالج العناصر بالتتابع، وده يمنع التداخل في عملية التعديل على نفس المورد.6. Database Constraints كخط دفاع أخير
حتى مع كل الحلول فوق، ثبت قواعد في قاعدة البيانات: مثلاًCHECK (quantity >= 0)
وUNIQUE
أو FOREIGN KEY
والـ triggers اللي تمنع القيم الخاطئة. القاعدة الجيدة: التحقق على مستوى الـ DB ليكون آخر خط دفاع.كود مصحح (بايثون + sqlalchemy مثال بسيط مع transaction)
Python:
from flask import Flask, request, jsonify
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Product # افترض وجود موديل SQLAlchemy
app = Flask(__name__)
engine = create_engine('postgresql://user:pass@localhost/shop')
Session = sessionmaker(bind=engine)
@app.route('/buy', methods=['POST'])
def buy():
product_id = request.json['product_id']
session = Session()
try:
# استخدم row-level lock بإرسال with_for_update=True
product = session.query(Product).with_for_update().filter_by(id=product_id).one()
if product.quantity <= 0:
session.rollback()
return jsonify({"status": "sold_out"}), 400
product.quantity -= 1
session.commit()
return jsonify({"status": "ok", "remaining": product.quantity})
except:
session.rollback()
raise
finally:
session.close()
اختيارات عملية حسب الحالة - إيه تختار؟
- لو شغلك على SQL DB وبتحتاج دقة عالية: استخدم transactions + SELECT FOR UPDATE.
- لو عندك نظام توزيع عالي (microservices) وتحتاج أداء: استخدم Redis atomic ops + idempotency أو queueing.
- لو التطبيق بيتعرض لطلبات متوازية كتير: احنم النظام بـ rate limiting وقدّم تجربة للمستخدم (notify when stock low).
- دائمًا ضيف مراقبة (monitoring) واختبارات تحميل (load testing) لمحاكاة الحالات المتزامنة قبل الإنتاج.
ازاي تختبر وتكشف Race Conditions؟
- اعمل اختبارات concurrent (مثلاً باستخدام ab, wrk, أو سكريبتات بايثون Threading) وحاول ترسل 100-1000 request متزامن على نفس المورد.
- راقب سجلات DB، راقب الأخطاء، وعدد المبيعات الفائتة.
- اختبر الحلول الخاصة بيك: transactions & locks - وخلي الاختبارات جزء من CI/CD.
الخلاصة - الكلام المهم اللي لازم تفتكره
- Race Condition حقيقة وممكن تسبب خسائر حقيقية في مواقع eCommerce والأنظمة المالية.
- المبدأ هو إن العمليات المتوازية بتحتاج آليات تزامن: سواء كانت قفل صف، عمليات ذرية، idempotency tokens، أو queueing.
- الحل الأمثل بيختلف حسب بنيتك: DB، حجم الترافيك، ونوع العملية.
- دايمًا اختبر في بيئة محكمة، وحط حماية على مستوى التطبيق وقاعدة البيانات معًا.

👆 أضغط على الصورة لمشاهدة الفيديو 👆
التعديل الأخير: