Пример можно найти здесь: https://github.com/afedulov/routing-data-source.
Spring предоставляет вариант DataSource, называемый AbstractRoutingDatasource
.Он может использоваться вместо стандартных реализаций DataSource и позволяет механизму определять, какой конкретный DataSource использовать для каждой операции во время выполнения.Все, что вам нужно сделать, это расширить его и обеспечить реализацию абстрактного determineCurrentLookupKey
метода.Это место для реализации вашей пользовательской логики для определения конкретного источника данных.Возвращенный объект служит ключом поиска.Обычно это String или en Enum, используемый в качестве квалификатора в конфигурации Spring (подробности будут приведены ниже).
package website.fedulov.routing.RoutingDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
Вам может быть интересно, что это за объект DbContextHolder и как он узнает, какой идентификатор DataSource возвращать?Имейте в виду, что метод determineCurrentLookupKey
будет вызываться всякий раз, когда TransactionsManager запрашивает соединение.Важно помнить, что каждая транзакция «связана» с отдельным потоком.Точнее, TransactionsManager связывает Connection с текущим потоком.Поэтому, чтобы отправлять разные транзакции в разные целевые источники данных, мы должны убедиться, что каждый поток может надежно определить, какой источник данных предназначен для его использования.Это делает естественным использование переменных ThreadLocal для привязки конкретного источника данных к потоку и, следовательно, к транзакции.Вот как это делается:
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();
}
}
Как видите, вы также можете использовать enum в качестве ключа, и Spring позаботится о его правильном разрешении на основе имени.Связанная конфигурация источника данных и ключи могут выглядеть следующим образом:
....
<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>
В этот момент вы можете обнаружить, что делаете что-то вроде этого:
@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
Теперь мы можем контролировать, какой источник данных будет использоваться ижду запросов как нам угодно.Выглядит хорошо!
... Или так?Прежде всего, эти статические вызовы методов магического DbContextHolder действительно торчат.Похоже, они не принадлежат бизнес-логике.И они не делают.Они не только не сообщают о цели, но и кажутся хрупкими и подверженными ошибкам (как насчет того, чтобы забыть очистить dbType).А что, если между setDbType и cleanDbType возникает исключение?Мы не можем просто игнорировать это.Мы должны быть абсолютно уверены в том, что мы сбрасываем dbType, в противном случае поток, возвращаемый в ThreadPool, может находиться в «неисправном» состоянии, пытаясь записать реплику при следующем вызове.Итак, нам нужно это:
@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 >_<
!Это определенно не похоже на то, что я хотел бы добавить в каждый метод только для чтения.Можем ли мы сделать лучше?Конечно!Этот паттерн «сделай что-нибудь в начале метода, затем сделай что-нибудь в конце» должен звучать как колокол.Аспекты на помощь!
К сожалению, этот пост уже слишком длинный, чтобы охватить тему пользовательских аспектов.Вы можете следить за деталями использования аспектов, используя эту ссылку .