【问题标题】:Routing Read-Write transactions to Primary and Read_only transactions to Replicas using Spring and Hibernate使用 Spring 和 Hibernate 将读写事务路由到主事务和只读事务到副本
【发布时间】:2012-03-01 11:12:10
【问题描述】:

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

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

【问题讨论】:

    标签: hibernate spring tomcat jpa entitymanager


    【解决方案1】:

    这就是我最终做的事情,而且效果很好。实体管理器只能有一个 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 地址或域将相同,只是不同的 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 的映射(枚举)(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 定义随后引用了数据源 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。

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

    【讨论】:

    • 哦,而且你需要确保MySQL JAR在Tomcat中,否则数据源(资源)将不起作用。
    • 以下是使用自定义注释对该方法的一些增强:fedulov.website/2015/10/14/…
    【解决方案2】:

    Spring 事务路由

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

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

    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 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 进行连接池。有关使用数据库连接池的好处的更多详细信息。

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

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

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

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

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

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

    基本上,actualDataSource 由 DataSource-Proxy 进一步包装并提供给 JPA ENtityManagerFactory。您可以查看source code on 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前缀的日志语句在Primary节点上执行,而使用high_performance_java_persistence_replica前缀的日志语句在Replica节点上执行。

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

    所有的源代码都可以在我的High-Performance Java Persistence GitHub 仓库中找到,所以你也可以试试。

    结论

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

    【讨论】:

    • 连同你的博客,我也关注了这个——fable.sh/blog/…,它在简单的情况下工作。我的项目是基于 Spring Boot 2 的,在我的休息控制器中,我的一种方法是读取然后写入。这里选择的默认数据源是从属,但在同一连接中它无法切换数据。许多人在不同的文章中报告了这个问题。在此处粘贴其中一些链接 -stackoverflow.com/questions/37561239/…
    • fedulov.website/2015/10/14/… 在 cmets 中也有人报告了问题。我想知道创建 2 个不同的 repos 是否分别包含 writeRepo 和 readRepo 包含 write 和 read 方法,分别连接到 master 和 slave 数据源是唯一的解决方案?
    • 在将 hibernate.connection.handling_mode 设置为 DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION 后工作,否则在设置 isCurrentTransactionReadOnly 标志之前调用 lookupKey 方法。
    • 如果您使用 RESOURCE_LOCAL,则不需要。相反,您需要hibernate.connection.provider_disables_autocommit
    【解决方案3】:

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

    我最终得到了一个精益解决方案,使用 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() 的结果(它是真的),以及在同一个调用中它是假的确定CurrentLookupKey() 方法中。

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

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

    【讨论】:

    • 您启动并运行它了吗?我有同样的问题。
    【解决方案4】:
    <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"/>
    

    【讨论】:

      猜你喜欢
      • 2021-06-07
      • 1970-01-01
      • 2014-11-12
      • 1970-01-01
      • 2011-06-13
      • 1970-01-01
      • 2018-08-02
      • 2015-03-26
      • 2015-05-10
      相关资源
      最近更新 更多