Пролетно маршрутизиране на транзакции
Първо, ще създадем DataSourceType
Java Enum, който дефинира нашите опции за маршрутизиране на транзакции:
public enum DataSourceType {
READ_WRITE,
READ_ONLY
}
За да насочим транзакциите за четене-запис към основния възел и транзакциите само за четене към възела Replica, можем да дефинираме ReadWriteDataSource
който се свързва с първичния възел и ReadOnlyDataSource
които се свързват с възела Replica.
Маршрутизирането на транзакциите за четене-запис и само за четене се извършва от Spring AbstractRoutingDataSource
абстракция, която се реализира от TransactionRoutingDatasource
, както е илюстрирано от следната диаграма:
TransactionRoutingDataSource
е много лесен за изпълнение и изглежда по следния начин:
public class TransactionRoutingDataSource
extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager
.isCurrentTransactionReadOnly() ?
DataSourceType.READ_ONLY :
DataSourceType.READ_WRITE;
}
}
По принцип проверяваме Spring TransactionSynchronizationManager
клас, който съхранява текущия транзакционен контекст, за да провери дали текущо изпълняваната Spring транзакция е само за четене или не.
determineCurrentLookupKey
методът връща стойността на дискриминатора, която ще се използва за избор на четене-запис или само за четене JDBC DataSource
.
Пролетна конфигурация на JDBC DataSource за четене-запис и само за четене
DataSource
конфигурацията изглежда по следния начин:
@Configuration
@ComponentScan(
basePackages = "com.vladmihalcea.book.hpjp.util.spring.routing"
)
@PropertySource(
"/META-INF/jdbc-postgresql-replication.properties"
)
public class TransactionRoutingConfiguration
extends AbstractJPAConfiguration {
@Value("${jdbc.url.primary}")
private String primaryUrl;
@Value("${jdbc.url.replica}")
private String replicaUrl;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource readWriteDataSource() {
PGSimpleDataSource dataSource = new PGSimpleDataSource();
dataSource.setURL(primaryUrl);
dataSource.setUser(username);
dataSource.setPassword(password);
return connectionPoolDataSource(dataSource);
}
@Bean
public DataSource readOnlyDataSource() {
PGSimpleDataSource dataSource = new PGSimpleDataSource();
dataSource.setURL(replicaUrl);
dataSource.setUser(username);
dataSource.setPassword(password);
return connectionPoolDataSource(dataSource);
}
@Bean
public TransactionRoutingDataSource actualDataSource() {
TransactionRoutingDataSource routingDataSource =
new TransactionRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(
DataSourceType.READ_WRITE,
readWriteDataSource()
);
dataSourceMap.put(
DataSourceType.READ_ONLY,
readOnlyDataSource()
);
routingDataSource.setTargetDataSources(dataSourceMap);
return routingDataSource;
}
@Override
protected Properties additionalProperties() {
Properties properties = super.additionalProperties();
properties.setProperty(
"hibernate.connection.provider_disables_autocommit",
Boolean.TRUE.toString()
);
return properties;
}
@Override
protected String[] packagesToScan() {
return new String[]{
"com.vladmihalcea.book.hpjp.hibernate.transaction.forum"
};
}
@Override
protected String databaseType() {
return Database.POSTGRESQL.name().toLowerCase();
}
protected HikariConfig hikariConfig(
DataSource dataSource) {
HikariConfig hikariConfig = new HikariConfig();
int cpuCores = Runtime.getRuntime().availableProcessors();
hikariConfig.setMaximumPoolSize(cpuCores * 4);
hikariConfig.setDataSource(dataSource);
hikariConfig.setAutoCommit(false);
return hikariConfig;
}
protected HikariDataSource connectionPoolDataSource(
DataSource dataSource) {
return new HikariDataSource(hikariConfig(dataSource));
}
}
/META-INF/jdbc-postgresql-replication.properties
ресурсния файл предоставя конфигурацията за JDBC DataSource
за четене-запис и само за четене компоненти:
hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect
jdbc.url.primary=jdbc:postgresql://localhost:5432/high_performance_java_persistence
jdbc.url.replica=jdbc:postgresql://localhost:5432/high_performance_java_persistence_replica
jdbc.username=postgres
jdbc.password=admin
jdbc.url.primary
свойството дефинира URL адреса на основния възел, докато jdbc.url.replica
дефинира URL адреса на възела Replica.
readWriteDataSource
Компонентът Spring дефинира JDBC DataSource
за четене и запис докато readOnlyDataSource
компонент дефинира JDBC DataSource
само за четене .
Имайте предвид, че както източниците на данни за четене-запис, така и само за четене използват HikariCP за обединяване на връзки.
actualDataSource
действа като фасада за източниците на данни за четене-запис и само за четене и се реализира с помощта на TransactionRoutingDataSource
полезност.
readWriteDataSource
се регистрира с помощта на DataSourceType.READ_WRITE
ключ и readOnlyDataSource
използвайки DataSourceType.READ_ONLY
ключ.
Така че, когато изпълнявате четене-запис @Transactional
метод, readWriteDataSource
ще се използва, докато се изпълнява @Transactional(readOnly = true)
метод, readOnlyDataSource
вместо това ще се използва.
Имайте предвид, че
additionalProperties
методът дефинираhibernate.connection.provider_disables_autocommit
Свойство Hibernate, което добавих към Hibernate, за да отложа придобиването на база данни за RESOURCE_LOCAL JPA транзакции.Не само, че
hibernate.connection.provider_disables_autocommit
ви позволява да използвате по-добре връзките към базата данни, но това е единственият начин, по който можем да накараме този пример да работи, тъй като без тази конфигурация връзката се придобива преди извикване наdetermineCurrentLookupKey
методTransactionRoutingDataSource
.
Останалите компоненти на Spring, необходими за изграждане на JPA EntityManagerFactory
се дефинират от AbstractJPAConfiguration
базов клас.
По принцип actualDataSource
допълнително се обвива от DataSource-Proxy и се предоставя на JPA EntityManagerFactory
. Можете да проверите изходния код на GitHub за повече подробности.
Време за тестване
За да проверим дали маршрутизирането на транзакциите работи, ще активираме дневника на заявките на PostgreSQL, като зададем следните свойства в postgresql.conf
конфигурационен файл:
log_min_duration_statement = 0
log_line_prefix = '[%d] '
log_min_duration_statement
настройката на свойството е за регистриране на всички PostgreSQL изрази, докато вторият добавя името на базата данни към SQL дневника.
И така, при извикване на newPost
и findAllPostsByTitle
методи, като този:
Post post = forumService.newPost(
"High-Performance Java Persistence",
"JDBC", "JPA", "Hibernate"
);
List<Post> posts = forumService.findAllPostsByTitle(
"High-Performance Java Persistence"
);
Можем да видим, че PostgreSQL регистрира следните съобщения:
[high_performance_java_persistence] LOG: execute <unnamed>:
BEGIN
[high_performance_java_persistence] DETAIL:
parameters: $1 = 'JDBC', $2 = 'JPA', $3 = 'Hibernate'
[high_performance_java_persistence] LOG: execute <unnamed>:
select tag0_.id as id1_4_, tag0_.name as name2_4_
from tag tag0_ where tag0_.name in ($1 , $2 , $3)
[high_performance_java_persistence] LOG: execute <unnamed>:
select nextval ('hibernate_sequence')
[high_performance_java_persistence] DETAIL:
parameters: $1 = 'High-Performance Java Persistence', $2 = '4'
[high_performance_java_persistence] LOG: execute <unnamed>:
insert into post (title, id) values ($1, $2)
[high_performance_java_persistence] DETAIL:
parameters: $1 = '4', $2 = '1'
[high_performance_java_persistence] LOG: execute <unnamed>:
insert into post_tag (post_id, tag_id) values ($1, $2)
[high_performance_java_persistence] DETAIL:
parameters: $1 = '4', $2 = '2'
[high_performance_java_persistence] LOG: execute <unnamed>:
insert into post_tag (post_id, tag_id) values ($1, $2)
[high_performance_java_persistence] DETAIL:
parameters: $1 = '4', $2 = '3'
[high_performance_java_persistence] LOG: execute <unnamed>:
insert into post_tag (post_id, tag_id) values ($1, $2)
[high_performance_java_persistence] LOG: execute S_3:
COMMIT
[high_performance_java_persistence_replica] LOG: execute <unnamed>:
BEGIN
[high_performance_java_persistence_replica] DETAIL:
parameters: $1 = 'High-Performance Java Persistence'
[high_performance_java_persistence_replica] LOG: execute <unnamed>:
select post0_.id as id1_0_, post0_.title as title2_0_
from post post0_ where post0_.title=$1
[high_performance_java_persistence_replica] LOG: execute S_1:
COMMIT
Регистрационните оператори, използващи high_performance_java_persistence
префиксът се изпълняваше на първичния възел, докато тези, използващи high_performance_java_persistence_replica
на възела Replica.
Така че всичко работи като чар!
Целият изходен код може да бъде намерен в моето високопроизводително хранилище на Java Persistence GitHub, така че можете да го изпробвате и вие.
Заключение
Трябва да сте сигурни, че сте задали правилния размер за вашите пулове за връзки, защото това може да направи огромна разлика. За това препоръчвам да използвате Flexy Pool.
Трябва да сте много усърдни и да се уверите, че маркирате съответно всички транзакции само за четене. Необичайно е, че само 10% от вашите транзакции са само за четене. Възможно ли е да имате такова приложение за най-много записване или да използвате транзакции за запис, при които издавате само изрази за заявка?
За пакетна обработка определено се нуждаете от транзакции за четене и запис, така че не забравяйте да активирате пакетирането на JDBC, като това:
<property name="hibernate.order_updates" value="true"/>
<property name="hibernate.order_inserts" value="true"/>
<property name="hibernate.jdbc.batch_size" value="25"/>
За пакетиране можете също да използвате отделен DataSource
който използва различен пул за връзки, който се свързва с основния възел.
Просто се уверете, че общият ви размер на връзката на всички пулове за връзки е по-малък от броя на връзките, с които е конфигуриран PostgreSQL.
Всяко групово задание трябва да използва специална транзакция, така че се уверете, че използвате разумен размер на партидата.
Нещо повече, искате да задържите заключванията и да завършите транзакциите възможно най-бързо. Ако пакетният процесор използва едновременно обработващи работници, уверете се, че размерът на асоциирания пул за връзки е равен на броя работници, така че те да не чакат другите да освободят връзките.