From 5b1e4633dc449880811594faa4f039b2db6a7d01 Mon Sep 17 00:00:00 2001 From: Raja Kolli Date: Sat, 2 Nov 2024 14:11:24 +0000 Subject: [PATCH 1/3] feat : use better way to delegate database --- jpa/boot-read-replica-postgresql/pom.xml | 6 +++- .../readreplica/config/DatabaseConfig.java | 20 ++++------- .../config/aop/ReadOnlyRouteInterceptor.java | 35 ------------------- .../config/routing/RoutingDataSource.java | 26 -------------- .../src/main/resources/application.yml | 25 ++++++++++--- .../ReadReplicaApplicationTests.java | 26 ++++++++------ 6 files changed, 48 insertions(+), 90 deletions(-) delete mode 100644 jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/aop/ReadOnlyRouteInterceptor.java delete mode 100644 jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/routing/RoutingDataSource.java diff --git a/jpa/boot-read-replica-postgresql/pom.xml b/jpa/boot-read-replica-postgresql/pom.xml index 9fd7d1786..6b8ae2b23 100644 --- a/jpa/boot-read-replica-postgresql/pom.xml +++ b/jpa/boot-read-replica-postgresql/pom.xml @@ -50,12 +50,16 @@ org.liquibase liquibase-core - org.postgresql postgresql runtime + + net.ttddyy.observation + datasource-micrometer-spring-boot + 1.0.5 + org.springframework.boot diff --git a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java b/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java index 2b69b19af..038437b99 100644 --- a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java +++ b/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java @@ -1,15 +1,13 @@ package com.example.demo.readreplica.config; -import com.example.demo.readreplica.config.routing.RoutingDataSource; import com.zaxxer.hikari.HikariDataSource; -import java.util.HashMap; -import java.util.Map; import javax.sql.DataSource; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; @Configuration(proxyBeanMethods = false) class DatabaseConfig { @@ -39,7 +37,7 @@ DataSourceProperties replicaDataSourceProperties() { } @Bean - @ConfigurationProperties(REPLICA_DATABASE_PROPERTY_KEY_PREFIX + ".configuration") + @ConfigurationProperties(REPLICA_DATABASE_PROPERTY_KEY_PREFIX + ".hikari") DataSource replicaDataSource(final DataSourceProperties replicaDataSourceProperties) { return replicaDataSourceProperties .initializeDataSourceBuilder() @@ -50,15 +48,9 @@ DataSource replicaDataSource(final DataSourceProperties replicaDataSourcePropert @Bean @Primary DataSource dataSource(final DataSource primaryDataSource, final DataSource replicaDataSource) { - final RoutingDataSource routingDataSource = new RoutingDataSource(); - - final Map targetDataSources = new HashMap<>(); - targetDataSources.put(RoutingDataSource.Route.PRIMARY, primaryDataSource); - targetDataSources.put(RoutingDataSource.Route.REPLICA, replicaDataSource); - - routingDataSource.setTargetDataSources(targetDataSources); - routingDataSource.setDefaultTargetDataSource(primaryDataSource); - - return routingDataSource; + LazyConnectionDataSourceProxy lazyConnectionDataSourceProxy = + new LazyConnectionDataSourceProxy(primaryDataSource); + lazyConnectionDataSourceProxy.setReadOnlyDataSource(replicaDataSource); + return lazyConnectionDataSourceProxy; } } diff --git a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/aop/ReadOnlyRouteInterceptor.java b/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/aop/ReadOnlyRouteInterceptor.java deleted file mode 100644 index 4b8049235..000000000 --- a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/aop/ReadOnlyRouteInterceptor.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.demo.readreplica.config.aop; - -import com.example.demo.readreplica.config.routing.RoutingDataSource; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Aspect -@Component -@Order(0) -class ReadOnlyRouteInterceptor { - - private static final Logger LOGGER = LoggerFactory.getLogger(ReadOnlyRouteInterceptor.class); - - @Around("@annotation(transactional)") - public Object proceed(ProceedingJoinPoint proceedingJoinPoint, Transactional transactional) - throws Throwable { - try { - if (transactional.readOnly()) { - RoutingDataSource.setReplicaRoute(); - LOGGER.info("Routing database call to the read replica"); - } else { - LOGGER.info("Routing database call to the writer"); - } - return proceedingJoinPoint.proceed(); - } finally { - RoutingDataSource.clearReplicaRoute(); - } - } -} diff --git a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/routing/RoutingDataSource.java b/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/routing/RoutingDataSource.java deleted file mode 100644 index 577340c9b..000000000 --- a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/routing/RoutingDataSource.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.demo.readreplica.config.routing; - -import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; - -public class RoutingDataSource extends AbstractRoutingDataSource { - - private static final ThreadLocal routeContext = new ThreadLocal<>(); - - public enum Route { - PRIMARY, - REPLICA - } - - public static void clearReplicaRoute() { - routeContext.remove(); - } - - public static void setReplicaRoute() { - routeContext.set(Route.REPLICA); - } - - @Override - protected Object determineCurrentLookupKey() { - return routeContext.get(); - } -} diff --git a/jpa/boot-read-replica-postgresql/src/main/resources/application.yml b/jpa/boot-read-replica-postgresql/src/main/resources/application.yml index 1de754fe5..cc10ed9d7 100644 --- a/jpa/boot-read-replica-postgresql/src/main/resources/application.yml +++ b/jpa/boot-read-replica-postgresql/src/main/resources/application.yml @@ -4,17 +4,19 @@ spring: datasource: password: postgres_write username: postgres_write - driver: org.postgresql.Driver + driverClassName: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/my_database configuration: + auto-commit: false poolName: primaryHikariPool replica: datasource: password: repl_password username: repl_user - driver: org.postgresql.Driver + driverClassName: org.postgresql.Driver url: jdbc:postgresql://localhost:15432/my_database - configuration: + hikari: + auto-commit: false poolName: replicaHikariPool jpa: @@ -23,4 +25,19 @@ spring: ddl-auto: validate threads: virtual: - enabled: true \ No newline at end of file + enabled: true + +# spring boot log level property +logging: + level: + read-replica-logger : DEBUG +jdbc: + excludedDataSourceBeanNames: dataSource + datasource-proxy: + multiline: false + logging: slf4j + query: + logger-name: read-replica-logger + log-level: DEBUG + enable-logging: true + enabled: true \ No newline at end of file diff --git a/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java b/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java index 3569fed33..4f9676f1a 100644 --- a/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java +++ b/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java @@ -4,11 +4,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; -import com.example.demo.readreplica.config.routing.RoutingDataSource; import java.time.LocalDateTime; -import java.util.Map; import java.util.concurrent.TimeUnit; import javax.sql.DataSource; +import net.ttddyy.dsproxy.support.ProxyDataSource; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -19,13 +18,15 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.test.util.ReflectionTestUtils; @SpringBootTest @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ReadReplicaApplicationTests { - @Autowired private RoutingDataSource routingDataSource; + @Autowired private DataSource dataSource; private JdbcTemplate primaryJdbcTemplate; @@ -38,13 +39,18 @@ class ReadReplicaApplicationTests { @BeforeAll void setUp() { - assertThat(routingDataSource).isNotNull(); - Map resolvedDataSources = routingDataSource.getResolvedDataSources(); - assertThat(resolvedDataSources).isNotEmpty().hasSize(2); - primaryJdbcTemplate = - new JdbcTemplate(resolvedDataSources.get(RoutingDataSource.Route.PRIMARY)); - replicaJdbcTemplate = - new JdbcTemplate(resolvedDataSources.get(RoutingDataSource.Route.REPLICA)); + assertThat(dataSource).isNotNull().isInstanceOf(LazyConnectionDataSourceProxy.class); + LazyConnectionDataSourceProxy lazyConnectionDataSourceProxy = + (LazyConnectionDataSourceProxy) dataSource; + DataSource targetDataSource = lazyConnectionDataSourceProxy.getTargetDataSource(); + assertThat(targetDataSource).isNotNull().isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) targetDataSource; + primaryJdbcTemplate = new JdbcTemplate(proxyDataSource.getDataSource()); + Object readOnlyDataSource = + ReflectionTestUtils.getField(lazyConnectionDataSourceProxy, "readOnlyDataSource"); + assertThat(readOnlyDataSource).isNotNull().isInstanceOf(ProxyDataSource.class); + proxyDataSource = (ProxyDataSource) readOnlyDataSource; + replicaJdbcTemplate = new JdbcTemplate(proxyDataSource.getDataSource()); } @Test From 9f200242c4bda0e254c8514ea73d148b3f59d5c0 Mon Sep 17 00:00:00 2001 From: Raja Kolli Date: Sat, 2 Nov 2024 15:07:40 +0000 Subject: [PATCH 2/3] fix : Issues with Junit --- .../docker-compose.yaml | 2 -- jpa/boot-read-replica-postgresql/pom.xml | 4 +-- .../readreplica/config/DatabaseConfig.java | 2 +- .../src/main/resources/application.yml | 34 +++++++++++++++---- .../ReadReplicaApplicationTests.java | 2 ++ .../resources/application-test.properties | 2 ++ 6 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 jpa/boot-read-replica-postgresql/src/test/resources/application-test.properties diff --git a/jpa/boot-read-replica-postgresql/docker-compose.yaml b/jpa/boot-read-replica-postgresql/docker-compose.yaml index 3f77dcb03..3e7644958 100644 --- a/jpa/boot-read-replica-postgresql/docker-compose.yaml +++ b/jpa/boot-read-replica-postgresql/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '2' - services: postgresql-master: image: 'bitnami/postgresql:latest' diff --git a/jpa/boot-read-replica-postgresql/pom.xml b/jpa/boot-read-replica-postgresql/pom.xml index 6b8ae2b23..4e8019971 100644 --- a/jpa/boot-read-replica-postgresql/pom.xml +++ b/jpa/boot-read-replica-postgresql/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.5 + 3.4.0-RC1 com.example.demo @@ -109,7 +109,7 @@ - 1.22.0 + 1.24.0 diff --git a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java b/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java index 038437b99..862496cc2 100644 --- a/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java +++ b/jpa/boot-read-replica-postgresql/src/main/java/com/example/demo/readreplica/config/DatabaseConfig.java @@ -22,7 +22,7 @@ DataSourceProperties primaryDataSourceProperties() { } @Bean - @ConfigurationProperties(PRIMARY_DATABASE_PROPERTY_KEY_PREFIX + ".configuration") + @ConfigurationProperties(PRIMARY_DATABASE_PROPERTY_KEY_PREFIX + ".hikari") DataSource primaryDataSource(final DataSourceProperties primaryDataSourceProperties) { return primaryDataSourceProperties .initializeDataSourceBuilder() diff --git a/jpa/boot-read-replica-postgresql/src/main/resources/application.yml b/jpa/boot-read-replica-postgresql/src/main/resources/application.yml index cc10ed9d7..90cbe6ec0 100644 --- a/jpa/boot-read-replica-postgresql/src/main/resources/application.yml +++ b/jpa/boot-read-replica-postgresql/src/main/resources/application.yml @@ -6,9 +6,11 @@ spring: username: postgres_write driverClassName: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/my_database - configuration: + hikari: auto-commit: false - poolName: primaryHikariPool + pool-name: primaryHikariPool + data-source-properties: + ApplicationName: ${spring.application.name} replica: datasource: password: repl_password @@ -18,11 +20,31 @@ spring: hikari: auto-commit: false poolName: replicaHikariPool - +################ Database ##################### + data: + jpa: + repositories: + bootstrap-mode: deferred jpa: open-in-view: false + show-sql: false hibernate: ddl-auto: validate + properties: + hibernate: + connection: + provider_disables_autocommit: true + jdbc: + time_zone: UTC + batch_size: 25 + lob.non_contextual_creation : true + generate_statistics: false + order_inserts: true + order_updates: true + query: + fail_on_pagination_over_collection_fetch: true + in_clause_parameter_padding: true + plan_cache_max_size: 4096 threads: virtual: enabled: true @@ -30,14 +52,14 @@ spring: # spring boot log level property logging: level: - read-replica-logger : DEBUG + read-replica-logger: DEBUG jdbc: - excludedDataSourceBeanNames: dataSource datasource-proxy: + enabled: true multiline: false logging: slf4j query: logger-name: read-replica-logger log-level: DEBUG enable-logging: true - enabled: true \ No newline at end of file + excluded-data-source-bean-names: dataSource \ No newline at end of file diff --git a/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java b/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java index 4f9676f1a..cf44554e1 100644 --- a/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java +++ b/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java @@ -19,8 +19,10 @@ import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.util.ReflectionTestUtils; +@ActiveProfiles("test") @SpringBootTest @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) diff --git a/jpa/boot-read-replica-postgresql/src/test/resources/application-test.properties b/jpa/boot-read-replica-postgresql/src/test/resources/application-test.properties new file mode 100644 index 000000000..36c42a367 --- /dev/null +++ b/jpa/boot-read-replica-postgresql/src/test/resources/application-test.properties @@ -0,0 +1,2 @@ +#since tests uses only jdbc template, connection optimizations handled by jps is gone, to enabling autocommit +spring.primary.datasource.hikari.auto-commit=true \ No newline at end of file From 76721755deddb276bf3258a159b474ab0e0d7f27 Mon Sep 17 00:00:00 2001 From: Raja Kolli Date: Sat, 2 Nov 2024 15:15:10 +0000 Subject: [PATCH 3/3] adds more documentation --- README.md | 2 +- jpa/boot-read-replica-postgresql/README.md | 1 + .../ReadReplicaApplicationTests.java | 42 +++++++++++++------ 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0919436cf..a5466aed2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The following table list all sample codes related to the spring boot integration | [Hibernate Envers Implementation using spring data JPA](./jpa/boot-data-envers) | The application, demonstrates how to apply hibernate envers to the spring boot project, monitor the system and alert when CPU usage is high or when system is down | Completed | | [Graph QL implementation using webflux](./graphql/boot-graphql-webflux) | The application, demonstrates the way to connect to database using graph ql using webflux | Completed | | [Hibernate 2nd Level Cache Using Redis](./jpa/boot-hibernate2ndlevelcache-sample) | The application, demonstrates how to apply Hibernate 2nd level cache using redis in a spring boot project , testing using QueryCounting, implemented hypersistence Repository instead of default JPARepository | Completed | -| [Read Replica Postgres](./jpa/boot-read-replica-postgresql) | The application, demonstrates saving the data in Postgresql and then read from replica instance | Completed | +| [Read Replica Postgres with connection optimization](./jpa/boot-read-replica-postgresql) | The application, demonstrates saving the data in Postgresql and then read from replica instance with optimized connection handling via LazyConnectionDataSourceProxy | Completed | | [BackgroundJobs and Scheduling using Jobrunr](./scheduler/boot-scheduler-jobrunr) | The application, demonstrates running background jobs and scheduling the tasks using [Jobrunr](https://www.jobrunr.io/en/) | Completed | | [MultiTenancy DB Based](./jpa/multitenancy/multitenancy-db) | The application, demonstrates running multi tenancy in JPA using different databases but same DDLs and DMLs | Completed | | [MultiTenancy Partition Based](./jpa/multitenancy/partition) | The application, demonstrates running multi tenancy in JPA using partition based i.e Shared Database with Shared table | Completed | diff --git a/jpa/boot-read-replica-postgresql/README.md b/jpa/boot-read-replica-postgresql/README.md index a5183405e..45fdbd419 100644 --- a/jpa/boot-read-replica-postgresql/README.md +++ b/jpa/boot-read-replica-postgresql/README.md @@ -4,6 +4,7 @@ This project is an example to show how we can separate read and write operations A read replica in Postgres is a database instance that receives data from a primary database instance and serves it to clients. Read replicas are useful for scaling database workloads, as they can offload read operations from the primary instance, allowing it to focus on more resource-intensive tasks such as writing data. This can improve the performance of the overall database system. Read replicas can also be useful for providing high availability, as they can take over read operations if the primary instance becomes unavailable for any reason. - All reads will go to reader instance and writes will go to writer instance + - Swithching between master and replica can be observed at docker compose logs or in application/logs when datasourceproxy is enabled. ![](../../images/replica.png) diff --git a/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java b/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java index cf44554e1..0ac072743 100644 --- a/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java +++ b/jpa/boot-read-replica-postgresql/src/test/java/com/example/demo/readreplica/ReadReplicaApplicationTests.java @@ -41,18 +41,36 @@ class ReadReplicaApplicationTests { @BeforeAll void setUp() { - assertThat(dataSource).isNotNull().isInstanceOf(LazyConnectionDataSourceProxy.class); - LazyConnectionDataSourceProxy lazyConnectionDataSourceProxy = - (LazyConnectionDataSourceProxy) dataSource; - DataSource targetDataSource = lazyConnectionDataSourceProxy.getTargetDataSource(); - assertThat(targetDataSource).isNotNull().isInstanceOf(ProxyDataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) targetDataSource; - primaryJdbcTemplate = new JdbcTemplate(proxyDataSource.getDataSource()); - Object readOnlyDataSource = - ReflectionTestUtils.getField(lazyConnectionDataSourceProxy, "readOnlyDataSource"); - assertThat(readOnlyDataSource).isNotNull().isInstanceOf(ProxyDataSource.class); - proxyDataSource = (ProxyDataSource) readOnlyDataSource; - replicaJdbcTemplate = new JdbcTemplate(proxyDataSource.getDataSource()); + setupJdbcTemplates(dataSource); + } + + private void setupJdbcTemplates(DataSource dataSource) { + assertThat(dataSource) + .isNotNull() + .withFailMessage("DataSource must not be null") + .isInstanceOf(LazyConnectionDataSourceProxy.class); + + LazyConnectionDataSourceProxy lazyProxy = (LazyConnectionDataSourceProxy) dataSource; + + // Setup primary template + DataSource targetDataSource = lazyProxy.getTargetDataSource(); + assertThat(targetDataSource) + .isNotNull() + .withFailMessage("Target DataSource must not be null") + .isInstanceOf(ProxyDataSource.class); + + primaryJdbcTemplate = + new JdbcTemplate(((ProxyDataSource) targetDataSource).getDataSource()); + + // Setup replica template + Object readOnlyDataSource = ReflectionTestUtils.getField(lazyProxy, "readOnlyDataSource"); + assertThat(readOnlyDataSource) + .isNotNull() + .withFailMessage("Read-only DataSource must not be null") + .isInstanceOf(ProxyDataSource.class); + + replicaJdbcTemplate = + new JdbcTemplate(((ProxyDataSource) readOnlyDataSource).getDataSource()); } @Test