Spring JPA Read Write splitting - 有事务使用write数据源

Elr*_*ond 5 spring tomcat hibernate jpa entitymanager

我有一个使用Hibernate/JPA,Spring和Jersey的应用程序.在我的应用程序上下文中,我设置数据源,定义实体管理器工厂,使用该实体管理器工厂设置事务管理器,并使用事务注释注释各种服务方法,因此我还有tx:annotation-driven definition to wire在我的交易经理需要的地方.这个设置很好,我已经能够读写得很好.我想转到数据库设置,我有一个主机与多个从属(MySQL).因此,我希望所有使用transactional注释的方法都使用指向主数据库服务器的数据源,并使用所有其他方法来使用从服务器的连接池.

我尝试创建两个不同的数据源,两个不同的实体管理器工厂和两个不同的持久性单元 - 至少可以说是丑陋的.我尝试了一个MySQL代理,但是我们遇到了更多问题.连接池已在servlet容器中处理.我可以在Tomcat中实现读取事务并将其定向到正确的数据库服务器的东西,还是有办法让所有那些使用事务注释注释的方法使用特定的数据源?

Vla*_*cea 12

Spring事务路由

要将读写事务路由到主节点,将只读事务路由到副本节点,我们可以定义一个ReadWriteDataSource连接到主节点的节点和一个ReadOnlyDataSource连接到副本节点的节点。

读写和只读事务路由是由 SpringAbstractRoutingDataSource抽象完成的,它是由 实现的TransactionRoutingDatasource,如下图所示:

使用 Spring 进行读写和只读事务路由

TransactionRoutingDataSource非常容易实现,如下所示:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return TransactionSynchronizationManager
            .isCurrentTransactionReadOnly() ?
            DataSourceType.READ_ONLY :
            DataSourceType.READ_WRITE;
    }
}
Run Code Online (Sandbox Code Playgroud)

基本上,我们检查存储当前事务上下文的 SpringTransactionSynchronizationManager类,以检查当前运行的 Spring 事务是否是只读的。

determineCurrentLookupKey方法返回将用于选择读写或只读 JDBC 的鉴别器值DataSource

DataSourceType只是一个基本的 Java 枚举,它定义了我们的事务路由选项:

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}
Run Code Online (Sandbox Code Playgroud)

Spring读写和只读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));
    }
}
Run Code Online (Sandbox Code Playgroud)

资源/META-INF/jdbc-postgresql-replication.properties文件提供了读写和只读 JDBCDataSource组件的配置:

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
Run Code Online (Sandbox Code Playgroud)

jdbc.url.primary属性定义主节点的 URL,而jdbc.url.replica定义副本节点的 URL。

SpringreadWriteDataSource组件定义了读写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_autocommitHibernate 属性,我将其添加到 Hibernate 以推迟 RESOURCE_LOCAL JPA 事务的数据库获取。

不仅hibernate.connection.provider_disables_autocommit可以让您更好地利用数据库连接,而且这是我们使此示例正常工作的唯一方法,因为如果没有此配置,将在调用该determineCurrentLookupKey方法之前获取连接TransactionRoutingDataSource

构建 JPA 所需的其余 Spring 组件EntityManagerFactory由基类定义AbstractJPAConfiguration

基本上,actualDataSource由 DataSource-Proxy 进一步包装并提供给 JPA ENtityManagerFactory。您可以在 GitHub 上查看源代码以了解更多详细信息。

测试时间

为了检查事务路由是否有效,我们将通过在postgresql.conf配置文件中设置以下属性来启用 PostgreSQL 查询日志:

log_min_duration_statement = 0
log_line_prefix = '[%d] '
Run Code Online (Sandbox Code Playgroud)

log_min_duration_statement属性设置用于记录所有 PostgreSQL 语句,而第二个属性设置将数据库名称添加到 SQL 日志中。

因此,在调用newPostfindAllPostsByTitle方法时,如下所示:

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

List<Post> posts = forumService.findAllPostsByTitle(
    "High-Performance Java Persistence"
);
Run Code Online (Sandbox Code Playgroud)

我们可以看到 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
Run Code Online (Sandbox Code Playgroud)

使用前缀的日志语句high_performance_java_persistence在主节点上执行,而使用 的日志high_performance_java_persistence_replica语句在副本节点上执行。

所以,一切都像魅力一样!

所有源代码都可以在我的High-Performance Java Persistence GitHub 存储库中找到,因此您也可以尝试一下。

结论

这个要求非常有用,因为单主数据库复制架构不仅提供容错和更好的可用性,而且允许我们通过添加更多副本节点来扩展读取操作。


Elr*_*ond 7

这就是我最终做的事情并且效果很好.实体管理器只能有一个bean用作数据源.所以我要做的就是创建一个在必要时在两者之间路由的bean.那个ben是我用于JPA实体经理的那个.

我在tomcat中设置了两个不同的数据源.在server.xml中,我创建了两个资源(数据源).

<Resource name="readConnection" auth="Container" type="javax.sql.DataSource"
          username="readuser" password="readpass"
          url="jdbc:mysql://readipaddress:3306/readdbname"
          driverClassName="com.mysql.jdbc.Driver"
          initialSize="5" maxWait="5000"
          maxActive="120" maxIdle="5"
          validationQuery="select 1"
          poolPreparedStatements="true"
          removeAbandoned="true" />
<Resource name="writeConnection" auth="Container" type="javax.sql.DataSource"
          username="writeuser" password="writepass"
          url="jdbc:mysql://writeipaddress:3306/writedbname"
          driverClassName="com.mysql.jdbc.Driver"
          initialSize="5" maxWait="5000"
          maxActive="120" maxIdle="5"
          validationQuery="select 1"
          poolPreparedStatements="true"
          removeAbandoned="true" />
Run Code Online (Sandbox Code Playgroud)

你可以在同一台服务器上安装数据库表,在这种情况下,ip地址或域名将是相同的,只是不同的dbs - 你得到了jist.

然后我在tomcat的context.xml文件中添加了一个资源链接,它引用了这些资源.

<ResourceLink name="readConnection" global="readConnection" type="javax.sql.DataSource"/>
<ResourceLink name="writeConnection" global="writeConnection" type="javax.sql.DataSource"/>
Run Code Online (Sandbox Code Playgroud)

这些资源链接是spring在应用程序上下文中读取的内容.

在应用程序上下文中,我为每个资源链接添加了一个bean定义,并添加了一个额外的bean定义,该定义引用了我创建的数据源路由器bean,它接受了之前创建的两个bean(bean定义)的映射(枚举).

<!--
Data sources representing master (write) and slaves (read).
-->
<bean id="readDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="readConnection" /> 
    <property name="resourceRef" value="true" />
    <property name="lookupOnStartup" value="true" />
    <property name="cache" value="true" />
    <property name="proxyInterface" value="javax.sql.DataSource" />  
</bean>

<bean id="writeDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName" value="writeConnection" />
    <property name="resourceRef" value="true" />
    <property name="lookupOnStartup" value="true" />
    <property name="cache" value="true" />
    <property name="proxyInterface" value="javax.sql.DataSource" />
</bean>

<!--
Provider of available (master and slave) data sources.
-->
<bean id="dataSource" class="com.myapp.dao.DatasourceRouter">
    <property name="targetDataSources">
      <map key-type="com.myapp.api.util.AvailableDataSources">
         <entry key="READ" value-ref="readDataSource"/>
         <entry key="WRITE" value-ref="writeDataSource"/>
      </map>
   </property>
   <property name="defaultTargetDataSource" ref="writeDataSource"/>
</bean>
Run Code Online (Sandbox Code Playgroud)

然后,实体管理器bean定义引用了dataSource bean.

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="persistenceUnitName" value="${jpa.persistenceUnitName}" />
    <property name="jpaVendorAdapter"> 
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> 
            <property name="databasePlatform" value="${jpa.dialect}"/>
            <property name="showSql" value="${jpa.showSQL}" />
        </bean>
    </property>
</bean>
Run Code Online (Sandbox Code Playgroud)

我在属性文件中定义了一些属性,但您可以使用您自己的特定值替换$ {}值.所以现在我有一个bean使用另外两个代表我的两个数据源的bean.一个bean是我用于JPA的bean.它没有发现任何路由发生.

所以现在是路由bean.

public class DatasourceRouter extends AbstractRoutingDataSource{

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException{
    // TODO Auto-generated method stub
    return null;
    }

    @Override
    protected Object determineCurrentLookupKey(){
    return DatasourceProvider.getDatasource();
    }

}
Run Code Online (Sandbox Code Playgroud)

实体管理器调用重写的方法来基本确定数据源.DatasourceProvider具有线程本地(线程安全)属性,具有getter和setter方法以及用于清理的clear数据源方法.

public class DatasourceProvider{
    private static final ThreadLocal<AvailableDataSources> datasourceHolder = new ThreadLocal<AvailableDataSources>();

    public static void setDatasource(final AvailableDataSources customerType){
    datasourceHolder.set(customerType);
    }

    public static AvailableDataSources getDatasource(){
    return (AvailableDataSources) datasourceHolder.get();
    }

    public static void clearDatasource(){
    datasourceHolder.remove();
    }

}
Run Code Online (Sandbox Code Playgroud)

我有一个通用的DAO实现,我使用的方法来处理各种例程JPA调用(getReference,persist,createNamedQUery和getResultList等).在调用entityManager做任何需要做的事情之前,我将DatasourceProvider的数据源设置为read或write.该方法可以处理传入的值,使其更具动态性.这是一个示例方法.

@Override
public List<T> findByNamedQuery(final String queryName, final Map<String, Object> properties, final int... rowStartIdxAndCount)
{
DatasourceProvider.setDatasource(AvailableDataSources.READ);
final TypedQuery<T> query = entityManager.createNamedQuery(queryName, persistentClass);
if (!properties.isEmpty())
{
    bindNamedQueryParameters(query, properties);
}
appyRowLimits(query, rowStartIdxAndCount);

return query.getResultList();
}
Run Code Online (Sandbox Code Playgroud)

AvailableDataSources是一个带有READ或WRITE的枚举,它引用相应的数据源.您可以在我的bean中在应用程序上下文中定义的映射中看到它.