إدارة الاتصالات والتجميع
إدارة الاتصالات والتجميع
في تطبيق ويب، تُمثّل اتصالات قاعدة البيانات موردًا مشتركًا ومحدودًا. كل طلب HTTP يحتاج إلى قاعدة البيانات يجب أن يستعير اتصالًا، وينجز عمله، ثم يُعيده فورًا. إن أخطأت في هذا — سواء بتسريب الاتصالات أو بفتح عدد كبير جدًا منها — سيتوقف تطبيقك عند أي حمل معتدل. يُرشدك هذا الدرس إلى الأنماط الاحترافية التي تمنع كلا الفشلَين.
لماذا فتح اتصال جديد في كل مرة يُشكّل مشكلة
إنشاء اتصال TCP بخادم قاعدة البيانات يستلزم البحث في DNS، والمصافحة الثلاثية TCP، والمصادقة عبر المشغّل، وإعداد الجلسة على جانب الخادم. على شبكة محلية يستغرق هذا عادةً من 20 إلى 100 مللي ثانية؛ وعلى شبكة سحابية يمكن أن يتجاوز 200 مللي ثانية بسهولة. لنقطة وصول ويب يجب أن تستجيب في أقل من 500 مللي ثانية، فإن إنفاق ثلث الميزانية على إنشاء الاتصال أمر غير مقبول.
علاوةً على زمن الاستجابة، تفرض خوادم قواعد البيانات حدًا صارمًا على الجلسات المتزامنة (MySQL يتيح 151 افتراضيًا؛ PostgreSQL يتيح 100). التطبيق القائم على DriverManager الذي يفتح اتصالًا لكل طلب سيُشبع هذا الحد تحت تزامن متواضع، ويرمي SQLNonTransientConnectionException للجميع.
واجهة DataSource
تُعدّ javax.sql.DataSource التجريد المعياري في JDBC لمصنع الاتصالات. عقدها بسيط:
جميع مكتبات التجميع (HikariCP، Apache DBCP2، c3p0، Tomcat JDBC Pool) تُنفّذ هذه الواجهة. كودك لا يستدعي إلا dataSource.getConnection() — ولا يعرف ولا يكترث بما إذا جاء الـ Connection المُعاد من تجمّع أم من مقبس جديد أم من صنّارة اختبار. هذا الفصل هو ما يجعل التصميم قابلًا للاختبار وقابلًا للنقل.
HikariCP — التجمّع المعياري في الصناعة
HikariCP هو التجمّع الافتراضي في Spring Boot منذ الإصدار 2.0، ويُعدّ على نطاق واسع الأسرع والأكثر موثوقية. أضفه إلى مشروعك بـ Maven:
أنشئ كائن DataSource وحيدًا أثناء بدء التطبيق — في تطبيق قائم على Servlet، يُعدّ ServletContextListener النقطة الصحيحة:
يستطيع أي Servlet أو DAO استرداد DataSource المشترك من ServletContext:
conn.close() على اتصال مُجمَّع، لا يُغلق HikariCP المقبس الأساسي — بل يُعيد تعيين حالة الجلسة ويُعيد المقبض المنطقي إلى التجمّع. نسيان استدعائها ضار مثلما يضر التسريب الحقيقي تمامًا: تظل الفتحة محجوزة إلى الأبد ويعلق التطبيق في انتظار اتصال لن يعود.
الضبط الصحيح لحجم التجمّع
خطأ شائع هو تعيين maximumPoolSize بقيمة مرتفعة جدًا بحجة أن "المزيد من الاتصالات = إنتاجية أعلى". العكس عادةً صحيح بمجرد تجاوز التزامن الأمثل لخادم قاعدة البيانات. يوصي فريق HikariCP وتوثيق PostgreSQL بالبدء بـ:
في بيئة الخدمات المصغّرة حيث تتشارك نسخ تطبيقات متعددة قاعدة بيانات واحدة، يجب أن يظل مجموع عدد الاتصالات عبر جميع النسخ ضمن max_connections في الخادم. عشر نسخ لكل منها تجمّع من 10 = 100 اتصال — وهذا بالضبط السقف الافتراضي لـ PostgreSQL. احسب ذلك قبل رفع أحجام التجمّعات.
JNDI — DataSource يديره الحاوي
في بيئة خادم تطبيقات (WildFly، GlassFish، Payara، Tomcat في الوضع المؤسسي) غالبًا لا تُنشئ التجمّع في الكود أصلًا. بدلًا من ذلك، يُهيّئ مسؤول الخادم التجمّع في وحدة تحكم إدارة الخادم ويُسجّله باسم في JNDI (Java Naming and Directory Interface). يبحث تطبيقك عنه:
يجب كذلك الإعلان عن مرجع مورد JNDI في web.xml (أو استخدام الحقن @Resource في مكوّن Jakarta EE):
مع CDI أو EJB يمكنك حقن DataSource مباشرةً وهو أكثر نظافةً:
ما يجري داخل التجمّع
فهم دورة الحياة يساعدك في تشخيص المشاكل:
- الاستعارة: يستدعي كودك
ds.getConnection(). يختار HikariCP اتصالًا خاملًا من التجمّع. إن لم يتوفر أي اتصال والتجمّع لم يبلغmaximumPoolSize، يفتح اتصالًا جديدًا. إن امتلأ التجمّع، ينتظر حتىconnectionTimeoutمللي ثانية ثم يرميSQLTransientConnectionException. - الاستخدام: تُنفّذ SQL على الاتصال المُستعار. إن تسبّب استثناء في تخطي
close()، ستكتشف خيط الصيانة في التجمّع التسريب في نهاية المطاف (إن كانleakDetectionThresholdمُهيَّأً) وتُسجّل تحذيرًا. - الإعادة: يُعيد
conn.close()تعيين autoCommit، ويُصفّي التحذيرات، ويُراجع أي معاملة غير مُلتزمة، ويُعلّم الاتصال متاحًا. - التحقق: قبل تسليم اتصال للمستدعي التالي، يُجري HikariCP تحققًا سريعًا (
isValid()أوconnectionTestQuery) لاكتشاف الاتصالات التي أغلقها خادم قاعدة البيانات بسببwait_timeoutأو اضطراب الشبكة. - الطرد: الاتصالات الخاملة لفترة أطول من
idleTimeout، أو الحية أطول منmaxLifetime، تُغلق بصمت وتُستبدل — للحفاظ على التجمّع منتعشًا.
autoCommit وأعدت اتصالًا إلى التجمّع دون استدعاء commit() أو rollback()، سيُجري HikariCP تراجعًا عند الإعادة — لكن المستعير التالي قد يواجه حالة غير متسقة في أقفال الصفوف المحتجزة حتى ذلك التراجع، مما يُسبّب أوقات انتظار غامضة. استخدم try-with-resources أو كتلة finally تستدعي دائمًا rollback() عند الخطأ وcommit() عند النجاح.
تمكين مقاييس HikariCP (اختياري لكن مفيد)
يكشف HikariCP إحصاءات التجمّع عبر JMX وMicrometer من الصندوق. في تطبيق Spring Boot تحصل على مقاييس التجمّع في Actuator (/actuator/metrics/hikaricp.connections) مجانًا. في تطبيق Servlet مستقل يمكنك الاستعلام برمجيًا:
راقب threadsAwaitingConnection في بيئة الإنتاج. القيم المستمرة فوق الصفر تعني أن تجمّعك صغير جدًا أو استعلاماتك تستغرق وقتًا طويلًا — وكلاهما إشارة قابلة للتصرف.
الخلاصة
تجميع الاتصالات عبر DataSource ليس تحسينًا تضيفه لاحقًا — بل هو متطلب صحة لأي تطبيق ويب. استخدم HikariCP (أو تجمّعًا يديره JNDI في خادم تطبيقات متكامل)، اضبط حجم التجمّع تجريبيًا، أعِد الاتصالات دائمًا بسرعة عبر try-with-resources، وراقب مقاييس التجمّع كي تكتشف الإشباع قبل أن يتحوّل إلى انقطاع في الخدمة. في الدرس القادم ستضع DataSource مُجمَّعًا حقيقيًا في الاستخدام لتنفيذ جمل CRUD فعلية باستخدام PreparedStatement.