المستوى المطلوب لهذا المقال: متوسط. يفترض إنك تعرف REST APIs الأساسية وعندك فكرة عن قواعد البيانات، لكن مش لازم تكون اشتغلت على أنظمة دفع قبل كده.
Idempotency Key: ازاي تمنع الخصم المزدوج لما الطلب يتكرر
لو عميل دفع مرة واحدة لكن اتخصم منه مرتين، المشكلة غالبًا مش في بوابة الدفع. المشكلة في إن السيرفر بتاعك بيعامل كل طلب HTTP كأنه طلب جديد، حتى لو كان نفس الطلب اتبعت مرتين. الحل في سطر واحد اسمه Idempotency-Key، وهنبنيه بالتفاصيل.
المشكلة باختصار
تخيّل عندك متجر بـ 50 ألف طلب دفع في اليوم. العميل يضغط زر Pay، الصفحة تتأخر شوية، فيضغط تاني. أو الموبايل يقطع النت بعد ما الطلب وصل فعلاً للسيرفر، فالتطبيق يعيد الإرسال تلقائيًا (retry). النتيجة: طلبين متطابقين يوصلوا للسيرفر، والسيرفر يخصم مرتين. حتى لو نسبة التكرار 1% بس، ده 500 خصم مزدوج في اليوم، وكل واحد فيهم شكوى عميل أو استرجاع فلوس.
المفهوم بمثال بسيط الأول
تخيّل شباك تذاكر سينما. انت معاك رقم حجز مكتوب على ورقة. لو رحت للموظف وقلتله "احجزلي على الرقم ده"، هو يحجز ويكتب الرقم في دفتره. لو رجعت تاني بنفس الورقة ونفس الرقم، الموظف يبص في دفتره ويلاقي الرقم ده اتحجز خلاص، فيقولك "تذكرتك جاهزة" ويديك نفس التذكرة. مش هيحجزلك كرسي تاني ولا يطلب منك فلوس تاني.
الرقم اللي على الورقة هو الـ Idempotency-Key. ودفتر الموظف هو مكان تخزين المفاتيح اللي اتعاملنا معاها. الفكرة كلها: نفس المفتاح يدخل، نفس الرد يطلع، والتأثير الجانبي (الخصم) يحصل مرة واحدة بس.
التعريف الدقيق
العملية تكون idempotent لو تنفيذها مرة واحدة زي تنفيذها عدة مرات بنفس المدخلات، من ناحية التأثير على حالة النظام. في HTTP، أفعال زي GET و PUT و DELETE تُعتبر idempotent بطبيعتها حسب توثيق MDN، لكن POST مش كذلك، لأنه بيُنشئ مورد جديد كل مرة. عشان كده عمليات الدفع (اللي بتكون POST) محتاجة آلية صريحة نضمن بيها الـ idempotency. الآلية دي هي ترويسة Idempotency-Key: العميل بيولّد مفتاح فريد (UUID مثلاً) لكل عملية، ويبعته مع كل محاولة لنفس العملية. السيرفر يخزّن المفتاح مع نتيجة أول تنفيذ، ولو وصله نفس المفتاح تاني يرجّع النتيجة المخزّنة بدون إعادة التنفيذ.
الحل خطوة بخطوة بالكود
الطريقة الأبسط والأقوى: عمود UNIQUE في قاعدة البيانات يمنع تسجيل نفس المفتاح مرتين. خلي الجدول كده في PostgreSQL:
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
response_body JSONB NOT NULL,
status_code INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
وفي Express، المعالج بياخد المفتاح من الترويسة، ويحاول يحجزه قبل ما ينفّذ الخصم. القيد هو اللي بيحسم السباق لو طلبين وصلوا في نفس اللحظة: