Spring Integration:延迟后发布两次消息

时间:2014-11-12 19:38:44

标签: java multithreading delay spring-integration delayed-execution

我正在使用以下XML代码段:

<int-amqp:inbound-channel-adapter acknowledge-mode="MANUAL" channel="commandQueue" concurrent-consumers="${commandConsumers:10}"
                                  queue-names="commands" connection-factory="connectionFactory"/>
<int:channel id="commandQueue"/>
<int:channel id="commands"/>
<int:chain input-channel="commandQueue" output-channel="commands">
    <int:delayer id="commandDelayer" default-delay="30000"/>
    <int:json-to-object-transformer id="commandTransformer" type="com.airwatch.chat.command.Command"/>
</int:chain>

<int:payload-type-router input-channel="commands">
....
....

它正在执行这些任务:

  1. 从名为&#39;命令&#39;。
  2. 的RabbitMQ队列中获取消息
  3. 延迟邮件执行30秒。
  4. 在指定的延迟后继续执行消息。
  5. 如果在启动上述代码的应用程序之前消息已存在于命令队列中,则在启动时,应用程序会在单独的线程中执行两次消息。

    我想我知道为什么会这样。

    一旦应用程序上下文完全初始化,Spring重新安排DelayHandler的消息存储中持久存储的消息。请参阅以下DelayHandler.java的代码段:

    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (!this.initialized.getAndSet(true)) {
            this.reschedulePersistedMessages();
        }
    }
    

    因此,如果消息在应用程序启动之前已经存在于RabbitMQ队列中,则在Spring上下文初始化期间,将从队列中拾取消息并将其添加到DelayHandler的消息存储库中。上下文初始化完成后,如果同时消息未从消息存储库中释放,则上面的代码片段会重新安排相同的消息。

    现在,当两个单独的线程正在执行相同的消息时,如果一个线程已执行,则应从消息存储库中删除该消息,而另一个线程不应继续执行。

    当线程被执行时,来自DelayHandler.java的下面一段代码允许第二个线程释放重复的消息,导致同一消息的重复执行,因为消息存储是SimpleMessageStore的一个实例,并且没有进一步检查以停止执行。

    private void doReleaseMessage(Message<?> message) {
        if (this.messageStore instanceof SimpleMessageStore
                || ((MessageStore) this.messageStore).removeMessage(message.getHeaders().getId()) != null) {
            this.messageStore.removeMessageFromGroup(this.messageGroupId, message);
            this.handleMessageInternal(message);
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("No message in the Message Store to release: " + message +
                        ". Likely another instance has already released it.");
            }
        }
    }
    

    这是Spring Integration中的错误吗?

1 个答案:

答案 0 :(得分:3)

哦,好吧!

这真是一个很好的小虫子。

感谢您指出这一点!

请提出JIRA issue,我们会在下一个版本中对此进行处理。

我可以解释发生了什么。

所有Spring Integration都从Lifecycle.start()开始工作。在您的情况下,<int-amqp:inbound-channel-adapter>从RabbitMQ接收消息并将其发送到集成流程。他们是delayed

仅在start之后,应用程序上下文才会引发ContextRefreshedEvent。即使DelayHandler收到来自messageStore的所有邮件,并且正如您所知,reschedules这些邮件也会被捕获。

因此,是的,我们可能有两个针对同一消息的预定任务。

有趣的是它仅适用于SimpleMessageStore,因为它对存储到removeMessage的邮件没有groups函数。

我看到几种变体作为解决方法:

  1. 延迟start的{​​{1}}。例如,从<int-amqp:inbound-channel-adapter>处理相同的ContextRefreshedEvent并将<inbound-channel-adapter>命令消息发送到@amqpAdapter.start()

  2. 自Spring Integration 4.1起,另一个选项可用,其名称为Idempotent Receiver。使用它可以放弃<control-bus>消息,我猜duplicate正好是idempotentKey。清洁Idempotent接收器模式!

  3. 还有一个选项位于messageId persistent下,我们真的可以依赖MessageStore操作。

  4. 关于此事的JIRA门票:https://jira.spring.io/browse/INT-3560