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

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

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

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

ما هي مشكلة “عدم تكرار الطلب” (Idempotency)؟

في عالم الـ APIs، مصطلح “Idempotent” يعني أن تنفيذ نفس العملية عدة مرات يعطي نفس النتيجة كما لو نُفذت مرة واحدة فقط. فكر فيها: لو طلبت من الخادم “أعطني بيانات المستخدم رقم 5” (عملية GET)، يمكنك أن تطلبها مليون مرة وستحصل دائمًا على نفس البيانات دون تغيير أي شيء في النظام. هذا الطلب بطبيعته Idempotent.

لكن المشكلة تظهر في العمليات التي تغير حالة النظام (Mutative requests)، مثل:

  • POST: لإنشاء مورد جديد (مثل إنشاء طلبية جديدة، أو إجراء عملية دفع).
  • PUT / PATCH: لتحديث مورد موجود.
  • DELETE: لحذف مورد.

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

الحل السحري: مفاتيح عدم تكرار الطلب (Idempotency Keys)

هنا يأتي دور “مفتاح عدم تكرار الطلب” أو الـ Idempotency Key. الفكرة بسيطة بعبقريتها: بدلًا من أن يكون الخادم “أهبل” وينفذ كل ما يأتيه، نجعله يتذكر الطلبات التي نفذها مؤخرًا.

المفتاح هو عبارة عن قيمة فريدة من نوعها (unique identifier) يقوم العميل (المتصفح أو تطبيق الموبايل) بإنشائها لكل عملية “حساسة” يرغب في تنفيذها. ثم يرسل هذا المفتاح مع الطلب، عادةً في ترويسة (Header) خاصة مثل Idempotency-Key.

كيف تعمل هذه المفاتيح؟ (خطوة بخطوة)

عندما يستقبل الخادم طلبًا يحتوي على Idempotency Key، يقوم بالآتي:

  1. التحقق من المفتاح: يبحث الخادم في مخزن مؤقت (cache) أو قاعدة بيانات عن هذا المفتاح.
  2. سيناريو (أ): المفتاح جديد ولم يُرَ من قبل.
    • ينفذ الخادم العملية المطلوبة (مثلاً، خصم المبلغ).
    • يخزن نتيجة العملية (سواء نجحت أم فشلت) مع المفتاح الأصلي.
    • يرسل الرد النهائي إلى العميل.
  3. سيناريو (ب): المفتاح موجود ومكرر.
    • يتجاهل الخادم تنفيذ العملية المطلوبة تمامًا.
    • يسترجع النتيجة الأصلية المخزنة والمربوطة بهذا المفتاح.
    • يرسل نفس الرد الأصلي إلى العميل مرة أخرى.

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

تطبيق عملي: لنكتب بعض الكود

دعنا نرى مثالاً بسيطاً باستخدام Node.js و Express. هذا مجرد مثال توضيحي، في التطبيقات الحقيقية ستحتاج إلى حلول أكثر قوة مثل Redis للتخزين المؤقت.

أولاً: جانب العميل (Client-Side)

قبل إرسال الطلب، نقوم بإنشاء مفتاح فريد. مكتبة مثل uuid ممتازة لهذا الغرض.


// لنفترض أنك تستخدم fetch في المتصفح
// 1. قم بتثبيت مكتبة UUID أو استخدم crypto المدمج
// npm install uuid
import { v4 as uuidv4 } from 'uuid';

async function processPayment() {
  const idempotencyKey = uuidv4(); // إنشاء مفتاح فريد لكل محاولة دفع

  try {
    const response = await fetch('/api/pay', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey // إرسال المفتاح في الترويسة
      },
      body: JSON.stringify({
        amount: 100,
        currency: 'USD',
        cardToken: 'tok_visa'
      })
    });

    const data = await response.json();
    console.log('Payment successful:', data);
  } catch (error) {
    console.error('Payment failed:', error);
    // هنا يمكنك تطبيق منطق إعادة المحاولة بنفس المفتاح
  }
}

ثانياً: جانب الخادم (Server-Side Middleware)

سنكتب “وسيطًا” (Middleware) في Express يعترض الطلبات ويطبق منطق عدم التكرار.


// على الخادم (server.js)
const express = require('express');
const app = express();

// للتوضيح فقط، في الواقع استخدم Redis أو ما شابه
const idempotencyStore = new Map();

// Middleware للتحقق من عدم التكرار
const idempotencyMiddleware = async (req, res, next) => {
  const idempotencyKey = req.headers['idempotency-key'];

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

  // إذا كان المفتاح موجودًا في المخزن، أرجع النتيجة المخزنة
  if (idempotencyStore.has(idempotencyKey)) {
    console.log(`[${idempotencyKey}] Request already processed. Returning cached response.`);
    const cachedResponse = idempotencyStore.get(idempotencyKey);
    return res.status(cachedResponse.statusCode).json(cachedResponse.body);
  }

  // إذا كان المفتاح جديدًا، نحتاج لتخزين الاستجابة بعد اكتمالها
  const originalJson = res.json;
  const originalStatus = res.status;

  res.status = (statusCode) => {
    res.statusCode = statusCode;
    return res;
  };
  
  res.json = (body) => {
    const responseToCache = { statusCode: res.statusCode || 200, body };
    idempotencyStore.set(idempotencyKey, responseToCache);
    
    // من الجيد تحديد عمر للمفتاح (مثلاً 24 ساعة)
    setTimeout(() => idempotencyStore.delete(idempotencyKey), 24 * 60 * 60 * 1000);
    
    res.json = originalJson; // استعادة الدالة الأصلية
    return res.status(responseToCache.statusCode).json(body);
  };

  next();
};

app.post('/api/pay', idempotencyMiddleware, (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'];
  console.log(`[${idempotencyKey}] Processing new payment request...`);

  // ... هنا منطق الدفع الفعلي مع بوابة الدفع ...
  // ... محاكاة لعملية ناجحة ...
  
  const paymentResult = { 
    success: true, 
    transactionId: `txn_${Date.now()}` 
  };

  res.status(201).json(paymentResult);
});

app.listen(3000, () => console.log('Server running on port 3000'));

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

بعد سنوات من التعامل مع هذه الأنظمة، اسمحوا لي أن أقدم لكم بعض النصائح العملية:

1. اختر المفتاح بعناية

يجب أن يكون المفتاح فريدًا عالميًا (Universally Unique) ويتم إنشاؤه من طرف العميل. استخدام UUID (v4) هو الخيار الأمثل. لا تستخدم أبدًا بيانات قد تتكرر مثل اسم المستخدم أو وقت الطلب وحده.

2. أين تخزن المفاتيح؟

المثال أعلاه يستخدم Map في الذاكرة، وهذا لا يصلح للإنتاج لأنه سيفقد كل شيء عند إعادة تشغيل الخادم ولا يعمل في بيئة متعددة الخوادم (multi-instance). الحل الأفضل هو استخدام مخزن خارجي سريع مثل Redis أو Memcached. ولا تنسَ وضع تاريخ انتهاء صلاحية (TTL) للمفتاح (مثلاً 24 ساعة) لتجنب امتلاء الذاكرة إلى الأبد.

3. انتبه لحالة السباق (Race Condition)

ماذا لو وصل طلبان بنفس المفتاح في نفس الملي ثانية؟ قد يتجاوز كلاهما فحص “هل المفتاح موجود؟” ويبدآن في تنفيذ العملية. الحل هو استخدام آلية قفل (locking). عند استلام مفتاح جديد، يجب “قفل” هذا المفتاح في Redis، ثم تنفيذ العملية، ثم تخزين النتيجة، وأخيرًا تحرير القفل. هذا يضمن أن طلبًا واحدًا فقط هو الذي سينفذ العملية.

4. ليست كل الطلبات بحاجة لهذا التعقيد

هذه التقنية ضرورية للعمليات التي تغير البيانات (POST, PUT, PATCH, DELETE) والتي لا يمكن تكرارها بأمان. أما طلبات GET و OPTIONS و HEAD فهي بطبيعتها آمنة للتكرار (idempotent)، فلا داعي لتعقيدها.


الخلاصة: نقرة واحدة، عملية واحدة 👍

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

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

أبو عمر

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

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

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

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

آخر المدونات

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

كانت واجهاتنا تتصرف بعشوائية: كيف أنقذتنا ‘آلات الحالة المحدودة’ (State Machines) من جحيم الفوضى؟

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

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

كانت صفحاتنا تطلب مئات الاستعلامات: كيف أنقذنا ‘التحميل الشغوف’ (Eager Loading) من جحيم مشكلة N+1؟

أشارككم قصة حقيقية من الميدان، يوم كادت إحدى صفحات موقعنا أن تنهار تحت وطأة مئات استعلامات قاعدة البيانات. سأشرح لكم بالتفصيل مشكلة N+1 وكيف كان...

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

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

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

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

كانت ذروة الاستخدام تقتل خوادمنا: كيف أنقذنا ‘تحديد المعدل’ (Rate Limiting) من جحيم الانهيار الكامل؟

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

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

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

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

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

من سيرفرات “الدلال” إلى جيش من المستنسخات: كيف أنقذتنا البنية التحتية كشيفرة (IaC)؟

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

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

كان أفضل مهندسينا يغادرون: كيف أنقذنا ‘مسار النمو المزدوج’ من جحيم فقدان الخبرات؟

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

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