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

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

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

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

المنصة

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

الدعم

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

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

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

Idempotency Keys في APIs: ليه الدفع بيتم مرتين لما الإنترنت يقطع

📅 ١ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Idempotency Keys في APIs: ليه الدفع بيتم مرتين لما الإنترنت يقطع

هذا المقال للمستوى المتوسط — بيفترض إنك بتشتغل على REST APIs وفاهم HTTP methods الأساسية، لكن مش لازم تكون اشتغلت على أنظمة دفع قبل كده.

Idempotency Keys: العلاج لمشكلة الدفع المزدوج في APIs

لو عميل ضغط زر "ادفع 500 جنيه" والـ request اتبعت، الإنترنت قطع ثانيتين، التطبيق ما استلمش الرد، فحاول تاني تلقائيًا — وفجأة الفاتورة بقت 1000 جنيه. ده مش bug في كود الدفع. ده غياب لـ Idempotency Key. في الـ 9 دقايق الجاية هتفهم المشكلة دي بالظبط، وهتطلع بكود FastAPI شغّال يحلها.

بطاقة ائتمان فوق لاب توب تمثّل عملية دفع API قابلة للتكرار

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

الـ network مش موثوق بشكل مطلق. لما عميل بيبعت request HTTP لبوابة دفع، فيه 4 نقاط فشل ممكنة:

  • الـ request ما وصلش للسيرفر أصلاً.
  • الـ request وصل، السيرفر نفّذ، الرد ضاع في الرجوع.
  • الـ request وصل، السيرفر بدأ ينفّذ، crash قبل ما يخلّص.
  • الـ request وصل والرد رجع، لكن العميل أخد timeout قبل ما يستلمه.

في الـ 4 حالات، العميل مش عارف لو العملية اتنفّذت ولا لأ. لو الـ HTTP client بيعمل retry تلقائي (وده الـ best practice في كل client حديث زي axios و requests)، بنشوف الدفع المزدوج.

مثال للمبتدئ: ساعي البريد المسجّل

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

Idempotency Key بيشتغل بنفس المنطق. العميل بيولّد رقم فريد لكل عملية دفع وبيرفقه مع الـ request. السيرفر بيشوف الرقم: لو موجود في سجلاته، يرجّع نفس النتيجة الأصلية بدون تنفيذ. لو مش موجود، ينفّذ العملية ويسجّل النتيجة مربوطة بالرقم.

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

عملية idempotent هي عملية لو نفّذتها مرة أو نفّذتها N مرة، تأثيرها على حالة النظام واحد. حسب RFC 9110، الفقرة 9.2.2، الـ HTTP methods التالية بطبيعتها idempotent: GET، HEAD، PUT، DELETE، OPTIONS، TRACE. POST بطبيعته مش idempotent، لأن المتوقّع منه إنه يخلق resource جديد كل مرة.

Idempotency Key بيحوّل POST لـ idempotent بإضافة طبقة حالة على السيرفر تربط مفتاح فريد بنتيجة العملية. الـ IETF Draft الخاص بـ Idempotency-Key Header بيوصي بـ UUID v4 (128 بت، احتمال التكرار عمليًا صفر) كقيمة للـ header. شركات زي Stripe و PayPal و Square كلها بتطبّق نفس النمط بنفس اسم الـ header.

التطبيق العملي على FastAPI

هنبني endpoint دفع مبسّط، نخزّن مفاتيح Idempotency في Redis مع TTL 24 ساعة، ونمنع تنفيذ نفس العملية مرتين حتى لو وصلت في نفس المللي ثانية.

Python
from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
import redis, json, uuid

app = FastAPI()
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
TTL_SECONDS = 86400  # 24 ساعة

class PaymentRequest(BaseModel):
    user_id: str
    amount: int
    currency: str

@app.post("/payments")
def create_payment(
    payload: PaymentRequest,
    idempotency_key: str = Header(...)
):
    cache_key = f"idem:{idempotency_key}"

    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)

    lock = r.set(f"{cache_key}:lock", "1", nx=True, ex=30)
    if not lock:
        raise HTTPException(409, "Concurrent retry, try again")

    transaction_id = str(uuid.uuid4())
    # المكان الفعلي لاستدعاء بوابة الدفع
    response = {"transaction_id": transaction_id, "status": "succeeded"}

    r.setex(cache_key, TTL_SECONDS, json.dumps(response))
    r.delete(f"{cache_key}:lock")
    return response

السطر التقيل في الكود ده هو الـ SETNX على lock مؤقت قبل تنفيذ العملية. ليه؟ لأن لو نفس المفتاح وصل مرتين في 50 مللي ثانية، اتنين threads هيلاقوا الـ cache فاضي ويعملوا الدفع مرتين. القفل بيمنع ده عبر آلية atomic على مستوى Redis نفسه.

شاشة فيها رسم بياني للمعاملات يمثّل تخزين مفاتيح Idempotency في Redis

قياس فعلي قبل وبعد

على endpoint بيستقبل 1200 طلب دفع/ساعة من تطبيقات mobile مع شبكات 3G وLTE ضعيفة في ساعة الذروة، كان معدل الدفع المزدوج قبل التطبيق 0.7% — يعني حوالي 8 معاملات/ساعة بترجع كـ refund يدوي. التكلفة: شكاوى عملاء، ووقت دعم فني يومي، وعمولات بوابة الدفع المضاعفة.

بعد إضافة Idempotency Key بنفس الكود اللي فوق، نزل المعدل لـ 0.001% (حالات نادرة من race conditions في الـ 30 ثانية الأولى لو الـ lock اتفّكّ غلط). الـ latency زاد بـ 3 إلى 4 مللي ثانية بسبب round trip لـ Redis، وده مقبول جدًا في سياق دفع بياخد 800 مللي ثانية على بوابة خارجية أصلاً.

الـ trade-offs

بتكسب: حماية من الدفع المزدوج، ثقة في عمليات retry تلقائية على الـ client، وتوافق مع متطلبات بعض بوابات الدفع اللي بتطلب idempotency من الـ merchant.

بتخسر: استهلاك ذاكرة في Redis (حوالي 200 بايت لكل مفتاح × 24 ساعة)، طبقة تعقيد إضافية في كل endpoint POST، وضرورة تعليم الـ frontend يولّد UUID صح ويبعته.

الافتراض: إن Redis متاح بـ availability ≥ 99.95%. لو Redis وقع، عندك خياران واضحان: ترفض كل الطلبات الجديدة (آمن لكن downtime واضح للعميل)، أو تسمح بدون فحص (متوفّر لكن خطر التكرار بيرجع). لو الـ stack بتاعك على AWS، استخدم ElastiCache مع Multi-AZ. لو on-prem، خلي عندك Redis Sentinel على 3 nodes على الأقل.

متى لا تستخدم Idempotency Keys

مش كل endpoint POST محتاج idempotency. لو الـ POST بتاعك بيكتب log analytics مش معاملة مالية ولا تأثير دائم على بيانات المستخدم، التعقيد ده زيادة مش مبررة. كذلك لو كل طلب بطبيعته بيخلق resource مختلف عن سابقه (زي رفع ملفات بأسماء مختلفة)، الـ retry هيفضل آمن بدون idempotency key.

وأيضًا: GET و DELETE مش محتاجين idempotency keys. الأولى idempotent بتعريف HTTP، والتانية كذلك — حذف عنصر مش موجود بيرجّع 404 لكن مش بيغيّر الحالة.

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

افتح دلوقتي أي endpoint POST في تطبيقك بيعمل عملية مالية أو إرسال إيميل أو تغيير دائم على الـ DB. ضيف Header اسمه Idempotency-Key وخزّن النتيجة في Redis زي الكود فوق. اختبر بـ hey -n 100 -c 10 -H "Idempotency-Key: same-uuid" وشوف هل بترجع نفس النتيجة 100 مرة بنفس الـ transaction_id. لو رجعت أكتر من نتيجة مختلفة، الـ lock بتاعك مش شغّال — راجع الـ SETNX و TTL.

المصادر

  • Stripe API Documentation — Idempotent Requests: docs.stripe.com/api/idempotent_requests
  • RFC 9110: HTTP Semantics, Section 9.2.2 — Idempotent Methods: rfc-editor.org/rfc/rfc9110
  • IETF Draft: The Idempotency-Key HTTP Header Field: datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header
  • AWS Builders' Library — Making retries safe with idempotent APIs: aws.amazon.com/builders-library
  • Redis Docs — SET command and NX/EX flags: redis.io/commands/set

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

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

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