如何将消息保存到数据库中并最终将响应发送到主题中?

时间:2019-07-03 10:38:01

标签: java transactions rabbitmq messagebroker eventual-consistency

我有以下RabbitMq使用者:

Consumer consumer = new DefaultConsumer(channel) {
    @Override
     public void handleDelivery(String consumerTag, Envelope envelope, MQP.BasicProperties properties, byte[] body) throws IOException {
            String message = new String(body, "UTF-8");
            sendNotificationIntoTopic(message);
            saveIntoDatabase(message);
     }
};

可能发生以下情况:

  1. 消息已成功发送到主题
  2. 与数据库的连接丢失,因此数据库插入失败。

结果是我们的数据不一致。

预期结果或者两个操作都成功执行了,或者根本没有执行。

任何解决方案如何实现?

P.S。

目前我有以下想法(请评论)

我们可以假设经纪人不会丢失任何消息。

我们必须订阅要发送的主题。

  1. 将条目保存到数据库中,并将字段status设置为值'pending'
  2. 尝试将数据发送到主题。如果发送成功-用值“成功”更新字段status
  3. 我们必须完成一项任务,即必须检查状态为待处理的行。目前可能有2种情况:
    3.1通知根本没有发送
    3.2已发送通知,但保存到数据库失败(概率很低,但可能)

    因此,我们必须以某种方式区分这两种情况:我们可以将主题中的消息存储在集合中,作业可以检查消息是否被接受。因此,如果作业找到一条与数据库行相对应的消息,我们必须将状态更新为“成功”。否则,我们必须从数据库中删除条目。

我认为我的想法有一些弱点(例如,如果我们有多节点应用程序,则必须将消息存储在hazelcast(或类似物)中,但这是假设失败的另一点)

4 个答案:

答案 0 :(得分:1)

以下是尝试取消确认模式https://servicecomb.apache.org/docs/distributed_saga_3/的示例,该模式应能够解决您的问题。您应该容忍通过队列两次提交数据的机会。这是一个示例:

  1. 定义抽象操作,并为该操作分配ID和时间戳。
  2. 写入状态挂接到数据库(您可以在与1相同的步骤中完成此操作)
  3. 编写一个侦听器,该侦听器将轮询数据库以查找状态为暂挂且早于“超时”的所有操作
  4. 对于每个待处理的操作,请通过具有指定ID的队列发送数据。
  5. 接收方应知道该ID,并且如果ID已被处理,则什么也不会发生。

6A 。如果您需要100%完成操作,则需要第二个队列,收件人方将在该队列中发布消息ID-DONE。如果不需要这种一致性,请跳过此步骤。或者,它可以发布ID-失败的失败原因。

6B 。提交方通过将状态DONE写入数据库来等待6A的消息来完成操作。

  • 一旦超过常规的超时时间或已超过某些重试限制。您将状态写入失败操作。
  • 您可以通过回退ID潜在地向收件人的操作方发送消息。

请注意,所有这些步骤均不涉及技术交易。您可以使用非事务性数据库来做到这一点。

我写的是“尝试取消确认模式”的一种变体,其中每个邮件接收者都应该知道如何管理自己的数据。

答案 1 :(得分:0)

  1. 在侦听器中,使用字段staus ='pending'保存数据库行
  2. 另一个作业(单独的线程)将从DB获得所有待处理的行,并为每行获取以下内容:
    2.1将数据发送到主题
    2.2保存到数据库

如果我们在步骤1上失败-一切正常-数据处于一致状态,因为作业对该数据一无所知

如果我们在步骤2.1上失败-没问题,下一个作业调用将尝试处理

如果我们在步骤2.2上失败-如果我们在此处失败-这意味着下一个作业调用将再次处理相同的数据。乍一看,您可以认为这是一个问题。但是您的使用者必须是幂等的-这意味着它必须了解消息已被处理并跳过处理。此要求是所有消息代理都保证消息将至少一次传递的结果。因此,我们的消费者无论如何都要准备好重复的消息。没问题了。

答案 2 :(得分:0)

如果有足够的时间修改设计,建议使用类似JTA的API来管理2phase提交。甚至weblogic和WebSphere也支持XA资源进行两阶段提交。

如果时间轴较短,建议执行以下操作以减少故障间隔。

  • 发送数据主题(不提交)(如果主题关闭,请重试间隔)
  • 将数据写入数据库
  • 提交数据库
  • 提交主题

仅当第4步失败时,这里才会发生失败。这将导致相同的消息再次发送。因此接收系统将收到重复的消息。每个消息在JMS2.0结构中都有唯一的messageID和CorrelationID。因此,找到重复项有点直截了当(但这将在接收系统中处理)

这两种情况也适用于集群环境。


针对您的情况严格,认为以下步骤可能有助于解决您的问题

为您的主题订阅一个listener listener-1。

进程1

  • 为消息msg-1添加状态为“待发送”的数据库条目
  • 发送消息msg-1到主题。如果任何主题失败,请重试发送 如果第2步在重试后失败,则process-1必须在发送任何新消息之前重新发送msg-1,或者第1步要回滚

Listener-1

  • 使用订阅的侦听器,从Topic中读取引用(meesageID / correlationID),并将数据库状态更新为SENT,并从主题中读取/删除消息。如果参考读取成功且数据库更新失败,则主题仍会出现消息。因此,下次阅读将更新数据库。如果数据库更新成功且消息删除失败。侦听器将再次读取并尝试更新已完成的消息。因此验证后可以忽略。

如果侦听器本身处于关闭状态,则主题将具有消息,直到侦听器读取消息为止。在此之前,SENT消息将处于“待发送”状态。

答案 3 :(得分:0)

这是我的操作方式的伪代码:(假设dao层具有事务处理能力,而您的消息传递层则没有)

    //Start a transaction
    try {
                String message = new String(body, "UTF-8");
               // Ordering is important here as I'm assuming the database has commit and rollback capabilities, but the messaging system doesnt. 
                saveIntoDatabase(message);
                sendNotificationIntoTopic(message);

    } catch (MessageDeliveryException) {
        // rollback the transaction
        // Throw a domain specific exception
    }
   //commit the transaction

场景:
1.如果数据库失败,则不会发送该消息,因为异常会破坏代码流。
2.如果数据库调用成功并且消息传递系统无法传递,请捕获异常并回滚数据库更改

记录和重播故障所需的所有操作都可以在此方法之外