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

يا أهلاً وسهلاً فيكم يا جماعة الخير. معكم أخوكم أبو عمر.

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

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

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

ما هي الكارثة التي نتحدث عنها؟ (الآثار الجانبية للمحاولات المتكررة)

في عالم الأنظمة الموزعة (Distributed Systems)، واللي كل تطبيقاتنا اليوم جزء منها، الفشل هو القاعدة وليس الاستثناء. الشبكة ممكن تقطع، الخادم ممكن يصير عليه ضغط، قاعدة البيانات ممكن تتأخر في الرد… كلها أمور واردة وطبيعية.

ولمواجهة هذا الفشل، أول حل بيخطر على بالنا هو “إعادة المحاولة” (Retry). إذا فشل الطلب، بنعيد إرساله. منطقي، صح؟

المشكلة بتصير لما يكون للطلب “أثر جانبي” (Side Effect). والأثر الجانبي هو أي عملية بتغير حالة النظام. أمثلة:

  • خصم مبلغ من حساب بنكي.
  • إرسال بريد إلكتروني.
  • إنشاء سجل جديد في قاعدة البيانات.
  • زيادة قيمة عدّاد في النظام.

لما نجمع بين إعادة المحاولة والأثر الجانبي، بنوقع في ورطة كبيرة:

طلب له أثر جانبي + إعادة محاولة بسبب فشل في الشبكة = تنفيذ الأثر الجانبي مرتين!

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

الحل السحري: مفهوم العمليات عديمة الأثر (Idempotency)

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

شو يعني “عديمة الأثر”؟ (التعريف البسيط)

العملية عديمة الأثر هي أي عملية لو نفذتها مرة واحدة، أو نفذتها 100 مرة، النتيجة النهائية على النظام بتكون نفسها. ما في أي تغيير إضافي بعد التنفيذ الأول.

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

بالمقابل، عملية سحب النقود من الصراف الآلي هي عملية ليست عديمة الأثر. لو سحبت 100 دينار، وبعدها كررت العملية، رح تسحب كمان 100 دينار. النتيجة النهائية مختلفة في كل مرة.

هدفنا كمصممين للنظم هو تحويل العمليات الحساسة اللي الها آثار جانبية (مثل الدفع) إلى عمليات عديمة الأثر.

كيف نطبق هذا المفهوم عمليًا في واجهاتنا البرمجية (APIs)؟

ممتاز، حكينا نظري كثير. خلينا نشوف كيف بنطبق الحكي هاد على أرض الواقع. الطريقة الأشهر والأكثر فعالية هي باستخدام ما يسمى “مفتاح عدم التكرار”.

الاستراتيجية الأولى: مفتاح عدم التكرار (Idempotency Key)

الفكرة عبقرية وبسيطة: العميل (سواء كان تطبيق موبايل أو واجهة ويب) هو من يقوم بإنشاء “هوية فريدة” لكل عملية يحاول تنفيذها. هذه الهوية بنسميها “مفتاح عدم التكرار” (Idempotency Key)، وعادة بتكون عبارة عن سلسلة نصية فريدة عالميًا (UUID).

خطوات التنفيذ:

  1. من جهة العميل (Client): قبل إرسال أي طلب حساس (مثل POST لإنشاء دفعة)، يقوم العميل بإنشاء UUID فريد.
  2. إرسال الطلب: يرسل العميل هذا المفتاح ضمن ترويسة الطلب (HTTP Header). المتعارف عليه هو استخدام ترويسة مثل Idempotency-Key.
  3. من جهة الخادم (Server): هنا يكمن السحر. عند استقبال الطلب، يقوم الخادم بالخطوات التالية:
    1. يقرأ قيمة Idempotency-Key من الترويسة.
    2. يبحث في مخزن مؤقت (مثل Redis أو جدول في قاعدة البيانات) عن هذا المفتاح.
    3. الحالة (أ): المفتاح غير موجود. هذا يعني أن هذا طلب جديد. يقوم الخادم بتنفيذ العملية كالمعتاد (مثلاً، خصم المبلغ)، ثم يخزن نتيجة العملية (النجاح أو الفشل) مع المفتاح في المخزن المؤقت، ويرسل الرد للعميل.
    4. الحالة (ب): المفتاح موجود. هذا يعني أن هذا الطلب هو إعادة محاولة لطلب سابق. هنا، الخادم لا يقوم بتنفيذ العملية مرة أخرى. بدلاً من ذلك، يسترجع النتيجة المحفوظة مسبقاً من المخزن المؤقت ويرسلها مباشرة للعميل.

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

مثال كود (بايثون مع Flask و Redis):


# مثال توضيحي مبسط
from flask import Flask, request, jsonify
import redis
import uuid

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

# مخزن وهمي للطلبات
payments_db = {} 

@app.route('/payments', methods=['POST'])
def create_payment():
    idempotency_key = request.headers.get('Idempotency-Key')
    if not idempotency_key:
        return jsonify({"error": "Idempotency-Key header is missing"}), 400

    # 1. التحقق إذا تم تخزين استجابة لهذا المفتاح
    cached_response = redis_client.get(f"idempotency:{idempotency_key}")
    if cached_response:
        # 2. إذا نعم، أرجع الاستجابة المخزنة
        print("Idempotency Key found, returning cached response.")
        return jsonify(json.loads(cached_response)), 200

    # 3. إذا لا، فهذا طلب جديد. نفذ العملية
    print("New Idempotency Key, processing request...")
    try:
        data = request.get_json()
        amount = data['amount']
        order_id = data['order_id']

        # --- هنا منطق الدفع الفعلي ---
        # لنفترض أنه ينجح ويولد معرف دفعة
        payment_id = f"pay_{uuid.uuid4().hex}"
        payments_db[payment_id] = {"order_id": order_id, "amount": amount, "status": "completed"}
        # -----------------------------

        response_data = {"payment_id": payment_id, "status": "completed"}
        
        # 4. خزن الاستجابة في Redis مع صلاحية (مثلاً 24 ساعة)
        redis_client.setex(f"idempotency:{idempotency_key}", 86400, json.dumps(response_data))

        return jsonify(response_data), 201

    except Exception as e:
        # في حالة حدوث خطأ، يمكنك أيضًا تخزين الاستجابة للخطأ
        error_response = {"error": str(e)}
        redis_client.setex(f"idempotency:{idempotency_key}", 86400, json.dumps(error_response))
        return jsonify(error_response), 500

استراتيجيات أخرى

  • استخدام PUT بشكل صحيح: في واجهات REST، طلبات GET, HEAD, OPTIONS, PUT, DELETE يجب أن تكون عديمة الأثر بطبيعتها. طلب PUT /users/123 يعني “أنشئ أو حدّث المستخدم 123 بهذه البيانات”. لو أرسلته 10 مرات، النتيجة النهائية هي نفسها. المشكلة دائمًا تكون مع POST الذي يعني “أنشئ موردًا جديدًا”. إرسال POST عشر مرات يعني إنشاء 10 موارد جديدة.
  • التحقق من الحالة قبل التنفيذ: قبل تنفيذ عملية ما، يمكنك التحقق من حالة الكيان. مثلاً، قبل معالجة دفعة للطلب رقم 55، تحقق أولاً: “هل حالة الطلب 55 هي ‘مدفوع’؟”. إذا كانت كذلك، فلا تفعل شيئًا. هذه الطريقة جيدة لكنها قد تكون عرضة لـ “حالات السباق” (Race Conditions) إذا لم يتم التعامل معها بحذر داخل معاملة قاعدة بيانات (Transaction).

نصائح من خبرة أبو عمر

بعد هذيك الليلة المشؤومة، صار موضوع الـ Idempotency هاجس عندي. وهذه شوية نصائح من القلب اكتسبتها مع الزمن:

  1. لا تثق بالشبكة أبدًا: هذه القاعدة الذهبية في تصميم النظم. افترض دائمًا أن العميل قد يعيد إرسال الطلب، وصمم نظامك على هذا الأساس.
  2. اجعل مفتاح عدم التكرار مسؤولية العميل: لا تحاول أن تخمن على الخادم إذا كان الطلب مكررًا أم لا. دع العميل يخبرك بصراحة عبر إرسال المفتاح.
  3. خزّن الاستجابة بأكملها: لا تكتفِ بتخزين علامة “تم التنفيذ”. خزّن نص الاستجابة الكامل الذي أرسلته للعميل في المرة الأولى. هذا يجعل تجربة العميل عند إعادة المحاولة سلسة تمامًا، كأن شيئًا لم يكن.
  4. ضع عمرًا افتراضيًا للمفتاح: لا تحتاج لتخزين مفاتيح عدم التكرار إلى الأبد. 24 ساعة هي مدة كافية ومنطقية لمعظم الحالات. هذا يمنع تضخم قاعدة بياناتك أو مخزنك المؤقت ببيانات قديمة.

الخلاصة: فكّر في “ماذا لو؟” 🤔

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

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

يلا يا جماعة، شدّوا حيلكم، وخلينا نبني أنظمة قوية وموثوقة زي شجر الزيتون اللي ما بتهزه ريح. 😉

أبو عمر

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

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

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

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

آخر المدونات

أتمتة العمليات

عملياتنا المعقدة كانت تموت بصمت: كيف أنقذنا ‘منسق سير العمل’ (Workflow Orchestrator) من جحيم الفشل غير المرئي؟

أشارككم قصة حقيقية عن معاناة فريقنا مع العمليات الخلفية التي كانت تفشل بصمت، وكيف كان الحل في تبني 'منسق سير العمل' (Workflow Orchestrator). اكتشفوا الفارق...

21 أبريل، 2026 قراءة المزيد
​معمارية البرمجيات

خدماتنا كانت تتشابك في فوضى: كيف أنقذتنا ‘المعمارية القائمة على الأحداث’ (EDA) من جحيم الاقتران المحكم؟

أشارككم قصة حقيقية من قلب المعركة البرمجية، كيف انتقلنا من نظام هش ومترابط إلى بنية مرنة وقابلة للتوسع باستخدام المعمارية القائمة على الأحداث (Event-Driven Architecture)....

21 أبريل، 2026 قراءة المزيد
ذكاء اصطناعي

نماذجنا اللغوية كانت عملاقة ومكلفة: كيف أنقذنا ‘تقطير النماذج’ (Model Distillation) من جحيم بطء الاستدلال والتكاليف الباهظة؟

أنا أبو عمر، مبرمج فلسطيني، وأشارككم اليوم قصة حقيقية من تجربتي عن كيفية ترويض نماذج الذكاء الاصطناعي العملاقة. سنغوص في تقنية "تقطير النماذج" (Model Distillation)...

21 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

الهياكل العظمية للواجهات (Skeleton Screens): كيف أنقذت مشروعي من جحيم شاشات التحميل؟

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

21 أبريل، 2026 قراءة المزيد
برمجة وقواعد بيانات

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

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

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