使用Spring实现访问主从数据库的读写和只读事务/事物的分离路由 -Vlad Mihalcea


由于单主数据库复制体系结构不仅提供了容错能力和更高的可用性,而且使我们能够通过添加更多从节点来扩展读取操作,由此形成对主数据库进行写入操作,而对复制主数据库的从数据库进行只读操作。

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,如下图所示:

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上查看源代码以获取更多详细信息。