مستويات العزل
مستويات العزل
حين تُنفَّذ عدة معاملات بالتزامن، يمكن أن تتداخل فيما بينها بأساليب محدّدة تمامًا. يُسمّي معيار SQL هذه الأنماط من التداخل الشذوذات التزامنية، ويُحدّد أربعة مستويات عزل تزيل هذه الشذوذات تدريجيًا — بتكلفة متصاعدة من القفل أو من إدارة الإصدارات. يُعدّ اختيار مستوى العزل المناسب أحد أكثر قرارات الأداء أثرًا في أي خدمة كثيفة البيانات.
الشذوذات التزامنية الأربع
قبل الخوض في المستويات ذاتها، من المفيد أن تمتلك نموذجًا ذهنيًا واضحًا لما يمكن أن يسوء.
- القراءة القذرة (Dirty Read): تقرأ المعاملة A صفًّا قامت المعاملة B بتعديله لكنها لم تُثبّته بعد. إذا تراجعت B عن تغييراتها، فقد عملت A على بيانات لم تكن موجودة أصلًا.
- القراءة غير المتكررة (Non-Repeatable Read): تقرأ المعاملة A صفًّا ثم تُعيد قراءته لاحقًا في المعاملة ذاتها فتجد قيمًا مختلفة، لأن B أثبتت تحديثًا في الفترة الفاصلة.
- القراءة الشبحية (Phantom Read): تُنفّذ المعاملة A استعلام نطاق مرتين. وبين التنفيذين، تُدرج B أو تحذف صفوفًا تقع ضمن النطاق. يُعيد الاستعلام الثاني مجموعة مختلفة من الصفوف — "أشباح" ظهرت أو اختفت.
- التحديث الضائع (Lost Update): تقرأ معاملتان قيمةً، وتُعدّلانها كلتاهما، والكتابة اللاحقة تستبدل السابقة بصمت. لم تُبلَّغ أيٌّ من المعاملتين بخطأ، لكن أحد التحديثين اندثر.
مستويات العزل الأربعة
يُحدّد معيار SQL المستويات من الأضعف إلى الأقوى. تدعم معظم قواعد البيانات الكبرى المستويات الثلاثة السفلية على الأقل؛ وتتخذ أغلبها Read Committed مستوىً افتراضيًا.
READ UNCOMMITTED
يمكن للمعاملة قراءة التغييرات غير المُثبَّتة من معاملات أخرى. جميع الشذوذات الأربع واردة. هذا المستوى لا يُستخدم تقريبًا في كود التطبيقات — له استخدامات متخصصة في لوحات التحليلات التي تتحمّل البيانات القديمة وتحتاج إلى الحد الأدنى من تنافس القفل.
READ COMMITTED (الافتراضي في PostgreSQL / Oracle)
ترى المعاملة فقط البيانات المُثبَّتة. القراءات القذرة مُستبعَدة. القراءات غير المتكررة والأشباح لا تزال ممكنة لأن القراءة الثانية ضمن المعاملة ذاتها قد ترى لقطة مختلفة عن الأولى.
REPEATABLE READ (الافتراضي في MySQL / MariaDB InnoDB)
تضمن قاعدة البيانات أنك إذا قرأت صفًّا، فإن القراءات اللاحقة للصف ذاته ضمن المعاملة ذاتها تُعيد القيم نفسها. القراءات القذرة والقراءات غير المتكررة مُستبعَدة. القراءات الشبحية ممكنة نظريًا وفق تعريف SQL-92 الصارم، لكن تطبيق InnoDB القائم على MVCC يمنعها عمليًا في معظم أنماط الاستعلام.
SERIALIZABLE
تُنفَّذ المعاملات كما لو كانت مُرتَّبة تسلسليًا تمامًا — واحدة تلو الأخرى. كل الشذوذات مُستبعَدة. تحقق قاعدة البيانات ذلك بأقفال النطاق (أو أقفال الشرط)، التي يمكن أن تُسبّب تنافسًا ملحوظًا وإغلاقات متبادلة (deadlocks) في أحمال العمل الكثيفة بالكتابة.
تحديد مستوى العزل في Spring
تُحدّد مستوى العزل عبر السمة isolation في @Transactional. تعكس أسماء الثوابت أسماء معيار SQL مباشرةً:
SERIALIZABLE في كل مكان "لتكون آمنًا" سيُفضي إلى إغلاقات متبادلة وأخطاء انتهاء المهلة تحت الحمل الإنتاجي. ابدأ بـ READ_COMMITTED وقِس الأداء، وتصاعد فقط حين تُثبت وجود شذوذة ملموسة في عبء عملك.
كيف تُنفّذ قواعد البيانات العزل
فهم التطبيق يُفسّر سبب كون بعض المستويات "مجانية" وأخرى مكلفة.
- MVCC (التحكم في التزامن متعدد الإصدارات): تستخدم PostgreSQL وMySQL InnoDB وOracle صور الصفوف ذات الإصدارات. يرى القارئون لقطة من البيانات المُثبَّتة مأخوذة في بداية المعاملة (أو بداية الجملة حسب المستوى) دون الحاجة إلى أقفال. هذا يجعل القراءات سريعة حتى عند مستويات عزل أعلى، لكنه يستهلك مساحة تخزين ومعالجة أكبر لجمع الإصدارات القديمة.
- الأقفال المشتركة والحصرية: يُضيف
SERIALIZABLEأقفال النطاق أو الشرط فوق MVCC أو يستخدم القفل ثنائي الطور. هذه تحجب الكتّاب المتزامنين ويمكن أن تتصاعد إلى إغلاقات متبادلة.
العزل في JPA / Hibernate
يُدخل Hibernate مفهومًا إضافيًا فوق مستوى قاعدة البيانات: ذاكرة التخزين المؤقت من المستوى الأول (سياق الاستمرارية). ضمن معاملة واحدة، حين يُحمّل Hibernate كيانًا، يُخزّنه في الذاكرة. الاستدعاءات اللاحقة لـ entityManager.find() بالمفتاح الأساسي ذاته تُعيد الكائن المُخزَّن لا صفًّا مُعاد استعلامه من قاعدة البيانات. هذا يعني أن Hibernate يُوفّر قراءات متكررة تلقائيًا للبحث عبر المفتاح الأساسي بصرف النظر عن مستوى عزل قاعدة البيانات — لكن فقط للبحث بالمفتاح الأساسي، لا لاستعلامات JPQL ذات النطاق.
entityManager.refresh(entity) لإجبار إعادة القراءة من قاعدة البيانات.
إرشادات عملية حسب حالة الاستخدام
- استعلامات التقارير للقراءة فقط:
READ_COMMITTED(أو أدنى) مع@Transactional(readOnly = true). السرعة مهمة؛ الأشباح في تقرير مقبولة. - أرصدة الحسابات / عدد المخزون:
REPEATABLE_READأو القفل التفاؤلي. لا يجب أن تحسب قيمة مشتقة من صف تغيّر تحتك. - حجوزات المقاعد / التذاكر:
SERIALIZABLEأو القفل المتشائم. تكلفة الشذوذة (الحجز المزدوج) تفوق عقوبة الأداء. - عمليات CRUD العامة:
READ_COMMITTED(الافتراضي). صحيح لغالبية منطق الأعمال.
الخلاصة
تُشكّل مستويات العزل SQL الأربعة — READ UNCOMMITTED وREAD COMMITTED وREPEATABLE READ وSERIALIZABLE — طيفًا يُتاجر بالتزامن في مقابل الأمان. يُعرضها Spring مباشرةً عبر سمة isolation في @Transactional. تعمل معظم التطبيقات بشكل صحيح عند READ_COMMITTED؛ تصاعد فقط حين يثبت وجود شذوذة ملموسة في عبء عملك. تُوفّر ذاكرة التخزين المؤقت من المستوى الأول في Hibernate قراءات متكررة للكيانات بصمت بصرف النظر عن مستوى قاعدة البيانات، وهو أمر مفيد لكنه قد يُوصل بيانات قديمة حين تحتاج إلى التحديث.