كانت إعادة المحاولة الشبكية تسبب كوارث: كيف أنقذنا ‘مفتاح عدم التكرار’ (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. 👍

أبو عمر

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

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

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

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

آخر المدونات

الحوسبة السحابية

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

أشارككم قصة حقيقية من قلب المعاناة مع الخوادم، وكيف انتقلنا من الفوضى والتعديلات اليدوية العشوائية إلى عالم منظم يمكن التنبؤ به بفضل مفهوم "البنية التحتية...

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

كانت طفرات الزوار المفاجئة تشلّ نظامنا: كيف أنقذتنا ‘قوائم الانتظار’ (Message Queues) من جحيم الانهيار وفقدان البيانات؟

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

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

شبكات الاحتيال كانت تنهبنا بصمت: كيف أنقذتنا الشبكات العصبونية الرسومية (GNNs) من جحيم الخسائر الخفية؟

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

11 مايو، 2026 قراءة المزيد
ادارة الفرق والتنمية البشرية

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

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

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

تغطية الكود 100% لكن الأخطاء تتسرب: كيف أنقذنا ‘الاختبار الطفري’ (Mutation Testing) من جحيم الثقة الزائفة؟

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

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

كان إعداد بيئة التطوير يستغرق أياماً: كيف أنقذتنا ‘حاويات التطوير’ (Dev Containers) من جحيم ‘تعمل على جهازي’؟

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

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