Race Condition: شرح الثغرة وحلول عملية سريعة!

x32x01
  • بواسطة x32x01 ||
فاكرين مشهد السينما لما كل الناس بتسحب فلوس من نفس الحساب في نفس اللحظة؟
ده مش بس مضحك - ده مثال درامي على نوع من الأخطاء اللي بتظهر في تطبيقات الويب والتطبيقات التجارية اسمه Race Condition.

وأكيد في مواقع eCommerce أو أنظمة الدفع ده ممكن يسبب خسائر حقيقية.

في البوست ده هنعرف الثغرة بالتفصيل، هنعرض أمثلة واقعية، هنشوف كود توضيحي حساس (كيفية حصول المشكلة) وبعدها ندي حلول عملية وآمنة علشان تحمي مشروعك.

يعني إيه Race Condition ببساطة ؟ 🏁

Race Condition ببساطة بتحصل لما اتنين أو أكتر من العمليات (requests) يحاولوا يوصلوا لنفس المورد أو نفس المتغير في نفس الوقت، والنتيجة بتعتمد على مين وصل الأول. في عالم الـ eCommerce ده ممكن يطلع عن طريق:
  • عمليتين شراء بتحاول تخصم نفس المنتج من الـ inventory في نفس اللحظة.
  • عمليتين سحب فلوس على نفس الحساب في آن واحد.
  • أو عملية زيادة أو نقصان في رصيد من غير قفل مناسب على العملية.

النتيجة: بيانات مضروبة (inconsistent) - يعني ممكن تبيع منتج أكثر من الكمية المتوفرة، أو يحصل سحب مزدوج من نفس الحساب.



مثال واقعي: مشكلة الـ Inventory في موقع بيع 🎯


تخيل صفحة المنتج فيها 1 قطعة بس متاحة. اتنين مستخدمين ضغطوا زر "اشترِ الآن" في نفس الوقت. لو الكود بيعمل خطوة بسيطة زي:
  1. يقرأ الكمية من قاعدة البيانات (quantity = 1)
  2. يقللها (quantity = quantity - 1)
  3. يحفظ القيمة الجديدة

لو الاتنين نفذوا خطوة 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})
لو تعمل requests متزامنة جداً هتلاقي إن الاتنين ممكن يرجعوا نجاح - لأن مفيش قفل أو معاملة Atomic هنا.



إزاي نحل المشكلة؟ استراتيجيات عملية 🔐


فيه حلول متعددة - والاختيار منهم بيعتمد على البنية اللي عندك (قاعدة بيانات، لغة، حجم الترافيك):

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()
الـ FOR UPDATE بيضمن إن أي عملية تانية تحاول تقرأ الصف ده هتنتظر لحد ما المعاملة تخلص - وده يمنع الـ Race Condition على مستوى الصف.



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
أو استخدم SETNX/GETSET مع منطق مناسب أو الـ RedLock للقفل الموزع.



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;
لو المعيار نصيب، بتقدر تعمل retry بعد إعادة قراءة القيمة.



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، حجم الترافيك، ونوع العملية.
  • دايمًا اختبر في بيئة محكمة، وحط حماية على مستوى التطبيق وقاعدة البيانات معًا.
Video thumbnail
👆 أضغط على الصورة لمشاهدة الفيديو 👆
 
التعديل الأخير:
المواضيع ذات الصلة
x32x01
  • x32x01
الردود
0
المشاهدات
819
x32x01
x32x01
x32x01
الردود
0
المشاهدات
646
x32x01
x32x01
x32x01
الردود
0
المشاهدات
726
x32x01
x32x01
x32x01
الردود
0
المشاهدات
937
x32x01
x32x01
x32x01
الردود
0
المشاهدات
729
x32x01
x32x01
x32x01
الردود
6
المشاهدات
756
x32x01
x32x01
x32x01
الردود
0
المشاهدات
714
x32x01
x32x01
x32x01
الردود
0
المشاهدات
718
x32x01
x32x01
x32x01
الردود
0
المشاهدات
583
x32x01
x32x01
x32x01
الردود
0
المشاهدات
821
x32x01
x32x01
الدخول أو التسجيل السريع
نسيت كلمة مرورك؟
إحصائيات المنتدى
المواضيع
1,829
المشاركات
2,027
أعضاء أكتب كود
468
أخر عضو
عبدالله احمد
عودة
أعلى