القفل التفاؤلي مع @Version
القفل التفاؤلي مع @Version
تواجه كل تطبيقات متعددة المستخدمين في نهاية المطاف مشكلة التحديث المفقود: تقرأ معاملتان الصف نفسه، تُعدّله كلٌّ منهما باستقلالية، ثم تكتب الثانية فوق الأولى دون أي تحذير. يُعدّ القفل التفاؤلي الحلَّ الخفيف والقابل للتوسع لأعباء العمل التي تكون فيها التعارضات نادرة. بدلًا من الحصول على قفل قاعدة بيانات عند قراءة البيانات، يكتشف التصادمات عند وقت الإيداع ويترك للتطبيق قرار التعامل معها.
الفكرة الأساسية: أعمدة الإصدار
يعمل القفل التفاؤلي بوضع رمز إصدار على كل صف. تدير JPA / Hibernate هذا تلقائيًا عند تعليق حقل بـ @Version:
لا تقرأ حقل version أو تكتب إليه بنفسك أبدًا. تقرأ Hibernate قيمته عند SELECT، وعند إصدار UPDATE تُضيف شرطًا على الإصدار:
إذا تطابقت جملة WHERE مع صفر صفوف — لأن معاملة أخرى رفعت الإصدار إلى 2 بالفعل — رمت Hibernate الاستثناء jakarta.persistence.OptimisticLockException الذي تُغلّفه Spring في org.springframework.orm.ObjectOptimisticLockingFailureException.
اختيار نوع حقل الإصدار المناسب
يدعم التعليق @Version عدة أنواع Java:
int/Integer— يزداد بمقدار 1؛ الاختيار الأبسط لمعظم الكيانات.long/Long— استخدمه عند تحديث الكيان بشكل متكرر جدًا وتخشى تجاوز نطاق العدد الصحيح (نادر لكن ممكن على مدى عقود).short/Short— نادر الاستخدام؛ نطاق صغير.java.sql.Timestamp— تضبطها Hibernate على طابع زمني لقاعدة البيانات الحالية عند كل تحديث. تجنّبه في البيئات المُجمَّعة أو السحابية حيث قد يتسبب الانجراف الزمني بين العقد في تعارضات وهمية أو تعارضات مفقودة.
Long إذا كان الكيان صفًا ساخنًا يُحدَّث مرات عديدة في الثانية؛ وإلا يكفي int.
التعامل مع OptimisticLockException في طبقة الخدمة
النمط المعياري هو اصطياد استثناء التعارض وإعادة المحاولة عددًا محدودًا من المرات:
يأتي @Retryable من Spring Retry (مكتبة spring-retry في مسار الفئات مع @EnableRetry على فئة الإعدادات). التراجع الأسي — 50 مللي ثانية، 100، 200 — يُقلل من ظاهرة القطيع الرعدي عندما تتصادم خيوط كثيرة على الصف نفسه.
@Transactional بالكامل، مما يعني إعادة جلب الكيان بأحدث إصدار. لا تُخزّن الكيان خارج حدود المعاملة وتعيد استخدامه عبر المحاولات — ذلك يُبطل الآلية برمّتها.
القفل التفاؤلي في Spring Data JPA
تحترم Spring Data REST وطرق المستودعات المخصصة @Version تلقائيًا. يمكنك أيضًا تصريح @Lock بوضع القفل التفاؤلي في طرق الاستعلام:
OPTIMISTIC_FORCE_INCREMENT هو الاختيار الصحيح حين يعمل الكيان كـ جذر كلي (aggregate root): تحميل أحد الآباء وتعديل كيان ابن فقط لن يحدّث إصدار الأب عادةً، تاركًا الكل غير محمي. الإجبار على زيادة إصدار الجذر يضمن اكتشاف أي تعديل متزامن على الكل الكامل.
ما لا يحمي منه القفل التفاؤلي
يمنع القفل التفاؤلي شذوذ التحديث المفقود داخل وحدة عمل JPA واحدة. لكنه لا:
- يمنع معاملتين من قراءة بيانات قديمة في آنٍ واحد — لا يُكتشف التعارض إلا عند الإيداع.
- يساعد عند تعديل الصف من خارج JPA (SQL خام، خدمة أخرى) إلا إذا حدّثت تلك الأدوات عمود الإصدار أيضًا.
- يحل مشاكل التنافس في سيناريوهات التعارض العالي. إذا كان الصف نفسه يُحدَّث عشرات المرات في الثانية، ستفشل معظم المحاولات ويزداد العبء. استخدم القفل المتشائم (الدرس 6) للصفوف الساخنة ذات التعارضات المضمونة.
OptimisticLockingFailureException إلى استجابة HTTP مناسبة. أضف دائمًا معالجًا عامًا للاستثناءات أو دالة @ExceptionHandler لهذا الاستثناء.
الخصائص الأدائية
القفل التفاؤلي رخيص للغاية حين تكون التعارضات نادرة:
- لا قفل يُمسك بين القراءة والكتابة — الصف لا يُحجب أبدًا، مما يسمح للقارئين والكاتبين بالمضي معًا دون انتظار.
- جملة WHERE إضافية — العبء الوحيد هو مقارنة عدد صحيح واحدة في شرط UPDATE، وهو أمر هيّن.
- عمود إضافي — عمود
INTبحجم 4 بايت لكل جدول؛ لا تُضف فهرسًا إلا إذا استعلمت بحسب الإصدار (نادرًا مفيد). - تكلفة إعادة المحاولة — إذا كانت التعارضات متكررة، تُضيف حلقات إعادة المحاولة رحلات ذهاب وإياب. راقب معدل التصادم؛ إذا تجاوز التعارض ~5% فهذا مؤشر على مراجعة استراتيجية القفل.
الخلاصة
أضف @Version إلى أي كيان قد يُحدَّث في وقت واحد. تتولى Hibernate إدارة زيادة الإصدار واكتشاف التصادمات تلقائيًا — مسؤوليتك الوحيدة هي اصطياد OptimisticLockingFailureException وإعادة المحاولة أو إظهار خطأ ذي معنى للمُستدعي. بالنسبة للجذور الكلية، استخدم OPTIMISTIC_FORCE_INCREMENT لحماية تغييرات الكيانات الأبناء. القفل التفاؤلي هو الخيار الافتراضي الصحيح لمعظم أعباء عمل CRUD؛ تحوّل إلى الأقفال المتشائمة فقط حين يكشف التوصيف أن التعارضات متكررة جدًا لدرجة تؤثر على الإنتاجية.