نقرة مزدوجة كلفتنا الآلاف: كيف أنقذني مفهوم ‘اللاتكرارية’ (Idempotency) من تكرار العمليات الكارثي؟

يا جماعة الخير، السلام عليكم ورحمة الله.

خلوني أرجع بالزمن كم سنة لورا. كنا في عز الشغل على نظام دفع إلكتروني جديد لأحد العملاء الكبار. الأدرينالين كان في السما، والفريق كله سهران الليالي عشان نطلق المشروع في وقته. أطلقنا النظام، والأمور بدت تمام التمام. احتفلنا وشربنا القهوة السادة واحنا مبسوطين على الإنجاز.

بعد يومين، بيوصلني اتصال من مدير المشروع، صوته فيه نبرة غريبة. “أبو عمر، تعال عالمكتب فوراً، في مصيبة!”. رحت وأنا قلبي مقبوض، لقيت الفريق كله متجمع وشايل هم الدنيا. المشكلة؟ أحد المستخدمين اشتكى إنه انخصم منه مبلغ عملية شراء مرتين! وبعد شوي، شكوى ثانية، وثالثة. خلال ساعة، اكتشفنا عشرات العمليات المكررة، والخسائر كانت بتتراكم بالآلاف.

قعدنا نحلل السجلات (logs) زي المجانين. شو اللي بصير؟ هل في حدا بهاجمنا؟ هل في خطأ في منطق الدفع نفسه؟ بعد ساعات من التوتر والبحث، لقينا النمط. كل العمليات المكررة كانت بتيجي من نفس المستخدم خلال ثوانٍ معدودة. السيناريو كان بسيط بشكل مرعب: المستخدم بيضغط على زر “ادفع الآن”، الإنترنت عنده بعلّق شوي، فبفكر إنه العملية ما مشيت، فبقوم “بكابس” على الزر مرة ثانية وثالثة بعصبية. نظامنا، بكل براءة، كان بستقبل كل طلب كأنه طلب جديد، وبخصم المبلغ مرة ثانية وثالثة.

هذيك الليلة ما نمتها، مش بس عشان أصلح المشكلة، بس عشان الدرس اللي تعلمته كان قاسي جداً. درس اسمه “اللاتكرارية” أو Idempotency. هذا المفهوم البسيط، لو كنا مطبقينه من الأول، كان وفّر علينا كل هالصداع والخسارة. ومن يومها، صار عندي هوس فيه، وبحاول أزرعه في عقل كل مطور بشتغل معه.

ما هي اللاتكرارية (Idempotency) ببساطة؟

انسوا المصطلحات المعقدة. اللاتكرارية ببساطة تعني: “تنفيذ نفس العملية عدة مرات يعطي نفس النتيجة وكأنك نفذتها مرة واحدة فقط”.

خلونا ناخذ مثال من حياتنا اليومية. تخيل إنك كبست على زر المصعد عشان تطلع لطابق معين. لو كبست عليه مرة واحدة، المصعد رح يستجيب ويجيك. طيب لو رجعت كبست عليه عشر مرات وانت مستعجل؟ هل رح يجيك عشر مصاعد؟ لأ طبعاً. رح يضل نفس المصعد جاييك. حالة النظام (المصعد قادم) لم تتغير بعد الطلب الأول. هذا بالضبط هو مفهوم اللاتكرارية.

بالمقابل، عملية سحب الفلوس من الصراف الآلي هي عملية غير تكرارية (Non-Idempotent). لو سحبت 100 دينار، رح ينخصم من حسابك 100. لو كررت العملية مرة ثانية، رح ينخصم كمان 100. النتيجة النهائية اختلفت.

في عالم الـ APIs، هذا المبدأ هو حجر الأساس لبناء أنظمة قوية وموثوقة.

ليش هالقصة مهمة لكل مطور؟ (لماذا هذا المفهوم حيوي؟)

يمكن حدا يحكي: “يا أبو عمر، خلص بنحط disable للزر بعد أول كبسة وخلصنا”. هذا حل سطحي وجزئي، لأنه المشكلة أعمق من مجرد نقرة مزدوجة من المستخدم.

مشاكل الشبكة لا مفر منها

القاعدة الذهبية الأولى في تصميم النظم الموزعة: “الشبكة غير موثوقة”. دائماً وأبداً.

الاتصال بين تطبيق العميل (موبايل أو متصفح) والخادم (Server) يمر عبر بحر من الموجهات (Routers) والكوابل والشبكات اللاسلكية. ممكن يصير أي شي في الطريق:

  • انقطاع مؤقت (Timeout): العميل أرسل الطلب، لكن الرد تأخر وما وصل. العميل ما بيعرف هل الخادم استلم الطلب ونفذه ولا لأ. شو رح يعمل؟ رح يحاول يرسله مرة ثانية!
  • إعادة المحاولة التلقائية (Automatic Retries): كثير من المكتبات البرمجية (libraries) ومتصفحات الويب مصممة لتعيد إرسال الطلبات الفاشلة تلقائياً بدون تدخل المستخدم.
  • أخطاء غريبة: ممكن الخادم ينفذ العملية بنجاح، لكن وهو بيرسل الرد الإيجابي للعميل، يصير خطأ في الشبكة والرد يضيع. العميل رح يفترض إن العملية فشلت ويعيد إرسالها.

إذا كانت عمليتك (مثل الدفع) غير تكرارية، كل سيناريو من هدول رح يسبب كارثة.

تكرار العمليات الكارثي

تخيل معي حجم الضرر لو تكررت عمليات حساسة:

  • مالياً: خصم مبالغ مضاعفة من بطاقات الائتمان (زي ما صار معنا).
  • بياناتياً: إنشاء حسابين لنفس المستخدم بنفس الإيميل، أو إضافة نفس المنتج مرتين للسلة.
  • لوجستياً: إرسال طلبين شحن لنفس الطلبية، مما يعني خصم الكمية من المخزون مرتين وإرسال شحنتين للعميل.
  • إزعاجياً: إرسال نفس الإيميل الترحيبي أو رسالة SMS للمستخدم 5 مرات متتالية. هذا يقتل تجربة المستخدم.

كيف نطبق اللاتكرارية في تصميم الـ APIs؟

الخبر الحلو إنه تطبيقها مش علم ذرة. الحل يكمن في فهم طبيعة طلبات HTTP واستخدام تقنية بسيطة وفعالة جداً.

فهم طبيعة طلبات HTTP

بروتوكول HTTP نفسه صمم بعض العمليات (Methods) لتكون تكرارية بطبيعتها:

  • GET, HEAD, OPTIONS: هذه عمليات قراءة فقط. يمكنك طلب بيانات مستخدم ألف مرة، وفي كل مرة ستحصل على نفس البيانات دون تغيير حالة النظام. هي تكرارية وآمنة.
  • PUT: هذه العملية تكرارية حسب التعريف. عندما تستخدم PUT /users/123 لتحديث بيانات مستخدم، فأنت ترسل الحالة النهائية الكاملة للمستخدم. لو أرسلت نفس الطلب 10 مرات، النتيجة النهائية ستكون واحدة: بيانات المستخدم 123 تم تحديثها.
  • DELETE: هذه العملية أيضاً تكرارية. لو أرسلت DELETE /orders/456، أول مرة سيتم حذف الطلب (وستحصل على رد 204 No Content مثلاً). لو أرسلته مرة أخرى، الطلب لم يعد موجوداً، ستحصل على رد 404 Not Found. قد يختلف الرد، لكن حالة النظام النهائية (الطلب محذوف) لم تتغير.

المشكلة الكبرى تكمن في POST.

عملية POST مصممة لإنشاء مورد جديد. كلما أرسلت POST /orders، يتوقع الخادم منك أنك تريد إنشاء طلب جديد. هنا تكمن الخطورة، وهنا نحتاج لحل مخصص.

الحل السحري: مفتاح اللاتكرارية (Idempotency-Key)

هذا هو الحل الذي أنقذنا ولا يزال يحمي أنظمتنا. الفكرة عبقرية في بساطتها:

  1. العميل (Client) يولد مفتاحاً فريداً: قبل إرسال أي عملية حساسة (مثل POST /payments)، يقوم العميل بإنشاء معرّف فريد لهذه العملية تحديداً. عادة ما يكون UUID (Universally Unique Identifier).
  2. العميل يرسل المفتاح مع الطلب: يتم إرسال هذا المفتاح في ترويسة (Header) الطلب. الترويسة المتعارف عليها هي Idempotency-Key.
  3. الخادم (Server) يتحقق من المفتاح: هذا هو قلب الآلية. عندما يستقبل الخادم الطلب:
    • أولاً: يبحث عن قيمة Idempotency-Key في الطلب.
    • ثانياً: يتحقق في قاعدة بيانات مؤقتة (مثل Redis) أو جدول مخصص: “هل سبق لي أن عالجت هذا المفتاح من قبل؟”.
    • إذا كان المفتاح جديداً (لم تتم معالجته من قبل):
      1. يقوم الخادم بتنفيذ العملية الحرجة (مثلاً، الخصم من البطاقة).
      2. يخزّن نتيجة العملية (الرد الذي سيرسله للعميل) مع المفتاح.
      3. يرسل الرد للعميل.
    • إذا كان المفتاح مكرراً (موجود ومُعالج مسبقاً):
      1. الخادم لا ينفذ العملية الحرجة مرة أخرى.
      2. ببساطة، يسترجع الرد الذي خزّنه في المرة الأولى ويرسله مرة أخرى للعميل.

بهذه الطريقة، حتى لو أرسل العميل نفس الطلب 10 مرات بسبب مشكلة في الشبكة، العملية الحقيقية (الدفع) ستنفذ مرة واحدة فقط. وفي كل مرة من المرات التسع التالية، سيحصل العميل على نفس الرد الناجح الذي حصل عليه في المرة الأولى، دون أي ضرر.

مثال كود (شبه كود بلغة Python)

هذا مثال بسيط يوضح الفكرة باستخدام إطار عمل Flask و Redis كذاكرة تخزين مؤقتة للمفاتيح.


from flask import Flask, request, jsonify
import redis
import uuid

app = Flask(__name__)
# افترض أن لدينا اتصال بـ Redis
redis_client = redis.Redis(decode_responses=True)

# دالة وهمية لتنفيذ عملية الدفع الفعلية
def process_the_actual_payment(data):
    print(f"--- معالجة عملية دفع حقيقية للطلب: {data['order_id']} ---")
    # ... هنا يتم التواصل مع بوابة الدفع ...
    # ... هذا هو الجزء الذي لا نريد تكراره أبداً ...
    payment_id = f"pay_{uuid.uuid4().hex[:10]}"
    print(f"--- تمت عملية الدفع بنجاح! رقم العملية: {payment_id} ---")
    return {"status": "success", "payment_id": payment_id}

@app.route('/api/payments', methods=['POST'])
def create_payment():
    idempotency_key = request.headers.get('Idempotency-Key')

    if not idempotency_key:
        return jsonify({"error": "Idempotency-Key header is required"}), 400

    # الخطوة 1: التحقق إذا كان الرد مخزناً مسبقاً
    cached_response = redis_client.get(f"idempotency:{idempotency_key}")
    if cached_response:
        print(f"طلب مكرر! إعادة الرد المخزن للمفتاح: {idempotency_key}")
        return cached_response # Flask يتطلب كائن Response، هنا للتبسيط

    # --- هذا هو الطلب الأول لهذا المفتاح ---
    print(f"طلب جديد بالمفتاح: {idempotency_key}")
    
    # الخطوة 2: تنفيذ العملية الحرجة
    try:
        payment_data = request.get_json()
        result = process_the_actual_payment(payment_data)
        response_body = jsonify(result)
        response_status = 201 # Created
    except Exception as e:
        # في حال فشلت العملية، لا نخزن المفتاح لكي يتمكن العميل من المحاولة مرة أخرى
        return jsonify({"error": str(e)}), 500

    # الخطوة 3: تخزين الرد قبل إرساله
    # نضع مدة صلاحية (مثلاً 24 ساعة) للمفتاح
    redis_client.setex(f"idempotency:{idempotency_key}", 86400, response_body.get_data(as_text=True))
    print(f"تم تخزين الرد للمفتاح: {idempotency_key}")
    
    return response_body, response_status

نصائح من دار أبو عمر

بعد سنين من التعامل مع هذا المفهوم، اسمحولي أقدم لكم كم نصيحة عملية:

  • وين نخزّن المفاتيح؟: استخدام ذاكرة تخزين مؤقت سريعة مثل Redis هو الخيار الأمثل. فهي سريعة جداً وتوفر ميزة تحديد مدة صلاحية للمفتاح (TTL)، فلا داعي لتخزين المفاتيح إلى الأبد. 24 ساعة عادة ما تكون كافية.
  • مين بولّد المفتاح؟: دائماً وأبداً، العميل هو المسؤول عن توليد المفتاح. لو قام الخادم بتوليده، فإن كل طلب جديد سيحتوي على مفتاح جديد وتفقد الآلية معناها. الهدف هو تتبع “نية” العميل لتنفيذ عملية معينة.
  • مش بس للـ POST: صحيح أن POST هي الحالة الأشهر، لكن قد تحتاج لتطبيق هذه الآلية على عمليات PATCH إذا كانت العملية التي تقوم بها ليست تكرارية بطبيعتها (مثلاً، PATCH /wallet مع عملية { "operation": "add", "amount": 10 }).
  • لا تعقّدها: لا تطبق هذه الآلية على كل API في نظامك. استخدمها فقط للعمليات الحرجة، غير التكرارية، والتي يترتب على تكرارها ضرر (مالي، بياناتي، …الخ).

الخلاصة: نقرة بسيطة، درس كبير 💡

في النهاية، قصة النقرة المزدوجة هذيك، رغم مرارتها في وقتها، كانت من أفضل الدروس اللي تعلمتها في مسيرتي. علمتني أن بناء أنظمة قوية لا يتعلق فقط بكتابة كود يعمل في الظروف المثالية، بل يتعلق بتوقع الفوضى والاستعداد لها: فوضى الشبكات، فوضى سلوك المستخدم، وفوضى الأخطاء غير المتوقعة.

اللاتكرارية (Idempotency) ليست مجرد مصطلح تقني فاخر، بل هي درع واقٍ لأنظمتك. هي شبكة الأمان التي تمسك بنظامك عندما يفشل كل شيء آخر.

نصيحتي الأخيرة لك: في المرة القادمة التي تصمم فيها API، خاصة إذا كان يتعامل مع عمليات حساسة، توقف لحظة واسأل نفسك هذا السؤال البسيط: “ماذا سيحدث لو تم إرسال هذا الطلب مرتين؟”. الإجابة على هذا السؤال قد توفر عليك آلاف الدولارات والكثير من ليالي القلق.

يلا، الله يعطيكم العافية وبشوفكم في مقالة جديدة. 🇵🇸

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

​معمارية البرمجيات

تطبيقنا الضخم كان وحشاً: كيف أنقذني “نمط الخانق” (Strangler Fig Pattern) من جحيم التحديثات المستحيلة؟

أشارككم قصة حقيقية من قلب المعركة مع نظام قديم ضخم، وكيف استطعنا ترويضه وتحديثه تدريجياً باستخدام استراتيجية "نمط الخانق" (Strangler Fig Pattern). هذه ليست مجرد...

27 مارس، 2026 قراءة المزيد
تسويق رقمي

ميزانيتي الإعلانية كانت ثقباً أسود: كيف أنقذني نموذج الإحالة متعدد اللمسات من إهدار المال؟

هل تشعر أن ميزانيتك الإعلانية تتبخر دون عائد واضح؟ اكتشف كيف ساعدني نموذج الإحالة متعدد اللمسات (Multi-Touch Attribution) في تحديد القنوات التسويقية الفعالة حقاً وإيقاف...

27 مارس، 2026 قراءة المزيد
الحوسبة السحابية

توقف منطقتنا السحابية بالكامل: كيف أنقذتني استراتيجية ‘التعافي من الكوارث متعددة المناطق’ من كارثة محققة؟

في لحظة توقف كل شيء، تحولت قهوة الصباح الهادئة إلى سباق مع الزمن. أشارككم قصتي مع توقف منطقة سحابية بالكامل، وكيف كانت خطة التعافي من...

27 مارس، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كنت مجرد مستهلك للكود: كيف حوّلتني ‘المساهمة في المصادر المفتوحة’ إلى مطور مطلوب؟

لسنوات طويلة، كنت كغيري من المبرمجين، أستهلك الأكواد والمكتبات البرمجية دون أن أفكر في ما وراءها. في هذه المقالة، أشارككم قصتي الشخصية وكيف أن خطوة...

27 مارس، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

خادمي الوحيد كان على وشك الانهيار: كيف أنقذني ‘موازن الأحمال’ (Load Balancer) من التوقف التام؟

في لحظة تحول فيها نجاح تطبيقي إلى كابوس، كان خادمي الوحيد يلفظ أنفاسه الأخيرة تحت ضغط المستخدمين. هذه قصتي وكيف أنقذني مفهوم بسيط وقوي اسمه...

27 مارس، 2026 قراءة المزيد
اختبارات الاداء والجودة

تحديثاتي كانت تكسر الواجهة بصمت: كيف أنقذني الاختبار البصري التراجعي (Visual Regression Testing) من كوارث غير متوقعة؟

أنا أبو عمر، مبرمج فلسطيني، وأروي لكم اليوم قصة حقيقية عن كارثة كادت أن تحدث في أحد مشاريعي. اكتشفت أن تحديثاً برمجياً بسيطاً كان يكسر...

27 مارس، 2026 قراءة المزيد
البودكاست