Spring Cloud Stream Kafka - 如何实现幂等性以支持分布式事务管理(最终一致性)

时间:2017-02-13 12:34:23

标签: spring-cloud microservices distributed-transactions spring-cloud-stream spring-kafka

我有以下典型情况:

  • 用于购买产品的订单服务。充当分布式交易的指挥官。
  • 包含产品及其库存清单的产品服务。
  • 付款服务。

        Orders DB               Products DB
           |                       |
    ---------------         ----------------          ----------------
    | OrderService  |       | ProductService |        | PaymentService |
     ---------------         ----------------          ----------------
           |                       |                         |
           |                --------------------             |
           --------------- | Kafka orders topic |-------------
                           ---------------------
    

正常流程将是:

  1. 用户订购产品。
  2. 订单服务在数据库中创建订单,并在Kafka主题“订单”中发布消息以保留产品(PRODUCT_RESERVE_REQUEST)。
  3. 产品服务会减少其数据库中的一个单位的产品库存,并在“订单”中发布消息,说明PRODUCT_RESERVED
  4. 订单服务获取PRODUCT_RESERVED条消息并命令付款发布消息PAYMENT_REQUESTED
  5. 付款服务通过PAYED
  6. 消息订购付款和答案
  7. 订单服务读取PAYED消息并将订单标记为已完成,完成交易。
  8. 我无法处理错误案例,例如:让我们假设:

    1. 付款服务无法对产品收费,因此会发布消息PAYMENT_FAILED
    2. 订单服务会对发布消息UNDO_PRODUCT_RESERVATION
    3. 作出反应
    4. 产品服务会增加数据库中的库存以取消预订并发布PRODUCT_UNRESERVATION_COMPLETED
    5. 订单服务完成交易,将订单的最终状态保存为CANCELLED_PAYMENT_FAILED。
    6. 在这种情况下,想象一下,无论出于何种原因,订单服务都会发布UNDO_PRODUCT_RESERVATION消息,但不会收到PRODUCT_UNRESERVATION_COMPLETED消息,因此它会重试发布另一个UNDO_PRODUCT_RESERVATION消息。

      现在,假设同一订单的两个UNDO_PRODUCT_RESERVATION消息最终到达ProductService。如果我处理它们,我最终可能会为产品设置无效库存。

      在这种情况下,我如何实现幂等性?

      更新

      按照Artem的说明我现在可以检测到重复的消息(通过检查消息头)并忽略它们但是仍然可能存在以下情况,我不应该忽略重复的消息:

      1. 订单服务发送UNDO_PRODUCT_RESERVATION
      2. 产品服务获取消息并开始处理,但在更新库存之前崩溃。
      3. 订购服务未收到回复,因此重试发送UNDO_PRODUCT_RESERVATION
      4. 产品服务知道这是一个重复的消息,但在这种情况下,它应该重复处理。
      5. 你能帮我提出一种支持这种情况的方法吗?我怎么能区分何时应该丢弃该消息或重新处理它?<​​/ p>

2 个答案:

答案 0 :(得分:2)

我们使用spring-integration-kafka在我们的微服务中使用Kafka生成和使用消息。在我们的例子中,我们将org.springframework.messaging.Message对象发送到主题,并在从字节数组反序列化后从主题中获取相同的类型。在Message实体中,除了消息有效负载之外还有消息ID,发送时间等标头值,这是您要从一个微服务转移到其他服务器的实际对象。我们使用唯一的message-id值来实现幂等性。在生产者方面,您必须实现一些逻辑以确保Message的message-id在多次生成时是相同的。这实际上与您的产品逻辑有关。在我们的例子中,我们使用发布事件使用本地事务,这在Chris Richardson的博客https://www.nginx.com/blog/event-driven-data-management-microservices/中有很好的描述。使用这种方法,我们可以在生产者端使用相同的message-id重新调用Message对象。在消费者方面,我们将所有消耗的消息id值保存到数据库,并在处理收到的消息之前检查此ID。如果我们看到一条消息的id在我们的持久存储中,我们就会忽略它。

在您的情况下,要实现幂等性:

  • 您应该在邮件中保留唯一标识符
  • 在生产者方面,您必须多次生成相同的标识符,
  • 在消费者方面,您必须检查收到的ID以检测它是否在之前消耗

关于UPDATE中描述的第二个场景,

我认为你应该改变主意。如果要实现更适合微服务架构的发布 - 订阅机制,则不应该在生产者端等待响应。在这种情况下,您等待其他消息以了解消费者是否消费该消息,如果消费者没有使用该消息,则再次发送消息。

下面的实施情况如何; 在生产者方面,您在生产者的交易中向Kafka发送消息。您应该提供一种机制来向kafka发送消息,只提交生产者端的事务。这是Atomicity问题,我给出了一个链接,上面显示了如何解决这个问题。

在消费者方面,您按顺序逐个轮询来自kafka主题的消息,并且只有在可以使用当前消息时才能获得下一条消息。如果没有消费,你就不应该得到下一条消息。因为下一条消息可能与当前消息有关,并且如果您使用下一条消息,则可能会损坏数据的一致性。消息没有消耗时,它不是生产者关心的问题。在消费者方面,您应该提供重试和重放机制来使用消息。

我认为你不应该等待生产者方面的回应。 Kafka是一个非常智能的工具,凭借其偏移提交功能,作为消费者,当您从主题轮询消息时,您不必使用消息。如果您在处理消息时遇到问题,则只需提交偏移量即可获取下一条消息。

通过上述实施,您不会遇到像&#34; 这样的问题我如何区分何时应丢弃该消息或重新处理该消息?&#34;

...问候

答案 1 :(得分:-1)

实际上,由于您提到的关于通过Apache Kafka组织多个微服务交易的复杂性,我开发了另一个概念并写了一篇关于它的博客。

如果您达到复杂状态,Kafka解决方案可能不再可行,您可能会发现它是一个有趣的读物。这里解释的时间太长了,但基本上它完全采用了Micro服务原则的J2EE容器,并且在Spring Boot + Netflix的帮助下,Micro Services之间提供了完整的事务支持。

Micro Services Fanout and Transaction Problems and Solutions with Spring Boot and Netflix