Ein Beispiel finden Sie hier:https://github.com/afedulov/routing-data- Quelle .
Spring bietet eine Variation von DataSource namens AbstractRoutingDatasource
. Es kann anstelle von Standard-DataSource-Implementierungen verwendet werden und ermöglicht einen Mechanismus, um zur Laufzeit zu bestimmen, welche konkrete DataSource für jede Operation verwendet werden soll. Alles, was Sie tun müssen, ist, es zu erweitern und eine Implementierung eines abstrakten determineCurrentLookupKey
bereitzustellen Methode. Hier können Sie Ihre benutzerdefinierte Logik implementieren, um die konkrete DataSource zu bestimmen. Das zurückgegebene Objekt dient als Suchschlüssel. Es ist normalerweise ein String oder Enum, das als Qualifizierer in der Spring-Konfiguration verwendet wird (Details folgen).
package website.fedulov.routing.RoutingDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
Sie fragen sich vielleicht, was dieses DbContextHolder-Objekt ist und woher es weiß, welche DataSource-ID zurückgegeben werden soll? Denken Sie daran, dass determineCurrentLookupKey
-Methode wird immer dann aufgerufen, wenn TransactionsManager eine Verbindung anfordert. Es ist wichtig, sich daran zu erinnern, dass jede Transaktion einem separaten Thread "zugeordnet" ist. Genauer gesagt bindet TransactionsManager Connection an den aktuellen Thread. Um verschiedene Transaktionen an verschiedene Ziel-DataSources zu senden, müssen wir daher sicherstellen, dass jeder Thread zuverlässig identifizieren kann, welche DataSource für seine Verwendung bestimmt ist. Dies macht es natürlich, ThreadLocal-Variablen zum Binden einer bestimmten Datenquelle an einen Thread und damit an eine Transaktion zu verwenden. So wird es gemacht:
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();
}
}
Wie Sie sehen, können Sie auch eine Aufzählung als Schlüssel verwenden, und Spring kümmert sich um die korrekte Auflösung basierend auf dem Namen. Die zugehörige DataSource-Konfiguration und Schlüssel könnten wie folgt aussehen:
....
<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>
An diesem Punkt könnten Sie feststellen, dass Sie so etwas tun:
@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
Jetzt können wir steuern, welche Datenquelle verwendet wird, und Anfragen nach Belieben weiterleiten. Sieht gut aus!
... Oder doch? Zunächst einmal fallen diese statischen Methodenaufrufe an einen magischen DbContextHolder wirklich auf. Sie sehen aus, als würden sie nicht zur Geschäftslogik gehören. Und das tun sie nicht. Sie kommunizieren nicht nur nicht den Zweck, sondern wirken auch zerbrechlich und fehleranfällig (wie wäre es, wenn Sie vergessen, den dbType zu bereinigen). Und was, wenn eine Ausnahme zwischen setDbType und cleanDbType ausgelöst wird? Wir können es nicht einfach ignorieren. Wir müssen absolut sicher sein, dass wir den dbType zurücksetzen, andernfalls befindet sich der Thread, der an den ThreadPool zurückgegeben wird, möglicherweise in einem „defekten“ Zustand und versucht, beim nächsten Aufruf in ein Replikat zu schreiben. Also brauchen wir das:
@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());
}
Huch >_<
! Dies sieht definitiv nicht nach etwas aus, das ich in jede Nur-Lese-Methode einbauen möchte. Können wir es besser machen? Natürlich! Dieses Muster von „tue etwas am Anfang einer Methode, dann tue etwas am Ende“ sollte eine Glocke läuten. Aspekte zur Rettung!
Leider ist dieser Beitrag schon zu lang geworden, um das Thema Custom-Aspekte zu behandeln. Sie können die Details zur Verwendung von Aspekten mit diesem nachverfolgen verlinken .