采取这个概念上简单的任务:消耗队列,为每个条目发送电子邮件。
一个简单的方法是:
while true:
entry = queue.pop()
sendMail();
问题在于,如果消费者在弹出后但在发送邮件之前/期间崩溃,则邮件将丢失。所以你把它改成:
while true:
entry = queue.peek()
sendMail();
queue.pop();
但是现在,如果消费者在邮寄之后崩溃,但在弹出之前,邮件会在消费者重新开始时再次发送。
处理此问题的最佳做法是什么?
此处发送电子邮件只是一个替代任务关键任务的示例。还假设队列的弹出是已发送邮件的唯一记录,因此邮件子系统本身不会记录任何内容。
答案 0 :(得分:3)
我在这里提出两个解决方案。第一个是根据我的经验提出的设计(可以在进一步的头脑风暴后详细阐述),第二个是简短快速的解决方案。看看,思考,你可以选择适合你的。
如果您计划创建容错和高可用性队列系统,则必须解决您面临的主要挑战。
如何确保没有邮件丢失?
了解您的生产者和消费者:为了设计解决方案,首先我们需要了解我们的生产者和消费者。 单一生产者 - 单一消费者。单一生产者 - 多个消费者。多个生产者 - 多个消费者。 最好的方法是创建一个迎合多个生产者的机制 - 多个消费者,除此之外,还可以配置以满足三种情景中的任何一种。
下一个问题;我们该怎么做? 简单的答案,如果我们能以某种方式创建一个可配置的机制,它能够接收多条消息并将其广播给多个消费者。该机制还能够读取配置,验证消息(可选,您也可以在消费者中添加消息),存储消息一小段时间,跟踪确认,将一条消息分解为多个,将许多消息聚合为一个,具有'行动计划'在处理超时或失败时实施'行动 - 计划'。
阐述机制:让我们称这个机制为 Broker 。因此,在您的解决方案中,代理将按以下方式放置。实线箭头是消息,虚线箭头是确认
我在这里避免进入经纪人的详细设计,因为它将脱离背景。
处理失败 :确定可能的失败点 1.生产者 2.消费者 经纪人 4.网络
生产者失败: - 如果存在复制,并且备用生产者继续发送邮件而不影响功能,则吞吐量可能会受到影响,直到原始生产者再次启动并运行。
对于消费者故障和网络故障,代理可以维护一种机制,该机制将收到消息,直到收到确认(让我们称之为ack,为了简洁起见)。一旦接收到ack,就删除对应于ack的消息。
消费者必须以不同的方式处理这种情况。让我们说,消费者在州内保留以下变量 一个。上次收到的消息 湾消费者状态=(活跃,休眠,重新开始)。
消费者启动时,其价值可以是(重新启动)。消费者的最后收到的消息将根据从代理接收的每条消息进行更新,并且状态将更改为ACTIVE。如果消费者尝试向代理发送确认并且连接超时,或者网络出现问题,则状态将更改为DORMANT,并保留。对于RE-STARTED和DORMANT的两个场景,执行验证是否完成了Last Receive消息的处理。如果是,它会再次将ack发送给代理并等待下一条消息。此时,接收到下一条消息,状态可以更改为ACTIVE,处理可以正常开始。
另一方面,经纪人只保留最后发送的消息,直到收到确认。为了克服代理的故障,可以准备主从配置,其中,在第一个变得不可用的情况下,复制代理的状态并且将消息重定向到另一个代理。
使用@Marcin提出的JMS。我亲自处理过RabbitMQ(http://previous.rabbitmq.com/v3_4_x/features.html)并认为对于大多数分布式计算场景而言,这只会起作用。您可以配置高可用性(http://previous.rabbitmq.com/v3_4_x/ha.html),它还带有一个很好的用户界面,您可以在其中监控队列和消息。
但是,我们鼓励您查看符合您需求的JMS系统。
希望这有帮助
答案 1 :(得分:3)
你的要求似乎不是试图解决两个将军的问题(没有确定性的解决方案/限制)? https://en.wikipedia.org/wiki/Two_Generals%27_Problem
Peek - Process - Remove
您希望仅在确保成功处理后删除,并确保正确删除。好吧,任何这些消息都可能丢失/程序可能在任何步骤崩溃。
最强大的消息传递队列依赖于一组ack +重复尝试(交付)来获得所需的行为(直到acks回来)。
但实际上不可能在每种情况下保证完美的行为。您只需最终权衡赔率并在重复(至少尝试)处理和“永不”(无限内存等)丢失消息之间进行工程折衷 - 特定于您的实际应用程序需求。再次不是一个新问题:),并且你不太可能需要为它编写临时代码 - 就像我提到的那样,大多数MQ都解决了这个问题。
答案 2 :(得分:1)
您可以使用JMS队列。它为您提供交易。正确处理后,消息将从队列中删除。
答案 3 :(得分:1)
如果你认真对待这个陈述:
“还假设队列的弹出是已发送邮件的唯一记录,”
然后你无法保证可靠的加工性能。
证明: 假设我有一个假设程序,保证可靠的处理属性。运行程序一次,一旦尝试发送电子邮件,我们就会通过导致电子邮件失败并同时终止线程来“干预”。然后,运行程序(它将重新生成我认为的新线程),直到程序确定是否发送了电子邮件(这必须是程序中的一个点,否则程序无限期地运行而不会取得进展。)现在说我们记录了程序的操作并在并行Universe中播放(对随机数生成器的任何调用应返回相同的),我们通过使电子邮件成功并同时终止线程进行干预。该程序必须与之前完成相同的事情,这是一个矛盾,因为该程序应该保证可靠的处理属性而不记录是否发送了电子邮件,并且在其中一个模拟中它必须是关于是否发送电子邮件的错误。
这是一个使用电子邮件系统的解决方案,该系统会告诉您是否已发送电子邮件,并且只有在您尚未发送电子邮件时才会发送电子邮件。
while true:
task = queue.peek()
if (task_email_sent_already(task)){
//Then we failed after emailing but before pop
goto pop_step;
}
if (task.done){
//Then we failed after doing the task but before sending email
goto email_step;
}
//run_task needs to be written transactionally to set task.done on completion.
//Think transactional memory with persistent logging.
run_task(task);
LABEL email_step
send_email_if_already_not_sent(task);
LABEL pop_step
queue.pop()
如果同一个任务被调用两次,send_email_if_already_not_sent不发送两封电子邮件,这一点很重要,否则上述代码可能会导致重复的电子邮件(如果电子邮件成功,但在task_email_already_sent返回true之前有一些“延迟时间”)。 )
如果您假设电子邮件成功的时间长度,但在task_email_already_sent返回true之前,即假设没有电子邮件的运行时间超过5秒,那么您可以在本地日志中写入在特定时间X发送了一封电子邮件,然后在检查task_email_sent_already之前旋转到X + 5秒。但当然这是有风险的,因为如果某些电子邮件的发送时间超过5秒,您可能会发送重复的电子邮件。
答案 4 :(得分:0)
你必须分担责任。您执行的主要操作:
让我们为责任对象分配行动:
因此,基于此,您需要:一个单独的发件人,只发送一封电子邮件并通知该操作的状态;单独的电子邮件消费者,知道该做什么以及从何处获取数据;一个单独的操作结果处理程序。
它应该像这样工作:
消费者选择另一封电子邮件......
然而,还有另一种选择。处理程序和消费者可以在一个实体中联合起来,但这种方法会导致明显的障碍。此外,您可以有两个队列:第一个队列应包含需要发送的电子邮件,另一个队列 - 传递给发件人。他们只是通过了,我们不知道他们是否已经被送过。和以前一样,发送者应该是一个单独的实体,应该通知成功的发送操作。通知消费者时,它会从第一个队列和第二个队列中删除电子邮件。如果消费者中存在异常,它会查找第二个队列,查找未发送的电子邮件并重新发送。
异步互动和单一责任是我们最好的朋友。
答案 5 :(得分:0)
制作人的问题是它不知道消息是否已被完全处理。所以:让消费者确认电子邮件已经到达,然后只弹出邮件。
还剩下一个小问题:邮件已处理完毕,但无法再发送确认信息。要解决此问题,请为每封邮件提供与邮件一起发送的唯一ID。消费者可以识别重复项(但是,它必须以某种方式保留最新收到的ID,以便这种方法能够在崩溃中幸存下来......)。