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

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

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

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

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

ما هي “عدم التكرار” (Idempotency) في عالم الـ APIs؟

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

أفضل مثال من حياتنا اليومية هو كبسة المصعد. لما تكبس على زر المصعد عشان تطلبه، هو بيستجيب من أول كبسة. لو ضليت تكبس على الزر عشر مرات زيادة، المصعد ما رح يجي أسرع ولا رح يصير أي شي جديد. الكبسة الأولى غيّرت حالة النظام (المصعد مطلوب)، وكل الكبسات اللي بعدها ما عملت أي تغيير إضافي. هذا هو جوهر الـ Idempotency.

في عالم الـ REST APIs، بعض طلبات الـ HTTP مصممة لتكون عديمة التكرار بطبيعتها:

  • GET, HEAD, OPTIONS, TRACE: هاي الطلبات آمنة وعديمة التكرار. طلب بيانات مستخدم 10 مرات رح يعطيك نفس البيانات كل مرة بدون ما يغير أي شي في النظام.
  • PUT: هذا الطلب عديم التكرار. لو أرسلت طلب PUT /users/123 لتحديث بيانات مستخدم معين بنفس البيانات 5 مرات، النتيجة النهائية رح تكون نفسها كأنك أرسلته مرة وحدة. هو بيستبدل المورد بالكامل.
  • DELETE: هذا الطلب عديم التكرار أيضاً. لو أرسلت طلب DELETE /posts/45، أول مرة رح يحذف المنشور. المرات اللي بعدها رح يرجعلك استجابة “Not Found 404″، لكن حالة النظام النهائية (المنشور محذوف) ما رح تتغير.

لكن وين المشكلة؟ المشكلة الكبيرة بتصير مع طلبات POST.

الكابوس: طلبات POST غير المتكررة

طلب الـ POST يُستخدم عادةً لإنشاء مورد جديد. كل طلب POST إلى نفس الـ endpoint يُفترض أن ينشئ مورداً جديداً ومنفصلاً. وهذا هو سبب الكارثة اللي صارت معنا:

  • النقرة الأولى: POST /payments -> تم إنشاء دفعة جديدة (وخصم المبلغ).
  • النقرة الثانية: POST /payments -> تم إنشاء دفعة جديدة أخرى (وخصم المبلغ مرة ثانية!).
  • النقرة الثالثة: POST /payments -> وهكذا دواليك…

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

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

بما أن طلب POST بطبيعته غير متكرر، لازم إحنا كمطورين نجعله يتصرف بطريقة متكررة في الحالات اللي بتتطلب هالشي. وهون بيجي دور “مفتاح عدم التكرار” أو الـ Idempotency Key.

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

عندما يستقبل الخادم (Server) الطلب، يقوم بالآتي:

  1. يقرأ المفتاح: يأخذ قيمة الـ Idempotency-Key من الهيدر.
  2. يبحث عن المفتاح: يبحث في مكان تخزين مؤقت (مثل قاعدة بيانات أو ذاكرة Redis) ليرى هل استقبل هذا المفتاح من قبل.
  3. السيناريو الأول (مفتاح جديد): إذا لم يجد المفتاح، فهذا يعني أن هذا طلب جديد. يقوم الخادم بالآتي:
    • يُخزن المفتاح فوراً ليمنع أي طلبات أخرى بنفس المفتاح من التنفيذ.
    • ينفذ العملية المطلوبة (مثلاً، خصم المبلغ).
    • يُخزن نتيجة الاستجابة (Response) التي سيرسلها للعميل (مثلاً، 201 Created مع تفاصيل الدفعة).
    • يرسل الاستجابة المخزنة للعميل.
  4. السيناريو الثاني (مفتاح مكرر): إذا وجد الخادم أن المفتاح موجود مسبقاً، فهذا يعني أن العملية قد تم تنفيذها من قبل. في هذه الحالة، الخادم لا ينفذ العملية مرة أخرى. بدلاً من ذلك، يقوم بالآتي:
    • يسترجع الاستجابة الأصلية التي خزنها عند تنفيذ الطلب لأول مرة.
    • يرسل نفس الاستجابة المخزنة مرة أخرى للعميل.

بهذه الطريقة، حتى لو أرسل العميل نفس الطلب 100 مرة بسبب خطأ شبكة أو نقرات متكررة، العملية الحساسة (الدفع) ستُنفذ مرة واحدة فقط! 🎯

لنطبق الأمر عملياً: مثال كود (يا مبرمجين!)

الحكي سهل، خلينا نشوف كيف ممكن نطبق هالكلام. رح أستخدم مثال بسيط باستخدام Node.js و Express، لكن المبدأ نفسه ينطبق على أي لغة أو إطار عمل (PHP, Python, Java, etc.).

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

العميل هو المسؤول عن إنشاء المفتاح الفريد. أفضل شيء هو استخدام UUID (Universally Unique Identifier).


// في تطبيق الويب أو الموبايل
// قبل إرسال طلب الدفع، قم بإنشاء مفتاح فريد

// في المتصفحات الحديثة، يمكن استخدام crypto API
const idempotencyKey = crypto.randomUUID(); 

async function processPayment() {
    try {
        const response = await fetch('/api/payments', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                // أهم جزء: إرسال المفتاح في الهيدر
                'Idempotency-Key': idempotencyKey 
            },
            body: JSON.stringify({
                amount: 99.99,
                currency: 'USD',
                orderId: 'ORD-12345'
            })
        });

        const data = await response.json();

        if (response.ok) {
            console.log('Payment successful:', data);
        } else {
            console.error('Payment failed:', data.error);
        }
    } catch (error) {
        // هذا هو المكان الذي تحدث فيه المشكلة عادةً
        // قد تحدث مشكلة في الشبكة هنا، وسيحاول المستخدم مرة أخرى
        console.error('Network error or server is down. Please try again.');
    }
}

// استدعاء الدالة عند النقر على الزر
document.getElementById('pay-button').addEventListener('click', processPayment);

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

هنا يكمن السحر الحقيقي. سنقوم بإنشاء Middleware في Express لاعتراض الطلبات والتحقق من المفتاح.

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


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

// سنستخدم كائن بسيط كمخزن مؤقت للتوضيح
// في الواقع، استخدم Redis أو قاعدة بيانات!
const idempotencyCache = {};

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

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

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

    // إذا كان المفتاح جديداً، نحتاج إلى تخزين الاستجابة بعد معالجتها
    // نحتفظ بدالة الإرسال الأصلية
    const originalJson = res.json;
    const originalSend = res.send;

    // نعدّل دالة الإرسال لتخزين النتيجة قبل إرسالها
    res.json = (body) => {
        if (res.statusCode >= 200 && res.statusCode  {
        if (res.statusCode >= 200 && res.statusCode  {
    try {
        console.log(`Processing payment for order ${req.body.orderId}...`);
        
        // ... هنا منطق معالجة الدفع الفعلي ...
        // const paymentResult = await processPaymentWithGateway(req.body);
        const paymentResult = { paymentId: `pay_${Date.now()}`, status: 'completed' };
        
        // عند إرسال الاستجابة، سيقوم الـ middleware بتخزينها تلقائياً
        res.status(201).json(paymentResult);

    } catch (error) {
        res.status(500).json({ error: 'Payment processing failed' });
    }
});

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

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

تطبيق المبدأ بسيط، لكن الشيطان يكمن في التفاصيل. إليكم بعض النصائح العملية من تجربتي:

1. من أين نأتي بالمفتاح؟

دائماً وأبداً، العميل هو من يجب أن يُنشئ المفتاح. لا تقم بإنشائه على الخادم. الهدف هو ربط عدة محاولات لنفس الطلب من العميل معاً. استخدم UUIDs (الإصدار 4 هو الأنسب) لأنها تضمن تفرّداً عالياً جداً.

2. أين نخزن الحالة؟

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

  • Redis: هو الخيار الأمثل في رأيي. سريع جداً، ويوفر ميزات مثل تعيين وقت انتهاء صلاحية للمفتاح (TTL) بسهولة، وعمليات ذرية (atomic) مثل SETNX التي تساعد في منع حالات السباق (Race Conditions).
  • قاعدة البيانات (SQL/NoSQL): يمكنك إنشاء جدول خاص لتخزين مفاتيح عدم التكرار مع الاستجابات. هذا الخيار أبطأ قليلاً من Redis لكنه موثوق جداً ومتوفر في معظم المشاريع.

3. متى تنتهي صلاحية المفتاح؟

لا يمكنك الاحتفاظ بالمفاتيح إلى الأبد، وإلا ستنفجر مساحة التخزين لديك. من الجيد تعيين وقت انتهاء صلاحية (TTL – Time To Live) للمفاتيح. مدة 24 ساعة هي مدة شائعة ومناسبة لمعظم الحالات. بعد 24 ساعة، يمكن للعميل إعادة محاولة نفس العملية إذا لزم الأمر بمفتاح جديد.

4. تعامل مع حالات السباق (Race Conditions)

ماذا لو وصل طلبان بنفس المفتاح في نفس اللحظة بالضبط؟ قد يتجاوز كلاهما فحص “هل المفتاح موجود؟” قبل أن يتمكن أحدهما من تخزينه. هذا يسمى “حالة سباق”. الحل هو استخدام قفل (lock) أو عملية ذرية. في Redis، يمكنك استخدام أمر SET key value NX الذي يقوم بتعيين المفتاح فقط إذا لم يكن موجوداً. هذا يضمن أن خادماً واحداً فقط هو من “يفوز” بالسباق وينفذ العملية.

الخلاصة: فكرة بسيطة تنقذ أنظمة عظيمة

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

تذكروا دائماً قصة العميل الذي دُفّع 5 مرات. استثمار بضع ساعات في تطبيق هذا المفهوم بشكل صحيح يمكن أن يوفر عليك أياماً من الصداع، وخسارة مالية، والأهم من ذلك، خسارة ثقة عملائك.

لا تستهينوا بقوة هذه الفكرة البسيطة، فهي الفرق بين نظام يثق به المستخدمون ونظام يسبب لهم الصداع. خليكم مرتبين في شغلكم، والله يوفق الجميع. 🙏

أبو عمر

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

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

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

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

آخر المدونات

تسويق رقمي

ميزانيتنا كانت تتبخر: كيف أنقذتنا ‘نماذج الإحالة المبنية على البيانات’ من جحيم مكافأة القناة الخطأ؟

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

20 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

واجهاتنا كانت فوضى: كيف أنقذنا “نظام التصميم” (Design System) من جحيم عدم الاتساق؟

أتذكر جيدًا ذلك الاجتماع الذي كاد أن يدفن مشروعنا. من فوضى الألوان والأزرار إلى واجهة متناغمة وفعّالة، أشارككم تجربتي العملية في بناء نظام تصميم (Design...

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

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

أشارككم قصة حقيقية من قلب المعركة البرمجية، حين كادت بياناتنا أن تضيع في فوضى صامتة. سنغوص معاً في عالم "التحكم في الوصول المتزامن" (Concurrency Control)...

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

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

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

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

مقابلاتي التقنية كانت صمتًا مُطبقًا: كيف أنقذني ‘التفكير بصوت عالٍ’ من جحيم الإجابات الفارغة؟

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

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

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

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

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

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

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

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

سجلاتنا كانت ضجيجًا بلا معنى: كيف أنقذتنا ‘إدارة السجلات المركزية’ من جحيم البحث عن إبرة في كومة قش؟

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

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

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

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

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