| Gestion des connexions non-transactionnelles #3 |
Lundi, 28 Mars 2011 00:00
|
|
Dans un deuxième article, nous avions configuré et étudié le comportement de la solution standard. Nous avions notamment constaté que, pour les méthodes en écriture comme pour les méthodes en lecture seule, une connexion était ouverte et réquisitionnée tout le long de l'appel. Dans ce nouvel article, nous allons proposer une nouvelle solution permettant de libérer la connexion à la base de données lorsqu'elle n'est pas nécessaire lors d'un appel en lecture seule, tout en conservant le contexte transactionnel pour les méthodes d'écriture. Présentation théorique de la solutionPour les méthodes en écriture, nous allons conserver la solution proposée dans l'article précédent. En revanche, pour les méthodes en lecture seule, nous allons modifier un peu le comportement de Spring et d'Hibernate en n'ouvrant pas une connexion au niveau de la couche Façade, mais en provoquant l'ouverture uniquement lorsque cela est nécessaire. Voici le diagramme de séquence revu et corrigé :
Le principe est donc de n'avoir une ouverture que lorsque Hibernate souhaite accéder à la base de données. Pour cela, au lieu d'ouvrir un contexte transactionnel complet sur la couche Façade, nous n'allons ouvrir qu'une session Hibernate seule, sans transaction ni connexion ouverte. Configuration de SpringNaturellement, la configuration de Spring commence à s'étoffer un peu, en comparaison de la configuration, très simple, de la solution précédente : <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <import resource="applicationContext-datasource.xml"></import> <import resource="applicationContext-facades.xml"></import> <bean class="org.springframework.orm.hibernate3.LocalSessionFactoryBean" id="sessionFactory" scope="singleton"> <property name="configLocation" value="classpath:hibernate/hibernate-read-only.cfg.xml"></property> <property name="dataSource" ref="datasource"></property> </bean> <bean class="org.springframework.orm.hibernate3.HibernateTransactionManager" id="transactionManager"> <property name="sessionFactory" ref="sessionFactory"></property> </bean> <bean class="org.springframework.orm.hibernate3.HibernateInterceptor" id="sessionOpeningInterceptor"> <property name="sessionFactory" ref="sessionFactory"></property> </bean> <bean class="org.springframework.transaction.interceptor.TransactionInterceptor" id="transactionOpeningInterceptor"> <property name="transactionManager" ref="transactionManager"></property> <property name="transactionAttributeSource"> <value> com.thug.facade.CustomerFacade.*=PROPAGATION_REQUIRES_NEW,-Throwable </value> </property> </bean> <bean class="com.thug.tools.CustomRegexpMethodPointcutAdvisor" id="transactionOpeningInterceptorPointCut"> <property name="advice" ref="transactionOpeningInterceptor"></property> <property name="patterns"> <list> <value>com.thug.facade.CustomerFacade.*</value> </list> </property> <property name="excludedPatterns"> <list> <value>com.thug.facade.CustomerFacade.(get).*$</value> </list> </property> </bean> <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" id="sessionOpeningPointCut"> <property name="advice" ref="sessionOpeningInterceptor"></property> <property name="patterns"> <list> <value>com.thug.facade.CustomerFacade.(get).*$</value> </list> </property> </bean> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" id="transactionBeanNameProxyCreator"> <property name="proxyTargetClass" value="true"> <property name="beanNames"> <list> <value>customerFacade</value> </list> </property> <property name="interceptorNames"> <list> <value>transactionOpeningInterceptorPointCut</value> <value>sessionOpeningPointCut</value> </list> </property> </property></bean> </beans> Si l'on commente un peu ce qui se trouve dans ce fichier Spring :
La classe "outil" d'exclusionSi vous souhaitez pouvoir exclure facilement des méthodes (au lieu de configurer toutes les méthodes à inclure), vous pouvez utiliser cette petite classe "outil", utilisée dans la configuration Spring précédente : import org.springframework.aop.support.AbstractRegexpMethodPointcut; import org.springframework.aop.support.JdkRegexpMethodPointcut; import org.springframework.aop.support.RegexpMethodPointcutAdvisor; import org.springframework.util.Assert; import org.springframework.util.StringUtils; public class CustomRegexpMethodPointcutAdvisor extends RegexpMethodPointcutAdvisor { private static final long serialVersionUID = 1L; private S tring[] excludedPatterns = new S tring[0]; @Override protected AbstractRegexpMethodPointcut createPointcut() { AbstractRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut(); if (this.excludedPatterns != null && this.excludedPatterns.length > 0) { pointcut.setExcludedPatterns(this.excludedPatterns); } return pointcut; } public void setExcludedPattern(S tring excludedPattern) { setExcludedPatterns(new S tring[] { excludedPattern }); } public void setExcludedPatterns(S tring[] excludedPatterns) { this.excludedPatterns = new S tring[excludedPatterns.length]; for (int i = 0; i < excludedPatterns.length; i++) { this.excludedPatterns[i] = StringUtils.trimWhitespace(excludedPatterns[i]); } } public S tring[] getExcludedPatterns() { return this.excludedPatterns; } } Configuration d'HibernateSi nous nous arrêtons ici dans la configuration du projet, nous aurons le comportement suivant :
Ce dernier point n'est pas pour nous arranger, puisque nous souhaitons fermer la connexion dès que Hibernate a effectué sa requête. Pour cela, nous allons ajouter une petite classe de notre cru nous permettant de fermer la connexion quand nous le désirons. Commençons par modifier le fichier de configuration d'Hibernate en ajoutant la ligne suivante : <event type="post-load"> <listener class="com.thug.tools.ConnectionCloserPostLoadEvenListener"></listener> </event> Cette configuration a pour effet d'appeler notre classe spécifique chaque fois qu'un chargement se termine. Voici le code correspondant de la classe : public class ConnectionCloserPostLoadEvenListener extends DefaultPostLoadEventListener implements PostLoadEventListener { private static final long serialVersionUID = 1L; public void onPostLoad(PostLoadEvent event) { super.onPostLoad(event); // if a connection, which does not take part into a transaction, is opened, we close it if (!event.getSession().getJDBCContext().isTransactionInProgress()) { event.getSession().getJDBCContext().getConnectionManager().manualDisconnect(); } } } Tout en conservant le comportement standard d'Hibernate (cf. l'appel au super.onPostLoad), nous fermons la connexion ouverture si et seulement si elle ne fait pas partie d'un contexte transactionnel : ainsi dans le cas des appels de méthodes en lecture seule, nous fermons la connexion ; mais dans le cas des méthodes en écriture, nous ne faisons rien, de façon à ne pas troubler la transaction. Résultat sur la méthode getCustomerEtudions maintenant les logs de la méthode getCustomer, afin de pouvoir comparer le résultat avec la solution précédente : 09:59:56.419 [INFO ] com.thug.AbstractTestCase - First call to facade. 09:59:56.446 [INFO ] com.thug.facade.CustomerFacade - start - getCustomer Jusqu'ici, aucune connexion n'a été ouverte. Nous sommes pourtant passés à travers les couches Façade et Service. Nous arrivons maintenant dans la couche DAO : 09:59:56.446 [INFO ] com.thug.dao.CustomerDao - get customer 09:59:56.449 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection Hibernate: select customer0_.id_customer as id1_0_0_, customer0_.first_name as first2_0_0_, customer0_.last_name as last3_0_0_ from customer customer0_ where customer0_.id_customer=? 09:59:56.462 [INFO ] com.thug.tools.MyConnection - Closing connection. L'appel à la méthode DAO commence avec l'ouverture d'une connexion. L'exécution de la requête standard s'effectue dans les conditions classiques, mais est directement suivie par la fermeture de la connexion ouverte. Ainsi, pendant l'appel aux Web Services suivant, la connexion est libérée : 09:59:56.463 [INFO ] com.thug.facade.CustomerFacade - Calling Web Services... 09:59:56.463 [INFO ] com.thug.client.ProviderWs - Web Services are slow. 10:00:01.627 [INFO ] com.thug.facade.CustomerFacade - Web Services called. Nous sommes ici revenus à la couche Façade où nous devons faire appel à nos adresses lazy-loadées. Là encore, une connexion est ouverte puis fermée automatiquement, avant la fin de l'appel façade. 10:00:01.628 [INFO ] com.thug.facade.CustomerFacade - Getting lazy addresses... 10:00:01.628 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection Hibernate: select addresses0_.id_customer as id4_1_, addresses0_.id_address as id1_1_, addresses0_.id_address as id1_1_0_, addresses0_.city as city1_0_, addresses0_.country as country1_0_ from address addresses0_ where addresses0_.id_customer=? 10:00:01.632 [INFO ] com.thug.tools.MyConnection - Closing connection. 10:00:01.632 [INFO ] com.thug.facade.CustomerFacade - Lazy addresses got. 10:00:01.632 [INFO ] com.thug.facade.CustomerFacade - end (5186ms) Mission accomplie. Le deuxième appel se déroule exactement de la même façon : 10:00:01.644 [INFO ] com.thug.AbstractTestCase - Second call to facade. 10:00:01.645 [INFO ] com.thug.facade.CustomerFacade - start - getCustomer 10:00:01.645 [INFO ] com.thug.dao.CustomerDao - get customer 10:00:01.645 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection Hibernate: select customer0_.id_customer as id1_0_0_, customer0_.first_name as first2_0_0_, customer0_.last_name as last3_0_0_ from customer customer0_ where customer0_.id_customer=? 10:00:01.646 [INFO ] com.thug.tools.MyConnection - Closing connection. 10:00:01.646 [INFO ] com.thug.facade.CustomerFacade - Calling Web Services... 10:00:01.646 [INFO ] com.thug.client.ProviderWs - Web Services are slow. 10:00:06.646 [INFO ] com.thug.facade.CustomerFacade - Web Services called. 10:00:06.647 [INFO ] com.thug.facade.CustomerFacade - Getting lazy addresses... 10:00:06.647 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection Hibernate: select addresses0_.id_customer as id4_1_, addresses0_.id_address as id1_1_, addresses0_.id_address as id1_1_0_, addresses0_.city as city1_0_, addresses0_.country as country1_0_ from address addresses0_ where addresses0_.id_customer=? 10:00:06.649 [INFO ] com.thug.tools.MyConnection - Closing connection. 10:00:06.650 [INFO ] com.thug.facade.CustomerFacade - Lazy addresses got. 10:00:06.650 [INFO ] com.thug.facade.CustomerFacade - end (5005ms) Résultat sur la méthode updateCustomerEn revanche, l'appel à la méthode updateCustomer, en écriture, ne change pas de la solution précédente : l'ouverture et la fermeture de la connexion se font avant et après l'accès à la façade. Nous conservons ainsi notre contexte transactionnel, comme nous le souhaitions. 10:00:06.738 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection 10:00:06.740 [INFO ] com.thug.facade.CustomerFacade - start - updateCustomerName 10:00:06.740 [INFO ] com.thug.dao.CustomerDao - get customer Hibernate: select customer0_.id_customer as id1_0_0_, customer0_.first_name as first2_0_0_, customer0_.last_name as last3_0_0_ from customer customer0_ where customer0_.id_customer=? 10:00:06.740 [INFO ] com.thug.facade.CustomerFacade - Calling Web Services... 10:00:06.740 [INFO ] com.thug.client.ProviderWs - Web Services are slow. 10:00:11.742 [INFO ] com.thug.facade.CustomerFacade - Web Services called. 10:00:11.742 [INFO ] com.thug.facade.CustomerFacade - Getting lazy addresses... Hibernate: select addresses0_.id_customer as id4_1_, addresses0_.id_address as id1_1_, addresses0_.id_address as id1_1_0_, addresses0_.city as city1_0_, addresses0_.country as country1_0_ from address addresses0_ where addresses0_.id_customer=? 10:00:11.744 [INFO ] com.thug.facade.CustomerFacade - Lazy addresses got. 10:00:11.744 [INFO ] com.thug.facade.CustomerFacade - Setting new last name... 10:00:11.745 [INFO ] com.thug.facade.CustomerFacade - Last name set. 10:00:11.745 [INFO ] com.thug.facade.CustomerFacade - Updating customer... 10:00:11.745 [INFO ] com.thug.dao.CustomerDao - update customer 10:00:11.747 [INFO ] com.thug.facade.CustomerFacade - Customer updated. 10:00:11.747 [INFO ] com.thug.facade.CustomerFacade - end (5007ms) Hibernate: update customer set first_name=?, last_name=? where id_customer=? 10:00:11.766 [INFO ] com.thug.tools.MyConnection - Closing connection. (Avantages et) inconvénientsNous n'allons pas revenir sur les avantages de libérer la connexion pour les méthodes en lecture seule (cf. les articles précédents). En revanche, nous allons étudier les inconvénients de cette solution. Tout d'abord, elle modifie le comportement d'Hibernate, à travers la surcharge de l'évènement post load. Bien que le comportement soit celui attendu et que ce dernier soit fonctionnel, nous entrons ici dans les arcanes du framework, rendant la solution un peu moins élégante. Il faut également prendre conscience que rien ne garantit qu'un appel à une méthode en lecture seule, libérant une connexion, soit capable d'en rouvrir une autre par la suite pour terminer le process. Si votre pool de connexion est vide, il va en résulter une attente de connexion libre, attente qui va dégrader les performances du temps d'appel, voire le faire échouer complètement si elle dépasse la limite maximale autorisée. Il faut cependant relativiser : si nous mettons en place cette solution, c'est justement pour soulager le pool de connexion. Une configuration adaptée de ce dernier devrait limiter l'apparition de cet effet secondaire. Un autre effet négatif est l'ouverture et la fermeture systématiques de connexions. Si votre process est mal conçu ou que vous abusez de cette solution (voire du lazy-loading), votre application va passer son temps à ouvrir et fermer des connexions, ce qui peut éventuellement ne pas plaire à votre DBA. Cependant, là encore des solutions existent : le système de cache permet déjà de limiter l'effet yoyo (cf. ci-dessous). Vous avez également la possibilité de placer une partie de votre process, dans lequel est groupé un ensemble de chargements de données, derrière une transaction temporaire (par exemple, une conversion d'un objet BO vers son homologue de la couche façade pourrait être spécialement localisée derrière une façade de conversion). A vous de mixer la solution proposée avec ce que vous faites déjà actuellement, en fonction de votre process métier. Amélioration de la solutionNous avons déjà rempli une bonne partie du contrat que nous nous étions fixé au début de cette série d'articles : libérer les connexions ouvertes inutilement. Cependant, la solution n'est pas encore parfaite : le second appel ouvre encore des connexions, alors que les données chargées sont les mêmes que lors du premier. Nous pouvons encore économiser deux ouvertures/fermetures avec la mise en place d'un cache. Dans le prochain article, nous allons mettre en place un cache de niveau 2 et constater les effets de ce dernier sur le comportement du second appel. |
| Mise à jour le Jeudi, 31 Mars 2011 07:22 |




