عمليات الدفع المتكررة كانت كارثة: كيف أنقذتنا ‘مفاتيح عدم التكرار’ (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) ليست ترفاً تقنياً، بل هي ضرورة أساسية في أي نظام يتعامل مع عمليات حساسة مثل المدفوعات، إنشاء الطلبات، أو أي إجراء له تكلفة مالية أو لوجستية. تطبيقها بشكل صحيح من اليوم الأول هو الفارق بين نظام ينام مهندسوه بسلام، ونظام يجعلهم يقضون لياليهم في إطفاء الحرائق وإرضاء العملاء الغاضبين.

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

أبو عمر

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

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

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

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

آخر المدونات

التكنلوجيا المالية Fintech

كانت قراراتنا الائتمانية صندوقاً أسود: كيف أنقذنا ‘الذكاء الاصطناعي القابل للتفسير’ (XAI) من جحيم التحيز والشكاوى التنظيمية؟

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

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

كانت أعطالنا تباغتنا في منتصف الليل: كيف أنقذنا Prometheus من جحيم المراقبة التفاعلية؟

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

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

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

أتذكر ذلك اليوم جيداً، طلب دمج (Pull Request) عالق لأسبوع، ونقاش حاد بين اثنين من أفضل المبرمجين حول تفصيل بسيط. كانت هذه هي القشة التي...

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

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

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

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

كان مطورنا الجديد ينتظر أياماً: كيف أنقذتنا ‘أتمتة إعداد البيئة’ من جحيم الأسبوع الأول الضائع؟

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

15 مايو، 2026 قراءة المزيد
نصائح برمجية

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

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

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

كانت خدماتنا تتحدث في نفس الوقت: كيف أنقذتنا ‘المعمارية القائِمَة على الأحداث’ (EDA) من جحيم الاقتران المحكم؟

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

15 مايو، 2026 قراءة المزيد
ذكاء اصطناعي

كانت نماذجنا تموت بصمت: كيف أنقذتنا ‘مراقبة تعلم الآلة’ (ML Monitoring) من كارثة التنبؤات الفاسدة؟

أشارككم قصة حقيقية من الميدان، حين كادت نماذج الذكاء الاصطناعي التي بنيناها بجهد أن تنهار بصمت. اكتشفوا معنا ما هي "مراقبة تعلم الآلة" (ML Monitoring)،...

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