العميل ضغط ‘شراء’ مرتين: كيف أنقذتني خاصية الـ Idempotency من كارثة مالية؟

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

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

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

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

هذا الموقف يا جماعة الخير، هو اللي خلاني أقدر قيمة مفهوم بسيط لكنه جبار في عالم الـ APIs: الـ Idempotency.

ما هي خاصية الـ Idempotency (أو “اللامتغيرة” بالعربي الفصيح)؟

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

خليني أعطيكم مثال من حياتنا اليومية عشان الصورة توضح:

  • مثال Idempotent (لامتغير): كبسة زر المصعد. لما تكبس على زر الطابق الخامس، المصعد بياخد الأمر وبيبدأ بالصعود. لو رجعت كبست على نفس الزر عشر مرات والمصعد طالع، ما رح يصير أي شيء جديد. الحالة النهائية (المصعد متجه للطابق الخامس) ما تغيرت بعد الكبسة الأولى.
  • مثال Non-Idempotent (متغير): سحب الفلوس من الصراف الآلي. لو طلبت سحب 100 دينار، رح ينخصم من حسابك 100 دينار. لو كررت العملية مرة ثانية، رح ينخصم كمان 100 دينار. كل عملية لها أثر جديد ومختلف.

في عالم الـ APIs، هذا المبدأ هو صمام الأمان تبعك.

ليش هالخاصية “بتسوى ذهب” في تصميم الـ API؟

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

حماية من أخطاء الشبكة (Network Glitches)

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

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

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

حماية من “الأصابع العجولة” للمستخدم

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

بناء أنظمة قوية وقابلة للتوسع (Robust & Scalable Systems)

في الأنظمة الموزعة (Distributed Systems) والـ Microservices، تتواصل الخدمات مع بعضها البعض عبر الشبكة. أحيانًا تفشل خدمة في منتصف الطريق، فتقوم أنظمة التنسيق (Orchestrators) مثل Kubernetes أو آليات إعادة المحاولة (Retry Mechanisms) بإعادة تنفيذ الطلب الفاشل. خاصية الـ Idempotency تضمن أن إعادة المحاولة هذه آمنة ولا تسبب تكرارًا للبيانات أو العمليات الحساسة.

كيف نطبق الـ Idempotency عمليًا؟ (هون الشغل الصح)

التطبيق العملي يعتمد على فهمنا لطبيعة طلبات بروتوكول HTTP وكيفية التعامل معها.

فهم طبيعة طلبات HTTP

بعض أنواع الطلبات هي Idempotent بطبيعتها، وبعضها لا:

  • GET, HEAD, OPTIONS, TRACE: هذه الطلبات تعتبر آمنة و Idempotent. طلب بيانات مستخدم 100 مرة لن يغير بيانات المستخدم.
  • PUT: هذا الطلب Idempotent. عندما تستخدم PUT لتحديث بيانات مورد على رابط معين (e.g., /users/123)، فإن إرسال نفس البيانات مرتين سيؤدي إلى نفس الحالة النهائية للمورد.
  • DELETE: هذا الطلب Idempotent أيضًا. حذف المورد /users/123 مرة أخرى بعد أن تم حذفه لن يغير شيئًا (سيرجع السيرفر غالبًا خطأ 404، لكن الحالة النهائية للنظام – المورد محذوف – لم تتغير).
  • POST: هذا هو التحدي الأكبر. طلب POST بطبيعته غير Idempotent. هو مصمم لإنشاء مورد جديد في كل مرة. إرسال طلب POST إلى /orders مرتين يعني إنشاء طلبين جديدين.

ترويض طلبات الـ POST: الحل السحري

بما أن معظم العمليات الحساسة (إنشاء معاملة مالية، إنشاء طلب جديد) تستخدم POST، فنحن بحاجة إلى طريقة لجعله “Idempotent بشكل مشروط”. الحل يكمن في استخدام ما يسمى بـ “مفتاح اللامتغيرية” أو Idempotency Key.

الفكرة كالتالي:

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

مثال كود (شغل نظيف)

هذا مثال بسيط باستخدام Node.js و Express لتوضيح الفكرة. تخيل أن هذا الكود جزء من السيرفر الخاص بك:


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

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

    // إذا لم يرسل العميل المفتاح، نرفض الطلب
    if (!idempotencyKey) {
        return res.status(400).json({ message: 'Idempotency-Key header is missing.' });
    }

    // 1. هل رأينا هذا المفتاح من قبل؟
    if (processedRequests.has(idempotencyKey)) {
        console.log(`Request مكرر: ${idempotencyKey}. سنعيد إرسال الرد المحفوظ.`);
        // 2. نعم، أعد إرسال الرد المحفوظ سابقًا
        const savedResponse = processedRequests.get(idempotencyKey);
        return res.status(savedResponse.statusCode).json(savedResponse.body);
    }

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

        // جهّز الرد الذي سنرسله ونحفظه
        const response = {
            statusCode: 201, // 201 Created
            body: { success: true, paymentId: paymentResult.id }
        };

        // 4. احفظ المفتاح والرد قبل إرساله للعميل
        processedRequests.set(idempotencyKey, response);
        
        // نصيحة: ضع تاريخ انتهاء صلاحية للمفتاح (TTL)
        // حتى لا تملأ الذاكرة بمفاتيح قديمة
        setTimeout(() => {
            processedRequests.delete(idempotencyKey);
        }, 24 * 60 * 60 * 1000); // 24 ساعة

        // أرسل الرد للعميل
        return res.status(response.statusCode).json(response.body);

    } catch (error) {
        // في حال حدوث خطأ أثناء المعالجة، يمكنك أيضًا حفظ رد الخطأ
        // بحيث إذا حاول العميل مرة أخرى بنفس المفتاح، يصله نفس الخطأ
        const errorResponse = {
            statusCode: 500,
            body: { success: false, message: 'An error occurred during payment processing.' }
        };
        processedRequests.set(idempotencyKey, errorResponse);
        return res.status(errorResponse.statusCode).json(errorResponse.body);
    }
});

ملاحظة هامة: من الضروري أن تكون عملية التحقق من المفتاح وحفظه (Check-and-Set) عملية ذرية (Atomic) لتجنب حالات السباق (Race Conditions) في الأنظمة التي تتعامل مع طلبات متزامنة كثيرة. استخدام قواعد بيانات مثل Redis مع أوامر مثل SETNX (Set if Not Exists) يحل هذه المشكلة بكفاءة.

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

  • لا تقتصد في المفتاح: استخدم دائمًا UUID (الإصدار 4 هو خيار ممتاز) لضمان أن يكون المفتاح فريدًا حقًا. لا تستخدم شيئًا يمكن تخمينه أو تكراره بالصدفة.
  • حدد فترة صلاحية للمفتاح (TTL): لا تحتفظ بالمفاتيح إلى الأبد! هذا يستهلك الذاكرة أو مساحة التخزين. فترة 24 ساعة كافية في معظم الحالات لتغطية أي محاولات إعادة إرسال منطقية.
  • طبقها بحكمة: لست بحاجة لتطبيق هذا المبدأ على كل طلبات POST في نظامك. ركز على العمليات الحساسة التي لا يجب أن تتكرر أبدًا: المدفوعات، إنشاء الطلبات، حجز المواعيد، إرسال رسائل لا يمكن التراجع عنها.
  • وثّقها للمطورين: إذا كنت تبني API ليستخدمه آخرون (سواء فريق الـ frontend في شركتك أو مطورون خارجيون)، تأكد من توثيق ضرورة استخدام Idempotency-Key بوضوح في الـ Documentation. اشرح لهم كيف يولدونه ومتى يرسلونه.

الخلاصة: فكرة بسيطة تحميك من كوابيس كبيرة 🙏

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

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

أتمنى أن تكون هذه المقالة قد أفادتكم. دمتم بخير وفي أمان الله.

أبو عمر

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

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

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

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

آخر المدونات

التوظيف وبناء الهوية التقنية

سيرتي الذاتية عبرت فلتر الـ ATS لكنها فشلت أمام المدير التقني: كيف أعدت بناءها لتتحدث لغة المهندسين؟

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

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

خدمة واحدة فاشلة كادت أن تسقط النظام بأكمله: كيف أنقذني نمط ‘قاطع الدائرة’ (Circuit Breaker) من كارثة متتالية؟

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

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

لقد ‘هاجمت’ تطبيقي بنفسي عمداً: كيف كشفت لي ‘هندسة الفوضى’ نقاط الضعف التي لم تظهرها الاختبارات التقليدية

أشارككم قصة حقيقية حول إطلاق فاشل كاد أن يدمر سمعتنا، وكيف قادتنا هذه التجربة المريرة إلى تبني "هندسة الفوضى" (Chaos Engineering). اكتشفوا معنا كيف يمكن...

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

عاصفة من الطلبات كادت أن تغرق تطبيقي: كيف أنقذتني طوابير الرسائل (Message Queues) من كارثة الجمعة السوداء؟

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

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