يا أهلاً وسهلاً فيكم، معكم أبو عمر.
قبل كم سنة، كنت شغال مع فريق على نظام دفع إلكتروني لمتجر ببيع منتجات حرفية فلسطينية. كان المشروع عزيز على قلوبنا، لأنه بدعم حرفيين وعائلات شغلهم كله واقف على هالمتجر. الأمور كانت ماشية زي الحلاوة، لحد ما بيوم صحينا على “ولعة”.
تلفوني ما وقف رن من الصبح، وصاحب المتجر بصرخ: “يا أبو عمر، في زبون انسحب من حسابه مرتين لنفس الطلبية! والزبون الثاني بحكي انخصم منه المبلغ ثلاث مرات!”. قلبي وقع بين رجلي. دخلنا على النظام، وشفنا الكارثة: معاملات دفع مكررة بالجملة. الزباين معصبين، والمصاري بتطير على رسوم استرجاع، والثقة بالموقع صارت بالأرض.
بعد ما هدينا الوضع وعملنا استرجاع يدوي للمبالغ (وشربنا كاسات شاي بالمرمية لنهدي راسنا)، قعدنا نحلل المشكلة. اكتشفنا إنه السبب كان بسيط وتافه لدرجة بتجلط: المستخدمين اللي عندهم نت بطيء كانوا بكبسوا على زر “ادفع الآن” أكثر من مرة، والنظام الغلبان كان بستقبل كل طلب كأنه طلب جديد، وبخصم المصاري كل مرة. كانت نقرة مزدوجة بريئة تكلفنا ثروة وسمعة.
هون كانت اللحظة اللي تعرفنا فيها على صديقنا المنقذ: الـ Idempotency Key. ومن يومها، صار جزء أساسي من أي نظام فيه معاملات حساسة بشتغل عليه. خلوني أحكيلكم الحكاية بالتفصيل.
ما هي الـ Idempotency؟ (فلسفة الكبسة الثانية الآمنة)
قبل ما نحكي عن المفاتيح، لازم نفهم المبدأ نفسه. كلمة “Idempotent” كلمة إنجليزية فاخرة شوي، بس معناها بسيط جداً.
تخيل إنك في مصعد وضغطت على زر الطابق الخامس. المصعد رح يستجيب ويبلش يطلع. طيب لو رجعت ضغطت على نفس الزر مرة ثانية وثالثة ورابعة وهو طالع؟ ولا رح يصير إشي جديد. النتيجة النهائية وحدة: المصعد رح يوصل الطابق الخامس. هذا الفعل (ضغط الزر) بنسميه idempotent.
بالمقابل، تخيل إنك بتسحب مصاري من الصراف الآلي. لو طلبت سحب 100 دينار، رح يطلعلك 100 دينار. لو كررت العملية مرة ثانية، رح يطلعلك 100 دينار ثانية، ويصير مجموع المسحوب 200. هذه العملية non-idempotent، لأن تكرارها بغير النتيجة النهائية.
في عالم الـ APIs، نفس المبدأ بينطبق على طلبات الشبكة (HTTP Requests):
- طلبات Idempotent (آمنة للتكرار): مثل
GET,PUT,DELETE. لو طلبت بيانات مستخدم (GET) ألف مرة، رح تجيك نفس البيانات. لو حدثت (PUT) عنوانه بنفس البيانات الجديدة ألف مرة، النتيجة النهائية وحدة. - طلبات Non-idempotent (خطرة عند التكرار): وأشهرها هو الـ
POST. هذا الطلب يُستخدم عادةً لإنشاء شيء جديد. إرسال طلبPOSTلإنشاء مستخدم جديد مرتين، يعني إنشاء مستخدمين اثنين. إرساله لعملية دفع مرتين، يعني خصم المبلغ مرتين!
جحيم المعاملات المكررة: ليش المشكلة كبيرة؟
ممكن حدا يحكي: “خلص يا أبو عمر، بنحط جافاسكربت يمنع المستخدم يكبس على الزر مرتين”. هاي فكرة منيحة، بس مش كافية أبدًا. الشبكات عالم فوضوي، والمشكلة ممكن تحصل لأسباب كثيرة غير كبسة المستخدم المزدوجة:
- انقطاع مؤقت في الشبكة: العميل أرسل الطلب، بس ما وصله الرد من السيرفر بسبب مشكلة نت. طبيعي إنه يحاول يرسل الطلب مرة ثانية.
- مهلة الطلب (Request Timeout): السيرفر أخذ وقت أطول من المعتاد ليرد، فالمتصفح أو العميل اعتبر إنه الطلب فشل وقام بإعادة إرساله تلقائيًا.
- آليات إعادة المحاولة (Retry Mechanisms): كثير من المكتبات البرمجية والأنظمة مصممة لتعيد إرسال الطلبات الفاشلة تلقائيًا.
والنتائج كارثية، زي ما شفنا بقصتنا:
- خسائر مالية: خصم مبالغ مضاعفة من العملاء، ورسوم بنكية للاسترجاع.
- فساد البيانات (Data Corruption): إنشاء حسابات مكررة، طلبيات مكررة، فوضى عارمة في قاعدة البيانات.
- تجربة مستخدم سيئة جدًا: العميل بفقد الثقة فورًا في نظامك لما يشوف مصاريه بتنسحب مرتين.
- صداع تشغيلي: فريق الدعم الفني بقضي وقته في حل مشاكل الزباين الغضبانين بدل ما يركز على تطوير المنتج. “شغلانة ما بتخلص”.
مفتاح عدم التكرار (Idempotency Key): المنقذ الخارق
الحل يكمن في آلية بسيطة وفعالة جدًا تسمى “مفتاح عدم التكرار” أو Idempotency Key. الفكرة هي إعطاء “بصمة” فريدة لكل عملية خطرة، حتى لو تم إرسالها 100 مرة.
كيف يعمل؟
الآلية بتشتغل بالخطوات التالية:
- العميل (Client): قبل إرسال الطلب الحساس (مثل طلب الدفع)، يقوم العميل بإنشاء معرّف فريد وخاص بهذه العملية. عادةً ما يكون سلسلة نصية عشوائية قوية مثل UUID (Universally Unique Identifier).
- الإرسال: يرسل العميل هذا المعرّف الفريد كجزء من الطلب، وغالبًا ما يكون في الـ Headers تحت اسم مثل
Idempotency-Key. - الخادم (Server): عند استقبال الطلب، يقوم الخادم بالآتي:
- يبحث عن الـ
Idempotency-Keyفي الـ headers. - يتحقق في مخزن مؤقت (Cache) أو قاعدة بيانات خاصة لديه: “هل رأيت هذا المفتاح من قبل؟”.
- إذا كان المفتاح جديدًا:
- “يقفل” هذا المفتاح لمنع أي عملية أخرى من استخدامه في نفس اللحظة (لمنع الـ Race Conditions).
- ينفذ العملية المطلوبة (مثلاً، يقوم بخصم المبلغ).
- يخزن نتيجة العملية (الرد الناجح أو الفاشل) مع المفتاح نفسه.
- يرسل الرد للعميل.
- إذا كان المفتاح موجودًا مسبقًا:
- لا يقوم بتنفيذ العملية مرة أخرى أبدًا.
- ببساطة، يسترجع الرد الذي خزنه في المرة الأولى ويرسله مرة أخرى للعميل، وكأن العملية تمت للتو.
- يبحث عن الـ
بهذه الطريقة، نضمن أن العملية التي تحمل بصمة فريدة (المفتاح) ستُنفذ مرة واحدة فقط، مهما حاول العميل إرسالها.
التطبيق العملي: يلا نكتب كود!
الحكي النظري حلو، بس خلينا نشوف كيف ممكن نطبق هالكلام. رح نستخدم مثال بسيط: نظام بلغة JavaScript، مع Express.js على الخادم.
مثال 1: جانب العميل (Client-Side)
في طرف العميل (سواء كان متصفح ويب أو تطبيق موبايل)، نحتاج لمكتبة لتوليد UUIDs مثل uuid.
// 1. قم بتثبيت المكتبة
// npm install uuid
// 2. في كود الجافاسكربت الخاص بك
import { v4 as uuidv4 } from 'uuid';
async function processPayment(paymentData) {
// إنشاء مفتاح فريد لهذه العملية *فقط*
const idempotencyKey = uuidv4();
try {
const response = await fetch('/api/pay', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// هنا السر! أرسل المفتاح في الـ Header
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(paymentData),
});
const result = await response.json();
console.log('Payment processed:', result);
// يمكنك الآن عرض رسالة نجاح للمستخدم
} catch (error) {
console.error('Payment failed:', error);
// ربما هنا تقوم آلية إعادة المحاولة بإرسال نفس الطلب بنفس المفتاح
}
}
// استدعاء الدالة عند ضغط المستخدم على زر الدفع
const paymentButton = document.getElementById('pay-btn');
paymentButton.addEventListener('click', () => {
const data = { amount: 100, currency: 'USD' };
processPayment(data);
});
مثال 2: جانب الخادم (Server-Side) – Middleware في Express.js
هنا يكمن السحر الحقيقي. سنقوم بإنشاء “برنامج وسيط” (Middleware) يعترض الطلبات قبل أن تصل إلى منطق الدفع الرئيسي.
ملاحظة: هذا مثال مبسط يستخدم كائن في الذاكرة لتخزين المفاتيح. في الأنظمة الحقيقية، يجب استخدام نظام تخزين دائم ومشترك مثل Redis أو قاعدة بيانات.
const express = require('express');
const app = express();
app.use(express.json());
// مخزن بسيط للمفاتيح والردود في الذاكرة (للتوضيح فقط)
// في الإنتاج، استخدم Redis أو قاعدة بيانات!
const idempotencyStore = new Map();
// هذا هو الـ Middleware السحري
const idempotencyMiddleware = (req, res, next) => {
// نبحث فقط في الطلبات التي قد تغير البيانات مثل POST
if (req.method !== 'POST') {
return next();
}
const idempotencyKey = req.get('Idempotency-Key');
// إذا لم يرسل العميل مفتاحًا، نتجاوز ونكمل (أو نرجع خطأ)
if (!idempotencyKey) {
return next();
}
// هل رأينا هذا المفتاح من قبل؟
if (idempotencyStore.has(idempotencyKey)) {
console.log(`[Idempotency] Key ${idempotencyKey} seen before. Returning cached response.`);
const cachedResponse = idempotencyStore.get(idempotencyKey);
// أرجع الرد المخزن فورًا
return res.status(cachedResponse.statusCode).json(cachedResponse.body);
}
// هذا مفتاح جديد، نحتاج لتخزين الرد بعد تنفيذه
// نتجاوز دالة res.json الأصلية
const originalJson = res.json;
res.json = (body) => {
const responseToCache = {
statusCode: res.statusCode,
body: body,
};
console.log(`[Idempotency] Caching response for key ${idempotencyKey}.`);
idempotencyStore.set(idempotencyKey, responseToCache);
// الآن يمكننا إرجاع الرد الأصلي
originalJson.call(res, body);
};
next();
};
// تطبيق الـ Middleware على مساراتنا الحساسة
app.use('/api/pay', idempotencyMiddleware);
app.post('/api/pay', (req, res) => {
const { amount, currency } = req.body;
// هنا يتم تنفيذ منطق الدفع الحقيقي (التواصل مع بوابة الدفع، الخ)
// سنقوم بمحاكاة عملية تأخذ بعض الوقت
console.log(`Processing payment for ${amount} ${currency}...`);
setTimeout(() => {
const paymentResult = { transactionId: `txn_${Date.now()}`, status: 'success' };
res.status(201).json(paymentResult);
}, 2000); // محاكاة تأخير لمدة ثانيتين
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
الآن، إذا أرسلت طلبين متتاليين بنفس الـ Idempotency-Key، ستلاحظ في سجلات الخادم أن عملية الدفع “Processing payment…” ستُطبع مرة واحدة فقط! الطلب الثاني سيحصل على الرد فورًا من الذاكرة المؤقتة.
نصائح أبو عمر الذهبية (خبرة سنين يا خال)
- لا تخترع العجلة: قبل أن تبني هذا النظام بنفسك، تحقق من مزود الخدمة الذي تتعامل معه. معظم بوابات الدفع المحترمة (مثل Stripe) لديها دعم مدمج لـ Idempotency Keys. اقرأ وثائقهم أولاً، فهذا يوفر عليك الكثير من العمل.
- توليد المفاتيح: يجب أن يتم توليد المفتاح من جهة العميل (Client) لضمان أن كل محاولة “أصلية” لها مفتاحها الخاص. استخدم دائمًا خوارزميات قوية مثل UUID v4 لتجنب تضارب المفاتيح.
- عمر المفتاح (Key Expiration): لا تخزن المفاتيح إلى الأبد! هذا سيؤدي إلى تضخم قاعدة بياناتك أو الـ Cache بلا فائدة. أفضل الممارسات هي تحديد فترة صلاحية للمفتاح، مثلاً 24 ساعة. هذا كافٍ للتعامل مع معظم محاولات الإعادة.
- أين تخزن المفاتيح؟: المثال أعلاه يستخدم الذاكرة، وهذا لا يصلح للأنظمة الحقيقية. إذا كان لديك أكثر من خادم (load balancer)، فلن تعمل الذاكرة. الحل الصحيح هو استخدام مخزن مشترك وسريع مثل Redis. إنه مثالي لهذه المهمة.
- التعامل مع الحالات الحرجة (Race Conditions): ماذا لو وصل طلبان بنفس المفتاح الجديد في نفس الملي ثانية؟ يجب أن يكون لديك آلية قفل (locking). عند استلام مفتاح جديد، يجب أن “تقفله” في Redis (مثلاً باستخدام
SETNX)، بحيث إذا حاول خادم آخر معالجة نفس المفتاح في نفس الوقت، سيفشل في الحصول على القفل وينتظر أو يرجع خطأ.
خلاصة الكلام
في عالم الأنظمة الموزعة والشبكات التي لا يمكن التنبؤ بسلوكها، لم تعد الـ Idempotency رفاهية، بل هي ضرورة قصوى. قد تبدو فكرة “مفتاح عدم التكرار” معقدة في البداية، لكنها في الحقيقة مبدأ بسيط يحميك من كوابيس فنية ومالية.
لا تستهينوا بقوة النقرة المزدوجة، ففي عالم البرمجة، قد تكلفك سمعة وثروة. طبقوا مفاتيح عدم التكرار في أنظمتكم الحساسة، وناموا قريري العين. 😉