أحمد حايس
الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء
أحمد حايس

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

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

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

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

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

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

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

Idempotency: ليه ضغط زر الدفع مرتين بيخصم العميل مرتين وإزاي تمنعه

متوسط9 يوليو 20265 دقائق قراءة
Idempotency: ليه ضغط زر الدفع مرتين بيخصم العميل مرتين وإزاي تمنعه

المستوى المطلوب: متوسط. يبدأ الشرح بمثال بسيط يفهمه أي مبتدئ، ثم يتعمّق بتعريف علمي دقيق وكود تنفيذي جاهز للنسخ.

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

Idempotency: اضغط زر الدفع مرتين، اتخصم مرة واحدة

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

الشبكة مش مضمونة. العميل يضغط "ادفع"، الطلب يوصل للسيرفر فعلاً ويتم الخصم، لكن الرد يضيع في الطريق بسبب timeout. المتصفح أو الموبايل بيعيد المحاولة تلقائيًا، فيوصل طلب تاني مطابق. السيرفر مش عارف إن ده نفس الطلب، فيخصم تاني.

ده مش سيناريو نادر. أي عملية فيها side effect حقيقي (خصم فلوس، إنشاء طلب، إرسال إيميل) معرّضة للمشكلة دي. والأخطر إنها بتحصل بصمت، من غير أي رسالة خطأ.

المثال الأول: التذكرة المرقّمة

تخيّل إنك رحت مكتب حكومي وأخدت تذكرة عليها رقم. قدّمت التذكرة للموظف، فنفّذ معاملتك وكتب نتيجتها جنب الرقم في دفتر. بعد شوية رجعت وقدّمت نفس التذكرة بنفس الرقم.

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

الرقم اللي على التذكرة هو الـ Idempotency-Key. ودفتر الموظف هو الـ cache اللي بيخزّن نتيجة أول طلب. أي طلب تاني بنفس الرقم بيرجّع النتيجة المخزّنة بدل ما ينفّذ من جديد.

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

عملية idempotent هي العملية اللي تنفيذها مرة واحدة أو عشر مرات بيدّي نفس النتيجة على حالة النظام. حسب معيار HTTP الرسمي (RFC 9110)، ميثودات زي GET وPUT وDELETE معرّفة إنها idempotent بطبيعتها. بينما POST مش idempotent، لأنه بيُنشئ مورد جديد في كل مرة.

المشكلة إن معظم عمليات الدفع والطلبات بتتعمل بـ POST. عشان كده ظهر ترويسة قياسية اسمها Idempotency-Key (فيه مسودة رسمية في IETF بتوصّفها) بتحوّل الـ POST من عملية خطيرة عند الإعادة لعملية آمنة. الفكرة: العميل بيولّد مفتاح فريد لكل عملية (مثلاً UUID v4)، والسيرفر بيستخدم المفتاح ده عشان يميّز الطلب المكرر.

الحل عمليًا: كود جاهز بـ FastAPI و Redis

الكود ده بيطبّق المبدأ. النقطة الحرجة هي استخدام عملية SET ... NX الذرّية في Redis، اللي بتحجز المفتاح لأول طلب بس. ده بيمنع حالة السباق لو وصل طلبان في نفس اللحظة بالظبط.

Python
import json
import redis
from fastapi import FastAPI, Header, HTTPException

app = FastAPI()
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
TTL_SECONDS = 24 * 60 * 60  # 24 ساعة، زي المدة اللي بتستخدمها Stripe

@app.post("/pay")
def pay(body: dict, idempotency_key: str = Header(...)):
    cache_key = f"idem:{idempotency_key}"

    # SET NX ذرّية: بترجّع True لأول طلب فقط
    first = r.set(cache_key, "processing", nx=True, ex=TTL_SECONDS)

    if not first:
        cached = r.get(cache_key)
        if cached == "processing":
            # الطلب الأصلي لسه بيتنفّذ، متعملش خصم تاني
            raise HTTPException(status_code=409, detail="نفس الطلب لسه بيتنفّذ")
        return json.loads(cached)  # رجّع نفس نتيجة أول طلب

    charge = do_charge(body)  # الخصم الحقيقي: بيتنفّذ مرة واحدة بس
    response = {"status": "paid", "amount": charge["amount"], "id": charge["id"]}
    r.set(cache_key, json.dumps(response), ex=TTL_SECONDS)
    return response

على جانب العميل، ولّد المفتاح مرة واحدة قبل الإرسال، واستخدم نفسه في كل إعادة محاولة:

JavaScript
const idempotencyKey = crypto.randomUUID();  // مرة واحدة قبل أول محاولة
await fetch("/pay", {
  method: "POST",
  headers: { "Idempotency-Key": idempotencyKey, "Content-Type": "application/json" },
  body: JSON.stringify({ amount: 50 }),
});

سيناريو واقعي بالأرقام

افترض إن عندك بوابة دفع بتستقبل 50 ألف عملية شراء يوميًا، ومعدل إعادة المحاولة بسبب الـ timeouts حوالي 1.5%. ده معناه ~750 طلب مكرر كل يوم. من غير idempotency، جزء كبير منهم ممكن يتحوّل لعمليات خصم مزدوجة، وكل واحدة منها شكوى عميل و chargeback محتمل.

مع الـ Idempotency-Key، الـ 750 طلب دول بيرجّعوا نفس النتيجة المخزّنة بدون خصم إضافي. تكلفة الحماية: قراءة واحدة من Redis لكل طلب، غالبًا في حدود 0.2 إلى 1 مللي ثانية، وهو رقم لا يُذكر مقارنة بزمن معالجة الدفع نفسه.

الأرقام دي مبنية على فرضية إن الـ Redis عندك في نفس شبكة السيرفر (latency منخفض). لو الـ Redis بعيد، الزمن ممكن يزيد.

كيف تتعامل بها الأنظمة الحقيقية

Stripe بتخزّن كود الحالة (status code) وجسم الرد (body) لأول طلب لكل مفتاح، سواء نجح أو فشل، فأي إعادة بترجّع نفس النتيجة المحفوظة. مفاتيحها بتتحذف تلقائيًا بعد 24 ساعة على الأقل، وطول المفتاح مسموح لحد 255 حرف. المبدأ نفسه بتستخدمه PayPal و Square وبوابات كبيرة تانية.

الـ trade-offs

المكسب واضح: أمان ضد الخصم المزدوج. لكن فيه ثمن لازم تعرفه:

  • حالة إضافية: محتاج تخزين (Redis أو جدول DB) لكل مفتاح، مع سياسة انتهاء صلاحية (TTL). نسيان الـ TTL بيخلّي التخزين يكبر بلا حدود.
  • round-trip زيادة: كل POST بيعمل قراءة/كتابة إضافية على المخزن. الثمن مللي ثانية، لكنه ثمن حقيقي على كل طلب.
  • مسؤولية العميل: لازم العميل يولّد مفتاح فريد ويعيد استخدامه في المحاولات المكررة. لو ولّد مفتاح جديد كل محاولة، الحماية بتفشل تمامًا.

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

لو العملية GET أو قراءة من غير side effect، مش محتاج idempotency key أصلاً، لأن الميثود idempotent بطبيعته. كمان لو نتيجة العملية بتتغير بمرور الوقت (مثلاً سعر صرف لحظي)، تخزين نتيجة أول طلب ممكن يرجّع قيمة قديمة؛ في الحالة دي لازم تربط المفتاح بـ fingerprint للـ payload وتتعامل مع الاختلاف كخطأ. وأخيرًا، للأنظمة الصغيرة اللي مفيهاش فلوس ولا عمليات خطيرة، الـ overhead ممكن ما يستاهلش.

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

افتح أهم endpoint فيه POST بيعمل side effect حقيقي (دفع، طلب، تحويل). ضيف قراءة header اسمه Idempotency-Key، وخزّن نتيجة أول طلب في Redis بـ TTL؛ استخدم SET NX عشان تتفادى حالة السباق. جرّب ترسل نفس الطلب مرتين بنفس المفتاح، ولازم تلاقي عملية خصم واحدة بس في قاعدة البيانات.

المصادر

  • Stripe API Reference — Idempotent requests: docs.stripe.com/api/idempotent_requests
  • Stripe Blog — Designing robust and predictable APIs with idempotency: stripe.com/blog/idempotency
  • RFC 9110 — HTTP Semantics (تعريف الميثودات الـ idempotent): rfc-editor.org/rfc/rfc9110
  • IETF Draft — The Idempotency-Key HTTP Header Field: datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header

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

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

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