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

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

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

الساعة 11 بالليل، وإذًا تلفوني برن. على الخط مدير قسم الدعم الفني، صوته متوتر: “أبو عمر، الحقنا! في مصيبة! عميل دفع فاتورة مرتين، والمصاري انخصمت من حسابه مرتين! والعميل معصّب وبدو يولّع الدنيا”.

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

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

هذيك الليلة، قررت إنه لازم نلاقي حل جذري لهالمشكلة، حل يخلي نظامنا صامد زي شجر الزيتون في وجه رياح الشبكات المتقلبة. وكان الحل اسمه: Idempotency-Key.

ما هو الـ Idempotency (عدم التكرار)؟ وليش هو مهم؟

قبل ما نغوص في التفاصيل التقنية، خلينا نبسّط المفهوم. كلمة “Idempotent” بتعني إنه لو كررت نفس العملية أكثر من مرة، النتيجة النهائية بتكون نفسها كأنك عملتها مرة واحدة بس.

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

في عالم الـ APIs، بعض الطلبات “آمنة” بطبيعتها:

  • GET: طلب جلب بيانات. لو طلبته مليون مرة، ما رح يغيّر إشي في النظام. هو Idempotent بطبيعته.
  • PUT: طلب تحديث مورد كامل. لو أرسلت نفس التحديث مرتين، النتيجة النهائية هي نفسها. هو Idempotent.
  • DELETE: طلب حذف مورد. لو طلبت حذفه أول مرة، رح ينحذف. لو طلبت حذفه مرة ثانية، رح يعطيك خطأ “غير موجود”، لكن الحالة النهائية للنظام (المورد محذوف) لم تتغير. فهو Idempotent.

المشكلة الحقيقية تكمن في طلبات POST، اللي هي طلبات إنشاء مورد جديد. كل طلب POST يُفترض أنه ينشئ شيئًا جديدًا. لو أرسلت طلب POST مرتين، رح ينشئ موردين جديدين. وهذا هو تمامًا ما حدث معنا في عملية الدفع.

جحيم إعادة المحاولة (Retry Hell) والشبكة غير الموثوقة

كمطورين، بنعرف إنه الشبكة عدو لا يمكن الوثوق به. الطلب ممكن ينجح، ممكن يفشل، أو ممكن يدخل في المنطقة الرمادية: الـ Timeout.

لما يصير Timeout، العميل (تطبيق الموبايل أو المتصفح) ما بيعرف شو صار على الخادم:

  1. هل الطلب لم يصل أصلًا للخادم؟
  2. هل وصل الطلب والخادم عالجه، لكن الرد ضاع في الطريق؟

إذا كان الجواب هو (1)، فإعادة المحاولة آمنة ومطلوبة. أما إذا كان الجواب (2)، فإعادة المحاولة ستؤدي إلى تنفيذ العملية مرتين. وهذه هي “المقامرة” اللي كنا بنلعبها بدون ما نعرف.

بطل القصة: الـ Idempotency-Key

الحل يكمن في جعل عملية الـ POST “الخطيرة” تتصرف كأنها Idempotent. وهنا يأتي دور الـ Idempotency-Key.

الفكرة بسيطة وعبقرية: العميل، قبل إرسال أي عملية حساسة (مثل الدفع)، يقوم بإنشاء “مفتاح فريد” لهذه العملية. هذا المفتاح هو مجرد سلسلة نصية فريدة، مثل UUID (Universally Unique Identifier). ثم يرسل هذا المفتاح مع الطلب كجزء من الـ Headers.

كيف تعمل الآلية خطوة بخطوة؟

  1. العميل (Client): قبل إرسال طلب POST لإنشاء عملية دفع، يقوم بإنشاء UUID v4 فريد. مثلا: "f1b2c3d4-e5f6-7890-1234-567890abcdef".
  2. العميل (Client): يرسل طلب الـ POST إلى /api/payments، ويضيف Header خاص: Idempotency-Key: f1b2c3d4-e5f6-7890-1234-567890abcdef.
  3. الخادم (Server): يستقبل الطلب. قبل تنفيذ أي منطق برمجي لعملية الدفع، يقوم بالآتي:
    • يقرأ قيمة الـ Idempotency-Key من الـ Header.
    • يبحث في قاعدة بيانات مؤقتة (مثل Redis) أو جدول خاص في قاعدة البيانات الأساسية عن هذا المفتاح.
  4. السيناريو الأول: المفتاح غير موجود.
    • هذا يعني أنها المرة الأولى التي يرى فيها الخادم هذا الطلب.
    • يقوم الخادم بتنفيذ عملية الدفع بشكل طبيعي.
    • قبل إرسال الرد الناجح للعميل، يقوم بتخزين نتيجة العملية (مثلاً: status code 201 والـ response body) مع الـ Idempotency Key في الكاش.
    • يرسل الرد الناجح (201 Created) للعميل.
  5. السيناريو الثاني: المفتاح موجود.
    • هذا يعني أن هذا الطلب (أو طلب مطابق له) قد تم استلامه ومعالجته من قبل.
    • الخادم لا يقوم بتنفيذ عملية الدفع مرة أخرى.
    • ببساطة، يسترجع النتيجة المخزنة مسبقًا والمرتبطة بهذا المفتاح.
    • يرسل نفس الرد الذي أرسله في المرة الأولى (مثلاً: status code 201 والـ response body المخزن) للعميل.

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

مثال بالكود (شغل عملي)

خلينا نشوف كيف ممكن يبدو هذا في الواقع.

جهة العميل (Client-Side – JavaScript)

هنا نستخدم مكتبة مثل uuid لإنشاء المفتاح وإضافته للطلب باستخدام fetch.


// أولاً، قم بتثبيت مكتبة uuid
// npm install uuid

import { v4 as uuidv4 } from 'uuid';

async function processPayment(paymentData) {
  const idempotencyKey = uuidv4(); // إنشاء مفتاح فريد لهذه العملية
  
  try {
    const response = await fetch('/api/payments', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey, // إضافة المفتاح في الـ Header
      },
      body: JSON.stringify(paymentData),
    });

    if (!response.ok) {
      // هنا يمكنك إضافة منطق إعادة المحاولة (Retry) بأمان
      // لأن الخادم سيضمن عدم التكرار
      throw new Error('Network response was not ok.');
    }

    const result = await response.json();
    console.log('Payment successful:', result);
    return result;

  } catch (error) {
    console.error('Payment failed:', error);
    // يمكنك إعادة المحاولة هنا مع نفس الـ idempotencyKey
  }
}

جهة الخادم (Server-Side – Pseudo-code for a Middleware)

أفضل طريقة لتطبيق هذا على الخادم هي من خلال Middleware يعترض الطلبات قبل وصولها إلى منطق العمل الرئيسي.


// هذا مثال توضيحي باستخدام Express.js و Redis
// const redisClient = require('./redis-client');

async function idempotencyMiddleware(req, res, next) {
  // نطبق هذا فقط على الطلبات الحساسة مثل POST
  if (req.method !== 'POST') {
    return next();
  }

  const idempotencyKey = req.headers['idempotency-key'];

  // إذا لم يتم إرسال المفتاح، نتجاوز الـ middleware
  if (!idempotencyKey) {
    return next();
  }

  try {
    // 1. ابحث عن المفتاح في Redis (الكاش)
    const cachedResponse = await redisClient.get(idempotencyKey);

    if (cachedResponse) {
      // 2. إذا وجدنا المفتاح، أرجع الرد المخزن
      console.log(`Idempotency key ${idempotencyKey} found in cache. Returning stored response.`);
      const responseData = JSON.parse(cachedResponse);
      return res.status(responseData.statusCode).json(responseData.body);
    }

    // 3. إذا لم نجد المفتاح، نحتاج لتخزين الرد بعد تنفيذه
    // نستخدم دالة لتجاوز res.json الأصلية
    const originalJson = res.json;
    const originalStatus = res.status;
    let responseBody;
    let statusCode;

    res.status = function(code) {
        statusCode = code;
        return originalStatus.call(this, code);
    };

    res.json = function(body) {
      responseBody = body;
      return originalJson.call(this, body);
    };
    
    // عند انتهاء الطلب، نقوم بتخزين النتيجة
    res.on('finish', async () => {
      if (statusCode && responseBody) {
        const dataToCache = {
          statusCode: statusCode,
          body: responseBody,
        };
        // قم بتخزين الرد في Redis مع مدة صلاحية (مثلاً 24 ساعة)
        await redisClient.set(idempotencyKey, JSON.stringify(dataToCache), 'EX', 24 * 60 * 60);
        console.log(`Response for key ${idempotencyKey} cached.`);
      }
    });

    // 4. اسمح للطلب بالمرور إلى الـ Controller الرئيسي
    next();

  } catch (error) {
    console.error('Idempotency middleware error:', error);
    next(error);
  }
}

ملاحظة هامة: المثال أعلاه هو تبسيط للفكرة. في الأنظمة الحقيقية، يجب التعامل مع حالات متقدمة مثل الـ Race Conditions (عندما يصل طلبان متطابقان في نفس الملي ثانية)، ويمكن حلها باستخدام أقفال (locks) على مستوى الكاش.

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

  • لا تطبقه على كل شيء: استخدم مفتاح عدم التكرار فقط على عمليات الـ POST (أو أحيانًا PATCH) التي تغير حالة النظام بشكل لا يمكن عكسه بسهولة، مثل إنشاء دفعة، أو إرسال طلب، أو حجز موعد. لا تضيع موارد الخادم على طلبات GET.
  • مدة صلاحية المفتاح (TTL): يجب أن يكون للمفتاح مدة صلاحية. لا تريد أن تمنع مستخدمًا من إجراء عملية دفع مشابهة بعد أسبوع لأنك خزنت المفتاح إلى الأبد. مدة 24 ساعة تعتبر بداية جيدة لمعظم الحالات.
  • المفتاح مسؤولية العميل: تأكد دائمًا أن العميل هو من يقوم بإنشاء المفتاح وإعادة استخدامه عند إعادة المحاولة. إذا قام الخادم بإنشائه، فستفقد الفائدة كلها.
  • ماذا تخزن؟ لا تخزن العملية نفسها، بل خزّن نتيجة العملية. خزّن الـ Status Code والـ Response Body. هذا يضمن أن العميل يحصل على نفس التجربة تمامًا عند إعادة المحاولة.

الخلاصة: من الفوضى إلى الموثوقية

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

لا تنتظر حتى تقع الكارثة. انظر إلى الـ APIs الخاصة بك اليوم، واسأل نفسك: ماذا سيحدث لو فشل اتصال العميل في اللحظة الحرجة؟ إذا لم يكن لديك جواب قاطع، فقد حان الوقت لتقديم الشكر لبطلنا الصامت، الـ Idempotency-Key. 👍

أبو عمر

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

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

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

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

آخر المدونات

برمجة وقواعد بيانات

تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من جحيم التوقفات المجدولة؟

هل سئمت من إيقاف الخدمة مع كل تحديث لهيكلة قاعدة البيانات؟ أشارككم قصة حقيقية وكيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من ليالي النشر الطويلة والمُجهدة،...

4 يونيو، 2026 قراءة المزيد
الشبكات والـ APIs

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

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

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

من التوقف التام إلى النجاة: كيف أنقذتنا استراتيجية “الضوء المرشد” (Pilot Light) يوم انقطعت السحابة؟

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

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

كانت مهمتي البرمجية للاختبار مجرد كود: كيف أنقذني توثيق القرارات من جحيم الصمت بعد المقابلة؟

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

4 يونيو، 2026 قراءة المزيد
التكنلوجيا المالية Fintech

من الانتظار لأيام إلى الدفع في ثوانٍ: كيف أنقذتنا شبكات الدفع الفوري من جحيم التحويلات البنكية؟

أسرد لكم من واقع تجربتي كـ "أبو عمر"، كيف عانينا من بطء وتكلفة التحويلات البنكية الدولية، وكيف جاءت شبكات الدفع الفوري ومعيار ISO 20022 لتكون...

4 يونيو، 2026 قراءة المزيد
البنية التحتية وإدارة السيرفرات

كان كل خادم لدينا ‘ندفة ثلج’ فريدة: كيف أنقذنا ‘الكود كبنية تحتية’ (IaC) من جحيم الانجراف اليدوي؟

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

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

كانت تغطية الاختبارات 100% لكن الأخطاء تتسرب: كيف أنقذنا “الاختبار الطفري” من جحيم الثقة الزائفة؟

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

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