السلام عليكم يا جماعة الخير، معكم أخوكم أبو عمر.
خلوني أرجع فيكم بالزمن لكم سنة لورا. كنا في عز الشغل، سهرانين على إطلاق ميزة دفع جديدة لواحد من أكبر عملائنا. القهوة ما كانت تفارق إيدينا، والعيون حمرا من كثر التحديق في الشاشات. أطلقنا الميزة الساعة 12 بالليل، والحمد لله، الأمور بدت تمام التمام. احتفلنا احتفال بسيط، وكل واحد فينا حط راسه على مخدته وهو بيحلم بالنجاح.
ما كملت نص ساعة إلا والتلفون برن… صوت مدير الدعم الفني على الطرف الثاني مذعور: “يا أبو عمر الحقنا! المصاري بتنخصم من العملاء مرتين! الفيسبوك ولّع شكاوي والناس معصبة!”.
نزلت عليّ الكلمات زي المي الباردة. فتحت اللابتوب بسرعة البرق وأنا بحاول أستوعب الكارثة. دخلت على سجلات الخادم (logs)، وإذ بي أرى المشهد المرعب: طلبات POST لإنشاء عمليات الدفع تصلنا مرتين، وأحياناً ثلاثة، لنفس المستخدم وبنفس المبلغ، وبفاصل زمني لا يتجاوز أجزاء من الثانية. المشكلة ما كانت من الكود تبعنا مباشرة، المشكلة كانت في “الكبسة المزدوجة” الشهيرة من المستخدم المستعجل، أو أحياناً بسبب ضعف شبكة الإنترنت عنده اللي بتخلي المتصفح يعيد إرسال الطلب “من رأسه”.
وقتها، تذكرت مفهوم كنت قرأت عنه زمان اسمه “Idempotency”. في هذيك الليلة، ما كان هالمفهوم مجرد مصطلح تقني، بل كان طوق النجاة اللي أنقذنا من جحيم الفوضى. اليوم، بدي أشارككم القصة كاملة، والدرس اللي تعلمناه بالطريقة الصعبة.
ما هو “عدم تكرار المعاملة” (Idempotency)؟ شرح مبسط للجدعان
ببساطة شديدة، “Idempotency” هي خاصية في عملية ما، بتخليك لو نفذتها مرة أو ألف مرة، النتيجة النهائية تكون واحدة وثابتة. ما فهمت؟ بسيطة.
تخيل حالك كبست على زر المصعد عشان تطلع على الطابق الخامس. المصعد رح يجيلك. طيب لو رجعت كبست على نفس الزر عشر مرات ورا بعض؟ هل رح يجيلك عشر مصاعد؟ طبعاً لأ. رح يجيلك نفس المصعد مرة واحدة. عملية “استدعاء المصعد” هي عملية “Idempotent”، تكرارها لا يغير النتيجة النهائية.
في عالم الـ APIs، بعض الطلبات (HTTP Methods) هي بطبيعتها “Idempotent”:
- GET, HEAD, OPTIONS, TRACE: هاي الطلبات آمنة أصلاً، لأنها ما بتغير أي شيء في الخادم. لو طلبت بيانات منتج معين مليون مرة، المنتج ما رح يتغير.
- PUT: هذا الطلب يعتبر Idempotent. لو أرسلت طلب
PUT /articles/123لتحديث مقال بمحتوى معين، سواء أرسلته مرة أو خمسين مرة، النتيجة النهائية هي أن المقال 123 سيحتوي على ذلك المحتوى. - DELETE: نعم، حتى الحذف! لو أرسلت
DELETE /users/456، أول مرة سيتم حذف المستخدم. ثاني مرة (وثالث ورابع مرة)، الخادم رح يرجعلك “404 Not Found”، لكن النتيجة النهائية على النظام ما تغيرت: المستخدم 456 محذوف.
المشكلة الكبيرة، واللي وقعنا فيها، هي مع طلبات POST. هذا الطلب مصمم لإنشاء شيء جديد. كل طلب POST /orders يُفترض أن ينشئ طلبية جديدة. وهنا تكمن الكارثة عند التكرار.
‘مفتاح عدم تكرار المعاملة’ (Idempotency Key): المنقذ من الفوضى
الحل يكمن في إجبار طلب الـ POST على أن يتصرف كأنه Idempotent. كيف؟ من خلال شيء بنسميه “مفتاح عدم تكرار المعاملة” أو بالإنجليزية “Idempotency Key”.
الفكرة عبقرية في بساطتها: العميل (المتصفح أو تطبيق الموبايل) هو من يقوم بإنشاء مُعرّف فريد (unique ID) لكل معاملة يحاول تنفيذها. ثم يقوم بإرسال هذا المُعرّف مع الطلب داخل الـ Header.
الخادم، من جهته، قبل تنفيذ أي عملية حساسة (مثل إنشاء طلبية أو خصم مبلغ)، يقوم بالتحقق من هذا المفتاح. إذا كان المفتاح جديداً، ينفذ العملية ويحفظ المفتاح مع نتيجتها. أما إذا كان المفتاح قد وصله من قبل، فهذا يعني أن الطلب مكرر، فيقوم الخادم بتجاهل تنفيذ العملية ويكتفي بإرجاع النتيجة المحفوظة مسبقاً.
كيف يعمل السحر؟ آلية العمل خطوة بخطوة
- العميل يُنشئ المفتاح: قبل إرسال طلب
POSTلإنشاء طلبية جديدة، يقوم كود الـ JavaScript في المتصفح بإنشاء مُعرّف فريد عالمياً (UUID). مثلاً:'f1b2c3d4-e5f6-7890-1234-abcdabcdabcd'. - العميل يُرسل الطلب: يتم إرفاق هذا المفتاح في هيدر خاص، وليكن اسمه
Idempotency-Key. - الخادم يستقبل الطلب لأول مرة:
- الخادم يقرأ الهيدر
Idempotency-Key. - يبحث عن قيمة هذا المفتاح في مكان تخزين مؤقت (مثل Redis أو جدول في قاعدة البيانات).
- لا يجد المفتاح. هذا يعني أنها معاملة جديدة.
- قبل أن يبدأ بالعملية الفعلية (الخصم من البطاقة مثلاً)، يقوم بتخزين المفتاح وحالة “قيد التنفيذ” (In Progress). هذا يمنع حدوث تضارب لو وصل طلب آخر بنفس المفتاح في نفس اللحظة.
- ينفذ العملية بنجاح (تم إنشاء الطلبية وخصم المبلغ).
- يُحدّث حالة المفتاح إلى “مكتمل” (Completed) ويخزن بجانبه الاستجابة الكاملة التي سيرسلها للعميل (Response Body and Status Code).
- يرسل الاستجابة الناجحة (e.g.
201 Created) للعميل.
- الخادم يقرأ الهيدر
- الخادم يستقبل الطلب المكرر (النقرة المزدوجة):
- بعد ثوانٍ معدودة، يصل طلب آخر بنفس البيانات وبنفس
Idempotency-Key. - الخادم يبحث عن المفتاح في مخزنه المؤقت.
- يجد المفتاح! وحالته “مكتمل”.
- هنا، لا يقوم الخادم بتنفيذ منطق إنشاء الطلبية مرة أخرى على الإطلاق. لا خصم جديد، لا طلبية جديدة.
- بكل بساطة، يسترجع الاستجابة التي خزنها سابقاً ويرسلها مرة أخرى للعميل كما هي.
- بعد ثوانٍ معدودة، يصل طلب آخر بنفس البيانات وبنفس
بهذه الطريقة، من وجهة نظر العميل، كل شيء يبدو طبيعياً. هو ضغط مرتين، وفي كل مرة جاءته استجابة ناجحة. لكن في الخادم، لم تتم العملية الحساسة إلا مرة واحدة فقط. وهذا هو الجمال كله.
كود يا مبرمج: مثال عملي باستخدام Node.js و Express
الكلام النظري جميل، لكن خلينا نشوف كود. هذا مثال مبسط جداً لفكرة “middleware” في Express.js يمكنه التعامل مع طلبات الـ Idempotency.
ملاحظة: في هذا المثال سأستخدم كائن بسيط في الذاكرة لتخزين المفاتيح. في بيئة الإنتاج الحقيقية، يجب استخدام حل دائم وسريع مثل Redis أو قاعدة بيانات.
// in-memory store (for demonstration only!)
// In production, use Redis or a database.
const idempotencyStore = new Map();
const idempotencyMiddleware = async (req, res, next) => {
const idempotencyKey = req.get('Idempotency-Key');
// If no key, just proceed. Maybe log a warning.
if (!idempotencyKey) {
return next();
}
// Check if we have this key in our store
if (idempotencyStore.has(idempotencyKey)) {
const cachedResponse = idempotencyStore.get(idempotencyKey);
// If it's still processing, you might return a 409 Conflict
if (cachedResponse.status === 'in-progress') {
return res.status(409).json({ message: 'Request is already being processed.' });
}
// If it's completed, return the cached response
console.log(`[Idempotency] Returning cached response for key: ${idempotencyKey}`);
return res.status(cachedResponse.statusCode).json(cachedResponse.body);
}
// --- New Request ---
// 1. Store the key as "in-progress"
idempotencyStore.set(idempotencyKey, { status: 'in-progress' });
console.log(`[Idempotency] New key received, processing: ${idempotencyKey}`);
// 2. We need to "spy" on the original res.json() to cache the response
const originalJson = res.json;
res.json = (body) => {
// Cache the successful response
if (res.statusCode >= 200 && res.statusCode < 300) {
const responseToCache = {
status: 'completed',
statusCode: res.statusCode,
body: body,
};
// Set an expiry for the key, e.g., 24 hours
idempotencyStore.set(idempotencyKey, responseToCache);
} else {
// If the request failed, we can remove the key to allow retries
idempotencyStore.delete(idempotencyKey);
}
// Call the original res.json()
return originalJson.call(res, body);
};
// 3. Proceed to the actual controller
next();
};
// How to use it in your Express app
// app.post('/api/orders', idempotencyMiddleware, createOrderController);
نصائح من قلب الميدان: خبرة أبو عمر
- أين يُولّد المفتاح؟ دائماً وأبداً في طرف العميل (Client-side). لا تجعل الخادم “يخمن” أن الطلب مكرر بناءً على محتوياته. اجعل الأمر صريحاً ومبنياً على مفتاح يرسله العميل.
- كم مدة تخزين المفتاح؟ لا تخزنه للأبد! هذا سيؤدي إلى تضخم قاعدة بياناتك أو ذاكرة Redis بلا داعي. فترة 24 ساعة تعتبر معياراً جيداً ومقبولاً في معظم الحالات. بعد 24 ساعة، من الآمن افتراض أن المستخدم لن يعيد إرسال نفس المعاملة.
- ماذا عن أخطاء الشبكة؟ هذه هي إحدى أروع فوائد هذا النمط. تخيل أن الخادم نفذ العملية بنجاح، لكن الاتصال انقطع قبل أن تصل الاستجابة للعميل. العميل سيعتقد أن العملية فشلت وسيحاول مرة أخرى. بوجود مفتاح الـ Idempotency، المحاولة الثانية لن تنفذ العملية مرة أخرى، بل ستعيد إرسال الاستجابة الناجحة المخزنة، وهذا بالضبط ما نريده.
- “خاوا” (إجبارياً): في فريقي، أصبح استخدام مفتاح Idempotency “خاوا” (إجبارياً) لأي نقطة نهاية (endpoint) تقوم بعملية إنشاء أو تعديل حساسة وغير قابلة للعكس بسهولة. لا تعتبره رفاهية، بل هو أساس لنظام قوي وموثوق.
الخلاصة والزبدة 🥑
في عالم اليوم، حيث نتوقع من الأنظمة أن تكون متاحة وموثوقة 100% من الوقت، لم تعد المشاكل مثل الطلبات المكررة مقبولة. المستخدم لا يرحم، وسمعة تطبيقك على المحك.
مفهوم “مفتاح عدم تكرار المعاملة” هو نمط تصميم بسيط لكنه فعال بشكل لا يصدق. يحول نقاط الضعف (الشبكات غير المستقرة، المستخدمين غير الصبورين) إلى جزء يمكن التعامل معه بأمان داخل نظامك.
نصيحتي الأخيرة لك: ما تستنى المشكلة تصير معك وتتعلم بالطريقة الصعبة زي ما صار معنا. كن استباقياً، وافهم هذا المفهوم جيداً، وادمجه في تصميم الواجهات البرمجية (APIs) الحساسة عندك من اليوم. صدقني، راحة بالك وتقدير عملائك يستحقان هذا الجهد البسيط. 👍