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

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.
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) –
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
Pourquoi ne pas utiliser le proxy MySQL pour diviser les opérations de lecture et d'écriture? Quelqu'un at-il essayé cela? – nylund