المعاملات والتخزين المؤقّت والأداء

مستويات العزل

18 دقيقة الدرس 3 من 13

مستويات العزل

حين تُنفَّذ عدة معاملات بالتزامن، يمكن أن تتداخل فيما بينها بأساليب محدّدة تمامًا. يُسمّي معيار SQL هذه الأنماط من التداخل الشذوذات التزامنية، ويُحدّد أربعة مستويات عزل تزيل هذه الشذوذات تدريجيًا — بتكلفة متصاعدة من القفل أو من إدارة الإصدارات. يُعدّ اختيار مستوى العزل المناسب أحد أكثر قرارات الأداء أثرًا في أي خدمة كثيفة البيانات.

الشذوذات التزامنية الأربع

قبل الخوض في المستويات ذاتها، من المفيد أن تمتلك نموذجًا ذهنيًا واضحًا لما يمكن أن يسوء.

  • القراءة القذرة (Dirty Read): تقرأ المعاملة A صفًّا قامت المعاملة B بتعديله لكنها لم تُثبّته بعد. إذا تراجعت B عن تغييراتها، فقد عملت A على بيانات لم تكن موجودة أصلًا.
  • القراءة غير المتكررة (Non-Repeatable Read): تقرأ المعاملة A صفًّا ثم تُعيد قراءته لاحقًا في المعاملة ذاتها فتجد قيمًا مختلفة، لأن B أثبتت تحديثًا في الفترة الفاصلة.
  • القراءة الشبحية (Phantom Read): تُنفّذ المعاملة A استعلام نطاق مرتين. وبين التنفيذين، تُدرج B أو تحذف صفوفًا تقع ضمن النطاق. يُعيد الاستعلام الثاني مجموعة مختلفة من الصفوف — "أشباح" ظهرت أو اختفت.
  • التحديث الضائع (Lost Update): تقرأ معاملتان قيمةً، وتُعدّلانها كلتاهما، والكتابة اللاحقة تستبدل السابقة بصمت. لم تُبلَّغ أيٌّ من المعاملتين بخطأ، لكن أحد التحديثين اندثر.
التحديث الضائع ليس مدرجًا في قائمة الشذوذات الواردة في معيار SQL-92، لكنه الأكثر ضررًا من الناحية العملية على الأرجح. القفل التفاؤلي (المُغطَّى في الدرس الخامس) هو الدفاع المعياري في JPA ضدّه.

مستويات العزل الأربعة

يُحدّد معيار SQL المستويات من الأضعف إلى الأقوى. تدعم معظم قواعد البيانات الكبرى المستويات الثلاثة السفلية على الأقل؛ وتتخذ أغلبها Read Committed مستوىً افتراضيًا.

READ UNCOMMITTED

يمكن للمعاملة قراءة التغييرات غير المُثبَّتة من معاملات أخرى. جميع الشذوذات الأربع واردة. هذا المستوى لا يُستخدم تقريبًا في كود التطبيقات — له استخدامات متخصصة في لوحات التحليلات التي تتحمّل البيانات القديمة وتحتاج إلى الحد الأدنى من تنافس القفل.

READ COMMITTED (الافتراضي في PostgreSQL / Oracle)

ترى المعاملة فقط البيانات المُثبَّتة. القراءات القذرة مُستبعَدة. القراءات غير المتكررة والأشباح لا تزال ممكنة لأن القراءة الثانية ضمن المعاملة ذاتها قد ترى لقطة مختلفة عن الأولى.

REPEATABLE READ (الافتراضي في MySQL / MariaDB InnoDB)

تضمن قاعدة البيانات أنك إذا قرأت صفًّا، فإن القراءات اللاحقة للصف ذاته ضمن المعاملة ذاتها تُعيد القيم نفسها. القراءات القذرة والقراءات غير المتكررة مُستبعَدة. القراءات الشبحية ممكنة نظريًا وفق تعريف SQL-92 الصارم، لكن تطبيق InnoDB القائم على MVCC يمنعها عمليًا في معظم أنماط الاستعلام.

SERIALIZABLE

تُنفَّذ المعاملات كما لو كانت مُرتَّبة تسلسليًا تمامًا — واحدة تلو الأخرى. كل الشذوذات مُستبعَدة. تحقق قاعدة البيانات ذلك بأقفال النطاق (أو أقفال الشرط)، التي يمكن أن تُسبّب تنافسًا ملحوظًا وإغلاقات متبادلة (deadlocks) في أحمال العمل الكثيفة بالكتابة.

تحديد مستوى العزل في Spring

تُحدّد مستوى العزل عبر السمة isolation في @Transactional. تعكس أسماء الثوابت أسماء معيار SQL مباشرةً:

import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Service; @Service public class AccountService { // الافتراضي — يستخدم الافتراضي لقاعدة البيانات (عادةً READ_COMMITTED) @Transactional public void transfer(long fromId, long toId, BigDecimal amount) { ... } // منع القراءات غير المتكررة: يُعيد الصف ذاته القيمة نفسها دائمًا @Transactional(isolation = Isolation.REPEATABLE_READ) public AccountSnapshot buildStatement(long accountId) { ... } // أقصى قدر من الأمان — استخدمه باعتدال؛ يُسبّب أعلى درجات التنافس @Transactional(isolation = Isolation.SERIALIZABLE) public void reserveSeat(long flightId, int seatNumber) { ... } }
طابق المستوى مع الشذوذة التي تدافع عنها فعلًا. التسرّع إلى SERIALIZABLE في كل مكان "لتكون آمنًا" سيُفضي إلى إغلاقات متبادلة وأخطاء انتهاء المهلة تحت الحمل الإنتاجي. ابدأ بـ READ_COMMITTED وقِس الأداء، وتصاعد فقط حين تُثبت وجود شذوذة ملموسة في عبء عملك.

كيف تُنفّذ قواعد البيانات العزل

فهم التطبيق يُفسّر سبب كون بعض المستويات "مجانية" وأخرى مكلفة.

  • MVCC (التحكم في التزامن متعدد الإصدارات): تستخدم PostgreSQL وMySQL InnoDB وOracle صور الصفوف ذات الإصدارات. يرى القارئون لقطة من البيانات المُثبَّتة مأخوذة في بداية المعاملة (أو بداية الجملة حسب المستوى) دون الحاجة إلى أقفال. هذا يجعل القراءات سريعة حتى عند مستويات عزل أعلى، لكنه يستهلك مساحة تخزين ومعالجة أكبر لجمع الإصدارات القديمة.
  • الأقفال المشتركة والحصرية: يُضيف SERIALIZABLE أقفال النطاق أو الشرط فوق MVCC أو يستخدم القفل ثنائي الطور. هذه تحجب الكتّاب المتزامنين ويمكن أن تتصاعد إلى إغلاقات متبادلة.

العزل في JPA / Hibernate

يُدخل Hibernate مفهومًا إضافيًا فوق مستوى قاعدة البيانات: ذاكرة التخزين المؤقت من المستوى الأول (سياق الاستمرارية). ضمن معاملة واحدة، حين يُحمّل Hibernate كيانًا، يُخزّنه في الذاكرة. الاستدعاءات اللاحقة لـ entityManager.find() بالمفتاح الأساسي ذاته تُعيد الكائن المُخزَّن لا صفًّا مُعاد استعلامه من قاعدة البيانات. هذا يعني أن Hibernate يُوفّر قراءات متكررة تلقائيًا للبحث عبر المفتاح الأساسي بصرف النظر عن مستوى عزل قاعدة البيانات — لكن فقط للبحث بالمفتاح الأساسي، لا لاستعلامات JPQL ذات النطاق.

@Transactional(isolation = Isolation.READ_COMMITTED) public void demonstrateFirstLevelCache(long orderId) { // البحث الأول: يصل إلى قاعدة البيانات Order order = orderRepository.findById(orderId).orElseThrow(); // محاكاة وقت معالجة أثناءه تُثبّت معاملة أخرى تغييراتها processPayment(order); // البحث الثاني: يُعيد الكائن ذاته من الذاكرة — بدون رحلة إلى قاعدة البيانات // حتى لو كان لدى قاعدة البيانات نسخة أحدث عند مستوى READ_COMMITTED Order sameOrder = orderRepository.findById(orderId).orElseThrow(); // order == sameOrder (المرجع ذاته) }
يمكن لذاكرة التخزين المؤقت من المستوى الأول أن تُخفي البيانات القديمة. إذا احتجت إلى رؤية حديثة لكيان في منتصف معاملة (مثلًا بعد استدعاء إجراء مُخزَّن يُعدّل الصف)، استدع 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 قراءات متكررة للكيانات بصمت بصرف النظر عن مستوى قاعدة البيانات، وهو أمر مفيد لكنه قد يُوصل بيانات قديمة حين تحتاج إلى التحديث.