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

时间:2012-02-08 23:22:04

标签: hibernate spring tomcat jpa entitymanager

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

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

4 个答案:

答案 0 :(得分: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" />

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

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

<ResourceLink name="readConnection" global="readConnection" type="javax.sql.DataSource"/>
<ResourceLink name="writeConnection" global="writeConnection" type="javax.sql.DataSource"/>

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

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

<!--
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>

然后,实体管理器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>

我在属性文件中定义了一些属性,但您可以使用自己的特定值替换$ {}值。所以现在我有一个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();
    }

}

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

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

}

我有一个通用的DAO实现,我使用的方法来处理各种例程JPA调用(getReference,persist,createNamedQUery&amp; 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();
}

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

答案 1 :(得分:2)

由于这是一个非常普遍的要求,因此以下答案基于我在博客上撰写的this article

Spring交易路由

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

读写和只读事务路由由Spring AbstractRoutingDataSource抽象完成,该抽象由TransactionRoutingDatasource实现,如下图所示:

Read-write and read-only transaction routing with Spring

TransactionRoutingDataSource非常易于实现,外观如下:

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

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

determineCurrentLookupKey方法返回鉴别符值,该鉴别符值将用于选择读写JDBC或只读JDBC DataSource

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

public enum  DataSourceType {
    READ_WRITE,
    READ_ONLY
}

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

/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 Spring组件定义了读写JDBC DataSource,而readOnlyDataSource组件定义了只读JDBC DataSource

请注意,读写数据源和只读数据源均使用HikariCP进行连接池。有关使用数据库连接池的好处的更多详细信息,请查看this article

actualDataSource充当读写数据源和只读数据源的外观,并使用TransactionRoutingDataSource实用程序实现。

使用readWriteDataSource键注册DataSourceType.READ_WRITE,使用readOnlyDataSource键注册DataSourceType.READ_ONLY

因此,当执行读写@Transactional方法时,将使用readWriteDataSource,而当执行@Transactional(readOnly = true)方法时,将使用readOnlyDataSource。 / p>

请注意,additionalProperties方法定义了hibernate.connection.provider_disables_autocommit Hibernate属性,我将其添加到Hibernate中以推迟RESOURCE_LOCAL JPA事务的数据库获取。

不仅hibernate.connection.provider_disables_autocommit使您可以更好地利用数据库连接,而且这是使本示例工作的唯一方法,因为如果没有此配置,则在调用{{1 }}方法determineCurrentLookupKey

有关TransactionRoutingDataSource配置的详细信息,请查看this article

构建JPA hibernate.connection.provider_disables_autocommit所需的其余Spring组件由AbstractJPAConfiguration基类定义。

基本上,EntityManagerFactory由DataSource-Proxy进一步包装,并提供给JPA actualDataSource。您可以检查source code on GitHub以获得更多详细信息。

测试时间

要检查事务路由是否可行,我们将通过在ENtityManagerFactory配置文件中设置以下属性来启用PostgreSQL查询日志:

postgresql.conf

log_min_duration_statement = 0 log_line_prefix = '[%d] ' 属性设置用于记录所有PostgreSQL语句,而第二个则将数据库名称添加到SQL日志中。

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

findAllPostsByTitle

我们可以看到PostgreSQL记录了以下消息:

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

List<Post> posts = forumService.findAllPostsByTitle(
    "High-Performance Java Persistence"
);

在主节点上执行使用[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 前缀的日志语句,而在副本节点上使用使用high_performance_java_persistence的日志语句。

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

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

结论

此要求非常有用,因为Single-Primary Database Replication体系结构不仅提供了容错能力和更高的可用性,而且允许我们通过添加更多副本节点来扩展读取操作。

答案 2 :(得分:1)

我有同样的需求:使用经典的MASTER / SLAVE来扩展readonly和writeonly数据库之间的连接以扩展读取。

我最终得到了一个精益解决方案,使用了Spring的AbstractRoutingDataSource基类。它允许您根据您编写的某些条件注入一个路由到多个数据源的数据源。

<bean id="commentsDataSource" class="com.nextep.proto.spring.ReadWriteDataSourceRouter">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry key="READ" value="java:comp/env/jdbc/readdb"/>
            <entry key="WRITE" value="java:comp/env/jdbc/writedb"/>
        </map>
    </property>
    <property name="defaultTargetDataSource" value="java:comp/env/jdbc/readdb"/>
</bean>

我的路由器看起来如下:

public class ReadWriteDataSourceRouter extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
    return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? "READ"
            : "WRITE";
}
}

我发现这很优雅,但问题是Spring在注入数据源之后似乎将事务设置为只读,因此它不起作用。我的简单测试是在我的readonly方法中检查TransactionSynchronizationManager.isCurrentTransactionReadOnly()的结果(这是真的),并在determineCurrentLookupKey()方法中检查同一个调用中的false。

如果您有想法......无论如何,您可以将测试基于TransactionSynchronizationManager以外的任何其他内容,这样可以正常工作。

希望这有帮助, 克里斯托夫

答案 3 :(得分:0)

<bean id="entityManagerFactory" 
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="filerp-pcflows" />
    <property name="dataSource" ref="pooledDS" />
    <property name="persistenceXmlLocation" value="classpath:powercenterCPCPersistence.xml" />
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="showSql" value="true" />
            <!--<property name="formatSql" value="true" />
            --><property name="generateDdl" value="false" />
            <property name="database" value="DB2" />
        </bean>
    </property>
</bean>

                                                   - &GT;

                 

<bean id="pool" autowire-candidate="false" class="org.apache.commons.pool.impl.GenericObjectPool" destroy-method="close">
    <property name="minEvictableIdleTimeMillis" value="300000"/>
    <property name="timeBetweenEvictionRunsMillis" value="60000"/>
    <property name="maxIdle" value="2"/>
    <property name="minIdle" value="0"/>
    <property name="maxActive" value="8"/>
    <property name="testOnBorrow" value="true"/>
</bean>

<bean id="dsConnectionFactory" class="org.apache.commons.dbcp.DataSourceConnectionFactory">
    <constructor-arg><ref bean="dataSource" /></constructor-arg>
</bean> 
<bean id="poolableConnectionFactory" class="org.apache.commons.dbcp.PoolableConnectionFactory">
    <constructor-arg index="0"><ref bean="dsConnectionFactory" /></constructor-arg>
    <constructor-arg index="1"><ref bean="pool" /></constructor-arg>
    <constructor-arg index="2"><null /></constructor-arg>
    <constructor-arg index="3"><value>select 1 from ${cnx.db2.database.creator}.TPROFILE</value></constructor-arg>
    <constructor-arg index="4"><value>false</value></constructor-arg>
    <constructor-arg index="5"><value>true</value></constructor-arg>
</bean>

<bean id="pooledDS" class="org.apache.commons.dbcp.PoolingDataSource"
    depends-on="poolableConnectionFactory">
    <constructor-arg>
        <ref bean="pool" />
    </constructor-arg>
</bean> 
<import resource="powercenterCPCBeans.xml"/>