مفتاح عدم تكرار المعاملة (Idempotency Key): طوق النجاة الذي أنقذنا من فوضى الطلبات المكررة

السلام عليكم يا جماعة الخير، معكم أخوكم أبو عمر.

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

ما كملت نص ساعة إلا والتلفون برن… صوت مدير الدعم الفني على الطرف الثاني مذعور: “يا أبو عمر الحقنا! المصاري بتنخصم من العملاء مرتين! الفيسبوك ولّع شكاوي والناس معصبة!”.

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

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

ما هو “عدم تكرار المعاملة” (Idempotency)؟ شرح مبسط للجدعان

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

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

في عالم الـ APIs، بعض الطلبات (HTTP Methods) هي بطبيعتها “Idempotent”:

  • GET, HEAD, OPTIONS, TRACE: هاي الطلبات آمنة أصلاً، لأنها ما بتغير أي شيء في الخادم. لو طلبت بيانات منتج معين مليون مرة، المنتج ما رح يتغير.
  • PUT: هذا الطلب يعتبر Idempotent. لو أرسلت طلب PUT /articles/123 لتحديث مقال بمحتوى معين، سواء أرسلته مرة أو خمسين مرة، النتيجة النهائية هي أن المقال 123 سيحتوي على ذلك المحتوى.
  • DELETE: نعم، حتى الحذف! لو أرسلت DELETE /users/456، أول مرة سيتم حذف المستخدم. ثاني مرة (وثالث ورابع مرة)، الخادم رح يرجعلك “404 Not Found”، لكن النتيجة النهائية على النظام ما تغيرت: المستخدم 456 محذوف.

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

‘مفتاح عدم تكرار المعاملة’ (Idempotency Key): المنقذ من الفوضى

الحل يكمن في إجبار طلب الـ POST على أن يتصرف كأنه Idempotent. كيف؟ من خلال شيء بنسميه “مفتاح عدم تكرار المعاملة” أو بالإنجليزية “Idempotency Key”.

الفكرة عبقرية في بساطتها: العميل (المتصفح أو تطبيق الموبايل) هو من يقوم بإنشاء مُعرّف فريد (unique ID) لكل معاملة يحاول تنفيذها. ثم يقوم بإرسال هذا المُعرّف مع الطلب داخل الـ Header.

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

كيف يعمل السحر؟ آلية العمل خطوة بخطوة

  1. العميل يُنشئ المفتاح: قبل إرسال طلب POST لإنشاء طلبية جديدة، يقوم كود الـ JavaScript في المتصفح بإنشاء مُعرّف فريد عالمياً (UUID). مثلاً: 'f1b2c3d4-e5f6-7890-1234-abcdabcdabcd'.
  2. العميل يُرسل الطلب: يتم إرفاق هذا المفتاح في هيدر خاص، وليكن اسمه Idempotency-Key.
  3. الخادم يستقبل الطلب لأول مرة:
    • الخادم يقرأ الهيدر Idempotency-Key.
    • يبحث عن قيمة هذا المفتاح في مكان تخزين مؤقت (مثل Redis أو جدول في قاعدة البيانات).
    • لا يجد المفتاح. هذا يعني أنها معاملة جديدة.
    • قبل أن يبدأ بالعملية الفعلية (الخصم من البطاقة مثلاً)، يقوم بتخزين المفتاح وحالة “قيد التنفيذ” (In Progress). هذا يمنع حدوث تضارب لو وصل طلب آخر بنفس المفتاح في نفس اللحظة.
    • ينفذ العملية بنجاح (تم إنشاء الطلبية وخصم المبلغ).
    • يُحدّث حالة المفتاح إلى “مكتمل” (Completed) ويخزن بجانبه الاستجابة الكاملة التي سيرسلها للعميل (Response Body and Status Code).
    • يرسل الاستجابة الناجحة (e.g. 201 Created) للعميل.
  4. الخادم يستقبل الطلب المكرر (النقرة المزدوجة):
    • بعد ثوانٍ معدودة، يصل طلب آخر بنفس البيانات وبنفس Idempotency-Key.
    • الخادم يبحث عن المفتاح في مخزنه المؤقت.
    • يجد المفتاح! وحالته “مكتمل”.
    • هنا، لا يقوم الخادم بتنفيذ منطق إنشاء الطلبية مرة أخرى على الإطلاق. لا خصم جديد، لا طلبية جديدة.
    • بكل بساطة، يسترجع الاستجابة التي خزنها سابقاً ويرسلها مرة أخرى للعميل كما هي.

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

كود يا مبرمج: مثال عملي باستخدام Node.js و Express

الكلام النظري جميل، لكن خلينا نشوف كود. هذا مثال مبسط جداً لفكرة “middleware” في Express.js يمكنه التعامل مع طلبات الـ Idempotency.

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


// in-memory store (for demonstration only!)
// In production, use Redis or a database.
const idempotencyStore = new Map();

const idempotencyMiddleware = async (req, res, next) => {
  const idempotencyKey = req.get('Idempotency-Key');

  // If no key, just proceed. Maybe log a warning.
  if (!idempotencyKey) {
    return next();
  }

  // Check if we have this key in our store
  if (idempotencyStore.has(idempotencyKey)) {
    const cachedResponse = idempotencyStore.get(idempotencyKey);
    
    // If it's still processing, you might return a 409 Conflict
    if (cachedResponse.status === 'in-progress') {
        return res.status(409).json({ message: 'Request is already being processed.' });
    }

    // If it's completed, return the cached response
    console.log(`[Idempotency] Returning cached response for key: ${idempotencyKey}`);
    return res.status(cachedResponse.statusCode).json(cachedResponse.body);
  }

  // --- New Request ---
  // 1. Store the key as "in-progress"
  idempotencyStore.set(idempotencyKey, { status: 'in-progress' });
  console.log(`[Idempotency] New key received, processing: ${idempotencyKey}`);

  // 2. We need to "spy" on the original res.json() to cache the response
  const originalJson = res.json;
  res.json = (body) => {
    // Cache the successful response
    if (res.statusCode >= 200 && res.statusCode < 300) {
      const responseToCache = {
        status: 'completed',
        statusCode: res.statusCode,
        body: body,
      };
      // Set an expiry for the key, e.g., 24 hours
      idempotencyStore.set(idempotencyKey, responseToCache);
    } else {
      // If the request failed, we can remove the key to allow retries
      idempotencyStore.delete(idempotencyKey);
    }
    // Call the original res.json()
    return originalJson.call(res, body);
  };

  // 3. Proceed to the actual controller
  next();
};


// How to use it in your Express app
// app.post('/api/orders', idempotencyMiddleware, createOrderController);

نصائح من قلب الميدان: خبرة أبو عمر

  • أين يُولّد المفتاح؟ دائماً وأبداً في طرف العميل (Client-side). لا تجعل الخادم “يخمن” أن الطلب مكرر بناءً على محتوياته. اجعل الأمر صريحاً ومبنياً على مفتاح يرسله العميل.
  • كم مدة تخزين المفتاح؟ لا تخزنه للأبد! هذا سيؤدي إلى تضخم قاعدة بياناتك أو ذاكرة Redis بلا داعي. فترة 24 ساعة تعتبر معياراً جيداً ومقبولاً في معظم الحالات. بعد 24 ساعة، من الآمن افتراض أن المستخدم لن يعيد إرسال نفس المعاملة.
  • ماذا عن أخطاء الشبكة؟ هذه هي إحدى أروع فوائد هذا النمط. تخيل أن الخادم نفذ العملية بنجاح، لكن الاتصال انقطع قبل أن تصل الاستجابة للعميل. العميل سيعتقد أن العملية فشلت وسيحاول مرة أخرى. بوجود مفتاح الـ Idempotency، المحاولة الثانية لن تنفذ العملية مرة أخرى، بل ستعيد إرسال الاستجابة الناجحة المخزنة، وهذا بالضبط ما نريده.
  • “خاوا” (إجبارياً): في فريقي، أصبح استخدام مفتاح Idempotency “خاوا” (إجبارياً) لأي نقطة نهاية (endpoint) تقوم بعملية إنشاء أو تعديل حساسة وغير قابلة للعكس بسهولة. لا تعتبره رفاهية، بل هو أساس لنظام قوي وموثوق.

الخلاصة والزبدة 🥑

في عالم اليوم، حيث نتوقع من الأنظمة أن تكون متاحة وموثوقة 100% من الوقت، لم تعد المشاكل مثل الطلبات المكررة مقبولة. المستخدم لا يرحم، وسمعة تطبيقك على المحك.

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

نصيحتي الأخيرة لك: ما تستنى المشكلة تصير معك وتتعلم بالطريقة الصعبة زي ما صار معنا. كن استباقياً، وافهم هذا المفهوم جيداً، وادمجه في تصميم الواجهات البرمجية (APIs) الحساسة عندك من اليوم. صدقني، راحة بالك وتقدير عملائك يستحقان هذا الجهد البسيط. 👍

أبو عمر

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

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

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

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

آخر المدونات

تجربة المستخدم والابداع البصري

كان كل فريق يغني على ليلاه: كيف أنقذ “نظام التصميم” مشروعنا من الفوضى البصرية؟

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

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

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

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

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

فاتورتنا السحابية كانت صندوقاً أسود: كيف أنقذتنا ثقافة ‘FinOps’ من جحيم الإنفاق غير المبرر؟

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

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

كان ملفي الشخصي على GitHub مدينة أشباح: كيف أنقذني ‘نموذج المساهمات الصغيرة’ من جحيم الرفض الصامت؟

أشارككم قصتي مع الرفض الصامت من الشركات وكيف حولت ملفي الشخصي على GitHub من أرض بور إلى واحة خضراء تثبت مهاراتي. اكتشفوا معي "نموذج المساهمات...

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

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

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

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

من سجون البيانات إلى ثورة التكنولوجيا المالية: قصتي مع الخدمات المصرفية المفتوحة (Open Banking)

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

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

كانت مفاتيحنا في ملفات نصية: كيف أنقذنا نظام إدارة الأسرار من جحيم التسريبات؟

أروي لكم قصة حقيقية من قلب المعركة البرمجية، كيف انتقلنا من فوضى تخزين كلمات المرور والمفاتيح في ملفات نصية إلى نظام آمن ومؤتمت. هذه المقالة...

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

اجتماعاتنا كانت تسرق وقتنا: كيف أنقذتنا ‘مصفوفة الأولويات’ من جحيم الاجتماعات غير المنتجة؟

كنا نغرق في بحر من الاجتماعات التي لا تنتهي، حتى أوشك مشروعنا على الانهيار. في هذه المقالة، أشارككم قصة حقيقية من تجربتي كأبو عمر، وكيف...

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