يا جماعة الخير، السلام عليكم ورحمة الله وبركاته. معكم أخوكم أبو عمر.
قبل كم سنة، كنت ماسك مشروع كبير لمتجر إلكتروني لأحد العملاء. الأمور كانت ماشية زي الحلاوة، والتطبيق كان سريع ومستقر، والعميل مبسوط. لحد ما إجا هداك اليوم المشؤوم. صحيت الصبح على تليفون من مدير المشروع، صوته معصّب ومتوتر: “أبو عمر، شو هالمصيبة اللي إحنا فيها؟ في عميل انخصم من بطاقته نفس المبلغ 5 مرات لعملية شراء وحدة!”.
قلبي وقع بين رجليّ. كيف صار هيك؟ فتحت الـ “Logs” (سجلات النظام) بسرعة، وإذ بلاقي إنه العميل المسكين، بسبب ضعف الإنترنت عنده، ضل يكبس على زر “إتمام الدفع” أكثر من مرة وهو مفكر إنه العملية ما مشيت. نظامنا، على نياته، استقبل كل طلب كأنه طلب جديد، ونفّذ عملية الدفع في كل مرة. طبعاً العميل ثارت ثائرته، وإحنا أكلنا بهدلة محترمة، وقضينا اليوم كله بنرجعله مصرياته وبنعتذر منه.
هذا الموقف علّمني درس قاسي لكنه ثمين: في عالم الأنظمة الموزعة والشبكات اللي مش دايماً مستقرة، الاعتماد على حظ المستخدم أو سرعة الإنترنت عنده هو وصفة لكارثة. من يومها، صار مبدأ الـ Idempotency أو “عدم التكرار” جزء أساسي من أي نظام ببنيه، وخصوصاً في العمليات الحساسة. خلونا اليوم نحكي عن هالمبدأ وكيف ممكن ينقذكم من مواقف “بتوجّع الراس” زي اللي صارت معي.
ما هي “عدم التكرار” (Idempotency) في عالم الـ APIs؟
المصطلح ممكن يكون غريب شوي، لكن فكرته بسيطة جداً. في الرياضيات وعلوم الحاسوب، العملية “عديمة التكرار” (Idempotent) هي العملية اللي لو طبقتها مرة وحدة أو طبقتها ألف مرة، النتيجة النهائية بتكون نفسها.
أفضل مثال من حياتنا اليومية هو كبسة المصعد. لما تكبس على زر المصعد عشان تطلبه، هو بيستجيب من أول كبسة. لو ضليت تكبس على الزر عشر مرات زيادة، المصعد ما رح يجي أسرع ولا رح يصير أي شي جديد. الكبسة الأولى غيّرت حالة النظام (المصعد مطلوب)، وكل الكبسات اللي بعدها ما عملت أي تغيير إضافي. هذا هو جوهر الـ Idempotency.
في عالم الـ REST APIs، بعض طلبات الـ HTTP مصممة لتكون عديمة التكرار بطبيعتها:
- GET, HEAD, OPTIONS, TRACE: هاي الطلبات آمنة وعديمة التكرار. طلب بيانات مستخدم 10 مرات رح يعطيك نفس البيانات كل مرة بدون ما يغير أي شي في النظام.
- PUT: هذا الطلب عديم التكرار. لو أرسلت طلب
PUT /users/123لتحديث بيانات مستخدم معين بنفس البيانات 5 مرات، النتيجة النهائية رح تكون نفسها كأنك أرسلته مرة وحدة. هو بيستبدل المورد بالكامل. - DELETE: هذا الطلب عديم التكرار أيضاً. لو أرسلت طلب
DELETE /posts/45، أول مرة رح يحذف المنشور. المرات اللي بعدها رح يرجعلك استجابة “Not Found 404″، لكن حالة النظام النهائية (المنشور محذوف) ما رح تتغير.
لكن وين المشكلة؟ المشكلة الكبيرة بتصير مع طلبات POST.
الكابوس: طلبات POST غير المتكررة
طلب الـ POST يُستخدم عادةً لإنشاء مورد جديد. كل طلب POST إلى نفس الـ endpoint يُفترض أن ينشئ مورداً جديداً ومنفصلاً. وهذا هو سبب الكارثة اللي صارت معنا:
- النقرة الأولى:
POST /payments-> تم إنشاء دفعة جديدة (وخصم المبلغ). - النقرة الثانية:
POST /payments-> تم إنشاء دفعة جديدة أخرى (وخصم المبلغ مرة ثانية!). - النقرة الثالثة:
POST /payments-> وهكذا دواليك…
هذا السلوك خطير جداً في تطبيقات مثل: أنظمة الدفع، حجز المواعيد، إنشاء الحسابات، أو أي عملية لا يجب أن تحدث إلا مرة واحدة فقط لكل إجراء من المستخدم.
الحل السحري: مفاتيح عدم التكرار (Idempotency Keys)
بما أن طلب POST بطبيعته غير متكرر، لازم إحنا كمطورين نجعله يتصرف بطريقة متكررة في الحالات اللي بتتطلب هالشي. وهون بيجي دور “مفتاح عدم التكرار” أو الـ Idempotency Key.
الفكرة عبقرية وبسيطة: العميل (التطبيق أو المتصفح) يقوم بإنشاء مُعرّف فريد (unique ID) لكل عملية “خطيرة” يريد تنفيذها. ثم يرسل هذا المعرّف مع الطلب في هيدر (Header) مخصص، غالباً ما يسمى Idempotency-Key.
عندما يستقبل الخادم (Server) الطلب، يقوم بالآتي:
- يقرأ المفتاح: يأخذ قيمة الـ
Idempotency-Keyمن الهيدر. - يبحث عن المفتاح: يبحث في مكان تخزين مؤقت (مثل قاعدة بيانات أو ذاكرة Redis) ليرى هل استقبل هذا المفتاح من قبل.
- السيناريو الأول (مفتاح جديد): إذا لم يجد المفتاح، فهذا يعني أن هذا طلب جديد. يقوم الخادم بالآتي:
- يُخزن المفتاح فوراً ليمنع أي طلبات أخرى بنفس المفتاح من التنفيذ.
- ينفذ العملية المطلوبة (مثلاً، خصم المبلغ).
- يُخزن نتيجة الاستجابة (Response) التي سيرسلها للعميل (مثلاً،
201 Createdمع تفاصيل الدفعة). - يرسل الاستجابة المخزنة للعميل.
- السيناريو الثاني (مفتاح مكرر): إذا وجد الخادم أن المفتاح موجود مسبقاً، فهذا يعني أن العملية قد تم تنفيذها من قبل. في هذه الحالة، الخادم لا ينفذ العملية مرة أخرى. بدلاً من ذلك، يقوم بالآتي:
- يسترجع الاستجابة الأصلية التي خزنها عند تنفيذ الطلب لأول مرة.
- يرسل نفس الاستجابة المخزنة مرة أخرى للعميل.
بهذه الطريقة، حتى لو أرسل العميل نفس الطلب 100 مرة بسبب خطأ شبكة أو نقرات متكررة، العملية الحساسة (الدفع) ستُنفذ مرة واحدة فقط! 🎯
لنطبق الأمر عملياً: مثال كود (يا مبرمجين!)
الحكي سهل، خلينا نشوف كيف ممكن نطبق هالكلام. رح أستخدم مثال بسيط باستخدام Node.js و Express، لكن المبدأ نفسه ينطبق على أي لغة أو إطار عمل (PHP, Python, Java, etc.).
أولاً: جهة العميل (Client-Side)
العميل هو المسؤول عن إنشاء المفتاح الفريد. أفضل شيء هو استخدام UUID (Universally Unique Identifier).
// في تطبيق الويب أو الموبايل
// قبل إرسال طلب الدفع، قم بإنشاء مفتاح فريد
// في المتصفحات الحديثة، يمكن استخدام crypto API
const idempotencyKey = crypto.randomUUID();
async function processPayment() {
try {
const response = await fetch('/api/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// أهم جزء: إرسال المفتاح في الهيدر
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify({
amount: 99.99,
currency: 'USD',
orderId: 'ORD-12345'
})
});
const data = await response.json();
if (response.ok) {
console.log('Payment successful:', data);
} else {
console.error('Payment failed:', data.error);
}
} catch (error) {
// هذا هو المكان الذي تحدث فيه المشكلة عادةً
// قد تحدث مشكلة في الشبكة هنا، وسيحاول المستخدم مرة أخرى
console.error('Network error or server is down. Please try again.');
}
}
// استدعاء الدالة عند النقر على الزر
document.getElementById('pay-button').addEventListener('click', processPayment);
ثانياً: جهة الخادم (Server-Side)
هنا يكمن السحر الحقيقي. سنقوم بإنشاء Middleware في Express لاعتراض الطلبات والتحقق من المفتاح.
ملاحظة: هذا الكود للتوضيح. في نظام حقيقي، ستستخدم Redis أو قاعدة بيانات لتخزين المفاتيح بدلاً من متغير بسيط في الذاكرة.
const express = require('express');
const app = express();
app.use(express.json());
// سنستخدم كائن بسيط كمخزن مؤقت للتوضيح
// في الواقع، استخدم Redis أو قاعدة بيانات!
const idempotencyCache = {};
// Middleware للتعامل مع مفاتيح عدم التكرار
const idempotencyMiddleware = (req, res, next) => {
const idempotencyKey = req.headers['idempotency-key'];
// إذا لم يتم إرسال المفتاح، اسمح للطلب بالمرور بشكل طبيعي (أو أرجع خطأ)
if (!idempotencyKey) {
return next();
}
// تحقق مما إذا كان المفتاح موجوداً في الكاش
if (idempotencyCache[idempotencyKey]) {
console.log(`[Idempotency] Request with key ${idempotencyKey} already processed. Returning cached response.`);
const cachedResponse = idempotencyCache[idempotencyKey];
// أرجع الاستجابة المخزنة سابقاً
return res.status(cachedResponse.statusCode).json(cachedResponse.body);
}
// إذا كان المفتاح جديداً، نحتاج إلى تخزين الاستجابة بعد معالجتها
// نحتفظ بدالة الإرسال الأصلية
const originalJson = res.json;
const originalSend = res.send;
// نعدّل دالة الإرسال لتخزين النتيجة قبل إرسالها
res.json = (body) => {
if (res.statusCode >= 200 && res.statusCode {
if (res.statusCode >= 200 && res.statusCode {
try {
console.log(`Processing payment for order ${req.body.orderId}...`);
// ... هنا منطق معالجة الدفع الفعلي ...
// const paymentResult = await processPaymentWithGateway(req.body);
const paymentResult = { paymentId: `pay_${Date.now()}`, status: 'completed' };
// عند إرسال الاستجابة، سيقوم الـ middleware بتخزينها تلقائياً
res.status(201).json(paymentResult);
} catch (error) {
res.status(500).json({ error: 'Payment processing failed' });
}
});
app.listen(3000, () => console.log('Server is running on port 3000'));
نصائح من خبرة أبو عمر
تطبيق المبدأ بسيط، لكن الشيطان يكمن في التفاصيل. إليكم بعض النصائح العملية من تجربتي:
1. من أين نأتي بالمفتاح؟
دائماً وأبداً، العميل هو من يجب أن يُنشئ المفتاح. لا تقم بإنشائه على الخادم. الهدف هو ربط عدة محاولات لنفس الطلب من العميل معاً. استخدم UUIDs (الإصدار 4 هو الأنسب) لأنها تضمن تفرّداً عالياً جداً.
2. أين نخزن الحالة؟
المثال أعلاه استخدم كائناً في الذاكرة، وهذا لا يصلح للإنتاج لأنه سيفقد كل شيء عند إعادة تشغيل الخادم، ولا يعمل مع وجود عدة خوادم (Load Balancer). الخيارات الأفضل هي:
- Redis: هو الخيار الأمثل في رأيي. سريع جداً، ويوفر ميزات مثل تعيين وقت انتهاء صلاحية للمفتاح (TTL) بسهولة، وعمليات ذرية (atomic) مثل
SETNXالتي تساعد في منع حالات السباق (Race Conditions). - قاعدة البيانات (SQL/NoSQL): يمكنك إنشاء جدول خاص لتخزين مفاتيح عدم التكرار مع الاستجابات. هذا الخيار أبطأ قليلاً من Redis لكنه موثوق جداً ومتوفر في معظم المشاريع.
3. متى تنتهي صلاحية المفتاح؟
لا يمكنك الاحتفاظ بالمفاتيح إلى الأبد، وإلا ستنفجر مساحة التخزين لديك. من الجيد تعيين وقت انتهاء صلاحية (TTL – Time To Live) للمفاتيح. مدة 24 ساعة هي مدة شائعة ومناسبة لمعظم الحالات. بعد 24 ساعة، يمكن للعميل إعادة محاولة نفس العملية إذا لزم الأمر بمفتاح جديد.
4. تعامل مع حالات السباق (Race Conditions)
ماذا لو وصل طلبان بنفس المفتاح في نفس اللحظة بالضبط؟ قد يتجاوز كلاهما فحص “هل المفتاح موجود؟” قبل أن يتمكن أحدهما من تخزينه. هذا يسمى “حالة سباق”. الحل هو استخدام قفل (lock) أو عملية ذرية. في Redis، يمكنك استخدام أمر SET key value NX الذي يقوم بتعيين المفتاح فقط إذا لم يكن موجوداً. هذا يضمن أن خادماً واحداً فقط هو من “يفوز” بالسباق وينفذ العملية.
الخلاصة: فكرة بسيطة تنقذ أنظمة عظيمة
في النهاية يا إخوان، بناء أنظمة موثوقة وقوية لا يتعلق فقط بكتابة كود يعمل في الظروف المثالية، بل يتعلق بالاستعداد لأسوأ السيناريوهات: شبكة بطيئة، مستخدم غير صبور، أو أخطاء غير متوقعة. مفاتيح عدم التكرار هي واحدة من أبسط الأدوات وأكثرها فعالية في ترسانتك كمطور لبناء الثقة في نظامك.
تذكروا دائماً قصة العميل الذي دُفّع 5 مرات. استثمار بضع ساعات في تطبيق هذا المفهوم بشكل صحيح يمكن أن يوفر عليك أياماً من الصداع، وخسارة مالية، والأهم من ذلك، خسارة ثقة عملائك.
لا تستهينوا بقوة هذه الفكرة البسيطة، فهي الفرق بين نظام يثق به المستخدمون ونظام يسبب لهم الصداع. خليكم مرتبين في شغلكم، والله يوفق الجميع. 🙏