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

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

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

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

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

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

ما هي الطلبات عديمة الأثر (Idempotency) يا أبو عمر؟

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

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

بالمقابل، العملية “غير عديمة الأثر” (Non-idempotent) هي اللي كل مرة بتنفذها بتعطي نتيجة جديدة. زي لما تسحب مصاري من الصراف الآلي. كل عملية سحب هي عملية جديدة بتنقص رصيدك. لو طلبت سحب 100 دينار مرتين، رح ينخصم من حسابك 200 دينار. وهون كانت مشكلتنا بالضبط.

ليش هالقصة مهمة للمطورين والـ APIs؟

في عالم الشبكات والـ APIs، إحنا بنعيش في بيئة فوضوية وغير مضمونة بطبيعتها. طلب الـ API اللي بطلع من عند العميل (المتصفح أو التطبيق) للسيرفر ممكن يواجه ألف قصة في الطريق:

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

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

كيف نطبق الـ Idempotency في الـ REST APIs؟

ممتاز، وصلنا للجانب العملي. في عالم الـ REST APIs، تطبيق هذا المبدأ بيعتمد بشكل كبير على نوع طلب الـ HTTP اللي بنستخدمه، وعلى حيلة ذكية راح أحكيلكم عنها.

استخدام أفعال HTTP الصحيحة

مش كل أفعال (methods) الـ HTTP زي بعضها من ناحية الـ Idempotency:

  • GET, HEAD, OPTIONS: هاي الأفعال بطبيعتها عديمة الأثر. طلب بيانات مستخدم معين 100 مرة ما بغير بياناته.
  • PUT: هذا الفعل “يجب” أن يكون عديم الأثر حسب المواصفات. لما تعمل PUT /users/123 وتحدد كل بيانات المستخدم، لو كررت الطلب بنفس البيانات، حالة المستخدم النهائية على السيرفر لازم تضلها نفسها.
  • DELETE: هذا الفعل أيضًا عديم الأثر. لما تعمل DELETE /orders/456، الطلب الأول بحذف الطلب وبيرجعلك مثلاً 204 No Content. لو كررت الطلب، السيرفر رح يرجعلك 404 Not Found لأنه الطلب انحذف، لكن النتيجة النهائية على النظام وحدة: الطلب رقم 456 محذوف.
  • POST: وهنا مكمن الخطر! هذا الفعل بطبيعته “غير عديم الأثر”. كل طلب POST /payments يُفترض أنه ينشئ عملية دفع جديدة. وهو بالضبط اللي سبب الكارثة في قصتي.

الحل السحري لطلبات POST: مفتاح عدم التكرار (Idempotency Key)

طيب يا أبو عمر، كيف بدنا نخلي عملية حساسة زي الدفع (اللي بتستخدم POST) عديمة الأثر؟ الجواب يكمن في استخدام ما يسمى بـ “مفتاح عدم التكرار” أو Idempotency Key.

الفكرة بسيطة وعبقرية:

  1. العميل (Client) قبل ما يرسل الطلب الحساس، يقوم بإنشاء مُعرّف فريد وخاص بهذه العملية (مثلاً UUID).
  2. العميل يرسل هذا المُعرّف الفريد ضمن هيدر (header) خاص في الطلب، المتعارف عليه هو Idempotency-Key.
  3. السيرفر (Server) لما يستقبل الطلب، أول شي بعمله هو قراءة هذا الهيدر.
  4. السيرفر يبحث في قاعدة بياناته أو في ذاكرة التخزين المؤقت (cache) عن هذا المفتاح.
  5. السيناريو الأول (مفتاح جديد): إذا كان المفتاح جديدًا ولم يره من قبل، يقوم السيرفر بتنفيذ العملية كالمعتاد (مثلاً، خصم المبلغ). بعد إتمام العملية بنجاح، يقوم بتخزين نتيجة العملية (الرد) مع المفتاح نفسه، ثم يرسل الرد للعميل.
  6. السيناريو الثاني (مفتاح مكرر): إذا وجد السيرفر أن المفتاح موجود عنده ومسجل من قبل، فهذا يعني أن هذا الطلب هو تكرار لطلب سابق. هنا، السيرفر لا يقوم بتنفيذ العملية مرة أخرى. بدلًا من ذلك، يقوم بجلب النتيجة التي خزنها سابقًا ويرسلها مباشرة للعميل.

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

مثال كود (باستخدام Node.js/Express كمثال توضيحي)

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


const express = require('express');
const { v4: uuidv4 } = require('uuid'); // لتوليد المفاتيح
const app = express();
app.use(express.json());

// في تطبيق حقيقي، استخدم Redis أو قاعدة بيانات مع TTL
const idempotencyCache = new Map();

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

    if (!idempotencyKey) {
        return res.status(400).json({ error: 'Idempotency-Key header is missing' });
    }

    // 1. هل رأينا هذا المفتاح من قبل؟
    if (idempotencyCache.has(idempotencyKey)) {
        console.log(`[${idempotencyKey}] Request مكرر. إعادة الرد المخزن.`);
        const cachedResponse = idempotencyCache.get(idempotencyKey);
        return res.status(cachedResponse.statusCode).json(cachedResponse.body);
    }

    try {
        // 2. هذا مفتاح جديد. لنقم بالعملية الحقيقية
        console.log(`[${idempotencyKey}] طلب جديد. جاري معالجة الدفع...`);
        const paymentDetails = req.body;
        
        // ... هنا تضع الكود الفعلي لعملية الدفع ...
        // const paymentResult = await processPayment(paymentDetails);
        const paymentResult = { id: `payment_${Date.now()}`, status: 'succeeded' }; // نتيجة وهمية

        const response = {
            statusCode: 201,
            body: { status: 'success', data: paymentResult }
        };

        // 3. تخزين النتيجة مع المفتاح قبل إرسال الرد
        idempotencyCache.set(idempotencyKey, response);
        console.log(`[${idempotencyKey}] تم تخزين النتيجة.`);

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

    } catch (error) {
        // في حال حدوث خطأ، لا تخزن المفتاح حتى يتمكن العميل من المحاولة مرة أخرى
        console.error(`[${idempotencyKey}] حدث خطأ أثناء المعالجة:`, error);
        return res.status(500).json({ error: 'An internal server error occurred' });
    }
});

const PORT = 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

شرح الكود: الكود أعلاه يوضح سير العمل. يستقبل طلب الدفع، يتحقق من وجود idempotency-key. إذا كان المفتاح موجودًا في الذاكرة المؤقتة، يعيد الرد المخزن. إذا لم يكن موجودًا، ينفذ عملية الدفع “الوهمية”، يخزن النتيجة والمفتاح، ثم يعيد الرد. هذا يضمن أن كود الدفع الفعلي لن يعمل إلا مرة واحدة لكل مفتاح.

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

بعد ما أكلت هداك المقلب، تعلمت كم شغلة مهمة بحب أشارككم فيها:

اجعل المفتاح فريدًا من جهة العميل

الأفضل دائمًا أن يقوم العميل (تطبيق الموبايل أو المتصفح) بتوليد المفتاح قبل إرسال الطلب. استخدام UUID (Universally Unique Identifier) هو الخيار الأمثل لأنه يضمن عدم تكرار المفاتيح بشكل شبه مستحيل.

لا تخزن المفاتيح إلى الأبد

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

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

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

وثّق الـ Idempotency في توثيق الـ API

إذا كان الـ API الخاص بك سيدعمه مطورون آخرون، فمن الضروري أن توضح في التوثيق (API Documentation) أنك تدعم الـ Idempotency، وكيفية استخدامه، وما هو اسم الهيدر المتوقع (Idempotency-Key).

الخلاصة: فكر فيها كبوليصة تأمين 📜

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

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

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

أبو عمر

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

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

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

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

آخر المدونات

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

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

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

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

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

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

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

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

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

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

طلبات المستخدمين كانت تنتظر في طابور لا ينتهي: كيف أنقذتني ‘قوائم انتظار الرسائل’ (Message Queues) من جحيم تجربة المستخدم البطيئة؟

أشارككم قصة حقيقية عن مشروع كاد أن يفشل بسبب بطء الاستجابة، وكيف كانت "قوائم انتظار الرسائل" (Message Queues) هي طوق النجاة. سنتعمق في هذا المفهوم،...

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

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

أنا أبو عمر، مطور برمجيات فلسطيني، وهذه قصتي مع إدارة الأموال اليدوية التي كانت كابوسًا شهريًا. سأشارككم كيف حولت "الخدمات المصرفية المفتوحة" (Open Banking) هذا...

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

طلباتي كانت تختفي بين الخدمات: كيف أنقذني ‘التتبع الموزع’ (Distributed Tracing) من جحيم تحليل الأعطال؟

أشارككم قصة حقيقية عن طلبات كانت تضيع في أنظمتنا المعقدة، وكيف كان التتبع الموزع (Distributed Tracing) هو المنقذ. سنتعمق في هذا المفهوم، من هو ولماذا...

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

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

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

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

خدماتي كانت تتحدث لغات مختلفة: كيف أنقذني اختبار العقود (Contract Testing) من جحيم التكامل الهش؟

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

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