يا جماعة الخير، السلام عليكم ورحمة الله. اسمحولي اليوم أحكيلكم قصة صارت معي قبل كم سنة، قصة علّمتني درس ما بنساه طول عمري في عالم البرمجة. كنا وقتها شغالين على نظام تجارة إلكترونية كبير، والضغط علينا كان “للركب”. وصلنا لمرحلة ربط بوابة الدفع، وكان الموعد النهائي للتسليم قريب جداً.
في ليلة من الليالي، والمشروع في آخر مراحله، بلّشت توصلنا رسايل من العميل زي المطر… “أبو عمر، في مشكلة كبيرة!”، “العملاء بشتكوا إنه تم خصم المبلغ مرتين وثلاث من بطاقاتهم!”. نزلت عليّ هالجملة زي الصاعقة. فتحت لوحة التحكم تبعت بوابة الدفع، وإذ بالكارثة: عمليات شراء مكررة لنفس الطلب، ومبالغ مسحوبة بالهبل بدون وجه حق. خربت الدنيا، وصار تلفوني يرن كل خمس دقايق من العميل ومن مدير الشركة، والكل معصّب.
بعد ليلة طويلة من القهوة والتحليل، اكتشفنا السبب. كان المستخدم أحياناً يضغط على زر “ادفع الآن” أكثر من مرة لأنه النت عنده بطيء، أو تطبيقنا على الموبايل كان فيه ميزة “إعادة المحاولة التلقائية” لو فشل الاتصال. كل ضغطة وكل إعادة محاولة كانت ترسل طلب جديد للسيرفر، والسيرفر المسكين ما كان يفرّق بينهم، فكان يخصم المبلغ مع كل طلب. كانت كل إعادة محاولة بمثابة كارثة مالية جديدة للعميل، وصداع جديد إلنا. يومها، عرفت إنه لازم نلاقي حل جذري لهالورطة، وهون بدأت رحلتنا مع ما يسمى بـ “مفاتيح عدم التكرار” أو الـ Idempotency Keys.
ما هي قصة عدم التكرار (Idempotency)؟
قبل ما نخوض في الحل، خلينا نفهم أصل المشكلة والمفهوم اللي بحلها. “Idempotency” هي كلمة ممكن تكون غريبة شوي، لكن معناها بسيط جداً. العملية “اللامكررة” (Idempotent) هي العملية اللي لو نفذتها مرة أو ألف مرة، النتيجة النهائية بتكون نفسها زي كأنك نفذتها مرة واحدة فقط.
خليني أعطيك مثال من حياتنا اليومية:
- مثال غير متكرر (Not Idempotent): كبسة الضوء في الغرفة. كل مرة بتكبسها، حالة الإضاءة بتتغير (من مطفأة لمضاءة، ومن مضاءة لمطفأة). النتيجة بعد كبستين مختلفة عن النتيجة بعد كبسة واحدة.
- مثال متكرر (Idempotent): كبسة المصعد للطابق الخامس. بعد ما تكبسها أول مرة ويضوي الزر، لو ضليت تكبس عليه مية مرة بعدها، ما رح يصير إشي جديد. المصعد رح يضل رايح على الطابق الخامس. النتيجة النهائية واحدة.
في عالم الواجهات البرمجية (APIs)، هذا المفهوم حيوي جداً. عمليات القراءة (GET) هي بطبيعتها متكررة، لأنها ما بتغير إشي في النظام. لكن المشكلة بتظهر في العمليات اللي بتغير الحالة زي إنشاء سجل جديد (POST) أو تحديثه (PUT/PATCH). تخيل لو كل طلب POST لإنشاء طلبية جديدة كان يتنفذ مع كل إعادة محاولة! هذا هو بالضبط الجحيم اللي كنا عايشين فيه.
الحل السحري: مفتاح عدم التكرار (Idempotency Key)
هون بيجي دور البطل في قصتنا: مفتاح عدم التكرار (Idempotency Key). الفكرة عبقرية في بساطتها. هي عبارة عن اتفاق بين العميل (التطبيق الأمامي أو أي نظام آخر) والخادم (التطبيق الخلفي).
آلية العمل كالتالي:
- العميل يبتكر المفتاح: قبل ما يرسل العميل طلب ممكن يتكرر (زي عملية دفع)، يقوم بإنشاء معرّف فريد وخاص لهذا الطلب. عادةً ما يكون هذا المعرّف عبارة عن UUID (Universally Unique Identifier).
- العميل يرسل المفتاح: يرسل العميل هذا المفتاح مع الطلب، غالباً في ترويسة (Header) خاصة مثل
Idempotency-Key. - الخادم يتحقق من المفتاح: لما يستقبل الخادم الطلب، أول إشي بيعمله هو إنه يطلع على هاي الترويسة.
- السيناريو الأول (مفتاح جديد): إذا الخادم ما شاف هاد المفتاح من قبل، بيعرف إنه هاي عملية جديدة. بيقوم بتنفيذ العملية كالمعتاد (مثلاً، خصم المبلغ)، وبعدين بيخزن نتيجة العملية (الرد كامل، مع حالة النجاح أو الفشل) ويربطها بهذا المفتاح الفريد، ثم يرسل الرد للعميل.
- السيناريو الثاني (مفتاح مكرر): أما إذا الخادم استقبل طلب تاني بنفس المفتاح، بيعرف فوراً إنه هاي إعادة محاولة لعملية سابقة. هون، الخادم لا يقوم بتنفيذ العملية مرة أخرى. بدلاً من ذلك، بيروح مباشرة على النتيجة اللي خزنها أول مرة، وبيرجعها للعميل كما هي.
بهذه الطريقة، العميل بيحصل على نفس الرد اللي كان رح يحصل عليه في المرة الأولى، والنظام بيضمن إنه العملية الخطيرة ما تتنفذ إلا مرة واحدة فقط. مشكلة انحلت!
مثال عملي: كيف تبني نظاماً يدعم عدم التكرار؟
الحكي حلو، بس خلينا نشوف كود. لنفترض إنه عنا سيرفر مكتوب بـ Node.js وإطار العمل Express، وبدنا نطبق هالمبدأ على نقطة نهاية (endpoint) لإنشاء عملية دفع.
أولاً: جهة العميل (Client-Side)
العميل هو المسؤول عن إنشاء المفتاح. باستخدام JavaScript، ممكن نعمل إشي زي هيك:
// 1. ننشئ مفتاح فريد (عادة قبل البدء بالعملية)
// يمكن استخدام مكتبة مثل 'uuid'
const idempotencyKey = 'd3b07384-952b-4432-8488-b2c035645e54'; // crypto.randomUUID()
// 2. عند إرسال الطلب، نضيف المفتاح في الترويسة
fetch('/api/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey, // <-- المفتاح السحري هنا
},
body: JSON.stringify({
amount: 100,
currency: 'USD',
orderId: 'ORD-123'
})
});
ثانياً: جهة الخادم (Server-Side – Express.js)
على الخادم، رح نستخدم “وسيط” (Middleware) ليعالج هاي الطلبات قبل ما توصل للمنطق الرئيسي. رح نحتاج لمكان نخزن فيه المفاتيح والردود، مثل قاعدة بيانات سريعة (Cache) زي Redis.
ملاحظة: الكود التالي هو للتوضيح. في بيئة الإنتاج، ستحتاج إلى معالجة الأخطاء وحالات أخرى بشكل أفضل.
const express = require('express');
const redis = require('redis'); // للتخزين المؤقت
const app = express();
const redisClient = redis.createClient(); // افترض أن Redis يعمل
app.use(express.json());
// الوسيط (Middleware) لمعالجة عدم التكرار
const idempotencyHandler = async (req, res, next) => {
// نهتم فقط بطلبات POST
if (req.method !== 'POST') {
return next();
}
const idempotencyKey = req.headers['idempotency-key'];
// إذا لم يرسل العميل المفتاح، نتجاهل ونكمل
if (!idempotencyKey) {
return next();
}
try {
// 1. تحقق إذا كان المفتاح موجود في Redis
const cachedResponse = await redisClient.get(idempotencyKey);
if (cachedResponse) {
// 2. إذا وجد، أرجع الرد المخزن
console.log(`[Idempotency] Request with key ${idempotencyKey} is a duplicate.`);
const responseData = JSON.parse(cachedResponse);
return res.status(responseData.statusCode).json(responseData.body);
}
// 3. إذا لم يوجد، نحفظ الرد بعد انتهاء العملية
const originalSend = res.send;
res.send = function (body) {
const responseToCache = {
statusCode: res.statusCode,
body: JSON.parse(body) // افترض أن الرد دائماً JSON
};
// نخزن الرد في Redis مع مدة صلاحية (مثلاً 24 ساعة)
redisClient.set(idempotencyKey, JSON.stringify(responseToCache), { EX: 24 * 60 * 60 });
originalSend.apply(res, arguments);
};
next(); // أكمل إلى المتحكم (Controller) الرئيسي
} catch (error) {
console.error('Redis error:', error);
next(); // في حالة فشل Redis، من الأفضل إكمال الطلب
}
};
// تطبيق الوسيط على كل المسارات أو مسارات محددة
app.use(idempotencyHandler);
// نقطة النهاية (Endpoint) لإنشاء عملية دفع
app.post('/api/payments', (req, res) => {
console.log(`[Payment] Processing new payment for order ${req.body.orderId}...`);
// ... منطق معالجة الدفع الفعلي هنا ...
// (التواصل مع بوابة الدفع، تحديث قاعدة البيانات، إلخ)
const paymentResult = { success: true, transactionId: 'txn_' + Date.now() };
res.status(201).json(paymentResult);
});
// ... باقي الكود ...
نصائح “أبو عمر” الذهبية لتطبيق عدم التكرار
من خلال التجربة والخطأ، تعلمت كم شغلة مهمة عن الموضوع، وحابب أشاركم إياها:
1. من أين يأتي المفتاح؟
دائماً وأبداً، المفتاح يجب أن يتم إنشاؤه من جهة العميل. لو قام الخادم بإنشائه، فإنه يفقد كل قيمته، لأن الخادم لا يعرف ما إذا كان الطلب الجديد هو إعادة محاولة لطلب قديم أم طلب جديد كلياً. العميل هو الوحيد الذي يملك هذه المعلومة.
2. مدة صلاحية المفتاح
لا تترك المفاتيح مخزنة إلى الأبد! هذا سيؤدي إلى تضخم قاعدة البيانات أو الـ Cache عندك بلا فائدة. قم بتعيين مدة صلاحية معقولة للمفتاح (مثلاً 24 ساعة). هذا يعطي العميل وقتاً كافياً لإعادة المحاولة بأمان، وفي نفس الوقت يحافظ على نظافة نظامك.
3. ماذا نخزن بالضبط؟
لا تخزن فقط “تم بنجاح”. قم بتخزين الرد الكامل الذي أرسلته للعميل في المرة الأولى، بما في ذلك رمز الحالة (Status Code) وجسم الرد (Response Body). إذا فشل الطلب في المرة الأولى بسبب خطأ في البيانات (مثلاً 400 Bad Request)، فيجب أن يحصل العميل على نفس الخطأ 400 في كل مرة يعيد فيها إرسال الطلب بنفس المفتاح.
4. ليست فقط للمدفوعات
صحيح أن المثال الأكثر شيوعاً هو عمليات الدفع، لكن هذا المبدأ مفيد لأي عملية “كتابة” لا تريد أن تتكرر:
- إنشاء طلبية جديدة في متجر إلكتروني.
- إرسال رسالة بريد إلكتروني أو إشعار.
- نشر تعليق على منشور.
- إجراء أي عملية تكلف مالاً أو وقتاً أو موارد.
5. تعامل مع الحالات الحرجة (Race Conditions)
ماذا لو وصل طلبان متطابقان يحملان نفس المفتاح إلى خادمين مختلفين في نفس اللحظة؟ هذا قد يؤدي إلى تنفيذ العملية مرتين. لحل هذه المشكلة، تحتاج إلى آلية قفل (Locking) على مستوى المفتاح. عند استلام طلب بمفتاح جديد، قم بـ “حجز” هذا المفتاح في مخزن مشترك (مثل Redis أو قاعدة البيانات) قبل بدء المعالجة. أي طلب آخر يأتي بنفس المفتاح أثناء المعالجة يجب أن ينتظر حتى يتم تحرير القفل.
الخلاصة… والزبدة 💡
في عالم الأنظمة الموزعة والشبكات غير الموثوقة، لم تعد “إعادة المحاولة” مجرد خيار، بل هي ضرورة. لكن إعادة المحاولة بدون تصميم واعٍ يمكن أن تحول نظامك إلى كابوس من البيانات المكررة والمشاكل اللامنتهية.
مفاتيح عدم التكرار (Idempotency Keys) ليست مجرد تقنية معقدة للمحترفين، بل هي نمط تصميم أساسي وبسيط يحل مشكلة حقيقية ومؤلمة. تطبيقها بشكل صحيح يحول واجهاتك البرمجية من هشة وقابلة للكسر إلى قوية وصلبة وموثوقة، ويمنحك راحة البال التي تستحقها كمطور.
نصيحتي الأخيرة: لا تنتظر وقوع الكارثة كما حدث معي. ابدأ اليوم بالتفكير في كيفية دمج هذا المفهوم في مشاريعك القادمة. القليل من التخطيط المسبق سيوفر عليك الكثير من الصداع ومكالمات العملاء الغاضبة في منتصف الليل. وصدقوني، النوم الهانئ لا يقدر بثمن. 👍