انتشار المعاملات
انتشار المعاملات
عندما تستدعي إحدى طرق الخدمة المُدارة بـ Spring طريقةً أخرى، وكلتاهما مُزيَّنة بـ @Transactional، يجب على Spring أن تقرر: هل يجب أن تنضم الطريقة الداخلية إلى المعاملة الحالية، أم تعلّقها وتفتح معاملة جديدة مستقلة، أم ربما تُنفَّذ دون أي معاملة على الإطلاق؟ هذا القرار يتحكم فيه خاصية propagation في @Transactional — أحد أكثر الإعدادات العملية وأكثرها إساءةً في الفهم في مجموعة Spring/JPA بأكملها.
إن فهم الانتشار يُمكّنك من استيعاب اتساق قاعدة البيانات وضغط حوض الاتصالات وسلوك التراجع الدقيق الذي يُنتج في الغالب أخطاء غامضة في الإنتاج.
كيف تُنفّذ Spring الانتشار
تلف Spring كل حبة (bean) مُصنَّفة بـ @Transactional في وكيل (proxy). عندما يستدعي مُستدعٍ طريقةً معاملاتية، يعترض الوكيل الاستدعاء ويطلب من PlatformTransactionManager توفير معاملة أو تعليقها وفق إعداد الانتشار. يُربط اتصال JDBC الفعلي بالخيط (thread) الحالي عبر TransactionSynchronizationManager. وبمجرد عودة الطريقة المُزيَّنة (أو رمي استثناء)، يُؤكّد الوكيل أو يتراجع ويُحرّر الاتصال.
A والطريقة B في نفس الحبة واستدعت A الطريقةَ B مباشرةً (this.B())، فلن يعترض وكيل Spring الاستدعاء أبدًا وسيُتجاهل إعداد الانتشار لـ B بصمت. احقن دائمًا مرجعًا لحبتك الخاصة عندما تحتاج إلى دلالات انتشار داخل الحبة.
مستويات الانتشار السبعة
تُعرّف Spring الانتشار كتعداد (enum) باسم org.springframework.transaction.annotation.Propagation. إليك جميع القيم السبع:
- REQUIRED (الافتراضي) — الانضمام إلى معاملة موجودة؛ وبدء معاملة جديدة إن لم توجد.
- REQUIRES_NEW — تعليق أي معاملة موجودة دائمًا وفتح معاملة جديدة مستقلة.
- NESTED — التشغيل داخل نقطة حفظ (savepoint) ضمن المعاملة الموجودة؛ والتراجع فقط إلى نقطة الحفظ عند الفشل، لا إلى المعاملة الخارجية بأكملها.
- SUPPORTS — الانضمام إن وُجدت معاملة؛ والتشغيل دون معاملة إن لم توجد.
- NOT_SUPPORTED — تعليق أي معاملة موجودة دائمًا والتنفيذ دونها.
- MANDATORY — يجب الانضمام إلى معاملة موجودة؛ ورمي
IllegalTransactionStateExceptionإن لم توجد. - NEVER — يجب التشغيل دون معاملة؛ ورمي استثناء إن وُجدت.
REQUIRED — حصان العمل
REQUIRED هو الافتراضي والخيار الصحيح للغالبية العظمى من طرق الخدمة. يُشارَك اتصال قاعدة بيانات واحد عبر مكدس الاستدعاء بأكمله، مما يعني أن جميع عمليات الكتابة تشترك في وحدة ذرية واحدة.
السلوك الرئيسي: إذا رمت reduceStock استثناءً غير محدد، فستُتراجع وحدة العمل بأكملها — بما في ذلك عملية save التي استُدعيت بالفعل. وهذا عادةً هو بالضبط ما تريده.
REQUIRES_NEW — معاملة مستقلة
تُخبر REQUIRES_NEW Spring بـتعليق معاملة المُستدعي، وفتح اتصال ثانٍ مستقل، وتأكيد أو التراجع عن ذلك الاتصال، ثم استئناف المعاملة الأصلية. المعاملتان جلستا قاعدة بيانات مستقلتان تمامًا.
REQUIRES_NEW، يُؤكَّد صف التدقيق بشكل مستقل، فلن تفقد أثر ما جرى محاولته.
REQUIRES_NEW بـاتصالين مفتوحَي JDBC في وقت واحد — أحدهما معلَّق والآخر نشط. مع حوض صغير (مثل الافتراضي 10 في HikariCP)، يمكن للاستخدام المتداخل العميق أو المتزامن العالي لـ REQUIRES_NEW أن يُستنفد الحوض ويُسبب جوع الخيوط (thread starvation). استخدمها بعناية وليس كافتراضي.
NESTED — تراجع جزئي قائم على نقطة الحفظ
تستخدم NESTED نقطة حفظ (savepoint) في JDBC لنحت وحدة فرعية من المعاملة الموجودة. إذا فشلت الطريقة الداخلية، يتراجع Spring فقط إلى نقطة الحفظ — لا تزال المعاملة الخارجية حية ويمكنها اختيار التأكيد أو المتابعة.
REQUIRES_NEW يكون العمل الداخلي مُؤكَّدًا بالفعل قبل انتهاء المعاملة الخارجية — لا يمكن للمعاملة الخارجية التراجع عنه. مع NESTED، يظل العمل الداخلي غير مُؤكَّد؛ فإذا تراجعت المعاملة الخارجية لاحقًا، ستُتراجع نقطة الحفظ الداخلية أيضًا.
SUPPORTS و NOT_SUPPORTED و MANDATORY و NEVER
تُستخدم هذه الأربعة بشكل أقل شيوعًا لكن لها أغراض واضحة:
- SUPPORTS — طرق مساعدة كثيفة القراءة تعمل مع أو بدون معاملة. مفيدة للاستعلامات التي تعرضها في سياقَي معاملة وغير معاملة.
- NOT_SUPPORTED — مفيدة لتصدير الدُفعات الطويلة للقراءة فقط حيث يُهدر الاحتفاظ بمعاملة مفتوحة اتصالًا. تُعلّق Spring أي معاملة مُستدعٍ وتُنفّذ الطريقة بشكل عادي.
- MANDATORY — تُطبّق أن الطريقة يجب أن تُستدعى دائمًا من داخل معاملة نشطة. رائعة كحارس في طبقات المستودعات الأدنى التي لا ينبغي استدعاؤها بدون سياق.
- NEVER — تُطبّق النظافة المعاملاتية في الطرق التي يجب ألا تلمس قاعدة البيانات بشكل معاملاتي (مثل المهام غير المتزامنة أو الخلفية التي تُطلَق عبر مُجدوِل).
اختيار الانتشار المناسب
شجرة قرار عملية:
- هل العملية جزء من نفس الوحدة الذرية للمُستدعي؟ → REQUIRED (الافتراضي).
- هل يجب أن تُؤكَّد العملية بغض النظر عما يفعله المُستدعي (مثل التدقيق والمقاييس)؟ → REQUIRES_NEW.
- هل يجب عزل الفشل الجزئي دون تحرير اتصال المُستدعي؟ → NESTED.
- هل الطريقة قراءة بحتة ولا تهتم بالمعاملات؟ → SUPPORTS أو أضف
readOnly = trueمعREQUIRED. - هل الاستدعاء بدون معاملة يشير إلى خطأ برمجي؟ → MANDATORY.
الخلاصة
انتشار المعاملات هو الآلية التي تستخدمها Spring لتنسيق الاستدعاءات المعاملاتية المتداخلة. تُشارك REQUIRED (الافتراضي) المعاملة؛ وتُنشئ REQUIRES_NEW وحدة مستقلة تُؤكَّد فورًا؛ وتُنشئ NESTED نقطة حفظ للتراجع الجزئي ضمن المعاملة الخارجية. إن فهم هذه الثلاثة يغطي غالبية سيناريوهات العالم الحقيقي. في الدرس القادم ستُضيف بُعدًا آخر — مستويات العزل — التي تتحكم في البيانات غير المُؤكَّدة التي يمكن للمعاملات المتزامنة رؤيتها.