خليني أحكيلكم قصة صارت معي قبل كم سنة، قصة علّمتني درس ما بنساه في عالم هندسة البرمجيات. كنا شغالين على نظام دفع لمتجر إلكتروني كبير، وكان يوم الإطلاق الكبير. الكل متحمس، والقهوة شغالة، والوضع “تحت السيطرة” زي ما بنحكي. أطلقنا الموقع، وبدأت الطلبات تنهال علينا… يا فرحة ما تمت!
بعد حوالي ساعة، بدأت توصلنا رسائل غاضبة من العملاء على صفحة الفيسبوك. واحد بحكي “خصمتوا مني مرتين!”، وواحد ثاني بصوّر كشف حسابه وبفرجينا خصمين لنفس الطلب. صار الهرج والمرج، والفريق كله توتر. أنا، كالعادة، بحب أروّق الأمور، قلتلهم “يا جماعة الخير، اهدوا شوي، خلينا نفهم شو اللي بصير”.
بعد تحليل سريع لسجلات الخادم (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.
آلية العمل خطوة بخطوة
الفكرة بسيطة جداً في جوهرها:
- العميل (Client-side) يبتكر المفتاح: قبل إرسال الطلب (مثلاً، عند الضغط على زر “الدفع”)، يقوم تطبيق العميل (سواء كان متصفح ويب أو تطبيق جوال) بإنشاء مُعرّف فريد لهذه العملية. عادةً ما يكون هذا المعرّف عبارة عن UUID (Universally Unique Identifier) مثل
f7c3b2a0-7a4f-4bde-9e7f-1b5e3d8a9e2a. - العميل يرسل المفتاح مع الطلب: يتم إرسال هذا المفتاح الفريد في هيدر (Header) خاص مع طلب الـ API. الهيدر المتعارف عليه هو
Idempotency-Key. - الخادم (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) ليست ترفاً تقنياً، بل هي ضرورة أساسية في أي نظام يتعامل مع عمليات حساسة مثل المدفوعات، إنشاء الطلبات، أو أي إجراء له تكلفة مالية أو لوجستية. تطبيقها بشكل صحيح من اليوم الأول هو الفارق بين نظام ينام مهندسوه بسلام، ونظام يجعلهم يقضون لياليهم في إطفاء الحرائق وإرضاء العملاء الغاضبين.
الزبدة: استثمر قليلاً من الوقت الآن في بناء جدار الحماية هذا، وسيوفر عليك ساعات لا تحصى من الألم والصداع في المستقبل. 👍