使用 Spring 和 Hibernate 将读写事务路由到主节点,将只读事务路由到副本节点

发布于 2025-01-04 13:12:14 字数 459 浏览 0 评论 0原文

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

我尝试创建两个不同的数据源,使用两个不同的实体管理器工厂和两个不同的持久单元 - 至少可以说很难看。我尝试了 MySQL 代理,但我们遇到了更多问题。连接池已在 servlet 容器中处理。我可以在 Tomcat 中实现一些读取事务并将其定向到正确的数据库服务器的功能吗?或者有没有一种方法可以让所有这些方法都用事务注释进行注释以使用特定的数据源?

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 manager with that entity manger factory, and have various service methods annotated with the transactional annotation, so I also have the tx:annotation-driven definition to wire in my transaction manager where needed. This setup works great, I've been able to read and write just fine. I would like to move to a DB setup where I have a Master with multiple slaves (MySQL). So I want all the methods annotated with transactional to use a data source pointing to the master db server, and all others to use a connection pool of the slaves.

I've tried creating two different datasources, with two different entity manager factories, and two different persistent units - ugly to say the least. I tried a MySQL Proxy but we had more problems with that then we need. The connection pooling is handled in the servlet container already. Could I implement something in Tomcat that reads the transaction and directs it to the right database server, or is there a way I could get all those methods annotated with the transactional annotation to use a particular datasource?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

沧笙踏歌 2025-01-11 13:12:14

Spring事务路由

为了将读写事务路由到Primary节点并将只读事务路由到Replica节点,我们可以定义一个连接到Primary节点的ReadWriteDataSource和一个ReadOnlyDataSource code> 连接到副本节点。

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

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

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

public class TransactionRoutingDataSource 
        extends AbstractRoutingDataSource {

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

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

defineCurrentLookupKey 方法返回鉴别器值,该值将用于选择读写或只读 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 Spring 组件定义了读写 JDBC DataSource,而 readOnlyDataSource 组件定义了只读 JDBC DataSource

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

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

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

因此,在执行读写@Transactional方法时,将使用readWriteDataSource,而在执行@Transactional(readOnly = true)方法时将使用readWriteDataSource ,将使用 readOnlyDataSource 来代替。

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

不仅是 hibernate.connection.provider_disables_autocommit 允许您更好地利用数据库连接,但它是我们可以使此示例正常工作的唯一方法,因为如果没有此配置,则在调用 defineCurrentLookupKey 方法 TransactionRoutingDataSource 之前获取连接。

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

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

测试时间

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

log_min_duration_statement = 0
log_line_prefix = '[%d] '

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

我们可以看到 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

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

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

所有源代码都可以在我的 高性能 Java Persistence GitHub 存储库中找到,所以你也可以尝试一下。

结论

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

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:

Read-write and read-only transaction routing with Spring

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

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<Post> posts = forumService.findAllPostsByTitle(
    "High-Performance Java Persistence"
);

We can see that PostgreSQL logs the following messages:

[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

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.

兰花执着 2025-01-11 13:12:14

这就是我最终所做的,而且效果很好。实体管理器只能有一个bean用作数据源。所以我必须做的是创建一个在必要时在两者之间路由的 bean。这本是我用于 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 地址或域将相同,只是数据库不同 - 您明白了。

然后,我在 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(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>

然后实体管理器 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 和 getResultList 等)。在调用entityManager 执行所需操作之前,我将DatasourceProvider 的数据源设置为读取或写入。该方法还可以处理传入的值,使其更加动态。这是一个示例方法。

@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 中定义的映射中看到这一点。

Here's what I ended up doing and it worked quite well. The entity manager can only have one bean to use as the data source. So what I had to do was to create a bean that routed between the two where necessary. That one ben is the one I used for the JPA entity manager.

I setup two different data sources in tomcat. In the server.xml I created two resources (data sources).

<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" />

You could have the database tables on the same server, in which case the ip addresses or domain would be the same, just different dbs - you get the jist.

I then added a resource link in the context.xml file in tomcat that referenced these to resources.

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

These resource links are what spring reads in the application context.

In the application context I added a bean definition for each resource link and added one additional bean definition that referenced a Datasource Router bean I created that takes in a map (enum) of the two previously created beans (bean definition).

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

The entity manager bean definition then referenced the 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>

I defined some properties in a properties file, but you can replace the ${} values with your own specific values. So now I have one bean that uses two other beans that represent my two data sources. The one bean is the one I use for JPA. It's oblivious of any routing happening.

So now the routing 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();
    }

}

The overridden method is called by the entity manager to determine the data source basically. The DatasourceProvider has a thread local (thread safe) property with a getter and setter method as well as the clear data source method for clean up.

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

}

I have a generic DAO implementation with methods I use to handle various routine JPA calls (getReference, persist, createNamedQUery & getResultList, etc.). Before it makes the call to the entityManager to do whatever it needs to do I set the DatasourceProvider's datasource to the read or write. The method can handle that value being passed in as well to make it a little more dynamic. Here is an example method.

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

The AvailableDataSources is an enum with READ or WRITE, which references the appropriate data source. You can see that in the map defined in my bean on the application context.

苏辞 2025-01-11 13:12:14

我有同样的需求:使用经典的主/从来扩展只读和只写数据库之间的连接以扩展读取。

我最终得到了一个精益的解决方案,使用 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 似乎在注入数据源后将事务设置为只读,因此它不起作用。我的简单测试是检查只读方法中 TransactionSynchronizationManager.isCurrentTransactionReadOnly() 的结果(为 true),以及在同一次调用中检查 certainCurrentLookupKey() 方法中为 false 的结果。

如果您有想法...无论如何,您可以将测试基于 TransactionSynchronizationManager 之外的任何其他内容,这会很好地工作。

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

I have the same kind of need : route the connection between a readonly and writeonly database using a classical MASTER / SLAVE to scale reads.

I end up with a lean solution, using the AbstractRoutingDataSource base class from spring. It allows you to inject a datasource that routes to several datasources based on some conditions that you write.

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

And my router simply looks like the following :

public class ReadWriteDataSourceRouter extends AbstractRoutingDataSource {

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

I find this quite elegant, but the problem here is that Spring seems to set the transaction to readonly after injecting the datasource, so it does not work. My simple test is to check the result of TransactionSynchronizationManager.isCurrentTransactionReadOnly() in my readonly methods (it is true), and in the determineCurrentLookupKey() method where it is false on the same call.

If you got ideas... Anyway you could base the test on anything else other than TransactionSynchronizationManager and this will work fine.

Hope this helps,
Christophe

心清如水 2025-01-11 13:12:14
<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>

-->

<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"/>

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

-->

<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"/>

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文