我有一个使用Hibernate/JPA、Spring和Jersey的应用程序。在我的应用程序上下文中,我设置了数据源,定义了一个实体管理器工厂,使用该实体管理器工厂设置了事务管理器,并使用事务性注释注释了各种服务方法,因此我还在需要的地方将tx:注释驱动的定义连接到我的事务管理器中。这个设置效果很好,我已经能够很好地读写了。我想转到DB设置,在那里我有一个具有多个从设备的主设备(MySQL)。因此,我希望所有带有事务注释的方法都使用指向主数据库服务器的数据源,而所有其他方法则使用从属数据库服务器的连接池。
我尝试创建了两个不同的数据源,具有两个不同的实体管理器工厂和两个不同的持久单元--至少可以说很难看。我尝试过MySQL代理,但我们遇到的问题比我们需要的多。连接池已经在servlet容器中处理了。我可以在Tomcat中实现一些读取事务并将其定向到正确的数据库服务器的东西吗?或者有没有一种方法可以让所有这些方法都使用事务注释来使用特定的数据源?
发布于 2012-02-22 01:22:01
这是我最终做的事情,它工作得很好。实体管理器只能使用一个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定义引用了我创建的Datasource Router 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方法以及用于清理的clear data source方法。
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是一个具有读或写功能的枚举,它引用适当的数据源。您可以在应用程序上下文的我的bean中定义的映射中看到这一点。
发布于 2020-06-17 00:33:14
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
方法返回将用于选择读写或只读DataSource
的鉴别器值。
DataSourceType
只是一个基本的Java Enum,它定义了我们的事务路由选项:
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。
JDBC组件定义了可读写的readWriteDataSource
DataSource
,而readOnlyDataSource
组件定义了只读的JDBC。
请注意,读写数据源和只读数据源都使用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
方法TransactionRoutingDataSource
之前获取的。
构建remaining所需的其余EntityManagerFactory
组件由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日志中。
因此,在调用newPost
和findAllPostsByTitle
方法时,如下所示:
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
的日志语句在副本节点上执行。
所以,每件事都像护身符一样工作!
所有的源代码都可以在我的High-Performance Java Persistence GitHub存储库中找到,所以你也可以尝试一下。
结论
这一要求非常有用,因为Single-Primary Database Replication架构不仅提供了容错和更好的可用性,而且允许我们通过添加更多副本节点来扩展读取操作。
发布于 2012-02-23 21:44:05
我也有同样的需求:使用经典的主/从来扩展读取,路由只读和只写数据库之间的连接。
我最终得到了一个精益的解决方案,使用了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似乎在注入数据源后将事务设置为只读,所以它不起作用。我的简单测试是在只读方法(这是真的)和determineCurrentLookupKey()方法中检查TransactionSynchronizationManager.isCurrentTransactionReadOnly()的结果,在同一调用中它是假的。
如果你有想法..。无论如何,您可以将测试建立在TransactionSynchronizationManager之外的任何其他基础上,这将很好地工作。
希望这能帮上忙,克里斯托夫
https://stackoverflow.com/questions/9203122
复制相似问题