المشكلة ليست في الضغط على زر مرتين فقط
تخيل أن تطبيقًا يرسل طلب إنشاء فاتورة، فتنجح المعالجة في الخادم لكن اتصال العميل ينقطع قبل وصول الرد. يعيد العميل الطلب بالمفتاح نفسه، وفي اللحظة ذاتها يصل طلب ثالث من عامل خلفي. قد تنشئ ثلاثة عمليات فاتورة واحدة ظاهريًا إذا كان الخادم يفحص وجود المفتاح ثم يكتب النتيجة بخطوتين منفصلتين. السؤال الصحيح ليس: هل يدعم API إعادة المحاولة؟ بل: هل يستطيع ضمان أثر تجاري واحد عندما تصل طلبات متزامنة، وتتعطل العمليات بين الحفظ والرد، وتُعاد الرسائل بعد دقائق؟ هذه زاوية مختلفة عن شرح معنى Idempotency نظريًا؛ إنها اختبار لترتيب الكتابة داخل قاعدة البيانات.
حدد العملية التجارية قبل اختيار المفتاح
يجب تعريف الأثر المراد منعه: إنشاء طلب، خصم مبلغ، حجز مقعد، إرسال رسالة، أم تعديل مورد. قد يكون إنشاء الطلب آمنًا من التكرار بينما إرسال إشعار تأكيد له أثر آخر يحتاج مفتاحًا مستقلًا. اربط المفتاح بمحاولة منطقية واحدة وليس باتصال HTTP واحد؛ يعاد استخدامه في المحاولات المتكررة للعملية نفسها فقط. لا تستخدم مفتاحًا عامًا لجميع عمليات المستخدم، لأن ذلك قد يمنع عمليات مشروعة. ولا تعتمد على تشابه نص الطلب وحده؛ فطلبان متساويان في القيمة قد يمثلان شراءين مقصودين. عرّف نطاق المفتاح بحسب المستأجر ونوع العملية وهوية صاحبها.
سجل طلبات الإعادة في قاعدة البيانات
أنشئ سجلًا يضم tenant_id و operation_type و idempotency_key و request_hash و state و result_reference و created_at و expires_at. اجعل الثلاثية الأولى فريدة بقيد UNIQUE في قاعدة البيانات، لا بشرط برمجي يُنفذ قبل INSERT. احسب بصمة معيارية للحقول المؤثرة في العملية مع استبعاد البيانات المتغيرة غير الدلالية مثل ترتيب مفاتيح JSON، وفق عقد واضح. عند تكرار المفتاح بجسم مختلف ارفض العملية بخطأ موضح بدل إعادة نتيجة تخص طلبًا آخر. احتفظ بالنتيجة النهائية أو مرجع آمن إليها، وراقب سياسات الاحتفاظ حتى لا يتحول السجل إلى مخزن دائم لمعلومات حساسة.
الحجز الذري يمنع سباق التحقق ثم الإدراج
النمط الضعيف هو SELECT للبحث عن المفتاح، ثم تنفيذ الأثر، ثم INSERT في سجل الإعادة. بين SELECT و INSERT قد يدخل عامل آخر ويجتاز الفحص نفسه. النمط الأفضل يحاول أولًا حجز المفتاح ذريًا داخل معاملة، ويعتمد على القيد الفريد لحسم المنافسة. إذا كان السجل موجودًا، اقرأ حالته وقرر هل تعيد النتيجة أم تبلغ العميل بأن التنفيذ جارٍ. يجب تحديد مستوى العزل والمعاملات بحسب قاعدة البيانات المستخدمة؛ لا تفترض أن قفلًا داخل ذاكرة عملية Node.js يحميك عندما يعمل التطبيق على خادمين. اختبر المنافسة فعليًا بدل الاكتفاء بمراجعة الكود.
صمم آلة حالات قابلة للفحص
الحالات العملية قد تكون reserved ثم processing ثم succeeded أو failed_terminal، مع حالة reconciliation_needed عندما يكون الأثر الخارجي غير مؤكد. لا تُعامل كل استثناء على أنه فشل نهائي؛ قد ينجح مزود الدفع ثم تنقطع الاستجابة. ضع وقت تحديث وآلية lease أو استعادة للحجز العالق، وحدد من يملك حق نقل الحالة. الطلب المكرر أثناء processing يمكنه تلقي 202 أو 409 وفق العقد الموثق، وليس بالضرورة إعادة تنفيذ العمل. أما الطلب المكرر بعد succeeded فيعيد مرجع النتيجة السابقة. ينبغي أن تكون تغييرات الحالات قابلة للتدقيق مع تجنب تسجيل بيانات دفع حساسة.
ماذا يحدث إذا سقط العامل في أسوأ لحظة؟
اختبر توقف العملية في أربع نقاط: قبل تسجيل المفتاح، بعد حجزه، بعد كتابة العملية التجارية، وبعد إرسال الأمر إلى خدمة خارجية وقبل حفظ الرد. كل نقطة تحتاج سلوك تعافٍ مختلفًا. عندما يكون سجل المفتاح والأثر التجاري في قاعدة بيانات واحدة، يمكن جمعهما داخل معاملة مناسبة. لكن إذا كان الأثر خارج قاعدة البيانات، فلا توجد معاملة سحرية تشمل شبكة ومزودًا خارجيًا تلقائيًا. استخدم outbox لنشر أوامر موثوقة أو مفتاح إعادة محاولة مدعومًا لدى المزود، ثم أعد المصالحة من سجل الأحداث. لا تعلن نجاحًا حتى تثبت حالة العملية الفعلية.
افصل إعادة الطلب عن إعادة تنفيذ الأثر الخارجي
إذا أرسلت رسالة بريد أو طلب دفع بعد تحديث قاعدة البيانات، فقد يتعطل العامل قبل تسجيل نجاح الإرسال. في إعادة التشغيل لا يجوز افتراض أن الرسالة لم تصل أو أن الخصم لم يحدث. احتفظ بمعرّف عملية لدى الطرف الخارجي، واطلب حالة المعاملة عندما تسمح واجهته بذلك. وإذا كانت الخدمة لا توفر وسيلة تحقق أو مفتاح Idempotency، فصمم مسار مراجعة وتعويض واضحًا بدل الادعاء بضمان exactly-once. غالبًا ما يكون الضمان العملي: معالجة طلبات مكررة مع أثر تجاري قابل للتحقق، وليس عدم تكرار كل رسالة على الشبكة.
نافذة صلاحية المفتاح جزء من العقد
بعض الأنظمة تنظف مفاتيح الإعادة بعد مدة محددة؛ عند انتهاء المدة قد يصبح المفتاح نفسه قابلًا للاستخدام مرة أخرى. لذلك يجب إعلان مدة الاحتفاظ أو ضمان أن معرف الطلب التجاري يبقى فريدًا بقاعدة مستقلة. لا تستخدم مدة ثابتة عشوائية من دون مراجعة زمن تأخر الرسائل والوظائف المؤجلة وطبيعة المخاطر. في أنظمة الطلبات قد تصل إعادة من طابور بعد يوم، وفي عمليات مالية قد تتطلب المصالحة أفقًا أطول. لا تجعل TTL بديلًا عن القيود الفريدة للمعرّفات التجارية؛ لكل منهما وظيفة مختلفة.
سياسة العميل: متى يعيد المحاولة ومتى يتوقف؟
العميل لا يعيد كل خطأ بالطريقة نفسها. قد يشير انقطاع الاتصال إلى نتيجة مجهولة تستحق الاستعلام أو الإعادة بالمفتاح ذاته، بينما يدل خطأ تحقق 400 على طلب يجب إصلاحه. استخدم exponential backoff مع jitter لتقليل التزامن العنيف، واحترم Retry-After عندما ينطبق، وضع سقفًا زمنيًا مرتبطًا بتجربة المستخدم. لا تغير المفتاح عند كل إعادة، ولا تعيد تلقائيًا POST غير المحمي من التكرار. وعندما تتلقى استجابة processing، اعرض حالة واضحة بدل إيهام المستخدم بفشل مؤكد. ينبغي أن يملك المستخدم مسارًا لمعرفة نتيجة العملية دون ضغط زر إنشاء جديد.
مصفوفة اختبارات تفشل فيها الحلول السطحية
جهز بيئة تجريبية فيها عداد واضح للآثار التجارية. أرسل 20 طلبًا متزامنًا بالمفتاح نفسه والجسم نفسه، ثم كررها بجسم مختلف، ثم أوقف العامل بعد كتابة قاعدة البيانات، ثم أعد تشغيله. افحص عدد الصفوف التجارية، لا عدد ردود HTTP الناجحة فقط. اختبر خادمين منفصلين يتشاركان قاعدة البيانات، وتحقق من أن أي قفل محلي لا يسبب نتيجة مضللة. كرر الاختبار مع انقطاع بين API والمزود الخارجي، ثم راجع سجل المصالحة. لا تعتبر تجربة واحدة إثباتًا؛ شغّل سيناريوهات متكررة وسجل عدد التكرارات غير المرغوبة.
جدول قرار عند وصول طلب مكرر
قارن أربع حالات: مفتاح جديد، مفتاح جارٍ، مفتاح مكتمل، ومفتاح مع جسم مختلف. المفتاح الجديد يحجز وينفذ؛ الجاري يعيد حالة تقدم أو تعارضًا موثقًا؛ المكتمل يعيد مرجع النتيجة؛ والمختلف يرفض حتى لا تختلط عمليتان. ليس هناك رمز HTTP واحد إلزامي لكل نظام، لكن الاتساق بين التوثيق والسلوك والاختبارات ضروري. افصل أيضًا بين الاستجابة المعادة وبين زمن التنفيذ الأصلي، لأن قياس سرعة API قد يصبح مضللًا إذا احتسبت نتائج cache للمفاتيح المكررة كعمليات تجارية جديدة. احتفظ بمقياس مستقل لإعادة استخدام المفاتيح.
قرارات الخادم عند إعادة المفتاح
| جديد | حجز ذري ثم تنفيذ | مرة واحدة |
| قيد المعالجة | حالة تقدم أو تعارض موثق | لا يبدأ أثرًا جديدًا |
| مكتمل | إعادة مرجع النتيجة | لا يكرر |
| الجسم مختلف | رفض موضح | لا يخلط عمليتين |
مؤشرات التشغيل التي تكشف العيب قبل العميل
راقب معدل تكرار المفاتيح، وعدد التعارضات أثناء المعالجة، ومتوسط زمن بقاء الحجز، ونسبة الحالات التي تحتاج مصالحة، وعدد الآثار التجارية المكررة المكتشفة. لا يكفي أن تكون نسبة HTTP 200 مرتفعة؛ فقد تعني إعادة نتائج قديمة بينما تتراكم أعمال معلقة. اربط السجلات بمعرف correlation لا يكشف هوية المستخدم أو بيانات حساسة، وأنشئ تنبيهًا عندما تبقى حالة processing أطول من حدود الخدمة المعتادة. راجع أيضًا حجم جدول المفاتيح وأثر التنظيف على الأداء. الأرقام المقترحة للاختبارات أمثلة وليست حدودًا قياسية صالحة لكل نظام.
قائمة قبول قبل نقل الميزة للإنتاج
تأكد من وجود قيد فريد في قاعدة البيانات، وتوحيد بصمة الطلب، ومعالجة إعادة المفتاح بجسم مختلف، وإعادة نتيجة ناجحة دون أثر جديد، وخطة للتعافي من crash بعد الأثر الخارجي، وآلية تنظيف موثقة، وقياس واضح للتكرار الحقيقي. راجع خصوصية البيانات في السجل، واختبر التزامن عبر أكثر من عامل، ثم نفذ تجربة تعطيل متعمد في بيئة اختبار. اجعل نجاح الاختبار مشروطًا بعدد الآثار النهائية ومطابقة السجل، لا بمجرد وصول الاستجابة. وإذا كان المزود الخارجي لا يدعم المصالحة، دوّن هذا القيد صراحةً في تقرير المخاطر بدل إخفائه.
المراجع والقراءة المرتبطة
توضح دلالات HTTP الرسمية متى تكون طرق الطلب قابلة للإعادة في RFC 9110: https://www.rfc-editor.org/rfc/rfc9110.html . وتشرح وثائق Stripe نموذج مفاتيح Idempotency بوصفه تطبيقًا محددًا لا معيارًا عامًا: https://docs.stripe.com/api/idempotent_requests . وللمقارنة مع شرح أوسع عن API منشور على إيجي تاج: https://egytag.com/ar/articles/idempotency-%D9%81%D9%8A-apis-%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D9%8A%D8%AC%D8%A8-%D8%A3%D9%84%D8%A7-%D9%8A%D9%83%D8%B1%D8%B1-retry-%D8%B9%D9%85%D9%84%D9%8A%D8%A9-%D8%A7%D9%84%D8%AF%D9%81%D8%B9-%D8%A3%D9%88-%D8%A7%D9%84%D8%A5%D9%86%D8%B4%D8%A7%D8%A1 . تختلف تفاصيل المفاتيح والمهلات وأكواد الاستجابة بين المزودين، ولذلك يلزم الرجوع إلى عقد الخدمة الفعلي.
شارك في تقييم ونقاش المقال
رأيك يضيف قيمة للمقال ويساعدنا على تحسين المحتوى والنقاش حوله.
النقاش
جارٍ تحميل التعليقات…