أذكر ذلك اليوم جيداً، كان يوم خميس هادئ نسبياً. كنت أحتسي فنجان القهوة وأراجع بعض الأكواد لمشروع متجر إلكتروني لأحد عملائنا الكرام. فجأة، رن هاتفي. على الطرف الآخر كان العميل، وصوته يرتجف من القلق: “أبو عمر، الحقنا يا زلمة! مصيبة!”.
قلبي نزل في ركبي، كما نقول. سألته بهدوء مصطنع: “خير ان شاء الله؟ شو اللي صاير؟”. أجاب بصوت متقطع: “في زبون اشترى غرض، والسيستم خصم من بطاقته مرتين! مرتين يا أبو عمر! هسّه عاملنا قصة وبدو يشتكي علينا!”.
في تلك اللحظة، تدور في رأسك كل السيناريوهات الكارثية. هل هناك خطأ في منطق الدفع؟ هل بوابة الدفع التي نتعامل معها فيها مشكلة؟ أغلقت الخط مع العميل ووعدته بحل المشكلة بأسرع وقت، وفتحت شاشة الأكواد والـ logs على مصراعيها. بدأت رحلة التحقيق.
بعد دقائق من البحث والتدقيق، وجدتُها. طلبا دفع متطابقان تماماً لنفس الطلب، من نفس المستخدم، بنفس المبلغ. الفارق الزمني بينهما؟ أقل من ثانية! هنا أدركت الحقيقة: لم يكن هناك خطأ برمجي كارثي، بل كان الأمر أبسط وأخطر بكثير. المستخدم، بسبب ضعف الاتصال بالإنترنت على الأغلب، ضغط على زر “تأكيد الدفع” مرتين… فدمر كل شيء.
هذه الحادثة لم تكن مجرد مشكلة عابرة، بل كانت درساً قاسياً في أهمية مبدأ برمجي أساسي غالباً ما يتم إهماله: مبدأ اللاتكرارية (Idempotency).
ما هو “مبدأ اللاتكرارية” (Idempotency) يا أبو عمر؟
ببساطة شديدة، العملية “اللاتكرارية” هي العملية التي لو نفذتها مرة واحدة أو عشر مرات، ستحصل دائماً على نفس النتيجة النهائية. لن يتغير شيء بعد التنفيذ الأول.
فكر في الأمر كمفتاح الإضاءة في غرفتك. عندما تضغط عليه مرة، يُضيء المصباح. إذا ضغطت عليه عشر مرات أخرى (وهو في وضع التشغيل)، هل ستزداد الإضاءة؟ بالطبع لا. سيبقى المصباح مضاءً. هذه عملية “لاتكرارية”.
في المقابل، فكر في عملية غير تكرارية، مثل إضافة مبلغ إلى حسابك البنكي. لو أضفت 100 دولار، ثم أضفت 100 دولار مرة أخرى، سيصبح لديك 200 دولار إضافية. كل عملية لها تأثير جديد. هذه عملية ليست لاتكرارية، وهي بالضبط ما حدث مع عميلنا المسكين.
اللاتكرارية في عالم الـ API
في عالم واجهات برمجة التطبيقات (APIs)، يرتبط هذا المبدأ ارتباطاً وثيقاً بأنواع طلبات HTTP:
- GET, PUT, DELETE: هذه الطلبات يفترض أن تكون لاتكرارية بطبيعتها. طلب بيانات مستخدم (GET) عشر مرات لن يغير بياناته. تحديث بيانات مستخدم (PUT) بنفس البيانات عشر مرات سيؤدي إلى نفس النتيجة. حذف مستخدم (DELETE) ثم محاولة حذفه مرة أخرى لن يفعل شيئاً جديداً (ستحصل على خطأ “غير موجود” ربما، لكن الحالة النهائية للنظام واحدة: المستخدم محذوف).
- POST: هذا هو “موقع الخطر”. طلبات POST تُستخدم لإنشاء موارد جديدة (إنشاء منشور، تسجيل طلب، إجراء عملية دفع). بطبيعتها، هي ليست لاتكرارية. إرسال طلب POST مرتين يعني إنشاء موردين جديدين، وهذا هو أصل المشكلة التي واجهناها.
لماذا تكرار الطلبات مشكلة حقيقية وليست مجرد “نقزة” مستخدم؟
قد تعتقد أن المشكلة تكمن فقط في المستخدمين الذين لا يصبرون ويضغطون على الأزرار كالمجانين. لكن الحقيقة أكثر تعقيداً. تكرار الطلبات يحدث لأسباب عديدة خارجة عن سيطرة المستخدم:
- تقلبات الشبكة: خصوصاً على شبكات الهاتف المحمول، قد يفقد الجهاز الاتصال للحظة بعد إرسال الطلب. المتصفح أو التطبيق، في محاولة لمساعدة المستخدم، قد يعيد إرسال الطلب تلقائياً عند عودة الاتصال.
- المهلات الزمنية (Timeouts): قد يرسل العميل (المتصفح) الطلب، ويستلمه الخادم (السيرفر) ويبدأ بمعالجته. لكن بسبب بطء في الشبكة أو ضغط على الخادم، لا تصل الاستجابة إلى العميل في الوقت المناسب. يعتقد العميل أن الطلب فشل، فيقوم بإعادة إرساله.
- آليات إعادة المحاولة (Retry Mechanisms): العديد من المكتبات البرمجية الحديثة (مثل Axios في عالم JavaScript) وأنظمة الخلفية (مثل Webhooks أو Message Queues) لديها منطق إعادة محاولة مدمج. إذا فشل طلب ما، ستحاول إرساله مرة أخرى تلقائياً. بدون تصميم لاتكراري، هذه الميزة المفيدة تتحول إلى قنبلة موقوتة.
الحل السحري: مفتاح اللاتكرارية (Idempotency Key)
الحمد لله، لكل مشكلة حل في عالم البرمجة. والحل هنا يكمن في مفهوم أنيق وفعال يسمى “مفتاح اللاتكرارية” (Idempotency Key). الفكرة بسيطة للغاية:
عندما يريد العميل تنفيذ عملية حساسة (مثل الدفع)، يقوم بإنشاء “هوية فريدة” لهذه العملية. هذه الهوية هي “مفتاح اللاتكرارية”. ثم يرسل هذا المفتاح مع الطلب، عادةً في ترويسة (Header) خاصة مثل
Idempotency-Key.
هذا المفتاح يحول العملية من “نفذ هذا الطلب” إلى “نفذ هذه العملية ذات الهوية X، إذا لم تكن قد نفذتها من قبل”.
كيف يتعامل الخادم مع هذا المفتاح؟
هنا يكمن الذكاء. عندما يستقبل الخادم طلباً يحمل مفتاح اللاتكرارية، يتبع الخطوات التالية:
- البحث عن المفتاح: يبحث الخادم في سجلاته (قاعدة بيانات أو ذاكرة تخزين مؤقت سريعة مثل Redis) ليرى هل استقبل هذا المفتاح من قبل.
- السيناريو الأول: مفتاح جديد: إذا لم يجد المفتاح، فهذا يعني أنها عملية جديدة.
- يقوم الخادم بتنفيذ العملية المطلوبة (مثلاً، خصم المبلغ من البطاقة).
- بعد نجاح العملية، يقوم بتخزين نتيجة الاستجابة (Response) مع مفتاح اللاتكرارية.
- يرسل الاستجابة للعميل كالمعتاد.
- السيناريو الثاني: مفتاح مكرر: إذا وجد الخادم المفتاح في سجلاته، فهذا يعني أن هذه العملية قد تم تنفيذها من قبل.
- لا يقوم بتنفيذ العملية مرة أخرى. وهذه هي النقطة الأهم.
- يسترجع الاستجابة التي قام بتخزينها في المرة الأولى.
- يرسل نفس الاستجابة المخزنة إلى العميل.
بهذه الطريقة، حتى لو أرسل المستخدم الطلب 100 مرة، سيتم الخصم من بطاقته مرة واحدة فقط، وفي كل مرة من الـ 99 المتبقية، سيحصل على نفس رسالة النجاح التي حصل عليها في المرة الأولى، دون أن يحدث أي شيء إضافي في الخلفية.
مثال عملي بالكود: لنبنيها سوا!
دعنا نرى كيف يمكن تطبيق هذا المفهوم في مثال بسيط باستخدام Node.js و Express. سنقوم بمحاكاة تخزين المفاتيح في الذاكرة (في الأنظمة الحقيقية، استخدم Redis أو قاعدة بيانات).
const express = require('express');
const app = express();
const { v4: uuidv4 } = require('uuid');
// في نظام حقيقي، استخدم Redis أو قاعدة بيانات لهذا الغرض
// In a real system, use Redis or a database for this
const idempotencyStore = new Map();
app.use(express.json());
// نقطة النهاية (Endpoint) الخاصة بالدفع
app.post('/api/charge', async (req, res) => {
const idempotencyKey = req.get('Idempotency-Key');
// تأكد من وجود المفتاح
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key header is missing.' });
}
// 1. هل رأينا هذا المفتاح من قبل؟
if (idempotencyStore.has(idempotencyKey)) {
console.log(`[${idempotencyKey}] طلب مكرر. إعادة إرسال الاستجابة المحفوظة.`);
const savedResponse = idempotencyStore.get(idempotencyKey);
// أعد إرسال نفس كود الحالة ونفس المحتوى
return res.status(savedResponse.status).json(savedResponse.body);
}
try {
// 2. هذا طلب جديد، لنقم بمعالجته
console.log(`[${idempotencyKey}] معالجة طلب جديد...`);
const { amount, currency } = req.body;
// --- هنا تضع منطق الدفع الفعلي ---
// --- Imagine your real payment logic here ---
console.log(`Charging ${amount} ${currency}...`);
// محاكاة عملية ناجحة
const paymentResult = {
transactionId: `txn_${uuidv4().split('-')[0]}`,
status: 'succeeded'
};
// --- نهاية منطق الدفع ---
const responsePayload = {
status: 201, // 201 Created
body: paymentResult
};
// 3. قبل إرسال الاستجابة، احفظها!
idempotencyStore.set(idempotencyKey, responsePayload);
console.log(`[${idempotencyKey}] تم حفظ الاستجابة.`);
// 4. أرسل الاستجابة للعميل
return res.status(responsePayload.status).json(responsePayload.body);
} catch (error) {
console.error(`[${idempotencyKey}] حدث خطأ أثناء المعالجة:`, error);
// من الجيد أيضاً حفظ استجابة الخطأ
const errorResponse = { status: 500, body: { error: 'Internal Server Error' } };
idempotencyStore.set(idempotencyKey, errorResponse);
return res.status(500).json({ error: 'Internal Server Error' });
}
});
// ... باقي إعدادات الخادم
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
console.log("To test, run:");
console.log("curl -X POST -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" -d '{"amount": 100, "currency": "USD"}' http://localhost:3000/api/charge");
});
نصائح من “الختيار”: خلاصة خبرتي في هذا المجال
بعد سنوات من التعامل مع هذه المشاكل، اسمحوا لي أن أقدم لكم بعض النصائح العملية:
- أين يتم إنشاء المفتاح؟ دائماً على جهة العميل (المتصفح، تطبيق الموبايل). العميل هو من يعرف أن المستخدم بدأ “عملية” جديدة. استخدم مكتبة لتوليد UUID (مثل `uuid` في JavaScript) لضمان التفرد.
- أين يتم تخزين المفاتيح؟ الخيار الأفضل هو ذاكرة تخزين مؤقت سريعة ومستقلة مثل Redis. إنه أسرع من قاعدة البيانات ويسمح لك بتعيين تاريخ انتهاء صلاحية للمفتاح (مثلاً، 24 ساعة)، وهذا مهم جداً لمنع تضخم الذاكرة إلى ما لا نهاية.
- ماذا تخزن بالضبط؟ لا تخزن فقط “نعم، لقد رأيت هذا المفتاح”. قم بتخزين الاستجابة الكاملة التي أرسلتها في المرة الأولى (كود الحالة، الترويسات، والمحتوى). هذا يضمن أن العميل سيحصل على نفس التجربة تماماً عند إعادة المحاولة.
- اللاتكرارية هي عقد: تعامل معها كعقد بينك وبين من يستخدم الـ API الخاص بك. وثّق بوضوح في الـ Documentation أن نقطة النهاية هذه تدعم اللاتكرارية، واشرح كيفية استخدام ترويسة
Idempotency-Key. هذا ما يفعله الكبار مثل Stripe وPayPal، وهو علامة على الاحترافية. - لا تبالغ في استخدامها: لست بحاجة لتطبيق هذا المبدأ على كل endpoints في نظامك. ركز على العمليات الحرجة التي تغير حالة النظام (Stateful operations) ولا يمكن التراجع عنها بسهولة، مثل إنشاء الطلبات، عمليات الدفع، أو إرسال رسائل هامة.
الخلاصة: لا تترك الأمر للصدفة 🧘♂️
في النهاية، بناء أنظمة قوية لا يتعلق فقط بكتابة كود يعمل في الظروف المثالية، بل يتعلق بتوقع الفوضى والاستعداد لها. تكرار الطلبات ليس احتمالاً، بل هو حقيقة لا مفر منها في عالم الويب المترابط وغير المستقر.
مبدأ اللاتكرارية يحول هذا الكابوس المحتمل من “يا إلهي، لقد تم خصم المبلغ مرتين!” إلى مجرد ملاحظة هادئة في سجلات الخادم: “طلب مكرر، تم تجاهله”. إنه الفرق بين نظام هش ينهار عند أول ضغطة زر مزدوجة، ونظام صلب وموثوق يحمي بيانات المستخدم وأمواله، ويمنحك أنت كمطور راحة البال التي تستحقها.
لا تنتظر حتى تتلقى مكالمة هاتفية مرعبة من عميل غاضب. كن استباقياً، واجعل اللاتكرارية جزءاً أساسياً من تصميم أنظمتك. برمجة سعيدة يا جماعة، والله يوفقكم. 👨💻