使用Spring實現訪問主從資料庫的讀寫和只讀事務/事物的分離路由 -Vlad Mihalcea

banq發表於2020-06-17

由於單主資料庫複製體系結構不僅提供了容錯能力和更高的可用性,而且使我們能夠通過新增更多從節點來擴充套件讀取操作,由此形成對主資料庫進行寫入操作,而對複製主資料庫的從資料庫進行只讀操作。

Spring @Transactional

在Spring應用程式中,Web @Controller呼叫一種@Service方法,該方法使用註釋進行@Transactional註釋。

預設情況下,Spring事務是可讀寫的,但是您可以通過註釋的read-only屬性將它們顯式配置為在只讀上下文中執行。

例如,以下ForumServiceImpl元件定義了兩種服務方法:

  • newPost,這需要在資料庫的“主”節點上執行的讀寫事務,以及
  • findAllPostsByTitle,它需要可以在資料庫副本節點上執行的只讀事務,因此減少了主節點上的負載

@Service
public class ForumServiceImpl
        implements ForumService {
 
    @PersistenceContext
    private EntityManager entityManager;
 
    @Override
    @Transactional
    public Post newPost(String title, String... tags) {
        Post post = new Post();
        post.setTitle(title);
 
        post.getTags().addAll(
            entityManager.createQuery("""
                select t
                from Tag t
                where t.name in :tags
                """, Tag.class)
            .setParameter("tags", Arrays.asList(tags))
            .getResultList()
        );
 
        entityManager.persist(post);
 
        return post;
    }
 
    @Override
    @Transactional(readOnly = true)
    public List<Post> findAllPostsByTitle(String title) {
        return entityManager.createQuery("""
            select p
            from Post p
            where p.title = :title
            """, Post.class)
        .setParameter("title", title)
        .getResultList();
    }
}

由於@Transactional註釋的readOnly屬性預設設定為false,因此該newPost方法使用讀寫事務上下文。

Spring事務路由

目標:將讀寫事務路由到主節點資料庫,將只讀事務路由到副本從節點資料庫。

我們可以定義一個ReadWriteDataSource連線主節點和ReadOnlyDataSource連線副本節點的。

讀寫事務路由由Spring AbstractRoutingDataSource抽象完成,由Spring 實現TransactionRoutingDatasource,如下圖所示:

使用Spring實現訪問主從資料庫的讀寫和只讀事務/事物的分離路由 -Vlad Mihalcea

TransactionRoutingDataSource實現非常簡單,如下所示:

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

基本上,我們檢查TransactionSynchronizationManager儲存當前事務上下文的Spring 類,以檢查當前執行的Spring事務是否為只讀。

該determineCurrentLookupKey方法返回鑑別符值,該鑑別符值將用於選擇讀寫JDBC或只讀JDBC DataSource。

DataSourceType僅僅是一個基本的Java列舉定義我們的事物路由選項:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

Spring讀寫和只讀JDBC 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。

readWriteDataSource限定讀寫JDBC DataSource,而readOnlyDataSource部件限定只讀JDBC DataSource。

請注意,讀寫資料來源和只讀資料來源均使用HikariCP進行連線池。有關使用資料庫連線池的好處的更多詳細資訊,請參閱本文

這些actualDataSource充當可讀寫和只讀資料來源的外觀,並使用該TransactionRoutingDataSource實用程式來實現。

在readWriteDataSource使用DataSourceType.READ_WRITE作為key註冊,readOnlyDataSource使用的DataSourceType.READ_ONLY作為key註冊。

因此,當執行讀寫@Transactional方法時,readWriteDataSource將使用,而當執行@Transactional(readOnly = true)方法時,readOnlyDataSource將使用。

請注意,該additionalProperties方法定義了hibernate.connection.provider_disables_autocommitHibernate屬性,我將其新增到Hibernate中以延遲RESOURCE_LOCAL JPA事務的資料庫獲取。
不僅hibernate.connection.provider_disables_autocommit使您可以更好地利用資料庫連線,而且這是我們使本示例工作的唯一方法,因為如果沒有此配置,則必須在呼叫determineCurrentLookupKeymethod 之前獲取連線TransactionRoutingDataSource。
有關hibernate.connection.provider_disables_autocommit配置的更多詳細資訊,請參閱[url=https://vladmihalcea.com/why-you-should-always-use-hibernate-connection-provider_disables_autocommit-for-resource-local-jpa-transactions/]本文[/url]。

構建JPA所需的其餘Spring元件EntityManagerFactory由AbstractJPAConfiguration基類定義。

基本上,actualDataSource進一步由DataSource-Proxy包裝,並提供給JPA ENtityManagerFactory。您可以在GitHub上檢視原始碼以獲取更多詳細資訊。

 

相關文章