使用 Spring 和 Hibernate 将读写事务路由到主节点,将只读事务路由到副本节点
我有一个使用 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
Spring事务路由
为了将读写事务路由到Primary节点并将只读事务路由到Replica节点,我们可以定义一个连接到Primary节点的
ReadWriteDataSource
和一个ReadOnlyDataSource
code> 连接到副本节点。读写和只读事务路由由 Spring
AbstractRoutingDataSource
抽象,由TransactionRoutingDatasource
实现,如下图所示:TransactionRoutingDataSource
非常容易实现,如下所示:基本上,我们检查 Spring TransactionSynchronizationManager 类,用于存储当前事务上下文,以检查当前运行的 Spring 事务是否是只读的。
defineCurrentLookupKey
方法返回鉴别器值,该值将用于选择读写或只读 JDBCDataSource
。DataSourceType
只是一个基本的 Java 枚举,它定义了我们的事务路由选项:Spring 读写和只读 JDBC 数据源配置
DataSource
配置如下所示:>/META-INF/jdbc-postgresql-replication.properties
资源文件提供读写和只读 JDBCDataSource
组件的配置:
jdbc.url.primary
属性定义主节点的 URL,而jdbc.url.replica
定义副本节点的 URL。readWriteDataSource
Spring 组件定义了读写 JDBCDataSource
,而readOnlyDataSource
组件定义了只读 JDBCDataSource
。actualDataSource
充当读写和只读数据源的外观,并使用TransactionRoutingDataSource
实用程序实现。readWriteDataSource
使用DataSourceType.READ_WRITE
键注册,readOnlyDataSource
使用DataSourceType.READ_ONLY
键注册。因此,在执行读写
@Transactional
方法时,将使用readWriteDataSource
,而在执行@Transactional(readOnly = true)
方法时将使用readWriteDataSource
,将使用readOnlyDataSource
来代替。构建 JPA EntityManagerFactory 所需的其余 Spring 组件由
AbstractJPAConfiguration
基类。基本上,
actualDataSource
由 DataSource-Proxy 进一步包装并提供给 JPAENtityManagerFactory
。您可以查看 GitHub 上的源代码了解更多详细信息。测试时间
为了检查事务路由是否有效,我们将通过在
postgresql.conf
配置文件中设置以下属性来启用 PostgreSQL 查询日志:log_min_duration_statement
属性设置用于记录所有 PostgreSQL 语句,而第二个则将数据库名称添加到 SQL 日志中。因此,当调用
newPost
和findAllPostsByTitle
方法时,如下所示:我们可以看到 PostgreSQL 记录了以下消息:
使用
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 aReadOnlyDataSource
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 theTransactionRoutingDatasource
, as illustrated by the following diagram:The
TransactionRoutingDataSource
is very easy to implement and looks as follows: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 JDBCDataSource
.The
DataSourceType
is just a basic Java Enum that defines our transaction routing options:Spring read-write and read-only JDBC DataSource configuration
The
DataSource
configuration looks as follows:The
/META-INF/jdbc-postgresql-replication.properties
resource file provides the configuration for the read-write and read-only JDBCDataSource
components:The
jdbc.url.primary
property defines the URL of the Primary node while thejdbc.url.replica
defines the URL of the Replica node.The
readWriteDataSource
Spring component defines the read-write JDBCDataSource
while thereadOnlyDataSource
component define the read-only JDBCDataSource
.The
actualDataSource
acts as a facade for the read-write and read-only data sources and is implemented using theTransactionRoutingDataSource
utility.The
readWriteDataSource
is registered using theDataSourceType.READ_WRITE
key and thereadOnlyDataSource
using theDataSourceType.READ_ONLY
key.So, when executing a read-write
@Transactional
method, thereadWriteDataSource
will be used while when executing a@Transactional(readOnly = true)
method, thereadOnlyDataSource
will be used instead.The remaining Spring components needed for building the JPA
EntityManagerFactory
are defined by theAbstractJPAConfiguration
base class.Basically, the
actualDataSource
is further wrapped by DataSource-Proxy and provided to the JPAENtityManagerFactory
. 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: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
andfindAllPostsByTitle
methods, like this:We can see that PostgreSQL logs the following messages:
The log statements using the
high_performance_java_persistence
prefix were executed on the Primary node while the ones using thehigh_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.
这就是我最终所做的,而且效果很好。实体管理器只能有一个bean用作数据源。所以我必须做的是创建一个在必要时在两者之间路由的 bean。这本是我用于 JPA 实体管理器的本。
我在 tomcat 中设置了两个不同的数据源。在 server.xml 中,我创建了两个资源(数据源)。
您可以将数据库表放在同一台服务器上,在这种情况下,IP 地址或域将相同,只是数据库不同 - 您明白了。
然后,我在 tomcat 的 context.xml 文件中添加了一个资源链接,该链接引用了这些资源。
这些资源链接是 spring 在应用程序上下文中读取的内容。
在应用程序上下文中,我为每个资源链接添加了一个 bean 定义,并添加了一个附加的 bean 定义,该定义引用了我创建的数据源路由器 bean,该数据源路由器 bean 接受先前创建的两个 bean(bean 定义)的映射(枚举)。
然后实体管理器 bean 定义引用 dataSource bean。
我在属性文件中定义了一些属性,但您可以将 ${} 值替换为您自己的特定值。现在我有一个 Bean,它使用另外两个 Bean 来代表我的两个数据源。其中一个 bean 是我用于 JPA 的 bean。它忽略了任何路由的发生。
现在是路由bean。
实体管理器调用重写的方法来基本上确定数据源。 DatasourceProvider 有一个线程本地(线程安全)属性,带有 getter 和 setter 方法以及用于清理的清除数据源方法。
我有一个通用的 DAO 实现,其中包含用于处理各种例程 JPA 调用的方法(getReference、persist、createNamedQUery 和 getResultList 等)。在调用entityManager 执行所需操作之前,我将DatasourceProvider 的数据源设置为读取或写入。该方法还可以处理传入的值,使其更加动态。这是一个示例方法。
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).
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.
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).
The entity manager bean definition then referenced the dataSource 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.
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.
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.
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.
我有同样的需求:使用经典的主/从来扩展只读和只写数据库之间的连接以扩展读取。
我最终得到了一个精益的解决方案,使用 spring 中的 AbstractRoutingDataSource 基类。它允许您注入一个数据源,该数据源根据您编写的某些条件路由到多个数据源。
我的路由器看起来如下所示:
我发现这非常优雅,但这里的问题是 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.
And my router simply looks like the following :
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
-->
-->