منع التكرار على نطاق واسع: أكثر من مجرد ترويسة طلب
استمع إلى المقال
جارٍ تجهيز الصوت…
هندسة التقنيات المالية
أبدأ بالأثر التجاري لا بالترويسة. عدم تكرار الأثر (Idempotency) عندي عقد تزامن يجعل الطلبات المتكررة تشير إلى عملية واحدة، وليس مرشحاً يكتشف النسخ بعد التنفيذ.

ما الذي يضمنه HTTP؟
يعرّف RFC 9110 الطريقة المتكافئة الأثر بأنها تعطي الأثر المقصود نفسه عند تكرار الطلب، ويصنف PUT وDELETE والطرق الآمنة ضمنها. لكن إنشاء دفعة يتم غالباً عبر POST، ولذلك يحتاج عقداً على مستوى التطبيق.
ينشئ العميل مفتاحاً، ويربطه الخادم بنطاق مثل (tenant, operation, key) وببصمة للحقول المؤثرة: المبلغ والعملة والوجهة ونمط التحصيل. إذا أُعيد المفتاح بحمولة مختلفة، فالنتيجة تعارض صريح، لا إعادة صامتة. توضح وثائق Stripe لـالطلبات المتكافئة الأثر مثالاً عملياً لحفظ النتيجة ومقارنة المعلمات، لكن مدة الاحتفاظ وسلوك الأخطاء خاصان بإصدار واجهتها.
عرّف نطاق المفتاح قبل تصميم الجدول
لا يصبح المفتاح مثل abc-123 فريداً على مستوى المنصة لمجرد أن العميل أنشأه. أضعه داخل نطاق صريح مثل (tenant_id, api_operation, idempotency_key) حتى لا يتعارض مفتاح إنشاء دفعة مع النص نفسه عند إنشاء استرداد، وحتى لا يستطيع مستأجر استنتاج عملية تخص مستأجراً آخر. ثم أحدد في عقد الواجهة من ينشئ المفتاح، وما طوله وصيغته، وأي عمليات تقبله، ومتى تنتهي صلاحيته.
أحوّل الحقول التي تغير الأثر المالي إلى تمثيل معياري ثم أحسب بصمتها. تشمل البصمة المبلغ والعملة والوجهة والحساب ونمط التحصيل، ولا تشمل عادةً معرف تتبع النقل أو ترتيب حقول JSON. أحتفظ بالبصمة وبقدر محمي من بيانات الطلب يكفي للتحقيق. إذا عاد المفتاح نفسه بمبلغ أو وجهة مختلفين، أرجع تعارضاً واضحاً؛ فإعادة النتيجة القديمة ستخفي خطأ العميل، وتشغيل الطلب الجديد سيكسر الضمان.
المطالبة قبل التنفيذ
يجب أن تمنع فهرسة فريدة وإدراج ذري عاملين متزامنين من الفوز بالمفتاح نفسه. يسجل الفائز عملية بحالة in_progress، ومعرّفاً معيارياً، وبصمة، ومالك مهلة عمل، ووقت انتهاء. يقرأ الخاسر السجل نفسه ويعيد 202 مع رابط الحالة أو النتيجة النهائية؛ لا يبدأ اتصالاً ثانياً بالمزود.
المشكلة الأصعب هي نجاح المزود ثم تعطل العامل قبل حفظ الرد. انتهاء مهلة العامل لا يثبت فشل الأثر. مرّر هوية ثابتة إلى المزود إن كان يدعمها، واحفظ مرجعه، وحوّل المهلة الملتبسة إلى outcome_unknown، ثم استعلم أو انتظر استدعاءً موقعاً.
أغلق نافذة التعطل
أتخيل دائماً العامل الذي أرسل 75 درهماً بنجاح ثم تعطل قبل تثبيت استجابة المزود. إذا انتهى عقد ملكيته وأعاد عامل جديد الإرسال، فقد أحصل على دفعتين صحيحتين. لذلك أنشئ سجل العملية الدائم قبل الأثر الخارجي، وأمرر إلى المصب مفتاحاً ثابتاً مشتقاً من معرفها، وأسجل مرجع طلب المزود قبل الإرسال أو معه متى سمح البروتوكول.
بعد مهلة غامضة لا أخمن. أستعلم عن حالة المزود بالمرجع المعروف، أو أنتظر استدعاءً لاحقاً موثوقاً، أو أوجّه العملية إلى المطابقة. يبقى السجل في outcome_unknown حتى يصل دليل يحسمه. ذاكرة مؤقتة للاستجابات توضع بعد استدعاء المزود لا تغلق هذه النافذة؛ المطلوب عملية قابلة للاسترداد تعرف معنى الأثر المالي، لا مجرد نسخة من رد HTTP.
قرر ما الذي تعيده الواجهة
يمكن لخطأ تحقق وقع قبل بدء التنفيذ ألا يحجز المفتاح إلى الأبد، لكن يجب أن ينص العقد على ذلك. بعد بدء التنفيذ، قد يكون تكرار النتيجة المستقرة، بما فيها بعض حالات الفشل، أكثر أماناً من التشغيل مرة أخرى. لا أعمم سلوك مزود واحد: أوثق لكل إصدار هل تُحفظ أخطاء الخادم، ومتى يمكن إعادة الطلب، وكيف يميز العميل بين فشل حاسم ونتيجة مجهولة.
لا أحتاج دائماً إلى تخزين جسم استجابة كبير. يمكن حفظ مرجع مورد غير قابل للتغيير وإعادة بناء التمثيل مع إصدار معلوم. المهم أن تعود المحاولات كلها إلى العملية نفسها، وأن يستطيع التحقيق تفسير سبب اختلاف شكل الرد إذا تغير إصدار العرض لاحقاً.
الاحتفاظ والتوسع
يجب أن تغطي مدة المفتاح نافذة إعادة العميل والطوابير غير المتصلة والاستدعاءات المتأخرة. الانتهاء المبكر يسمح لطلب قديم بإنشاء أثر جديد، والاحتفاظ الأبدي يزيد كلفة التخزين والخصوصية. وفي نشر متعدد المناطق، قد تسمح المزامنة اللاحقة لمنطقتين بالمطالبة بالمفتاح؛ استخدم منطقة مرجعية للنطاق أو مخزناً ذا اتساق خطي أو قبولاً مؤقتاً قبل تخصيص الهوية.
قِس تنازع المفاتيح، وعمر in_progress، وتعارض البصمات، والنتائج المجهولة، والإعادات بعد الانتهاء، والمفاتيح الساخنة. واختبر طلبات متزامنة مكررة، لا حملاً من مفاتيح فريدة فقط.
قاعدتي التصميمية هي نشر العقد للعميل ثم فرض الهوية نفسها في المصب. لا أعد وجود الترويسة ضماناً؛ فالضمان يأتي من مطالبة ذرية وعملية قابلة للاسترداد.
العقد الذي أنشره وأختبره
أنشر العمليات المدعومة ونطاق المفتاح ومدة الاحتفاظ وسلوك اختلاف البصمة واستجابة in_progress وقواعد إعادة الأخطاء. أوضح أيضاً ما يحدث بعد انتهاء الصلاحية، وكيف يستعلم العميل عن العملية، وأي معرف يجب أن يحفظه. هذا يمنع مكتبات العملاء والبوابات من ابتكار سياسات إعادة متعارضة.
ثم أختبر العقد بحقن الفشل لا بأمثلة المسار السعيد فقط. أوقف عاملاً بعد اتصال المزود، وأطلق مئة طلب متزامن بالمفتاح نفسه، وأغير حقلاً واحداً مع الاحتفاظ بالمفتاح، وأعيد الطلب بعد انتهاء المدة، وأجرب التحويل بين منطقتين. أفحص عدد الآثار الخارجية وسجل العملية معاً. معيار النجاح عندي أثر واحد قابل للتفسير؛ فقد تبدو استجابات HTTP متطابقة بينما تحرك المال مرتين في الخلفية.
قبل الإطلاق أراجع تسلسل الملكية من بوابة الواجهة إلى خدمة المجال ثم مهايئ المزود. أتأكد أن أي إعادة آلية تستخدم الهوية الدائمة نفسها، وأن قاعدة البيانات لا تسمح بفائزين متزامنين، وأن انتهاء عقد العامل لا يحول تلقائياً إلى إرسال جديد. أوثق كذلك من يملك الحالات القديمة والمجهولة وكيف تظهر للدعم والعميل.
أراجع لوحة التشغيل بحثاً عن مفاتيح ساخنة أو عمليات عالقة أو تعارضات بصمة أو إعادات بعد الانتهاء. وإذا تغيرت مدة الاحتفاظ أو استراتيجية المناطق أو عقد المزود، أعيد اختبار النافذة كاملة. ضمان عدم تكرار الأثر سلسلة؛ أضعف حد فيها يمكن أن يعيد المال مرتين حتى لو كانت نقطة الدخول سليمة.
أفصل أخيراً بين منع تكرار الطلب ومنع تكرار الحدث. قد تنفذ العملية مرة واحدة ثم يصل الاستدعاء اللاحق أكثر من مرة، لذلك يحتاج المستهلك إلى معرف حدث وسجل معالجة محلي. وقد يصل حدثان مختلفان للعملية نفسها، فلا يجوز حذف الثاني لمجرد تشابه الحمولة. أختبر هوية العملية وهوية التسليم كل واحدة عند حدها، وأربطهما للدليل من دون دمجهما في مفتاح واحد مبهم.
ما القرار الذي كنت ستتخذه؟
شارك سؤالاً أو تجربة أو رأياً مخالفاً. التعليقات متاحة للأعضاء المسجلين للحفاظ على نقاش مهني ومفيد.