نقرة مزدوجة وثروة ضائعة: كيف أنقذت مفاتيح عدم التكرار (Idempotency Keys) مشروعنا؟

يا أهلاً وسهلاً فيكم، معكم أبو عمر.

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

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

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

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

ما هي الـ Idempotency؟ (فلسفة الكبسة الثانية الآمنة)

قبل ما نحكي عن المفاتيح، لازم نفهم المبدأ نفسه. كلمة “Idempotent” كلمة إنجليزية فاخرة شوي، بس معناها بسيط جداً.

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

بالمقابل، تخيل إنك بتسحب مصاري من الصراف الآلي. لو طلبت سحب 100 دينار، رح يطلعلك 100 دينار. لو كررت العملية مرة ثانية، رح يطلعلك 100 دينار ثانية، ويصير مجموع المسحوب 200. هذه العملية non-idempotent، لأن تكرارها بغير النتيجة النهائية.

في عالم الـ APIs، نفس المبدأ بينطبق على طلبات الشبكة (HTTP Requests):

  • طلبات Idempotent (آمنة للتكرار): مثل GET, PUT, DELETE. لو طلبت بيانات مستخدم (GET) ألف مرة، رح تجيك نفس البيانات. لو حدثت (PUT) عنوانه بنفس البيانات الجديدة ألف مرة، النتيجة النهائية وحدة.
  • طلبات Non-idempotent (خطرة عند التكرار): وأشهرها هو الـ POST. هذا الطلب يُستخدم عادةً لإنشاء شيء جديد. إرسال طلب POST لإنشاء مستخدم جديد مرتين، يعني إنشاء مستخدمين اثنين. إرساله لعملية دفع مرتين، يعني خصم المبلغ مرتين!

جحيم المعاملات المكررة: ليش المشكلة كبيرة؟

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

  • انقطاع مؤقت في الشبكة: العميل أرسل الطلب، بس ما وصله الرد من السيرفر بسبب مشكلة نت. طبيعي إنه يحاول يرسل الطلب مرة ثانية.
  • مهلة الطلب (Request Timeout): السيرفر أخذ وقت أطول من المعتاد ليرد، فالمتصفح أو العميل اعتبر إنه الطلب فشل وقام بإعادة إرساله تلقائيًا.
  • آليات إعادة المحاولة (Retry Mechanisms): كثير من المكتبات البرمجية والأنظمة مصممة لتعيد إرسال الطلبات الفاشلة تلقائيًا.

والنتائج كارثية، زي ما شفنا بقصتنا:

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

مفتاح عدم التكرار (Idempotency Key): المنقذ الخارق

الحل يكمن في آلية بسيطة وفعالة جدًا تسمى “مفتاح عدم التكرار” أو Idempotency Key. الفكرة هي إعطاء “بصمة” فريدة لكل عملية خطرة، حتى لو تم إرسالها 100 مرة.

كيف يعمل؟

الآلية بتشتغل بالخطوات التالية:

  1. العميل (Client): قبل إرسال الطلب الحساس (مثل طلب الدفع)، يقوم العميل بإنشاء معرّف فريد وخاص بهذه العملية. عادةً ما يكون سلسلة نصية عشوائية قوية مثل UUID (Universally Unique Identifier).
  2. الإرسال: يرسل العميل هذا المعرّف الفريد كجزء من الطلب، وغالبًا ما يكون في الـ Headers تحت اسم مثل Idempotency-Key.
  3. الخادم (Server): عند استقبال الطلب، يقوم الخادم بالآتي:
    • يبحث عن الـ Idempotency-Key في الـ headers.
    • يتحقق في مخزن مؤقت (Cache) أو قاعدة بيانات خاصة لديه: “هل رأيت هذا المفتاح من قبل؟”.
    • إذا كان المفتاح جديدًا:
      1. “يقفل” هذا المفتاح لمنع أي عملية أخرى من استخدامه في نفس اللحظة (لمنع الـ Race Conditions).
      2. ينفذ العملية المطلوبة (مثلاً، يقوم بخصم المبلغ).
      3. يخزن نتيجة العملية (الرد الناجح أو الفاشل) مع المفتاح نفسه.
      4. يرسل الرد للعميل.
    • إذا كان المفتاح موجودًا مسبقًا:
      1. لا يقوم بتنفيذ العملية مرة أخرى أبدًا.
      2. ببساطة، يسترجع الرد الذي خزنه في المرة الأولى ويرسله مرة أخرى للعميل، وكأن العملية تمت للتو.

بهذه الطريقة، نضمن أن العملية التي تحمل بصمة فريدة (المفتاح) ستُنفذ مرة واحدة فقط، مهما حاول العميل إرسالها.

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

الحكي النظري حلو، بس خلينا نشوف كيف ممكن نطبق هالكلام. رح نستخدم مثال بسيط: نظام بلغة JavaScript، مع Express.js على الخادم.

مثال 1: جانب العميل (Client-Side)

في طرف العميل (سواء كان متصفح ويب أو تطبيق موبايل)، نحتاج لمكتبة لتوليد UUIDs مثل uuid.

// 1. قم بتثبيت المكتبة
// npm install uuid

// 2. في كود الجافاسكربت الخاص بك
import { v4 as uuidv4 } from 'uuid';

async function processPayment(paymentData) {
  // إنشاء مفتاح فريد لهذه العملية *فقط*
  const idempotencyKey = uuidv4();

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

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

// استدعاء الدالة عند ضغط المستخدم على زر الدفع
const paymentButton = document.getElementById('pay-btn');
paymentButton.addEventListener('click', () => {
    const data = { amount: 100, currency: 'USD' };
    processPayment(data);
});

مثال 2: جانب الخادم (Server-Side) – Middleware في Express.js

هنا يكمن السحر الحقيقي. سنقوم بإنشاء “برنامج وسيط” (Middleware) يعترض الطلبات قبل أن تصل إلى منطق الدفع الرئيسي.

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

const express = require('express');
const app = express();
app.use(express.json());

// مخزن بسيط للمفاتيح والردود في الذاكرة (للتوضيح فقط)
// في الإنتاج، استخدم Redis أو قاعدة بيانات!
const idempotencyStore = new Map();

// هذا هو الـ Middleware السحري
const idempotencyMiddleware = (req, res, next) => {
  // نبحث فقط في الطلبات التي قد تغير البيانات مثل POST
  if (req.method !== 'POST') {
    return next();
  }

  const idempotencyKey = req.get('Idempotency-Key');

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

  // هل رأينا هذا المفتاح من قبل؟
  if (idempotencyStore.has(idempotencyKey)) {
    console.log(`[Idempotency] Key ${idempotencyKey} seen before. Returning cached response.`);
    const cachedResponse = idempotencyStore.get(idempotencyKey);
    // أرجع الرد المخزن فورًا
    return res.status(cachedResponse.statusCode).json(cachedResponse.body);
  }

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

// تطبيق الـ Middleware على مساراتنا الحساسة
app.use('/api/pay', idempotencyMiddleware);

app.post('/api/pay', (req, res) => {
  const { amount, currency } = req.body;

  // هنا يتم تنفيذ منطق الدفع الحقيقي (التواصل مع بوابة الدفع، الخ)
  // سنقوم بمحاكاة عملية تأخذ بعض الوقت
  console.log(`Processing payment for ${amount} ${currency}...`);
  setTimeout(() => {
    const paymentResult = { transactionId: `txn_${Date.now()}`, status: 'success' };
    res.status(201).json(paymentResult);
  }, 2000); // محاكاة تأخير لمدة ثانيتين
});

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

الآن، إذا أرسلت طلبين متتاليين بنفس الـ Idempotency-Key، ستلاحظ في سجلات الخادم أن عملية الدفع “Processing payment…” ستُطبع مرة واحدة فقط! الطلب الثاني سيحصل على الرد فورًا من الذاكرة المؤقتة.

نصائح أبو عمر الذهبية (خبرة سنين يا خال)

  • لا تخترع العجلة: قبل أن تبني هذا النظام بنفسك، تحقق من مزود الخدمة الذي تتعامل معه. معظم بوابات الدفع المحترمة (مثل Stripe) لديها دعم مدمج لـ Idempotency Keys. اقرأ وثائقهم أولاً، فهذا يوفر عليك الكثير من العمل.
  • توليد المفاتيح: يجب أن يتم توليد المفتاح من جهة العميل (Client) لضمان أن كل محاولة “أصلية” لها مفتاحها الخاص. استخدم دائمًا خوارزميات قوية مثل UUID v4 لتجنب تضارب المفاتيح.
  • عمر المفتاح (Key Expiration): لا تخزن المفاتيح إلى الأبد! هذا سيؤدي إلى تضخم قاعدة بياناتك أو الـ Cache بلا فائدة. أفضل الممارسات هي تحديد فترة صلاحية للمفتاح، مثلاً 24 ساعة. هذا كافٍ للتعامل مع معظم محاولات الإعادة.
  • أين تخزن المفاتيح؟: المثال أعلاه يستخدم الذاكرة، وهذا لا يصلح للأنظمة الحقيقية. إذا كان لديك أكثر من خادم (load balancer)، فلن تعمل الذاكرة. الحل الصحيح هو استخدام مخزن مشترك وسريع مثل Redis. إنه مثالي لهذه المهمة.
  • التعامل مع الحالات الحرجة (Race Conditions): ماذا لو وصل طلبان بنفس المفتاح الجديد في نفس الملي ثانية؟ يجب أن يكون لديك آلية قفل (locking). عند استلام مفتاح جديد، يجب أن “تقفله” في Redis (مثلاً باستخدام SETNX)، بحيث إذا حاول خادم آخر معالجة نفس المفتاح في نفس الوقت، سيفشل في الحصول على القفل وينتظر أو يرجع خطأ.

خلاصة الكلام

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

لا تستهينوا بقوة النقرة المزدوجة، ففي عالم البرمجة، قد تكلفك سمعة وثروة. طبقوا مفاتيح عدم التكرار في أنظمتكم الحساسة، وناموا قريري العين. 😉

أبو عمر

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

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

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

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

آخر المدونات

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

كانت بيئاتنا تتغير من وراء ظهورنا: كيف أنقذتنا ‘البنية التحتية كشيفرة’ (IaC) من جحيم الانحراف التكويني؟

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

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

من مبرمج شبح إلى خبير مطلوب: كيف أنقذني “البناء في العلن” من الغموض الوظيفي؟

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

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

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

كنا نلاحق أشباح المحتالين الذين يسبقوننا دائمًا بخطوة، حتى اكتشفنا قوة نماذج كشف الحالات الشاذة (Anomaly Detection). في هذه المقالة، أشارككم كـ "أبو عمر" رحلتنا...

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

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

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

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

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

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

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

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

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

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

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

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

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