2010-12-08 19 views
7

En utilisant Spring et Hibernate, je veux écrire dans une base de données master MySQL, et lire d'autres esclaves répliqués dans une application Web Java basée sur le cloud.Comment configurer Hibernate pour lire/écrire sur différentes sources de données?

Je ne trouve pas de solution transparente pour le code d'application. Je ne veux pas vraiment devoir changer mes DAO pour gérer différentes SessionFactories, car cela semble vraiment compliqué et couple le code avec une architecture de serveur spécifique.

Est-il possible de dire à Hibernate d'acheminer automatiquement les requêtes CREATE/UPDATE à une source de données, et SELECT à une autre? Je ne veux pas faire de sharding ou quoi que ce soit basé sur le type d'objet - juste acheminer différents types de requêtes à différentes sources de données.

+0

Avez-vous des requêtes UPDATE/CREATE et SELECT dans le même DAO/service? Une option pourrait être de les séparer (rendant le réglage de leurs sources de données beaucoup plus facile) –

+0

Hmm, cela semble être l'option la plus sensée que j'ai vu jusqu'ici. Je pense que je pourrais essayer si il n'y a pas d'option plus «transparente». Merci! – Deejay

+0

Pourquoi ne pas utiliser le proxy MySQL pour diviser les opérations de lecture et d'écriture? Quelqu'un at-il essayé cela? – nylund

Répondre

3

Vous pouvez créer 2 usines de session et Hava un BaseDao enveloppant les 2 usines (ou les 2 hibernateTemplates si vous les utilisez) et utiliser les méthodes get avec sur l'usine et les méthodes saveOrUpdate avec l'autre

+0

C'est une solution parfaite, mais cela provoque une tentative illégale d'associer un objet à deux sessions ouvertes. voir http://stackoverflow.com/questions/19403066/two-hibernate-transaction-manager-illegal-attempt-to-associate-proxy-with-two-o, veuillez nous aider –

1

Je ne Je pense que décider que SELECTs doit aller à un DB (un esclave) et CREATE/UPDATES devrait aller à un autre (maître) est une très bonne décision. Les raisons sont les suivantes:

  • réplication n'est pas instantanée, de sorte que vous pouvez créer quelque chose dans le DB de maître et, dans le cadre de la même opération, sélectionnez-le de l'esclave et notez que les données n'a pas encore atteint la esclave.
  • Si l'un des esclaves est en panne, vous ne devriez pas être empêché d'écrire des données dans le maître, car dès que l'esclave est de retour, son état sera synchronisé avec le maître. Dans votre cas, vos opérations d'écriture dépendent à la fois du maître et de l'esclave.
  • Comment définiriez-vous ensuite la transactionnalité si vous utilisez en fait 2 dbs?

Je vous conseille d'utiliser le DB principal pour tous les flux WRITE, avec toutes les instructions dont ils pourraient avoir besoin (que ce soit SELECTs, UPDATE ou INSERTS). Ensuite, l'application traitant des flux en lecture seule peut lire depuis le DB esclave. Je conseillerais également d'avoir des DAO séparés, chacun avec ses propres méthodes, de sorte que vous fassiez une distinction claire entre les flux en lecture seule et les flux d'écriture/mise à jour.

+0

Merci pour vos commentaires. 1) Le cache Hibernate ne gérera-t-il pas cette situation?Quand nous arrivons à l'échelle horizontale, le plan est d'utiliser EHCache avec Terracotta. 2) Je ne suis pas certain de comprendre pourquoi les opérations d'écriture dépendent de la disponibilité de l'esclave? 3) Si quelque chose est écrit et ne renvoie aucune erreur, alors c'est assez bon pour moi. – Deejay

+1

2) Je pensais que par exemple, une opération UPDATE suppose un SELECT initial puis un UPDATE sur l'opération qui a été récupérée du SELECT, ainsi vous pourriez finir par faire le SELECT initial sur l'esclave et le UPDATE sur le master, ce qui peut conduire à des difficultés à définir des transactions et à des problèmes quand la réplication est encore en cours (l'esclave ne contient pas toutes les données que le maître a). – octav

+0

1) La réplication MySQL est un processus indépendant des outils ORM, donc je ne pense pas que cela puisse aider ici. – octav

13

Un exemple peut être trouvé ici: https://github.com/afedulov/routing-data-source.

enter image description here

Spring fournit une variante de DataSource, appelée AbstractRoutingDatasource. Il peut être utilisé à la place des implémentations DataSource standard et permet à un mécanisme de déterminer la DataSource concrète à utiliser pour chaque opération au moment de l'exécution. Tout ce que vous devez faire est de l'étendre et de fournir une implémentation d'une méthode abstraite determineCurrentLookupKey. C'est l'endroit où implémenter votre logique personnalisée pour déterminer la DataSource concrète. L'objet retourné sert de clé de recherche. Il s'agit généralement d'une chaîne ou d'une énumération, utilisée comme qualificatif dans la configuration de Spring (les détails suivront).

package website.fedulov.routing.RoutingDataSource 

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 

public class RoutingDataSource extends AbstractRoutingDataSource { 
    @Override 
    protected Object determineCurrentLookupKey() { 
     return DbContextHolder.getDbType(); 
    } 
} 

Vous demandez peut-être ce qui est cet objet DbContextHolder et comment savoir quel identifiant DataSource pour revenir? Gardez à l'esprit que la méthode determineCurrentLookupKey sera appelée à chaque fois que TransactionsManager demande une connexion. Il est important de se rappeler que chaque transaction est "associée" à un fil séparé. Plus précisément, TransactionsManager lie la connexion au thread en cours.Par conséquent, afin d'envoyer différentes transactions à différentes sources de données cibles, nous devons nous assurer que chaque thread peut identifier de manière fiable quelle DataSource est destinée à être utilisée. Cela rend naturel l'utilisation de variables ThreadLocal pour lier DataSource spécifique à un thread et, par conséquent, à une transaction. Voici comment cela se fait:

public enum DbType { 
    MASTER, 
    REPLICA1, 
} 

public class DbContextHolder { 

    private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>(); 

    public static void setDbType(DbType dbType) { 
     if(dbType == null){ 
      throw new NullPointerException(); 
     } 
     contextHolder.set(dbType); 
    } 

    public static DbType getDbType() { 
     return (DbType) contextHolder.get(); 
    } 

    public static void clearDbType() { 
     contextHolder.remove(); 
    } 
} 

Comme vous le voyez, vous pouvez également utiliser un ENUM comme la clé et le printemps prendra soin de résoudre correctement en fonction du nom. Configuration DataSource associée et les touches pourraient ressembler à ceci:

.... 
<bean id="dataSource" class="website.fedulov.routing.RoutingDataSource"> 
<property name="targetDataSources"> 
    <map key-type="com.sabienzia.routing.DbType"> 
    <entry key="MASTER" value-ref="dataSourceMaster"/> 
    <entry key="REPLICA1" value-ref="dataSourceReplica"/> 
    </map> 
</property> 
<property name="defaultTargetDataSource" ref="dataSourceMaster"/> 
</bean> 

<bean id="dataSourceMaster" class="org.apache.commons.dbcp.BasicDataSource"> 
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/> 
    <property name="url" value="${db.master.url}"/> 
    <property name="username" value="${db.username}"/> 
    <property name="password" value="${db.password}"/> 
</bean> 
<bean id="dataSourceReplica" class="org.apache.commons.dbcp.BasicDataSource"> 
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/> 
    <property name="url" value="${db.replica.url}"/> 
    <property name="username" value="${db.username}"/> 
    <property name="password" value="${db.password}"/> 
</bean> 

A ce stade, vous pourriez vous retrouver à faire quelque chose comme ceci:

@Service 
public class BookService { 

    private final BookRepository bookRepository; 
    private final Mapper    mapper; 

    @Inject 
    public BookService(BookRepository bookRepository, Mapper mapper) { 
    this.bookRepository = bookRepository; 
    this.mapper = mapper; 
    } 

    @Transactional(readOnly = true) 
    public Page<BookDTO> getBooks(Pageable p) { 
    DbContextHolder.setDbType(DbType.REPLICA1); // <----- set ThreadLocal DataSource lookup key 
                // all connection from here will go to REPLICA1 
    Page<Book> booksPage = callActionRepo.findAll(p); 
    List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class); 
    DbContextHolder.clearDbType();    // <----- clear ThreadLocal setting 
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements()); 
    } 

    ...//other methods 

Maintenant, nous pouvons contrôler qui DataSource sera utilisé et les demandes avant que nous S'il vous plaît. Cela semble bon!

... Ou le fait-il? Tout d'abord, ces appels de méthodes statiques à un DbContextHolder magique tiennent vraiment le coup. Ils semblent ne pas appartenir à la logique métier. Et ils ne le font pas. Non seulement ils ne communiquent pas le but, mais ils semblent fragiles et sujets aux erreurs (que diriez-vous d'oublier de nettoyer le dbType). Et si une exception est lancée entre setDbType et cleanDbType? Nous ne pouvons pas l'ignorer. Nous devons être absolument sûrs de réinitialiser le dbType, sinon le ThreadPool retourné dans le ThreadPool pourrait être dans un état "cassé", essayant d'écrire dans une réplique lors de l'appel suivant. Nous avons donc besoin de ceci:

@Transactional(readOnly = true) 
    public Page<BookDTO> getBooks(Pageable p) { 
    try{ 
     DbContextHolder.setDbType(DbType.REPLICA1); // <----- set ThreadLocal DataSource lookup key 
                // all connection from here will go to REPLICA1 
     Page<Book> booksPage = callActionRepo.findAll(p); 
     List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class); 
     DbContextHolder.clearDbType();    // <----- clear ThreadLocal setting 
    } catch (Exception e){ 
     throw new RuntimeException(e); 
    } finally { 
     DbContextHolder.clearDbType();    // <----- make sure ThreadLocal setting is cleared   
    } 
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements()); 
    } 

Yikes ! Cela ne ressemble certainement pas à quelque chose que je voudrais mettre dans chaque méthode de lecture seule. Pouvons-nous faire mieux? Bien sûr! Ce schéma de «faire quelque chose au début d'une méthode, puis faire quelque chose à la fin» devrait sonner la cloche. Aspects à la rescousse!

Malheureusement, cet article est déjà trop long pour couvrir le sujet des aspects personnalisés. Vous pouvez suivre les détails de l'utilisation des aspects en utilisant ce link.

+1

Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et fournir le lien pour référence. Les réponses à lien uniquement peuvent devenir invalides si la page liée change. –

+0

D'accord. La réponse a été éditée. –

+0

@JeenBroekstra Si cela ne vous dérange pas, s'il vous plaît supprimer votre downvote. –

1

Essayez de cette façon: https://github.com/kwon37xi/replication-datasource

Il fonctionne très bien et très facile à mettre en œuvre sans aucune annotation supplémentaire ou un code. Il nécessite seulement @Transactional(readOnly=true|false). J'ai utilisé cette solution avec Hibernate (JPA), Spring JDBC Template, iBatis.

+0

Pouvez-vous répondre http://stackoverflow.com/questions/43247593/spring-boot-read-write-split. J'utilise la même chose mais je ne travaille pas –

0

Vous pouvez utiliser DDAL pour implémenter la base de données maître d'écriture et la base de données esclave de lecture dans une DefaultDDRDataSource sans modifier votre Daos, et plus encore, l'équilibrage de charge DDAL pour les bases de données mulit-esclaves. Il ne repose pas sur le printemps ou hiberner. Il y a un projet de démonstration pour montrer comment l'utiliser: https://github.com/hellojavaer/ddal-demos et la démo1 est juste ce que vous avez décrit la scène.