مصادر الأحداث
مصادر الأحداث
في الأنظمة التقليدية، تُخزِّن جداول قاعدة البيانات الحالة الراهنة للكيان. حين تُشحن طلبية، تُحدَّث صف في الجدول. الحالة السابقة — مسودة، انتظار الدفع، مكتملة — تختفي إلى الأبد. مصادر الأحداث (Event Sourcing) يعكس هذا المنطق: بدلاً من تخزين الحالة الراهنة، يُخزَّن سجل مرتَّب لكل حدث حصل. الحالة الراهنة مجرد نتيجة مشتقة، تُحسَب بإعادة تشغيل تلك الأحداث من البداية (أو من لقطة محفوظة). السجل هو الحقيقة؛ والجدول مجرد ذاكرة مؤقتة.
هذه ليست فكرة جديدة. دفتر أستاذ البنك، وسجل الإيداعات في Git، ودفتر القيد المحاسبي — كلها تُطبّق مصادر الأحداث بطبيعتها. الجديد هو تطبيق هذا المبدأ بوعي على حالة التطبيق في الأنظمة الموزعة على نطاق واسع.
النموذج الجوهري
الحدث حقيقة غير قابلة للتغيير: شيء حدث. تُسمَّى الأحداث بالفعل الماضي — OrderPlaced، PaymentReceived، ItemShipped. يحمل كل حدث حمولته (الحقول ذات الصلة بتلك الواقعة)، وختم زمني، ورقم تسلسلي أو رقم إصدار. تُلحق الأحداث بـمخزن الأحداث؛ ولا تُحدَّث أبداً ولا تُحذف.
لقراءة الحالة الراهنة، يُعيد المستهلك تشغيل سيل الأحداث: يبدأ من الصفر (أو من أقرب لقطة) ويُطبّق كل حدث على دالة اختزال حتى يصل إلى اللحظة الحاضرة. مثال على طلبية:
لماذا لا تكتفي بتحديث صف في الجدول؟
الجواب فيما تفقده مع الصف القابل للتعديل:
- سجل التدقيق: من غيَّر حالة هذه الطلبية إلى "ملغاة"؟ متى؟ لماذا؟ مع مصادر الأحداث كل تعديل حقيقة دائمة وقابلة للاستعلام.
- السفر عبر الزمن: يمكنك إعادة بناء شكل الكيان بدقة تامة في أي لحظة مضت. هذا لا غنى عنه في التصحيح، والامتثال القانوني (GDPR، SOX)، وحل النزاعات.
- إسقاطات جديدة: يريد الفريق عرض تحليل لم يُخطَّط له منذ ثلاث سنوات. في قاعدة البيانات القابلة للتعديل، البيانات التاريخية ضائعة. في مخزن الأحداث، أعد تشغيل التاريخ كله وابنِ أي نموذج قراءة جديد.
- التكامل عبر الأحداث: يمكن للخدمات الأخرى الاشتراك في سيل الأحداث والحفاظ على نماذج القراءة الخاصة بها — مما يُلغي مشكلة الكتابة المزدوجة التي رأيتها في درس CQRS.
اللقطات — ترويض تكلفة إعادة التشغيل
إعادة تشغيل آلاف الأحداث في كل مرة تحتاج فيها إلى الحالة الراهنة مُكلفة. الحل هو اللقطات (Snapshots) الدورية: احفظ الحالة المختزلة كاملةً عند الإصدار N، ثم أعد تشغيل الأحداث التي تلت تلك النقطة فقط. استراتيجية شائعة: التقاط لقطة كل 50 أو 100 حدث. يحتفظ مخزن الأحداث بالتاريخ كاملاً؛ واللقطة مجرد ذاكرة مؤقتة لتحسين الأداء.
مخزن الأحداث في التطبيق العملي
أبرز الخيارات لمخزن الأحداث:
- EventStoreDB — مبني خصيصاً لمصادر الأحداث: سيلٌ لكل كيان مُجمَّع، وإسقاطات مدمجة، واشتراكات، ولقطات. المرجع الأول في تطبيق هذا النمط.
- Apache Kafka — سجل ذو إنتاجية عالية يُستخدم مخزناً للأحداث. الموضوعات ذات الاستبقاء الطويل (أسابيع أو إلى أجل غير مسمى) تحفظ التاريخ. شائع حين يُغذّي السيل ذاته كلاً من مصادر الأحداث والمراسلة بين الخدمات.
- PostgreSQL + جدول للإلحاق فقط — نقطة بداية عملية: جدول واحد بأعمدة
(aggregate_id, version, event_type, payload JSONB, occurred_at)، وقيد فريد على(aggregate_id, version)للكشف عن تعارضات التزامن التفاؤلي، وفهرس جزئي علىaggregate_id.
المقايضات والمخاطر
مصادر الأحداث التزام معماري جوهري. افهم التكاليف:
- نماذج القراءة المؤجَّلة: تُحدَّث الإسقاطات بشكل غير متزامن. ثمة نافذة (من ميلي ثوانٍ إلى ثوانٍ قليلة عادةً) يتأخر فيها نموذج القراءة عن مخزن الأحداث. صمِّم واجهتك لاستيعاب ذلك — تحديثات تفاؤلية، أو أخبر المستخدم بأن "تغييراتك قيد المعالجة".
- تطور المخطط: الأحداث غير قابلة للتغيير، لكن قواعد العمل تتغير. طوِّر مخططات الأحداث بعناية باستخدام الرفع (upcasting) أو تعديد الإصدارات أو استراتيجيات المخطط المرن. لا تحذف الأحداث القديمة أبداً ولا تُعدِّلها.
- الاستعلام أصعب: لا يمكنك كتابة
SELECT * FROM orders WHERE status = 'paid'على سجل أحداث. كل الاستعلامات الحرة يجب أن تتجه إلى الإسقاطات. خطِّط لنماذج القراءة مسبقاً، أو تقبَّل إعادة التشغيل لبناء نماذج جديدة عند الحاجة. - تعقيد تشغيلي: أصبحت تُدير مخزن أحداث وعمال إسقاط وعدة قواعد بيانات للقراءة بدلاً من واحدة. هذا التعقيد يستحق عناءه على نطاق واسع؛ وقد لا يستحق ذلك لفريق صغير أو خدمة ذات حركة منخفضة.
مثال واقعي: خدمة الطلبيات في التجارة الإلكترونية
متجر إلكتروني كبير يعالج 10 ملايين طلبية يومياً. كل طلبية مُجمَّعة تُصدر في المتوسط 8 أحداث (أُنشئت، دُفعت، جُمعت، عُبِّئت، صُنِّفت، شُحنت، في الطريق، سُلِّمت). هذا 80 مليون حدث يومياً — أي نحو 925 حدثاً في الثانية عند الذروة. يتعامل EventStoreDB أو موضوع Kafka ذو استبقاء 30 يوماً مع هذا بيسر تام. نماذج القراءة المبنية من السيل تخدم واجهة برمجة التطبيقات الموجهة للعميل (مدعومة بـRedis، قراءات دون ميلي ثانية)، ولوحة انتخاب المستودع (Postgres)، وخط أنابيب تحليلات الاحتيال (ClickHouse).
حين أراد فريق مكافحة الاحتيال إشارةً جديدة — "أبلِغ عن أي طلبية وردت مدفوعةً بعد أكثر من 10 دقائق من إنشائها" — بنوا إسقاطاً جديداً بإعادة تشغيل تاريخ 30 يوماً كاملاً. في قاعدة البيانات التقليدية لم تكن هذه البيانات تُحفظ أصلاً؛ في مخزن الأحداث كانت دائماً موجودة.