2012-02-08 4 views
5

У меня есть приложение, использующее Hibernate/JPA, с Spring и Jersey. В моем контексте приложения я устанавливал источник данных, определял фабрику диспетчера сущностей, устанавливал диспетчер транзакций с этой фабрикой ящиков сущностей и имел различные методы обслуживания, аннотированные с аннотацией транзакций, поэтому у меня также есть определение, основанное на аннотациях: в моем менеджере транзакций, где это необходимо. Эта настройка отлично работает, я умею хорошо читать и писать. Я хотел бы перейти к настройке БД, где у меня есть Мастер с несколькими подчиненными (MySQL). Поэтому я хочу, чтобы все методы, аннотированные транзакцией, использовали источник данных, указывающий на сервер master db, и все остальные, чтобы использовать пул соединений для ведомых устройств.Spring JPA Read Write splitting - есть транзакционное использование источника данных записи

Я попытался создать два разных источника данных с двумя разными фабриками управления сущностями и двумя разными постоянными единицами - уродливо, если не сказать больше. Я попробовал MySQL Proxy, но у нас было больше проблем с тем, что нам нужно. Пул соединений уже обрабатывается в контейнере сервлетов. Могу ли я реализовать что-то в Tomcat, которое считывает транзакцию и направляет ее на нужный сервер базы данных, или есть способ, которым я мог бы получить все эти методы, аннотированные аннотацией транзакций, чтобы использовать конкретный источник данных?

ответ

7

Вот что я в итоге сделал, и все получилось очень хорошо. У менеджера объектов может быть только один компонент для использования в качестве источника данных. Так что я должен был сделать, чтобы создать фасоль, которая маршрутизировалась между ними там, где это необходимо. Этот 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 - вы получите тэк.

Затем я добавил ссылку ресурса в файле context.xml в tomcat, который ссылался на эти ресурсы.

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

Эти ссылки ресурсов - это то, что весна читает в контексте приложения.

В контексте приложения я добавил определение компонента для каждой ссылки ресурса и добавил еще одно определение bean-компонента, которое ссылалось на bean-компонент Datasource, который я создал, который принимает карту (перечисление) двух ранее созданных 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> 

Определения компонента управления сущностью, а затем ссылаются на компонент dataSource.

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

Я определил некоторые свойства в файле свойств, но вы можете заменить значения $ {} вашими конкретными значениями. Итак, теперь у меня есть один компонент, который использует два других компонента, которые представляют мои два источника данных. Один компонент - тот, который я использую для JPA. Это не замечает никакой маршрутизации.

Итак, теперь фасоль.

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, сохраняются, 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 это перечисление с чтения или записи, которая ссылается на соответствующий источник данных. Вы можете видеть это на карте, определенной в моем компоненте в контексте приложения.

+0

О, и вам нужно убедиться, что MySQL JAR находится в Tomcat, иначе источники данных (ресурс) не будут работать. – Elrond

+0

Спасибо! Это хорошая идея, я попробую. – Stony

+1

Вот некоторые улучшения этого метода с использованием пользовательских аннотаций: http://fedulov.website/2015/10/14/dynamic-datasource-routing-with-spring/ –

1

У меня такая же потребность: маршрутизируйте соединение между базой данных readonly и writeonly, используя классический MASTER/SLAVE для масштабирования.

В итоге я получаю худощавое решение, используя базовый класс 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"; 
} 
} 

Я нахожу это довольно элегантно, но проблема в том, что весна, кажется, установить сделку по READONLY после введения источника данных, так что это не работает. Мой простой тест - проверить результат TransactionSynchronizationManager.isCurrentTransactionReadOnly() в моих методах readonly (это правда) и в методе defineCurrentLookupKey(), где он является ложным при одном и том же вызове.

Если у вас есть идеи ... В любом случае вы можете основывать тест на чем-либо другом, кроме TransactionSynchronizationManager, и это будет работать нормально.

Надеется, что это помогает, Christophe

+0

вы его запустили? У меня такая же проблема. –

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> 

->

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

 Смежные вопросы

  • Нет связанных вопросов^_^