Routing Read-Write transactions to Primary and Read_only transactions to Replicas using Spring and Hibernate

前端 未结 4 1781
没有蜡笔的小新
没有蜡笔的小新 2021-01-31 12:46

I have an application that uses Hibernate/JPA, with Spring and Jersey. In my application context I set the data source, define an entity manager factory, set the transaction man

4条回答
  •  后悔当初
    2021-01-31 13:33

    Spring transaction routing

    To route the read-write transactions to the Primary node and read-only transactions to the Replica node, we can define a ReadWriteDataSource that connects to the Primary node and a ReadOnlyDataSource that connect to the Replica node.

    The read-write and read-only transaction routing is done by the Spring AbstractRoutingDataSource abstraction, which is implemented by the TransactionRoutingDatasource, as illustrated by the following diagram:

    The TransactionRoutingDataSource is very easy to implement and looks as follows:

    public class TransactionRoutingDataSource 
            extends AbstractRoutingDataSource {
    
        @Nullable
        @Override
        protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager
                .isCurrentTransactionReadOnly() ?
                DataSourceType.READ_ONLY :
                DataSourceType.READ_WRITE;
        }
    }
    

    Basically, we inspect the Spring TransactionSynchronizationManager class that stores the current transactional context to check whether the currently running Spring transaction is read-only or not.

    The determineCurrentLookupKey method returns the discriminator value that will be used to choose either the read-write or the read-only JDBC DataSource.

    The DataSourceType is just a basic Java Enum that defines our transaction routing options:

    public enum  DataSourceType {
        READ_WRITE,
        READ_ONLY
    }
    

    Spring read-write and read-only JDBC DataSource configuration

    The DataSource configuration looks as follows:

    @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 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));
        }
    }
    

    The /META-INF/jdbc-postgresql-replication.properties resource file provides the configuration for the read-write and read-only JDBC DataSource components:

    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
    

    The jdbc.url.primary property defines the URL of the Primary node while the jdbc.url.replica defines the URL of the Replica node.

    The readWriteDataSource Spring component defines the read-write JDBC DataSource while the readOnlyDataSource component define the read-only JDBC DataSource.

    Note that both the read-write and read-only data sources use HikariCP for connection pooling. For more details about the benefits of using database connection pooling.

    The actualDataSource acts as a facade for the read-write and read-only data sources and is implemented using the TransactionRoutingDataSource utility.

    The readWriteDataSource is registered using the DataSourceType.READ_WRITE key and the readOnlyDataSource using the DataSourceType.READ_ONLY key.

    So, when executing a read-write @Transactional method, the readWriteDataSource will be used while when executing a @Transactional(readOnly = true) method, the readOnlyDataSource will be used instead.

    Note that the additionalProperties method defines the hibernate.connection.provider_disables_autocommit Hibernate property, which I added to Hibernate to postpone the database acquisition for RESOURCE_LOCAL JPA transactions.

    Not only that the hibernate.connection.provider_disables_autocommit allows you to make better use of database connections, but it's the only way we can make this example work since, without this configuration, the connection is acquired prior to calling the determineCurrentLookupKey method TransactionRoutingDataSource.

    The remaining Spring components needed for building the JPA EntityManagerFactory are defined by the AbstractJPAConfiguration base class.

    Basically, the actualDataSource is further wrapped by DataSource-Proxy and provided to the JPA ENtityManagerFactory. You can check the source code on GitHub for more details.

    Testing time

    To check if the transaction routing works, we are going to enable the PostgreSQL query log by setting the following properties in the postgresql.conf configuration file:

    log_min_duration_statement = 0
    log_line_prefix = '[%d] '
    

    The log_min_duration_statement property setting is for logging all PostgreSQL statements while the second one adds the database name to the SQL log.

    So, when calling the newPost and findAllPostsByTitle methods, like this:

    Post post = forumService.newPost(
        "High-Performance Java Persistence",
        "JDBC", "JPA", "Hibernate"
    );
    
    List posts = forumService.findAllPostsByTitle(
        "High-Performance Java Persistence"
    );
    

    We can see that PostgreSQL logs the following messages:

    [high_performance_java_persistence] LOG:  execute : 
        BEGIN
    
    [high_performance_java_persistence] DETAIL:  
        parameters: $1 = 'JDBC', $2 = 'JPA', $3 = 'Hibernate'
    [high_performance_java_persistence] LOG:  execute : 
        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 : 
        select nextval ('hibernate_sequence')
    
    [high_performance_java_persistence] DETAIL:  
        parameters: $1 = 'High-Performance Java Persistence', $2 = '4'
    [high_performance_java_persistence] LOG:  execute : 
        insert into post (title, id) values ($1, $2)
    
    [high_performance_java_persistence] DETAIL:  
        parameters: $1 = '4', $2 = '1'
    [high_performance_java_persistence] LOG:  execute : 
        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 : 
        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 : 
        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 : 
        BEGIN
        
    [high_performance_java_persistence_replica] DETAIL:  
        parameters: $1 = 'High-Performance Java Persistence'
    [high_performance_java_persistence_replica] LOG:  execute : 
        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
    

    The log statements using the high_performance_java_persistence prefix were executed on the Primary node while the ones using the high_performance_java_persistence_replica on the Replica node.

    So, everything works like a charm!

    All the source code can be found in my High-Performance Java Persistence GitHub repository, so you can try it out too.

    Conclusion

    This requirement is very useful since the Single-Primary Database Replication architecture not only provides fault-tolerance and better availability, but it allows us to scale read operations by adding more replica nodes.

提交回复
热议问题