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

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

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

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

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

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

ما هي مشكلة الطلبات المكررة؟ ولماذا هي كابوس المطورين؟

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

  • مدفوعات مزدوجة: كما حدث معنا، وهي السيناريو الأخطر الذي يؤدي لخسائر مالية وفقدان ثقة المستخدم.
  • إنشاء حسابات مكررة: مستخدم يسجل في خدمتك، فيجد نفسه بحسابين بنفس البريد الإلكتروني بسبب طلب مكرر.
  • إرسال إشعارات مزدوجة: إرسال نفس البريد الإلكتروني أو الرسالة النصية مرتين، مما يزعج المستخدمين.
  • فساد البيانات (Data Corruption): إنشاء سجلات مكررة في قاعدة البيانات، مما يجعل التحليلات والتقارير كابوسًا حقيقيًا.

السبب في هذه المشاكل غالبًا ما يكون خارج عن سيطرة الخادم مباشرةً:

  1. نقرات المستخدم المزدوجة: المستخدم قليل الصبر هو القاعدة وليس الاستثناء.
  2. مشاكل الشبكة: قد يقوم المتصفح أو تطبيق الموبايل بإعادة إرسال الطلب تلقائيًا إذا لم يستلم ردًا في الوقت المناسب (Timeout).
  3. الـ Retries المنطقية: بعض المكتبات البرمجية (Libraries) مصممة لإعادة المحاولة تلقائيًا عند فشل الشبكة.

مفهوم الـ ‘Idempotency’: السحر الذي يضمن تنفيذ العملية مرة واحدة فقط

قبل ما نحكي عن الحل، لازم نفهم أصل المشكلة والحل النظري. في عالم الـ APIs والشبكات، مصطلح “Idempotent” يعني أن تكرار نفس الطلب عدة مرات يُنتج نفس النتيجة والتأثير الذي يُنتجه طلب واحد فقط.

لنبسطها بمثال من الحياة اليومية: زر استدعاء المصعد. لو ضغطت عليه مرة، سيتم استدعاء المصعد. لو ضغطت عليه عشر مرات متتالية، هل سيتم استدعاء عشرة مصاعد؟ لا، النتيجة النهائية واحدة، وهي استدعاء المصعد مرة واحدة. هذا السلوك هو الـ Idempotency.

في عالم HTTP، بعض الأفعال (Verbs) هي Idempotent بطبيعتها:

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

المشكلة الكبيرة تكمن في الفعل POST، الذي يُستخدم عادةً لإنشاء مورد جديد (Create). بطبيعته، هو غير Idempotent. كل طلب POST يُفترض به أن يُنشئ شيئًا جديدًا. ولهذا السبب، طلب POST لإنشاء دفعة مالية مرتين، يعني إنشاء دفعتين.

وهذا هو مربط الفرس: كيف نجعل عملية POST الحساسة تتصرف بشكل Idempotent؟

مفاتيح عدم التكرار (Idempotency Keys): كيف تعمل هذه الأداة السحرية؟

الحل يكمن في آلية بسيطة وفعالة جدًا تُعرف بـ “مفاتيح عدم التكرار”. الفكرة هي أن العميل (الواجهة الأمامية أو التطبيق) والخادم يتفقان على “كلمة سر” فريدة لكل عملية حساسة.

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

  1. العميل يُنشئ مفتاحًا فريدًا: قبل إرسال الطلب الحساس (مثل إنشاء دفعة)، يقوم العميل بتوليد سلسلة نصية فريدة تمامًا، غالبًا ما تكون من نوع UUID (Universally Unique Identifier). هذا هو “مفتاح عدم التكرار”.
  2. العميل يرسل المفتاح مع الطلب: يقوم العميل بإرفاق هذا المفتاح في ترويسة (Header) الطلب، تحت اسم متفق عليه، مثل Idempotency-Key.
  3. الخادم يستقبل الطلب ويفحص المفتاح:
    • عندما يستقبل الخادم الطلب، أول شيء يفعله هو قراءة قيمة Idempotency-Key.
    • ثم يبحث في مكان تخزين مؤقت (مثل Redis أو جدول في قاعدة البيانات) ليرى هل استقبل هذا المفتاح من قبل.
  4. سيناريوهان محتملان:
    • إذا كان المفتاح جديدًا: هذا يعني أنه الطلب الأول. يقوم الخادم بتنفيذ العملية كالمعتاد (مثلاً، معالجة الدفع). بعد أن ينتهي بنجاح، يقوم بتخزين نتيجة العملية (الرد + كود الحالة) ويربطها بالمفتاح الذي استقبله، ثم يرسل الرد للعميل.
    • إذا كان المفتاح موجودًا مسبقًا: هذا يعني أن الطلب مكرر! هنا، لا يقوم الخادم بتنفيذ العملية مرة أخرى أبدًا. بدلًا من ذلك، يذهب مباشرة إلى المكان الذي خزّن فيه النتيجة الأصلية، يسترجعها، ويرسلها مرة أخرى للعميل كما لو أن العملية تمت الآن.

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

مثال عملي: لنبنيها خطوة بخطوة

خلونا نشوف مثال بسيط جدًا يوضح الفكرة. سنستخدم شيئًا يشبه JavaScript للتعبير عن الفكرة.

أولًا: جهة العميل (Client-Side)

العميل هو المسؤول عن توليد المفتاح وإرساله. يمكن استخدام مكتبة مثل uuid لتوليد المفاتيح.


// في الواجهة الأمامية (React, Vue, أو حتى Vanilla JS)

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

import { v4 as uuidv4 } from 'uuid';

async function createPayment(paymentDetails) {
  // 1. نُنشئ مفتاحًا فريدًا لهذه العملية *فقط*
  const idempotencyKey = uuidv4();

  try {
    const response = await fetch('/api/payments', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // 2. نُرفق المفتاح في الترويسة (Header)
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify(paymentDetails),
    });

    if (!response.ok) {
      // هنا يمكنك إضافة منطق لإعادة المحاولة بنفس المفتاح
      console.error('فشل الطلب، يمكن إعادة المحاولة بنفس المفتاح');
    }

    return await response.json();
  } catch (error) {
    // إذا حدث خطأ في الشبكة، يمكن إعادة المحاولة بنفس المفتاح
    console.error('خطأ في الشبكة، يمكن إعادة المحاولة بنفس المفتاح', error);
  }
}

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

هنا يكمن السحر الحقيقي. سنكتب Middleware (برنامج وسيط) في إطار عمل مثل Express.js لاعتراض الطلبات ومعالجتها.


// في الخادم (Node.js/Express)

// سنستخدم كائن بسيط كمثال لقاعدة بيانات مؤقتة لتخزين المفاتيح.
// في الواقع، يجب استخدام Redis أو قاعدة بيانات حقيقية.
const idempotencyStore = new Map();

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

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

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

  // 1. نفحص إذا كان المفتاح موجودًا لدينا
  if (idempotencyStore.has(idempotencyKey)) {
    console.log(`[Idempotency] طلب مكرر بالمفتاح: ${idempotencyKey}. سنعيد الرد المخزن.`);
    
    // 2. إذا كان موجودًا، نعيد الرد المخزن مسبقًا
    const cachedResponse = idempotencyStore.get(idempotencyKey);
    return res.status(cachedResponse.statusCode).json(cachedResponse.body);
  }

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

  // نُعيد تعريف دالة res.json لنتمكن من اعتراض الرد وتخزينه
  res.json = (body) => {
    const responseToCache = {
      statusCode: res.statusCode,
      body: body,
    };
    console.log(`[Idempotency] طلب جديد بالمفتاح: ${idempotencyKey}. سنخزن الرد.`);
    
    // 3. نخزن الرد مع المفتاح
    idempotencyStore.set(idempotencyKey, responseToCache);
    
    // ثم نرسل الرد الأصلي
    originalJson.call(res, body);
  };
  
  // نكمل تنفيذ الطلب الأصلي
  next();
}

// استخدام الـ Middleware في تطبيق Express
app.use(idempotencyMiddleware);

// والآن، الـ Route الخاص بالدفع محمي!
app.post('/api/payments', (req, res) => {
  // ... منطق معالجة الدفع هنا ...
  // هذا الكود لن يُنفذ إلا مرة واحدة لكل مفتاح
  console.log('... جاري معالجة عملية دفع جديدة ...');
  const paymentResult = { status: 'success', transactionId: 'txn_12345' };
  
  res.status(201).json(paymentResult);
});

نصيحة من أبو عمر: المثال أعلاه للتوضيح فقط. في بيئة الإنتاج الحقيقية (Production)، لا تستخدم كائنًا أو Map في الذاكرة لتخزين المفاتيح، لأنها ستُفقد مع كل إعادة تشغيل للخادم. استخدم أداة تخزين خارجية ومستمرة مثل Redis، فهي سريعة جدًا ومثالية لهذه المهمة.

نصائح من خبرة أبو عمر: كيف تستخدم مفاتيح عدم التكرار كالمحترفين؟

تطبيق المفهوم بشكل صحيح يتطلب الانتباه لبعض التفاصيل الدقيقة.

تخزين الحالة: أين نحفظ المفاتيح والردود؟

كما ذكرت، Redis هو الخيار الأمثل لمعظم الحالات. إنه سريع جدًا (In-memory) ويوفر ميزات مثل تعيين مدة صلاحية للمفتاح (TTL) بسهولة. إذا لم يكن Redis متاحًا، يمكن استخدام جدول مخصص في قاعدة بياناتك الأساسية، ولكن كن حذرًا من تأثير ذلك على الأداء.

مدة الصلاحية: إلى متى يبقى المفتاح صالحًا؟

لا يمكنك تخزين المفاتيح إلى الأبد، وإلا ستنفجر قاعدة بياناتك أو ذاكرة Redis. من الضروري تعيين مدة صلاحية (Expiration Time / TTL). مدة 24 ساعة هي مدة شائعة ومناسبة لمعظم الحالات. هذا يعني أن الخادم “ينسى” المفتاح بعد 24 ساعة، وأي طلب جديد بنفس المفتاح بعد هذه المدة سيُعامل كطلب جديد تمامًا. هذا منطقي، فمن غير المحتمل أن يكون هناك طلب مكرر شرعي بعد يوم كامل.

ماذا عن الأخطاء؟

هذه نقطة مهمة جدًا. ماذا لو كان الطلب الأول الذي استلمته بالمفتاح X قد فشل بسبب خطأ في الخادم (500 Internal Server Error)؟ هل يجب أن تخزن هذا الخطأ وترجعه لكل الطلبات المكررة التالية؟

القاعدة العامة هي: لا تخزن أبدًا نتائج أخطاء الخادم (5xx). إذا فشل الخادم، يجب أن تترك المجال للعميل لإعادة المحاولة لاحقًا على أمل أن يكون الخادم قد تعافى. لكن، يمكنك تخزين أخطاء العميل (4xx)، مثل خطأ التحقق من صحة البيانات (400 Bad Request)، لأن هذا الخطأ سيتكرر بغض النظر عن عدد المحاولات إذا لم يقم العميل بتصحيح طلبه.

الخلاصة: طلب واحد، نتيجة واحدة، وراحة بال لا تقدر بثمن ✅

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

الدرس الذي تعلمته في تلك الليلة الباردة كان قاسيًا، ولكنه كان ثمينًا. لا تنتظروا حتى تحدث معكم “مصيبة الدفع المزدوج”. كونوا سبّاقين، وفكروا في كل السيناريوهات المحتملة، واستخدموا الأدوات والمفاهيم الصحيحة لبناء أنظمة قوية وقادرة على الصمود. تذكروا دائمًا: الوقاية خير من ألف جلسة Debugging في منتصف الليل.

أتمنى لكم التوفيق في مشاريعكم، ودمتم سالمين.

أبو عمر

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

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

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

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

آخر المدونات

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

كانت تحديثات قاعدة البيانات توقف خدمتنا: كيف أنقذتنا استراتيجيات ‘الهجرة بدون توقف’ (Zero-Downtime Migration)

أشارككم قصة حقيقية عن ليلة كابوسية بسبب تحديث قاعدة بيانات، وكيف تعلمنا بالطريقة الصعبة أهمية استراتيجيات الهجرة بدون توقف (Zero-Downtime Migration). اكتشفوا معنا التقنيات والخطوات...

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

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

من قلب المعاناة مع الارتهان لمزود سحابي واحد، أسرد لكم حكايتنا وكيف كانت استراتيجية "تعدد السحابات" (Multi-cloud) طوق النجاة. هذه ليست مجرد مقالة تقنية، بل...

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

كانت المقابلات التقنية كابوساً: كيف أنقذني ‘البروتوكول الخماسي’ من جحيم التجمّد أمام السبورة البيضاء؟

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

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

كانت عمليات الإطلاق تغرق خوادمنا: كيف أنقذنا “طابور الانتظار الافتراضي” من جحيم انهيار الخدمة؟

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

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

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

في عالم التكنولوجيا المالية، كانت بيانات بطاقات الدفع كنزًا للمخترقين وقنبلة موقوتة في أنظمتنا. في هذه المقالة، أشارككم قصة حقيقية وكيف كانت تقنية 'الترميز' (Tokenization)...

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

كانت بنيتنا التحتية تتغير في الظلام: كيف أنقذنا Terraform من جحيم ‘من غيّر هذا؟’

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

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

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

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

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

كانت الأخطاء الساذجة تصل إلى مستودعنا: كيف أنقذتنا ‘خطافات Git’ من جحيم ‘لقد نسيت تشغيل المدقق’؟

أشارككم قصة حقيقية عن كيف كانت الأخطاء البسيطة تسبب لنا صداعًا في الفريق، وكيف استخدمنا خطافات Git (Git Hooks) وأداة Husky لأتمتة فحوصات الجودة ومنع...

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