假设我们有一个用户、钱包REST微服务和一个API网关,它将这些东西粘合在一起。当Bob在我们的网站上注册时,我们的API网关需要通过user微服务创建一个用户,并通过wallet微服务创建一个钱包。

下面是一些可能出错的情况:

用户Bob创建失败:没关系,我们只是向Bob返回一个错误消息。我们使用SQL事务,所以没有人在系统中看到Bob。一切都很好:) 用户Bob已经创建,但是在我们的钱包创建之前,我们的API网关硬崩溃了。我们现在有一个没有钱包的用户(数据不一致)。 创建了用户Bob,当我们创建钱包时,HTTP连接断开。钱包的创建可能成功,也可能失败。

有什么解决方案可以防止这种数据不一致的发生?是否存在允许事务跨越多个REST请求的模式?我读过维基百科上关于两阶段提交的页面,它似乎涉及到这个问题,但我不确定如何在实践中应用它。这篇原子分布式事务:一篇RESTful设计论文看起来也很有趣,尽管我还没有读过。

或者,我知道REST可能不适合这个用例。处理这种情况的正确方法可能是完全放弃REST并使用不同的通信协议,如消息队列系统?或者我应该在我的应用程序代码中强制一致性(例如,通过有一个后台作业来检测不一致并修复它们,或者通过在我的用户模型上有一个“state”属性与“creating”,“created”值等)?


当前回答

不合理的事情:

使用REST服务的分布式事务。REST服务根据定义是无状态的,因此它们不应该是跨多个服务的事务边界的参与者。您的用户注册用例场景是有意义的,但是使用REST微服务创建用户和钱包数据的设计并不好。

什么会让你头疼:

EJBs with distributed transactions. It's one of those things that work in theory but not in practice. Right now I'm trying to make a distributed transaction work for remote EJBs across JBoss EAP 6.3 instances. We've been talking to RedHat support for weeks, and it didn't work yet. Two-phase commit solutions in general. I think the 2PC protocol is a great algorithm (many years ago I implemented it in C with RPC). It requires comprehensive fail recovery mechanisms, with retries, state repository, etc. All the complexity is hidden within the transaction framework (ex.: JBoss Arjuna). However, 2PC is not fail proof. There are situations the transaction simply can't complete. Then you need to identify and fix database inconsistencies manually. It may happen once in a million transactions if you're lucky, but it may happen once in every 100 transactions depending on your platform and scenario. Sagas (Compensating transactions). There's the implementation overhead of creating the compensating operations, and the coordination mechanism to activate compensation at the end. But compensation is not fail proof either. You may still end up with inconsistencies (= some headache).

最好的选择是什么:

Eventual consistency. Neither ACID-like distributed transactions nor compensating transactions are fail proof, and both may lead to inconsistencies. Eventual consistency is often better than "occasional inconsistency". There are different design solutions, such as: You may create a more robust solution using asynchronous communication. In your scenario, when Bob registers, the API gateway could send a message to a NewUser queue, and right-away reply to the user saying "You'll receive an email to confirm the account creation." A queue consumer service could process the message, perform the database changes in a single transaction, and send the email to Bob to notify the account creation. The User microservice creates the user record and a wallet record in the same database. In this case, the wallet store in the User microservice is a replica of the master wallet store only visible to the Wallet microservice. There's a data synchronization mechanism that is trigger-based or kicks in periodically to send data changes (e.g., new wallets) from the replica to the master, and vice-versa.

但是如果需要同步响应呢?

重构微服务。如果使用队列的解决方案不能工作,因为服务使用者需要立即得到响应,那么我宁愿重新构造User和Wallet功能,将其配置在同一个服务中(或者至少在同一个VM中,以避免分布式事务)。是的,它离微服务更近了一步,也更接近了一个庞然大物,但它会让你免去一些头疼的事情。

其他回答

在微服务世界中,服务之间的通信应该通过rest客户端或消息队列。有两种方法可以跨服务处理事务,具体取决于服务之间的通信方式。我个人更喜欢消息驱动的体系结构,这样长时间的事务对用户来说应该是一个无阻塞的操作。 让我们举个例子来解释一下:

使用事件Create user创建用户BOB,并将消息推送到消息总线。 订阅此事件的钱包服务可以创建用户对应的钱包。

The one thing which you have to take care is to select a robust reliable message backbone which can persists the state in case of failure. You can use kafka or rabbitmq for messaging backbone. There will be a delay in execution because of eventual consistency but that can be easily updated through socket notification. A notifications service/task manager framework can be a service which update the state of the transactions through asynchronous mechanism like sockets and can help UI to update show the proper progress.

不合理的事情:

使用REST服务的分布式事务。REST服务根据定义是无状态的,因此它们不应该是跨多个服务的事务边界的参与者。您的用户注册用例场景是有意义的,但是使用REST微服务创建用户和钱包数据的设计并不好。

什么会让你头疼:

EJBs with distributed transactions. It's one of those things that work in theory but not in practice. Right now I'm trying to make a distributed transaction work for remote EJBs across JBoss EAP 6.3 instances. We've been talking to RedHat support for weeks, and it didn't work yet. Two-phase commit solutions in general. I think the 2PC protocol is a great algorithm (many years ago I implemented it in C with RPC). It requires comprehensive fail recovery mechanisms, with retries, state repository, etc. All the complexity is hidden within the transaction framework (ex.: JBoss Arjuna). However, 2PC is not fail proof. There are situations the transaction simply can't complete. Then you need to identify and fix database inconsistencies manually. It may happen once in a million transactions if you're lucky, but it may happen once in every 100 transactions depending on your platform and scenario. Sagas (Compensating transactions). There's the implementation overhead of creating the compensating operations, and the coordination mechanism to activate compensation at the end. But compensation is not fail proof either. You may still end up with inconsistencies (= some headache).

最好的选择是什么:

Eventual consistency. Neither ACID-like distributed transactions nor compensating transactions are fail proof, and both may lead to inconsistencies. Eventual consistency is often better than "occasional inconsistency". There are different design solutions, such as: You may create a more robust solution using asynchronous communication. In your scenario, when Bob registers, the API gateway could send a message to a NewUser queue, and right-away reply to the user saying "You'll receive an email to confirm the account creation." A queue consumer service could process the message, perform the database changes in a single transaction, and send the email to Bob to notify the account creation. The User microservice creates the user record and a wallet record in the same database. In this case, the wallet store in the User microservice is a replica of the master wallet store only visible to the Wallet microservice. There's a data synchronization mechanism that is trigger-based or kicks in periodically to send data changes (e.g., new wallets) from the replica to the master, and vice-versa.

但是如果需要同步响应呢?

重构微服务。如果使用队列的解决方案不能工作,因为服务使用者需要立即得到响应,那么我宁愿重新构造User和Wallet功能,将其配置在同一个服务中(或者至少在同一个VM中,以避免分布式事务)。是的,它离微服务更近了一步,也更接近了一个庞然大物,但它会让你免去一些头疼的事情。

所有分布式系统都存在事务一致性问题。最好的方法是像你说的那样,进行两阶段提交。在挂起状态下创建钱包和用户。创建该用户后,进行单独调用以激活该用户。

最后一个调用应该是安全可重复的(以防连接断开)。

这将需要最后一次调用知道两个表(这样就可以在单个JDBC事务中完成)。

或者,您可能想要考虑一下为什么您如此担心没有钱包的用户。你认为这会引起问题吗?如果是这样,也许将它们作为单独的rest调用是一个坏主意。如果用户没有钱包就不应该存在,那么您可能应该将钱包添加到用户中(在最初的POST调用中创建用户)。

恕我直言,微服务架构的一个关键方面是事务被限制在单个微服务中(单一责任原则)。

在当前示例中,User创建将是一个自己的事务。用户创建将把USER_CREATED事件推入事件队列。钱包服务将订阅USER_CREATED事件并创建钱包。

一个简单的解决方案是使用用户服务创建用户,并使用消息总线,其中用户服务发出其事件,钱包服务在消息总线上注册,监听用户创建的事件并为用户创建钱包。同时,如果用户进入钱包UI查看他的钱包,检查用户是否刚刚创建,并显示您的钱包正在创建中,请过一段时间再检查