المعالجة غير المتزامنة والمراسلة

الأمان من التكرار وإزالة التكرار

18 دقيقة الدرس 6 من 10

الأمان من التكرار وإزالة التكرار

في أي نظام رسائل موزّع، قد تصل الرسالة الواحدة أكثر من مرة. يعيد المنتج الإرسال بعد انتهاء المهلة، أو يتعطل الوسيط في منتصف التأكيد، أو يتسبب انقسام الشبكة في تأكيد التسليم على جانب دون الآخر. هذه ليست حالات حافة — بل هي واقع يومي على نطاق واسع. الحل هو تصميم كل مستهلك ليكون آمناً من التكرار (idempotent) واستخدام إزالة التكرار (deduplication) حيثما لا يكفي الأمان من التكرار وحده.

ما معنى الأمان من التكرار؟

العملية آمنة من التكرار إذا أنتجت تنفيذها مرات عديدة نفس النتيجة التي ينتجها تنفيذها مرة واحدة تماماً. المصطلح مستمد من الرياضيات: تطبيق نفس الدالة مراراً يبقي الحالة كما هي بعد التطبيق الأول.

أمثلة عملية:

  • آمن من التكرار: ضبط بريد المستخدم إلى alice@example.com. تنفيذ UPDATE عشر مرات يُبقي نفس الصف.
  • غير آمن من التكرار: خصم 10 دولارات من المحفظة. تنفيذ هذه العملية عشر مرات يخصم 100 دولار.
  • آمن من التكرار: تحديد حالة طلب إلى SHIPPED. الانتقال من SHIPPED إلى SHIPPED لا يغيّر شيئاً.
  • غير آمن من التكرار: زيادة عداد المشاهدات. كل رسالة مكررة تضخّم العدد.
المبدأ الأساسي: لا تفترض أبداً أن الرسالة تصل مرة واحدة بالضبط. صمّم مستهلكيك بحيث تكون الرسالة المكررة بلا أثر ضار. التسليم "مرة على الأقل" هو أأمن ضمان تستطيع معظم الوسطاء تقديمه؛ أما "مرة واحدة بالضبط" فمكلف وكثيراً ما يكون وهماً على مستوى التطبيق.

مفاتيح الأمان من التكرار (Idempotency Keys)

النمط المعياري هو إرفاق مفتاح أمان فريد ومستقر بكل رسالة عند نقطة الإنشاء — عادةً UUID يولّده المنتج. يسجّل المستهلك المفاتيح التي عالجها بالفعل. عند استلام رسالة يتحقق: هل رأيت هذا المفتاح من قبل؟ إذا كانت الإجابة نعم، يؤكد الاستلام ويتجاهل الرسالة دون إعادة تنفيذ العملية.

يجب أن يكون المفتاح:

  • فريداً عالمياً — UUID v4 أو مفتاح مركّب مثل order_id:event_type:attempt.
  • مستقراً عبر محاولات الإعادة — يجب أن يرسل المنتج نفس المفتاح في كل إعادة محاولة للعملية المنطقية ذاتها، لا أن يولّد UUID جديداً في كل مرة.
  • مخزناً بصورة دائمة — في Redis مع مهلة انتهاء، أو في جدول قاعدة بيانات، لتحمّل إعادة تشغيل المستهلك.
Idempotency key deduplication flow Producer key: "uuid-7f3a" msg (x2 retry) Message Broker delivers ≥ 1 time duplicate msg Consumer 1. Lookup key in store 2a. Seen → ack, skip 2b. New → process + save Dedup Store Redis / DB table Step 1 Step 2 Step 3 — guard Step 4 Key must be stable across retries — generated once by the producer, not per attempt.
فحص مفتاح الأمان من التكرار: يتحقق المستهلك من المفتاح في مخزن دائم قبل تنفيذ العملية الجانبية.

أين تُخزَّن المفاتيح المعالَجة؟

مخزن إزالة التكرار مكوّن حاسم. الخيارات الشائعة:

  • Redis SET NX مع TTL — سريع (دون ميلي ثانية)، مناسب للمفاتيح التي يمكن أن تنتهي صلاحيتها (مثلاً 24 ساعة). الأمر SET key 1 EX 86400 NX يُعيد 1 عند الكتابة الأولى (نفّذ) و0 عند التكرار (تجاهل). هذا هو الخيار الأكثر شيوعاً في الأنظمة عالية الإنتاجية.
  • قيد فريد في قاعدة البيانات — جدول بفهرس UNIQUE على مفتاح إزالة التكرار. INSERT IGNORE أو ON CONFLICT DO NOTHING يمنع المعالجة المزدوجة ذرياً. أبطأ قليلاً لكنه دائم وترانزاكشني — يمكنك تحديث صف العمل وإدراج المفتاح في ترانزاكشن واحدة.
  • إزالة التكرار على مستوى الوسيط — بعض الوسطاء (AWS SQS FIFO، RabbitMQ مع إضافة Dedup) يقبلون MessageDeduplicationId ويرفضون التكرار خلال نافذة زمنية (5 دقائق لـ SQS FIFO). هذا يُفرغ المنطق لكنه لا يحمي من الإعادات على مستوى التطبيق خارج تلك النافذة.
نصيحة عملية: ادمج الطبقتين. استخدم إزالة التكرار على مستوى الوسيط للتعامل مع الإعادات السريعة (خلال ثوانٍ)، وقيد فريد في قاعدة البيانات للتعامل مع الإعادات المتأخرة التي تصل بعد انتهاء نافذة الوسيط. قيد قاعدة البيانات هو خط دفاعك الأخير.

الذرية: مشكلة الإنفاق المزدوج

تنشأ مشكلة دقيقة لكن بالغة الأهمية إذا أجريت الفحص في مخزن إزالة التكرار وتنفيذ منطق العمل كخطوتين منفصلتين. بين هاتين الخطوتين قد تعالج خيط آخر نفس الرسالة. الحل هو فحص-وضبط ذري:

  • مع Redis: SET key 1 EX 86400 NX أمر ذري واحد — لا شرط للتسابق.
  • مع قاعدة بيانات علائقية: اغلف إدراج المفتاح وعملية العمل في ترانزاكشن واحدة. قيد الفرادة سيُفشل الترانزاكشن الثانية ويُرجع التراجع بشكل نظيف.
تجنّب "فحص ثم تصرّف": لا تقم أبداً بـSELECT → قرار → INSERT كثلاث عبارات منفصلة بدون ترانزاكشن أو عملية ذرية. في ظل التحميل المتزامن، قد يجتاز مستهلكان كليهما فحص SELECT ويتابعان التنفيذ، مما ينتج تكراراً. هذا هو سباق التسابق الكلاسيكي TOCTOU.

جعل العمليات غير الآمنة آمنة من التكرار

حين لا تستطيع إعادة كتابة العملية الأساسية لتكون آمنة بطبيعتها (مثل تحصيل دفعة من بوابة دفع خارجية)، النمط هو التحصين بآلة حالة:

  1. قبل استدعاء الخدمة الخارجية، اكتب سجلاً في قاعدة البيانات بحالة PENDING ومفتاح الأمان من التكرار، ضمن ترانزاكشن.
  2. إذا فشلت الكتابة بتعارض مفتاح فريد، فعامل آخر بدأ أو أكمل العملية — توقف وأعد.
  3. استدعِ الخدمة الخارجية، ثم حدّث السجل إلى COMPLETE مع النتيجة.
  4. عند أي عطل بين الخطوتين 2 و3، تجد وظيفة الاسترداد صف PENDING وتعيد المحاولة — مُرسلةً نفس مفتاح الأمان إلى API الخارجية (Stripe وPayPal وغيرهما يدعمون هذا) فتقوم بوابة الدفع بإزالة التكرار من جهتها.
Payment idempotency fence with status machine Charge Event key: "k-001" DB: Insert key=k-001 status=PENDING Conflict → stop (duplicate) Payment Gateway POST /charge Idempotency-Key: k-001 DB: Update key=k-001 status=COMPLETE Crash between Insert and Update → recovery job retries with same key; gateway deduplicates.
تحصين الدفع بآلة الحالة: حالة PENDING تحرس الاستدعاء الخارجي؛ البوابة تزيل التكرار أيضاً بالمفتاح.

TTL وانتهاء صلاحية المفاتيح

لا تحتاج مفاتيح إزالة التكرار إلى البقاء إلى الأبد. اختر TTL يغطي نافذة الإعادة مع هامش مريح. إذا كانت سياسة الإعادة تستسلم بعد ساعة واحدة بالتراجع الأسي، فإن TTL بمدة 24 ساعة آمن. لعمليات الدفع التي قد يطعن فيها العميل بعد أيام، احتفظ بالمفاتيح لمدة 7 إلى 30 يوماً. عند انتهاء صلاحية المفاتيح، تقبل أن تُعالَج رسالة تُعاد إرسالها بعد النافذة مجدداً — تأكد من أن ذلك مقبول لحالة استخدامك، أو استخدم سجلات قاعدة بيانات دائمة للعمليات ذات المخاطر العالية.

قائمة مراجعة عملية

  • كل منتج يُسند مفتاح UUID من الأمان من التكرار مرة واحدة ويعيد استخدامه في جميع محاولات الإعادة.
  • كل مستهلك يفحص المفتاح ذرياً قبل تنفيذ العملية الجانبية.
  • يستخدم مخزن إزالة التكرار عملية ذرية (SET NX أو قيد فريد).
  • يحدث منطق العمل وتسجيل المفتاح في نفس الترانزاكشن حيثما أمكن.
  • يكون TTL طويلاً بما يكفي لتغطية نافذة الإعادة الكاملة بزيادة.
  • تستقبل الخدمات الخارجية (بوابات الدفع، مزودو البريد الإلكتروني) نفس المفتاح لإزالة التكرار باستقلالية.