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

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

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

قلبي وقتها نزل عند ركبي، زي ما بحكوها. فكرة إنه نظامنا بخربط وبخصم مصاري الناس بالزيادة هي كابوس أي مبرمج. على طول جمعت الفريق، وصرنا زي خلية النحل، واحد براجع سجلات قاعدة البيانات (Logs)، وواحد بفحص بوابة الدفع، وأنا كنت بحاول أحلل طلبات الـ API اللي وصلت سيرفراتنا. بعد ساعات من البحث والتدقيق، اكتشفنا إشي غريب: وصلنا طلبين متطابقين تمامًا لنفس عملية الدفع، بفارق ثانيتين بس! الطلب الأول نجح، والثاني كمان نجح، وبالتالي العميل المسكين اندفع مرتين.

لكن السؤال المحيّر: ليش العميل يطلب مرتين؟ تواصلنا مع العميل وفهمنا منه إنه الشبكة عنده كانت بطيئة، ضغط على زر “تأكيد الدفع”، وما صار إشي، فكّر إنه الضغطة ما انحسبت، فضغط مرة ثانية. وهون كانت الكارثة. التطبيق من جهة العميل أرسل الطلب الأول، وبسبب ضعف الشبكة، ما وصله رد من السيرفر تبعنا بالوقت المناسب (Timeout). فمنطق إعادة المحاولة (Retry Logic) في التطبيق اشتغل تلقائيًا وأرسل الطلب مرة ثانية. بالنسبة لسيرفرنا، هدول طلبين جداد ومنفصلين، فقام بمعالجتهم الاثنين. यहीं से بدأت رحلتنا مع مفهوم الـ Idempotency.

ما هي مشكلة الطلبات المكررة؟

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

لما هاي المشاكل تصير مع عمليات حساسة زي:

  • إنشاء عملية دفع.
  • تقديم طلب شراء.
  • إرسال حوالة مالية.
  • حجز مقعد في طيارة.

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

مفهوم عدم التكرار (Idempotency)

في الرياضيات وعلوم الحاسوب، العملية “Idempotent” هي العملية اللي لو نفذتها مرة أو ألف مرة، النتيجة النهائية بتكون نفسها كأنك نفذتها مرة واحدة بس.

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

في عالم الـ REST APIs، بعض الأفعال (Verbs) مصممة لتكون idempotent بطبيعتها:

  • GET: طلب البيانات. لو طلبت بيانات مستخدم ألف مرة، ما رح يتغير إشي في بياناته.
  • PUT: تحديث مورد بالكامل. لو أرسلت طلب لتحديث اسم مستخدم ليصير “أحمد”، وأرسلت نفس الطلب 5 مرات، النتيجة النهائية هي إن اسمه رح يضل “أحمد”.
  • DELETE: حذف مورد. لو حذفت مستخدم، ورجعت طلبت حذفه مرة ثانية، رح تلاقي إنه محذوف أصلًا. النتيجة النهائية واحدة.

المشكلة الحقيقية تكمن في POST، اللي غالبًا ما يستخدم لإنشاء موارد جديدة. كل طلب POST يُفترض أن يُنشئ شيئًا جديدًا.

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

بعد ما فهمنا المشكلة، كان لازم نلاقي حل يخلي عمليات الـ POST الحساسة عنا تتصرف كأنها Idempotent. الحل هو نمط تصميمي مشهور جدًا، بتستخدمه شركات عملاقة زي Stripe وPayPal، واسمه “Idempotency Keys”.

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

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

بهذه الطريقة، حتى لو العميل أرسل نفس الطلب 10 مرات بنفس المفتاح، العملية الحقيقية (مثل خصم المال) رح تتنفذ مرة واحدة فقط! والعميل رح يوصله نفس الجواب في كل مرة، كأنه الطلب نجح من أول مرة.

مثال عملي بالكود

لنفترض أن العميل (تطبيق ويب مكتوب بـ JavaScript) يريد إنشاء عملية دفع:

كود جهة العميل (Client-Side)


// 1. إنشاء مفتاح فريد لكل محاولة دفع (وليس لكل ضغطة زر)
// يجب تخزين هذا المفتاح لضمان استخدامه في محاولات الإعادة لنفس العملية
function createPayment(paymentData) {
    let idempotencyKey = localStorage.getItem('current_payment_key');
    if (!idempotencyKey) {
        idempotencyKey = self.crypto.randomUUID(); // e.g., 'f1c7a4b2-a8f8-4f5f-8d1e-2d8a9b3c4d5e'
        localStorage.setItem('current_payment_key', idempotencyKey);
    }

    fetch('/api/payments', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Idempotency-Key': idempotencyKey, // 2. إرسال المفتاح في الترويسة
        },
        body: JSON.stringify(paymentData)
    })
    .then(response => {
        if (response.ok) {
            // نجحت العملية، يمكننا الآن حذف المفتاح
            localStorage.removeItem('current_payment_key');
            return response.json();
        }
        // إذا فشل الطلب بسبب مشكلة شبكة، المحاولة التالية ستستخدم نفس المفتاح
    })
    .then(data => console.log('Payment successful:', data))
    .catch(error => console.error('Payment failed:', error));
}

منطق جهة الخادم (Server-Side) – مثال مبسط باستخدام Express.js

هنا سنصمم Middleware بسيط لاعتراض الطلبات والتحقق من المفتاح.


// لنفترض أننا نستخدم Redis لتخزين المفاتيح والنتائج
const redisClient = require('./redisClient');

async function idempotencyMiddleware(req, res, next) {
    // هذا المنطق ينطبق فقط على العمليات التي تحتاج لضمان عدم التكرار
    if (req.method !== 'POST') {
        return next();
    }

    const idempotencyKey = req.headers['idempotency-key'];

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

    const cacheKey = `idempotency:${idempotencyKey}`;

    try {
        // 1. تحقق إذا كانت النتيجة مخزنة مسبقًا
        const cachedResponse = await redisClient.get(cacheKey);
        if (cachedResponse) {
            console.log(`[${idempotencyKey}] Returning cached response.`);
            const { status, body } = JSON.parse(cachedResponse);
            return res.status(status).json(body);
        }

        // 2. إذا لم تكن مخزنة، سنحتاج لتخزينها بعد المعالجة
        // نستخدم خدعة صغيرة لاعتراض الجواب قبل إرساله
        const originalJson = res.json;
        const originalStatus = res.status;

        res.json = (body) => {
            // فقط خزّن النتيجة إذا كانت العملية ناجحة (2xx)
            if (res.statusCode >= 200 && res.statusCode  {
    // ... هنا منطق معالجة الدفع الحقيقي ...
    // هذا الكود سيتنفذ مرة واحدة فقط لكل مفتاح
    console.log(`[${req.headers['idempotency-key']}] Processing new payment...`);
    const paymentResult = { transactionId: 'txn_' + Date.now(), status: 'completed' };
    res.status(201).json(paymentResult);
});

ملاحظة هامة: المثال أعلاه هو نسخة مبسطة للتوضيح. في الأنظمة الحقيقية، يجب التعامل مع حالات أكثر تعقيدًا مثل “السباق” (Race Conditions) حيث يصل طلبان بنفس المفتاح في نفس اللحظة تمامًا. يمكن حل هذه المشكلة باستخدام آليات القفل (Locking) في Redis أو قاعدة البيانات.

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

  • لا تخزن المفاتيح إلى الأبد: لا يوجد سبب يدعو للاحتفاظ بمفتاح عدم التكرار إلى ما لا نهاية. حدد فترة صلاحية (TTL – Time To Live) معقولة، مثلاً 24 ساعة. هذا يمنع قاعدة بياناتك من الامتلاء ببيانات قديمة لا فائدة منها.
  • اختر وسيلة التخزين المناسبة: Redis يعتبر خيارًا ممتازًا لتخزين المفاتيح والنتائج بسبب سرعته الفائقة ودعمه المدمج لفترة الصلاحية (TTL). إذا لم يكن Redis متاحًا، يمكن استخدام جدول عادي في قاعدة بياناتك، لكن تأكد من وجود فهرس (index) على عمود المفتاح لتسريع البحث.
  • العميل هو المسؤول عن المفتاح: تأكد من أن منطق إنشاء المفتاح موجود بالكامل في جهة العميل. إذا قام الخادم بإنشاء المفتاح، فإننا نفقد كل الفائدة المرجوة.
  • خزّن الاستجابة الناجحة فقط: من الأفضل أن تقوم بتخزين نتيجة الطلب فقط عندما يكون ناجحًا (HTTP Status 2xx). إذا فشل الطلب (بسبب خطأ في البيانات مثلاً – 4xx، أو خطأ في الخادم – 5xx)، لا تقم بتخزين هذه النتيجة. هذا يسمح للعميل بتصحيح الخطأ وإعادة المحاولة بنفس المفتاح.
  • ليست كل الـ APIs تحتاج لهذا: لا تبالغ في استخدام هذا النمط. طبقه فقط على العمليات الحرجة وغير المتكررة بطبيعتها (Critical, Non-Idempotent Actions) مثل إنشاء الطلبات والمدفوعات.

الخلاصة: راحة بال لا تقدر بثمن

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

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

أبو عمر

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

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

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

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

آخر المدونات

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

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

أروي لكم يا جماعة قصة حقيقية من قلب المعركة البرمجية، يوم كادت القراءات الشبحية (Phantom Reads) أن تدمر مشروعنا. في هذه المقالة، أغوص معكم في...

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

كانت قاعدة بياناتنا تحتضر: كيف أنقذنا ‘التخزين المؤقت’ (Caching) من جحيم الاستعلامات المتكررة؟

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

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

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

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

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

كانت بنيتنا التحتية قصرًا من ورق: كيف أنقذنا Terraform من جحيم التغييرات اليدوية وانحراف الإعدادات؟

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

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

المسار الوظيفي المزدوج: كيف أنقذنا خيرة مهندسينا من جحيم الاختيار بين الإدارة والكود؟

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

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

كانت اختباراتنا تنهار عشوائياً: كيف أنقذنا Playwright من جحيم الاختبارات المتقشرة (Flaky Tests)؟

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

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

كانت طرفيتي سجناً: كيف أنقذنا ‘الباحث التقريبي’ (Fuzzy Finder) من جحيم البحث في الـ History؟

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

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