أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Idempotency Key بالعربي: امنع سحب الفلوس مرتين لما العميل يضغط "ادفع" تاني

📅 ٢٠ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Idempotency Key بالعربي: امنع سحب الفلوس مرتين لما العميل يضغط "ادفع" تاني
لو عميل ضغط "ادفع"، النت قطع قبل ما يشوف الرد، ضغط تاني، السيرفر ممكن يسحب الفلوس مرتين. Idempotency Key بيحل المشكلة دي في 20 سطر كود.

بطاقة ائتمان وشاشة دفع إلكتروني ترمز لمعالجة المعاملات المالية بدون تكرار

Idempotency في APIs: ليه لازم تعرفها قبل أول deploy في production

المشكلة باختصار

في أي نظام فيه POST endpoint بيعمل side effect (دفع، إرسال بريد، إنشاء طلب)، فيه سيناريو حتمي:

  1. العميل بعت طلب الدفع.
  2. السيرفر نفّذ الـ charge فعلاً على Stripe/Paymob.
  3. الرد (HTTP 200) اتقطع في النت أو timeout قبل ما يوصل للعميل.
  4. العميل اعتقد إن الطلب فشل، فضغط "ادفع" تاني.
  5. السيرفر عمل charge تاني. العميل اتسحب منه الفلوس مرتين.

ده مش سيناريو نادر. Stripe نشرت في توثيقها الرسمي إن حوالي 1% إلى 3% من طلبات الدفع بتتعرض لـ retry من طرف العميل. في نظام بـ 100 ألف طلب دفع يومياً، ده معناه آلاف الحالات المحتملة للتكرار.

مثال بسيط قبل التعريف العلمي (للمبتدئين)

تخيل أنت واقف قدام ATM. ضغطت "اسحب 1000 جنيه". الماكينة قعدت تفكر 10 ثواني. شاشتها اتجمّدت. مقتنعت إنها معلّقة فضغطت "اسحب 1000" تاني. السؤال: هل المفروض تطلعلك 1000 ولا 2000؟

الإجابة الصح: 1000. لأن الماكينة لازم تعرف إن الضغطتين دول نفس الطلب، مش طلبين مختلفين. طريقتها في المعرفة دي: كل طلب جواه رقم تسلسلي فريد. لو الماكينة شافت الرقم ده قبل كده، بترجّع نفس النتيجة اللي سجّلتها أول مرة بدون ما تنفذ السحب تاني.

ده بالظبط الفرق بين "الماكينة بتنفذ كل ضغطة" وبين "الماكينة بتنفذ كل طلب فريد مرة واحدة". اللي بيخلّيها فريدة: مفتاح اسمه Idempotency Key.

التعريف العلمي الدقيق

في الرياضيات، العملية f تكون idempotent لو f(f(x)) = f(x). يعني تنفيذها مرة أو N مرة على نفس الدخل يدّي نفس الناتج ونفس الأثر الجانبي.

في HTTP، حسب RFC 9110: GET, PUT, DELETE مفروض يكونوا idempotent بطبيعتهم. POST و PATCH مش idempotent افتراضياً — وده المكان اللي بيحتاج تدخّل يدوي.

المعيار الصناعي للتدخل ده نزل Stripe في 2015 ولسه لحد دلوقتي هو المرجع: العميل يبعت header اسمه Idempotency-Key، قيمته UUID عشوائي يولّده من جنبه. السيرفر بيخزّن أول response لهذا المفتاح، ولو نفس المفتاح جه تاني بيرجّع الـ response المخزّن بدون إعادة تنفيذ. IETF حالياً بتكتب معيار رسمي بنفس الفكرة في draft-ietf-httpapi-idempotency-key-header.

رسم شبكي لخوادم متصلة يمثل تدفق طلبات API موزعة بين client و server

الشكل غير الآمن (قبل Idempotency)

POST /payments
Content-Type: application/json

{ "amount": 1000, "currency": "EGP", "order_id": "ord_42" }

لو العميل بعت الطلب ده مرتين، السيرفر هيعمل charge مرتين. ده bug إنتاج حقيقي.

الشكل الآمن

POST /payments
Content-Type: application/json
Idempotency-Key: 7a3f1e9b-2c5d-4a6b-9e3f-1d8c5b7a4f92

{ "amount": 1000, "currency": "EGP", "order_id": "ord_42" }

العميل بيولّد الـ UUID مرة واحدة قبل الطلب. أي retry بيبعت نفس المفتاح. السيرفر بيستخدمه كبصمة.

تنفيذ Middleware كامل بـ Node.js و Redis

JavaScript
import { createClient } from "redis";
const redis = createClient();
await redis.connect();

const TTL_SECONDS = 24 * 60 * 60; // 24 ساعة

export async function idempotency(req, res, next) {
  const key = req.header("Idempotency-Key");
  if (!key) return next();

  const cacheKey = `idem:${req.method}:${req.path}:${key}`;
  const cached = await redis.get(cacheKey);

  if (cached) {
    const { status, body } = JSON.parse(cached);
    res.setHeader("Idempotent-Replayed", "true");
    return res.status(status).json(body);
  }

  // lock عشان نمنع race condition لو نفس الـ key جه مرتين في نفس اللحظة
  const lock = await redis.set(`${cacheKey}:lock`, "1", {
    NX: true,
    EX: 30,
  });
  if (!lock) return res.status(409).json({ error: "in_progress" });

  // hook on finish
  const originalJson = res.json.bind(res);
  res.json = (body) => {
    redis.setEx(
      cacheKey,
      TTL_SECONDS,
      JSON.stringify({ status: res.statusCode, body })
    );
    return originalJson(body);
  };
  next();
}

الكود ده بيعمل 3 حاجات: بيقرا الـ header، بيشيك Redis، بيحط lock قصير عشان طلبين متوازيين بنفس المفتاح ما يتنفذوش مع بعض (race condition). التخزين بيحصل بعد ما الـ handler يخلّص ويرجّع response.

قياس فعلي

سيناريو: نظام دفع بـ 100,000 طلب يومياً، 2% retry rate بسبب شبكة ضعيفة (= 2,000 طلب مكرر).

بدون Idempotencyمع Idempotency
Duplicate charges~2,000 / يوم0
Redis memory0~180–220 MB
Latency إضافي0+2 إلى +4 ms (Redis lookup)
متوسط مبلغ كل duplicate500 جنيه—
الخسارة المحتملة اليومية~1 مليون جنيه0

الأرقام دي مبنية على معدلات إعادة المحاولة اللي نشرتها Stripe في مدونتها الرسمية عن Idempotency، بتطبيقها على حجم transactions متوسط لمتجر e-commerce عربي.

Trade-offs اللي لازم تعرفها

بتكسب: أمان كامل من duplicates، retry آمن من طرف العميل حتى لو network flaky، تجربة مستخدم أحسن لأن العميل يقدر يضغط تاني بدون خوف.

بتخسر:

  • Redis memory (كل key + response cached، TTL لازم يتحط).
  • Latency صغيرة (+2–4ms لكل طلب بسبب الـ lookup).
  • تعقيد زيادة في error handling — لو الطلب فشل في نص التنفيذ، لازم قرار واضح: تحفظ الـ failed response ولا تسيب العميل يعيد؟

التوصية: احفظ الـ failed response لو الفشل نهائي (validation error، insufficient funds). ماتحفظش لو الفشل مؤقت (timeout على Stripe) علشان retry يكون ممكن.

متى لا تستخدم هذه الطريقة

  • GET requests: idempotent بطبعها، مش محتاجة مفتاح.
  • Counters و increments: أحياناً التكرار هو الصح (POST /views). هنا الـ idempotency هتكسرلك المنطق.
  • Keys بدون TTL: الذاكرة بتنفجر بعد أسابيع. دايماً حط TTL بين 24 ساعة و 7 أيام حسب الـ domain.
  • Multi-tenant بدون namespace: لو شركتين بيستخدموا نفس الـ key بالصدفة، الثانية هتاخد response الأولى. خلي الـ cache key {tenant_id}:{key}.

الخطوة التالية

افتح أقرب POST endpoint عندك بيعمل شغل مالي أو يرسل email أو ينشئ طلب. ضيف middleware بيقرا Idempotency-Key من الـ headers ويخزّن الـ response في Redis لمدة 24 ساعة. اختبره بـ curl بنفس المفتاح مرتين — المفروض الرد يكون 100% متطابق والـ charge يحصل مرة واحدة بس.

مصادر

  • Stripe — Idempotent Requests (documentation)
  • Stripe Engineering — Designing robust and predictable APIs with idempotency
  • IETF — draft-ietf-httpapi-idempotency-key-header
  • RFC 9110 — Idempotent Methods
  • AWS Builder's Library — Making retries safe with idempotent APIs
  • MDN — Idempotent (Glossary)

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة