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

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

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

رن تلفوني نص الليل، وإذا هو المدير التقني صوتُه مكركب. “أبو عمر، الحقنا! في مصيبة!”. نزلت جري على المكتب، ولقيت الدنيا والعة. لوحة التحكم تبعتنا بتعرض عمليات دفع مكررة لنفس المستخدمين، والمبالغ بالآلاف. يعني العميل بيحاول يشتري غرض بـ 100 دولار، النظام بيخصم منه 200 أو 300 دولار!

بعد ساعات من التحليل والتدقيق، اكتشفنا أصل المشكلة. بعض المستخدمين، بسبب ضعف الإنترنت عندهم، كانوا يضغطوا على زر “ادفع الآن” مرتين أو ثلاث بسرعة. وفي حالات أخرى، كانت الشبكة تعمل “Timeout” عند العميل، فيظن أن العملية فشلت ويعيد المحاولة، بينما طلبُه الأول كان قد وصل لخوادمنا وقيد التنفيذ. النتيجة؟ كارثة. طلبات `POST` مكررة لنفس عملية الدفع، والنظام تبعنا الغلبان كان ينفذها كلها بدون ما يميز.

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

ما هي الـ “Idempotency”؟ (شو يعني هالحكي؟)

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

في عالم الـ REST APIs، هذا المفهوم حيوي جدًا. بعض طلبات HTTP هي “Idempotent” بطبيعتها:

  • GET: لما تطلب بيانات منتج، لو طلبتها ألف مرة، رح تحصل على نفس البيانات (إلا إذا تغيرت على الخادم، وهذا موضوع آخر).
  • PUT: لما تحدث بيانات مستخدم كاملة وتقول “اجعل الاسم = أحمد والعمر = 30″، لو أرسلت هذا الطلب ألف مرة، سيبقى الاسم “أحمد” والعمر “30”.
  • DELETE: لما تحذف المنتج رقم 123، أول طلب سيحذفه، وكل الطلبات التالية ستجد أنه محذوف أصلًا، فالنتيجة النهائية (المنتج 123 غير موجود) واحدة.

لكن المصيبة الكبرى تكمن في طلبات POST. هذا النوع من الطلبات بطبيعته غير Idempotent. كل طلب `POST` لـ `/orders` يُفترض به إنشاء طلب جديد. وهذا بالضبط ما كان يحدث معنا في قصة نظام الدفع.

جحيم الطلبات المكررة: وين بتصير المشكلة؟

المشكلة لا تقتصر على نقرة المستخدم المزدوجة. هناك عدة سيناريوهات تؤدي إلى طلبات مكررة كارثية:

  1. أخطاء الشبكة: العميل يرسل طلبًا، لكنه لا يتلقى ردًا بسبب انقطاع مؤقت في الشبكة. بشكل تلقائي، قد تعيد مكتبة الشبكات (Retry Logic) إرسال الطلب مرة أخرى.
  2. نقرات المستخدم المتعددة: المستخدم قليل الصبر الذي يضغط على الزر كأنه يشارك في سباق.
  3. تحديث الصفحة: المستخدم يقوم بعملية شراء، ثم يضغط على زر التحديث (Refresh) في المتصفح، مما قد يؤدي إلى إعادة إرسال بيانات النموذج (Form).
  4. منطق العميل (Client-side Logic): أحيانًا يكون الخطأ في كود الواجهة الأمامية الذي يرسل الطلب أكثر من مرة عن غير قصد.

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

مفتاح الخلاص: الـ Idempotency-Key

الحل الذي تبنته الشركات الكبرى مثل Stripe وPaypal هو نمط تصميم بسيط وعبقري: استخدام ما يسمى بـ “مفتاح اللامتغيرة” أو Idempotency-Key. الفكرة هي أن يقوم العميل (التطبيق أو المتصفح) بإنشاء مُعرّف فريد لكل عملية حساسة يريد ضمان عدم تكرارها.

كيف يعمل هذا المفتاح السحري؟

الآلية تسير كالتالي:

  1. العميل (Client): قبل إرسال طلب `POST` لإنشاء دفعة جديدة، يقوم العميل بإنشاء سلسلة نصية فريدة (مثل UUID). لنسميها `key-123`.
  2. إرسال الطلب: يرسل العميل طلب `POST` كالمعتاد، ولكنه يضيف “هيدر” (Header) خاصًا اسمه Idempotency-Key ويضع فيه القيمة `key-123`.
  3. الخادم (Server) – المرة الأولى:
    • يستقبل الخادم الطلب ويرى الهيدر Idempotency-Key: key-123.
    • يبحث في قاعدة بيانات مؤقتة (مثل Redis) أو جدول خاص: “هل رأيت هذا المفتاح من قبل؟”.
    • الجواب “لا”. إذن، هذه عملية جديدة.
    • يقوم الخادم بتنفيذ العملية (مثل إنشاء الدفعة)، ثم يحفظ نتيجة العملية (مثلاً، “نجحت العملية، رقم الفاتورة 555”) ويربطها بالمفتاح `key-123`.
    • يرسل الرد الناجح للعميل.
  4. الخادم (Server) – المرة الثانية (الطلب المكرر):
    • بعد ثوانٍ، يصل طلب `POST` آخر بنفس البيانات ونفس الهيدر Idempotency-Key: key-123.
    • يبحث الخادم مرة أخرى: “هل رأيت هذا المفتاح من قبل؟”.
    • الجواب “نعم”.
    • هنا يكمن السحر: الخادم لا ينفذ العملية مرة أخرى. بل يذهب مباشرة إلى النتيجة المحفوظة سابقًا والمرتبطة بالمفتاح `key-123` ويرسلها مرة أخرى للعميل.

النتيجة؟ العميل قام بالدفع مرة واحدة فقط. الخادم ضمن عدم تكرار العملية. والمستخدم في كلتا الحالتين حصل على رد يفيد بنجاح العملية، دون أي خصومات إضافية. مشكلة بآلاف الدولارات حُلّت بسطرين كود ومفهوم أنيق.

مثال عملي: هيا نطبق المفهوم بالكود

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

جانب العميل (Client-Side – JavaScript)

عندما يضغط المستخدم على زر الدفع، سنقوم بتوليد مفتاح فريد باستخدام مكتبة مثل uuid.


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

import { v4 as uuidv4 } from 'uuid';

async function processPayment(paymentData) {
  // 1. توليد مفتاح فريد لهذه المحاولة
  const idempotencyKey = uuidv4();

  try {
    const response = await fetch('/api/payments', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // 2. إرسال المفتاح في الهيدر
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify(paymentData),
    });

    if (!response.ok) {
      // تعامل مع أخطاء الخادم
      throw new Error('Network response was not ok.');
    }

    const result = await response.json();
    console.log('Payment successful:', result);
    // يمكنك هنا تعطيل زر الدفع لمنع المزيد من النقرات
    
  } catch (error) {
    console.error('Payment failed:', error);
    // هنا يمكنك السماح للمستخدم بإعادة المحاولة، والتي ستولد مفتاحًا جديدًا
    // أو إذا كنت تريد إعادة نفس الطلب بنفس المفتاح، يجب أن تحفظ المفتاح
  }
}

جانب الخادم (Server-Side – Node.js/Express Middleware)

هنا العمل الحقيقي. سنبني “وسيطًا” (Middleware) يعترض الطلبات قبل أن تصل إلى منطق العمل الرئيسي.

ملاحظة: هذا المثال للتوضيح. في بيئة الإنتاج، ستستخدم قاعدة بيانات سريعة مثل Redis لتخزين المفاتيح ونتائجها مع تحديد مدة صلاحية (TTL).


// مثال مبسط باستخدام ذاكرة التطبيق (لا تستخدمه في الإنتاج!)
const idempotencyCache = new Map();

const idempotencyMiddleware = async (req, res, next) => {
  // هذا الوسيط يعمل فقط على طلبات POST
  if (req.method !== 'POST') {
    return next();
  }
  
  const key = req.get('Idempotency-Key');

  // إذا لم يتم توفير المفتاح، استمر كالمعتاد
  if (!key) {
    return next();
  }

  // 1. هل رأينا هذا المفتاح من قبل؟
  if (idempotencyCache.has(key)) {
    console.log(`Idempotency HIT for key: ${key}`);
    const cachedResponse = idempotencyCache.get(key);
    // 2. إذا نعم، أرجع النتيجة المحفوظة
    return res.status(cachedResponse.statusCode).json(cachedResponse.body);
  }

  // للتغلب على حالة وصول طلبين بنفس المفتاح في نفس اللحظة (Race Condition)
  // يجب إضافة حالة "قيد المعالجة"
  if (idempotencyCache.has(`${key}-processing`)) {
      return res.status(429).json({ message: 'Request is already being processed.' });
  }
  idempotencyCache.set(`${key}-processing`, true);


  // نلتقط الاستجابة الأصلية قبل إرسالها
  const originalJson = res.json;
  const originalStatus = res.status;
  let responseBody;
  let responseStatus;

  res.status = (statusCode) => {
      responseStatus = statusCode;
      return originalStatus.call(res, statusCode);
  };
  
  res.json = (body) => {
    responseBody = body;
    return originalJson.call(res, body);
  };

  // بعد انتهاء الطلب، نحفظ النتيجة
  res.on('finish', () => {
    if (responseStatus && responseBody) {
        console.log(`Idempotency CACHING response for key: ${key}`);
        // 3. نحفظ النتيجة مع المفتاح
        idempotencyCache.set(key, {
            statusCode: responseStatus,
            body: responseBody,
        });
        // إزالة علامة "قيد المعالجة"
        idempotencyCache.delete(`${key}-processing`);
        
        // في نظام حقيقي، ضع مدة صلاحية للمفتاح (مثلاً، 24 ساعة)
        setTimeout(() => idempotencyCache.delete(key), 24 * 60 * 60 * 1000);
    }
  });

  // 4. إذا كان المفتاح جديدًا، اسمح للطلب بالمرور إلى منطق العمل الرئيسي
  next();
};

// ... في ملف تطبيق Express الرئيسي
app.use(idempotencyMiddleware);

app.post('/api/payments', (req, res) => {
  // ... هنا منطق العمل الحقيقي لإنشاء الدفعة في قاعدة البيانات ...
  // هذا الكود سيعمل مرة واحدة فقط لكل مفتاح
  const payment = createPaymentInDB(req.body);
  res.status(201).json({ status: 'success', paymentId: payment.id });
});

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

  • لا تستخدمه لكل شيء: تطبيق هذا النمط يضيف بعض التعقيد. استخدمه فقط للعمليات الحساسة غير المتغيرة (Non-idempotent) مثل إنشاء الموارد (`POST`) التي لها تبعات مالية أو تشغيلية.
  • مدة صلاحية المفتاح: لا تحتفظ بالمفاتيح إلى الأبد. هذا يستهلك الذاكرة. مدة 24 ساعة كافية لمعظم الحالات. بعد 24 ساعة، إذا أعاد العميل إرسال نفس المفتاح، يمكنك اعتباره طلبًا جديدًا.
  • التوثيق هو صديقك: إذا كنت تبني API ليستخدمها آخرون، وثّق بوضوح أي نقاط نهاية (Endpoints) تدعم `Idempotency-Key` وكيفية استخدامها.
  • العميل هو المسؤول: منطق توليد المفتاح وإعادة المحاولة يجب أن يكون على عاتق العميل. الخادم فقط يستجيب للمفتاح المقدم له.

الخلاصة: نقرة واحدة آمنة أفضل من ألف نقرة كارثية 🧘‍♂️

يا جماعة، في عالمنا الرقمي سريع الخطى، الأخطاء الصغيرة يمكن أن تكون لها عواقب وخيمة. الاستثمار في بناء أنظمة قوية وقادرة على الصمود (Resilient) ليس رفاهية، بل هو ضرورة. مفهوم الـ Idempotency، ورغم بساطته، هو أحد أقوى الأدوات في جعبتنا كمطورين لضمان سلامة البيانات وموثوقية خدماتنا.

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

أبو عمر

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

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

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

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

آخر المدونات

التوسع والأداء العالي والأحمال

قاعدة بياناتنا كانت تنهار: كيف أنقذنا التخزين المؤقت (Caching) من جحيم الاستعلامات المتكررة؟

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

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

كانت بنيتنا التحتية قصرًا من ورق: كيف أنقذنا Terraform من جحيم الإعداد اليدوي؟

أشارككم تجربتي كـ "أبو عمر"، مبرمج فلسطيني، مع الفوضى التي تسببها إدارة السيرفرات اليدوية. سنكتشف معًا كيف حولت أداة Terraform بنيتنا التحتية من قصر ورقي...

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

كان أفضل مهندسينا يرحلون: كيف أنقذ “سلم المسار الوظيفي” شركتنا من جحيم الركود؟

أشارككم قصة حقيقية عن كيفية مواجهتنا لمشكلة "نزيف العقول" في فريقنا الهندسي. نستعرض بالتفصيل كيف قمنا ببناء "سلم مسار وظيفي" (Career Ladder) واضح وشفاف أنقذنا...

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