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

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

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

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

في تلك الليلة، لم ننم حتى أصلحنا المشكلة بشكل “مؤقت” وسريع. لكن الدرس الأهم تعلمناه في اليوم التالي ونحن نعيد بناء المنطق البرمجي: لا تثق أبداً بالشبكة، واستعد دائماً للأسوأ. وهنا كان بطل القصة هو مفهوم الـ Idempotency.

ما هي الـ Idempotency (عدم التكرار)؟ وليش هي مهمة؟

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

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

في عالم الـ APIs، هذا المفهوم حاسم. تخيل معي هذه السيناريوهات:

  • طلب GET /users/123: لو طلبته مرة أو مية مرة، رح يرجعلك نفس بيانات المستخدم رقم 123. هذه العملية بطبيعتها Idempotent.
  • طلب DELETE /orders/456: أول مرة رح تحذف الطلب. ثاني مرة (وثالث ورابع…) رح تحاول تحذف طلب مش موجود أصلاً، فالنظام رح يرجعلك خطأ “غير موجود” أو ببساطة ما رح يعمل إشي. النتيجة النهائية وحدة: الطلب 456 محذوف. هذه العملية أيضاً Idempotent.
  • طلب POST /payments: وهنا المصيبة. كل مرة بتبعث فيها هذا الطلب، النظام بفترض إنها عملية دفع جديدة تماماً. لو بعثته مرتين، رح تتم عمليتي دفع. هذه العملية ليست Idempotent بطبيعتها، وهي سبب الكارثة اللي صارت معنا.

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

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

طيب، كيف بنحوّل عملية POST خطيرة زي الدفع لعملية آمنة وغير متكررة؟ الحل يكمن في شيء بسيط وعبقري اسمه “مفتاح عدم التكرار” أو Idempotency Key.

آلية العمل خطوة بخطوة

الفكرة بسيطة جداً في جوهرها:

  1. العميل (Client-side) يبتكر المفتاح: قبل إرسال الطلب (مثلاً، عند الضغط على زر “الدفع”)، يقوم تطبيق العميل (سواء كان متصفح ويب أو تطبيق جوال) بإنشاء مُعرّف فريد لهذه العملية. عادةً ما يكون هذا المعرّف عبارة عن UUID (Universally Unique Identifier) مثل f7c3b2a0-7a4f-4bde-9e7f-1b5e3d8a9e2a.
  2. العميل يرسل المفتاح مع الطلب: يتم إرسال هذا المفتاح الفريد في هيدر (Header) خاص مع طلب الـ API. الهيدر المتعارف عليه هو Idempotency-Key.
  3. الخادم (Server-side) يستقبل ويفحص: عندما يستقبل الخادم الطلب، أول شيء يفعله هو النظر في هذا الهيدر.
    • إذا كان المفتاح جديداً: هذا يعني أنها عملية جديدة لم نرها من قبل. يقوم الخادم بتنفيذ العملية كالمعتاد (مثلاً، خصم المبلغ)، ثم يقوم بتخزين نتيجة هذه العملية مع المفتاح نفسه في قاعدة بيانات أو ذاكرة تخزين مؤقت (Cache) مثل Redis.
    • إذا كان المفتاح مكرراً: هذا يعني أن الخادم قد رأى هذا المفتاح من قبل. هنا، لا يقوم الخادم بتنفيذ العملية مرة أخرى! بدلاً من ذلك، يذهب إلى المكان الذي خزّن فيه النتيجة السابقة، ويسترجعها، ويرسلها مرة أخرى للعميل وكأن العملية تمت الآن للتو.

بهذه الطريقة، حتى لو ضغط المستخدم على زر الدفع 10 مرات بسبب ضعف الإنترنت، سيتم إرسال نفس الـ Idempotency-Key في كل مرة، ولن تتم عملية الخصم إلا مرة واحدة فقط. شغل مرتب ونظيف!

مثال برمجي (شبه كود باستخدام Node.js/Express)

لتقريب الصورة، تخيل أن لدينا خادم مكتوب بلغة JavaScript وإطار العمل Express. يمكن أن يبدو منطق التعامل مع مفتاح عدم التكرار هكذا:


// نفترض أن لدينا اتصال مع Redis لتخزين المفاتيح
const redisClient = require('./redis-client');

app.post('/api/payments', async (req, res) => {
    const idempotencyKey = req.headers['idempotency-key'];

    // الخطوة 1: التأكد من وجود المفتاح
    if (!idempotencyKey) {
        return res.status(400).json({ error: 'Idempotency-Key header is required.' });
    }

    try {
        // الخطوة 2: البحث عن المفتاح في الكاش (Redis)
        const cachedResponse = await redisClient.get(`idempotency:${idempotencyKey}`);

        if (cachedResponse) {
            // وجدنا المفتاح! هذا طلب مكرر.
            // نرجع النتيجة المخزنة سابقاً
            console.log(`Request مكرر: ${idempotencyKey}. returning cached response.`);
            const responseData = JSON.parse(cachedResponse);
            return res.status(responseData.status).json(responseData.body);
        }

        // الخطوة 3: هذا طلب جديد. لننفذ العملية...
        const paymentResult = await processPayment(req.body); // هذه هي دالة الدفع الحقيقية

        // الخطوة 4: تخزين النتيجة قبل إرسالها للعميل
        const responseToCache = {
            status: 201, // Created
            body: paymentResult
        };
        
        // نخزن النتيجة في Redis مع مدة صلاحية (مثلاً 24 ساعة)
        await redisClient.set(`idempotency:${idempotencyKey}`, JSON.stringify(responseToCache), 'EX', 24 * 60 * 60);

        // إرسال الرد للعميل
        return res.status(201).json(paymentResult);

    } catch (error) {
        // في حال حدوث خطأ أثناء معالجة الدفع
        console.error('Error processing payment:', error);
        return res.status(500).json({ error: 'An internal server error occurred.' });
    }
});

نصائح عملية من خبرتي

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

  • أين تولّد المفتاح؟ دائماً وأبداً على طرف العميل (Client). لا تقم بتوليده على الخادم. الهدف هو أن يظل المفتاح نفسه لكل محاولات إعادة إرسال نفس الطلب من العميل.
  • ماذا عن صلاحية المفتاح؟ لا تترك المفاتيح في قاعدة بياناتك إلى الأبد. ضع لها تاريخ انتهاء صلاحية (مثلاً 24 ساعة أو 7 أيام). هذا يمنع قاعدة البيانات من الامتلاء ببيانات قديمة غير ضرورية.
  • ماذا تخزن؟ لا تخزن فقط “هل تم تنفيذ الطلب أم لا”. خزّن الاستجابة الكاملة (الـ Response Body وكود الحالة Status Code). بهذه الطريقة، عندما يأتي طلب مكرر، يمكنك إرجاع نفس الرد الذي حصل عليه العميل في المرة الأولى بالضبط.
  • التعامل مع الأخطاء: ماذا لو فشلت العملية في المرة الأولى؟ لا تخزن شيئاً تحت هذا المفتاح. اسمح للعميل بإعادة المحاولة بنفس المفتاح حتى تنجح العملية مرة واحدة، وعندها فقط قم بتخزين النتيجة الناجحة.
  • ليس لكل الـ APIs: لا ترهق نفسك ونظامك بتطبيق هذا المفهوم على كل شيء. ركز على العمليات الحرجة والحساسة فقط، وهي بشكل أساسي طلبات POST التي تُحدث تغييراً لا يمكن التراجع عنه بسهولة.

الخلاصة: لا تنتظر الكارثة!

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

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

الزبدة: استثمر قليلاً من الوقت الآن في بناء جدار الحماية هذا، وسيوفر عليك ساعات لا تحصى من الألم والصداع في المستقبل. 👍

أبو عمر

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

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

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

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

آخر المدونات

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

تحديثات قاعدة البيانات بدون توقف: كيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من جحيم التوقفات المجدولة؟

هل سئمت من إيقاف الخدمة مع كل تحديث لهيكلة قاعدة البيانات؟ أشارككم قصة حقيقية وكيف أنقذنا نمط التوسيع والتعاقد (Expand/Contract) من ليالي النشر الطويلة والمُجهدة،...

4 يونيو، 2026 قراءة المزيد
الشبكات والـ APIs

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

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

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

من التوقف التام إلى النجاة: كيف أنقذتنا استراتيجية “الضوء المرشد” (Pilot Light) يوم انقطعت السحابة؟

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

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

كانت مهمتي البرمجية للاختبار مجرد كود: كيف أنقذني توثيق القرارات من جحيم الصمت بعد المقابلة؟

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

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

من الانتظار لأيام إلى الدفع في ثوانٍ: كيف أنقذتنا شبكات الدفع الفوري من جحيم التحويلات البنكية؟

أسرد لكم من واقع تجربتي كـ "أبو عمر"، كيف عانينا من بطء وتكلفة التحويلات البنكية الدولية، وكيف جاءت شبكات الدفع الفوري ومعيار ISO 20022 لتكون...

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

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

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

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

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

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

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