هذا المقال يتطلب مستوى: متوسط. لو انت مبتدئ، الأمثلة هتوصلك خطوة بخطوة؛ ولو محترف، ركّز على آخر جزء الخاص بالـ race condition لأنه النقطة اللي بيغلط فيها أغلب الناس.
الـ Idempotency: ليه ضغطة زر واحدة ممكن تخصم العميل مرتين
لو زرار "ادفع" عندك ممكن يخصم مرتين لما العميل يضغط مرتين أو النت يقطع، الحل مش في منع الضغط. الحل مفتاح واحد اسمه Idempotency-Key. المقال ده بيوريك ليه بيحصل، وإزاي تمنعه بكود شغّال.
الفكرة الأول بمثال بسيط
تخيّل زرار المصعد. لو ضغطت عليه عشر مرات، المصعد بييجي مرة واحدة. الضغطة التانية والتالتة مالهاش أي تأثير جديد. ده بالظبط معنى "idempotent": تكرار نفس العملية بيدّي نفس النتيجة، مش نتيجة مضاعفة.
دلوقتي الشرح العلمي بالتفاصيل: عملية توصف بإنها idempotent لو تنفيذها مرة واحدة زي تنفيذها N مرة على نفس المدخل بالظبط. في بروتوكول HTTP، الأفعال GET وPUT وDELETE معرّفة رسميًا كـ idempotent. لكن POST مش كده — وده بالظبط مصدر المشكلة، لأن إنشاء عملية دفع بيتعمل غالبًا بـ POST.
المشكلة باختصار
العميل بيبعت POST /payments. الرد اتأخر بسبب الشبكة. المتصفح أو تطبيق الموبايل بيعيد المحاولة تلقائيًا. السيرفر بيستقبل طلبين، وبيخصم مرتين. اللي بيحصل فعلاً إن الطلب الأول اتنفذ تمام، بس رده ضاع في الطريق، فالعميل فاكر إنه فشل.
الافتراض هنا عشان نحط رقم: عندك 200 ألف طلب دفع يوميًا، ونسبة إعادة الإرسال 0.5% بس. ده معناه حوالي 1000 خصم مكرر كل يوم. رقم كبير جدًا على أي بوابة دفع، وكل واحد منهم شكوى عميل واسترجاع فلوس.
الحل: مفتاح ثبات لكل عملية
الفكرة بسيطة. العميل بيولّد مفتاح فريد (UUID مثلاً) لكل عملية، وبيبعته في هيدر Idempotency-Key. السيرفر بيخزّن المفتاح مع نتيجة أول تنفيذ. أي طلب تاني بنفس المفتاح بياخد النتيجة المخزّنة من غير ما يخصم تاني.
كود شغّال (Python + Flask + Redis)
import json
import redis
from flask import Flask, request, jsonify
app = Flask(__name__)
r = redis.Redis()
@app.post("/payments")
def create_payment():
key = request.headers.get("Idempotency-Key")
if not key:
return jsonify(error="Idempotency-Key required"), 400
cached = r.get(f"idem:{key}")
if cached: # الطلب اتنفذ قبل كده
return jsonify(json.loads(cached)), 200
# نفّذ الخصم مرة واحدة بس
result = charge_card(amount=request.json["amount"])
r.set(f"idem:{key}", json.dumps(result), ex=86400) # يعيش 24 ساعة
return jsonify(result), 200
هنا Redis بيخزّن المفتاح 24 ساعة عبر ex=86400. أي إعادة محاولة جوه المدة دي بترجّع نفس الرد فورًا، والخصم بيحصل مرة واحدة بس.