رحلة في معمارية Event Sourcing: أسرار الـ Continuous Reading والـ Projections 🧠🔄

 تخيل إنك بتبني نظام محفظة إلكترونية زي "فودافون كاش" أو تطبيق دفع بنكي 💳📱. في معمارية Event Sourcing، إحنا مش بنحفظ الرصيد النهائي للمستخدم في قاعدة البيانات برقم ثابت (زي balance = 500). إحنا بنحفظ كل حركة تمت على الحساب كحدث مستقل لا يتغير (Immutable Event Log):


  • 📥 MoneyDeposited (+1000 EGP)

  • 📤 MoneyTransferred (-300 EGP)

  • 🛒 MerchantPaid (-200 EGP)

عشان شاشة الموبايل بتاعة العميل تعرض رصيده الحالي (500 جنيه) أو تبعتله إشعار فوري لحظة الخصم، محتاجين نعمل Continuous Reading (قراءة مستمرة) لشريط الأحداث ده وتطبيق خوارزميات تحول الأحداث لرؤية حية (Read Model/Projection) من غير ما توقف أو تفوّت حدث واحد! 🔄⚡

سيناريو المثال العملي: تطبيق "كاش مصر" 🏦

عندنا خدمتين منفصلتين تماماً (CQRS Pattern):

  1. الـ Write Engine (Event Store): جدول بيسجل الأحداث ورا بعض بـ Sequence Number تصاعدي فريد (1, 2, 3, ...).

  2. الـ Projection Service (Continuous Reader): خدمة شغلانتها 24/7 تقرأ الأحداث الجديدة وتحدث جدول الرصيد وسجل المعاملات للشاشة.

التحدي الهندسي هنا: إزاي الـ Reader يقدر يقرأ الأحداث الجديدة باستمرار وبسرعة فائقة (Real-time) من غير ما يضغط على قاعدة البيانات ومن غير ما يكرر أو ينسى أرقام؟ 🧐

التقنيات الأساسية للقراءة المستمرة (Continuous Reading Techniques) 🛠️

1. القراءة الدورية بالـ Offset (Offset-Based Polling) ⏳

  • كيف تعمل: القارئ بيفضل يسأل الـ Event Store كل مسافة زمنية صغيرة (مثلاً كل 200ms): "هاتلي الأحداث اللي رقمها أكبر من آخر رقم أنا قريته (last_processed_offset)".

  • الاستعلام: SELECT * FROM Events WHERE sequence_id > 1050 ORDER BY sequence_id ASC LIMIT 100;

  • المميزات: سهلة التنفيذ جداً ومضمونة.

  • العيوب: تستهلك موارد قاعدة البيانات لو مفيش أحداث جديدة (Empty Queries)، وفيها نسبة تأخير زمني (Latency) بحد أدنى وقت الـ Polling.

2. الاشتراكات المباشرة والـ Streaming (Push/Subscription Model) 📡

  • كيف تعمل: بدال ما القارئ يفضل يسأل، بيفتح قناة اتصال حية مستمرة (زي gRPC Streaming أو WebSockets أو Server-Sent Events) مع الـ Event Store. أول ما ينزل حدث جديد، الـ Store بيزّه فوراً للـ Reader!

  • خوارزمية Catch-up Subscriptions:

    1. القارئ يبدأ في وضع الـ Historical Pull يقرا الأحداث القديمة من 0 لحد آخر حدث موجود دلوقتي.

    2. أول ما يوصل للحدث الأخير، يتحول تلقائياً وشفافاً لوضع الـ Live Stream ويستقبل الأحداث الحية لحظياً 🚀.

3. تتبع سجلات التغيير المباشر (CDC - Change Data Capture) 🕵️‍♂️

  • كيف تعمل: باستخدام أدوات زي Debezium مع Apache Kafka، التقنية دي بتقرأ الـ Write-Ahead Log (WAL) الخاص بقاعدة البيانات (زي Postgres أو MySQL) من بره لبره، من غير ما تبعت استعلامات SELECT للـ DB نهائياً!

  • المميزات: صفر تأثير على أداء الجداول الأساسية، وسرعة قراءة قريبة جداً من الزمن الفعلي (Near Real-Time).

4. نمط الصندوق الخارجي (Transactional Outbox Pattern) 📦

  • كيف تعمل: لما تكتب حدث في الـ Event Store، بتكتبه في نفس الـ Database Transaction في جدول تاني اسمه Outbox.

  • شغلانة background worker صغير إنه يعمل Continuous Reading للـ Outbox ده، يبعت الأحداث لـ Message Broker (زي RabbitMQ أو Kafka)، وبعدين يمسحها أو يعلم عليها كـ Processed.

الخوارزميات وآليات الضبط المتقدمة (Core Algorithms & Mechanics) 🧠

1. خوارزمية تتبع علامات الحفظ (Offset Checkpointing Algorithm) 📍
عشان لو السيرفر اتفصل أو حصل فيه Crash، يعرف كمل منين أول ما يقوم:

  • القارئ بيعالج الأحداث حدث ورا حدث.

  • كل N أحداث (أو كل ثانية)، بيكتب آخر Sequence ID اتعالج بنجاح في قاعدة بيانات سريعة (زي Redis).

  • القاعدة الذهبية: تجنب كِتابة الـ Offset إلا بعد إتمام كتابة التحديث في الـ Read Model بـ Transaction موحدة، عشان تضمن إن مفيش حدث يضيع.

2. خوارزمية منع التكرار (Idempotency Algorithm) 🛡️
بسبب مشاكل الشبكة، القراءة المستمرة ممكن تبعتلك نفس الحدث مرتين (At-Least-Once Delivery). إزاي نمنع حساب المبلغ مرتين؟

  • كل حدث بيبقى معاه Event ID فريد (UUID).

  • الـ Projection بيحتفظ بجدول أحدث Processed Event IDs.

  • المنطق: قبل ما يضيف المبلغ للرصيد، بيفحص: هل الـ Event ID ده اتعالج قبل كده؟ لو أيوة -> ينط للحدث اللي بعده فوراً ويرفض إعادة التطبيق.

3. خوارزمية إعادة التشغيل الكامل (Zero-Downtime Projection Replay Algorithm) 🔄
تخيل اكتشفت إن كان في بغ (Bug) في معادلة حساب الخصم وعايز تعيد حساب كل التاريخ من أول وجديد:

  1. ابنِ جدول Read Model جديد فاضي بجانب القديم (ReadModel_v2).

  2. شغّل Continuous Reader جديد يبدأ من Sequence ID = 0 يقرأ كل الشريط من يوم بداية التطبيق ويملأ ReadModel_v2.

  3. أول ما القارئ الجديد يوصل للـ Live events ويجيب القديم كله، اعمل Switch حركة المرور (Traffic) للجدول الجديد.

  4. امسح الجدول القديم (ReadModel_v1) بدون ما يتأثر المستخدم ولا ثانية واحدة!

Comments