أصبح نمط Event Sourcing (ES) المقترن بـ Command Query Responsibility Segregation (CQRS) العمود الفقري المعماري للعديد من الأنظمة عالية القابلية للتوسع. بينما تُوثَّق فوائد جانب القراءة بشكل جيد، غالباً ما يُعد جانب الكتابة عنق زجاجة كبير: الكتابة المتسلسلة للأحداث إلى الخلفية التخزينية. عندما تصل ملايين الأوامر في وقت واحد، قد تعاني التطبيقات البسيطة من تنافس المعاملات، وتصعيد القفل، وتشبع الإدخال/الإخراج. في هذه المقالة، نستكشف استراتيجيات متقدمة لتحسين هذه مسارات الكتابة، مع التركيز على التجميع وميكانيكيات الإضافة فقط.
تحدي تنافس الكتابة
في تنفيذ قياسي لـ Event Sourcing، تؤدي كل أمر إلى حدث واحد أو أكثر من أحداث المجال يجب تثبيتها. إذا كان كل حدث يُطلق معاملة قاعدة بيانات منفصلة، فإن الحمل الناتج عن رحلات الشبكة واستدعاءات المعاملات يمكن أن يعيق الإنتاجية. علاوة على ذلك، يتطلب التحكم في التزامن التفاؤلي (OCC) مقارنة الإصدارات أثناء كل عملية كتابة. إذا استهدفت عدة أوامر نفس الكيان (Aggregate) في وقت واحد، فقد تفشل في التحقق من الصحة وتحتاج إلى إعادة المحاولة، مما يؤدي إلى مشاكل قطيع الرعد.
لتحقيق إنتاجية عالية، يجب علينا التحول من الإدراج سطراً بسطر إلى عمليات مجمعة وذات إضافة فقط. يقلل هذا النهج من حمل القفل ويزيد من كفاءة الإدخال/الإخراج.
استراتيجيات التجميع عالي الإنتاجية
يقلل تجميع الأحداث من عدد رحلات الشبكة إلى قاعدة البيانات. بدلاً من تثبيت الأحداث بشكل فردي، تجمع التطبيق الأحداث في مخزن مؤقت محلي ثم تفريغها على دفعات. ومع ذلك، يمكن أن يؤدي التجميع البسيط إلى إدخال تأخير. المفتاح هو موازنة حجم الدفعة مع متطلبات الوقت الحقيقي.
يتضمن نمط شائع عامل خلفية يراقب طابوراً في الذاكرة. عندما يصل الطابق إلى حجم معين أو عتبة زمنية، يقوم العامل بتثبيت الدفعة كوحدة ذرية واحدة. يضمن هذا إما تثبيت جميع الأحداث في الدفعة أو عدم تثبيت أي منها، مما يحافظ على اتساق البيانات دون التضحية بالأداء.
// كود زائف لحافلة أحداث عالية الإنتاجية
class EventBatcher:
def __init__(self, batch_size=100, flush_interval_ms=50):
self.buffer = []
self.batch_size = batch_size
self.flush_interval = flush_interval_ms
def append(self, event):
self.buffer.append(event)
if len(self.buffer) >= self.batch_size:
self.flush()
def flush(self):
if not self.buffer:
return
# بدء معاملة قاعدة بيانات واحدة للدفعة بأكملها
try:
with db.transaction():
for event in self.buffer:
db.insert("events", event)
db.commit()
except IntegrityError:
# معالجة تعارضات التزامن هنا
retry_buffer(self.buffer)
finally:
self.buffer = []
يقلل هذا النهج بشكل كبير من حمل سجل المعاملات لأن قاعدة البيانات تحتاج فقط إلى كتابة سجل تثبيت واحد لعشرات أو مئات من الأحداث. ومع ذلك، يجب على المطورين توخي الحذر: يزيد التجميع من نطاق فشل الحادث. إذا فشلت دفعة، يجب التراجع عن جميع الأحداث في تلك الدفعة، مما قد يتطلب منطق إعادة محاولة معقداً.
ميكانيكيات التخزين ذو الإضافة فقط
يعد Event Sourcing بطبيعته ذا إضافة فقط. على عكس قواعد البيانات العلائقية التقليدية التي تقوم بتحديث الصفوف الموجودة (مما قد يسبب انقسام الصفحات والتجزئة)، يسمح إرفاق سجلات جديدة بإدخال/إخراج تسلسلي، وهو أسرع بكثير من الكتابات العشوائية على معظم وسائط التخزين.
للاستفادة من ذلك، فكر في استخدام هياكل السجلات ذات الإضافة فقط أو قواعد البيانات المصممة للكتابات التسلسلية، مثل Apache Kafka، أو Amazon Kinesis، أو متاجر الأحداث المتخصصة مثل EventStoreDB. تم تحسين هذه الأنظمة للإنتاجية العالية في الكتابة من خلال فصل مسار الكتابة عن مسار القراءة تماماً.
عند تنفيذ السجلات ذات الإضافة فقط، تأكد من أن محرك قاعدة البيانات الخاص بك يدعم التجزئة الفعالة. يضمن التجزئة حسب معرف الكيان أو معرف المستأجر أن الكتابات المتعلقة بالأحداث تبقى محلية، مما يقلل بشكل أكبر من تنافس القفل. على سبيل المثال، يجب كتابة جميع الأحداث لكيان Order واحد إلى نفس التجزئة لضمان الاتساق التسلسلي.
التزامن التفاؤلي مع الكتابات المجمعة
يتطلب تنفيذ OCC في بيئة مجمعة إدارة دقيقة للإصدارات. بدلاً من التحقق من الإصدار قبل كل حدث على حدة، يمكنك التحقق من الإصدار في بداية الدفعة. إذا تغير الإصدار منذ استلام الأمر، يتم إبطال الدفعة بأكملها وإعادة محاولتها.
يفترض هذا النهج أنه خلال الوقت المستغرق لكتابة الدفعة، من غير المرجح أن يتغير حالة الكيان بشكل كبير. بينما هذا مقايضة، فإنه يسمح بمزامنة أعلى بكثير. بالنسبة للأنظمة التي تتطلب خطية صارمة، قد تكون آليات القفل الإضافية أو أرقام التسلسل ضرورية، ولكن لمعظم مجالات الأعمال، فإن الاتساق النهائي المقدم بواسطة OCC المجمعة مقبول.
الخاتمة
يعد تحسين مسار الكتابة في نظام Event Sourcing أمراً حاسماً لتحقيق القياس الذي يعد به CQRS. من خلال الجمع بين التجميع عالي الإنتاجية واستراتيجيات التخزين ذات الإضافة فقط، يمكن للمطورين تقليل زمن الاستجابة بشكل كبير وزيادة إنتاجية النظام. يكمن المفتاح في موازنة أحجام الدفعات، وإدارة تعارضات التزامن بسلاسة، واختيار محركات التخزين التي تتفوق في الكتابات التسلسلية. مع نمو نظامك، ستصبح هذه التحسينات هي الفرق بين تطبيق بطيء وآخر يقيس بسلاسة تحت الحمل الثقيل.