RabbitMQ消息队列(九):Publisher的消息确认机制

来源:互联网 发布:java链表 编辑:程序博客网 时间:2024/06/14 18:15
在前面的文章中提到了queue和consumer之间的消息确认机制:通过设置ack。那么Publisher能不到知道他post的Message有没有到达queue,甚至更近一步,是否被某个Consumer处理呢?毕竟对于一些非常重要的数据,可能Publisher需要确认某个消息已经被正确处理。

      在我们的系统中,我们没有是实现这种确认,也就是说,不管Message是否被Consume了,Publisher不会去care。他只是将自己的状态publish给上层,由上层的逻辑去处理。如果Message没有被正确处理,可能会导致某些状态丢失。但是由于提供了其他强制刷新全部状态的机制,因此这种异常情况的影响也就可以忽略不计了。

     对于某些异步操作,比如客户端需要创建一个FileSystem,这个可能需要比较长的时间,甚至要数秒钟。这时候通过RPC可以解决这个问题。因此也就不存在Publisher端的确认机制了。

     那么,有没有一种机制能保证Publisher能够感知它的Message有没有被处理的?答案肯定的。在这里感谢笑天居士同学:他在我的《RabbitMQ消息队列(三):任务分发机制》文后留言一起讨论了问题,而且也查找了一些资料。在这里我整理了一下他转载和一篇文章和原创的一篇文章。衔接已经附后。


1. 事务机制 VS Publisher Confirm

       如果采用标准的 AMQP 协议,则唯一能够保证消息不会丢失的方式是利用事务机制 -- 令 channel 处于 transactional 模式、向其 publish 消息、执行 commit 动作。在这种方式下,事务机制会带来大量的多余开销,并会导致吞吐量下降 250% 。为了补救事务带来的问题,引入了 confirmation 机制(即 Publisher Confirm)。

     为了使能 confirm 机制,client 首先要发送 confirm.select 方法帧。取决于是否设置了 no-wait 属性,broker 会相应的判定是否以 confirm.select-ok 进行应答。一旦在 channel 上使用 confirm.select方法,channel 就将处于 confirm 模式。处于 transactional 模式的 channel 不能再被设置成 confirm 模式,反之亦然。
    一旦 channel 处于 confirm 模式,broker 和 client 都将启动消息计数(以 confirm.select 为基础从 1 开始计数)。broker 会在处理完消息后,在当前 channel 上通过发送 basic.ack 的方式对其进行 confirm 。delivery-tag 域的值标识了被 confirm 消息的序列号。broker 也可以通过设置 basic.ack 中的 multiple 域来表明到指定序列号为止的所有消息都已被 broker 正确的处理了。

       在异常情况中,broker 将无法成功处理相应的消息,此时 broker 将发送 basic.nack 来代替 basic.ack 。在这个情形下,basic.nack 中各域值的含义与 basic.ack 中相应各域含义是相同的,同时 requeue 域的值应该被忽略。通过 nack 一或多条消息broker 表明自身无法对相应消息完成处理,并拒绝为这些消息的处理负责。在这种情况下,client 可以选择将消息 re-publish 。

     在 channel 被设置成 confirm 模式之后,所有被 publish 的后续消息都将被 confirm(即 ack) 或者被 nack 一次。但是没有对消息被 confirm 的快慢做任何保证,并且同一条消息不会既被 confirm 又被 nack 。


2. 消息在什么时候确认

broker 将在下面的情况中对消息进行 confirm :
  • broker 发现当前消息无法被路由到指定的 queues 中(如果设置了 mandatory 属性,则 broker 会先发送 basic.return
  • 非持久属性的消息到达了其所应该到达的所有 queue 中(和镜像 queue 中)
  • 持久消息到达了其所应该到达的所有 queue 中(和镜像 queue 中),并被持久化到了磁盘(被 fsync)
  • 持久消息从其所在的所有 queue 中被 consume 了(如果必要则会被 acknowledge)

broker 会丢失持久化消息,如果 broker 在将上述消息写入磁盘前异常。在一定条件下,这种情况会导致 broker 以一种奇怪的方式运行。例如,考虑下述情景:

   1.  一个 client 将持久消息 publish 到持久 queue 中
   2.  另一个 client 从 queue 中 consume 消息(注意:该消息具有持久属性,并且 queue 是持久化的),当尚未对其进行 ack
   3.  broker 异常重启
   4.  client 重连并开始 consume 消息

   在上述情景下,client 有理由认为消息需要被(broker)重新 deliver 。但这并非事实:重启(有可能)会令 broker 丢失消息。为了确保持久性,client 应该使用 confirm 机制。如果 publisher 使用的 channel 被设置为 confirm 模式,publisher 将不会收到已丢失消息的 ack(这是因为 consumer 没有对消息进行 ack ,同时该消息也未被写入磁盘)。


3. 编程实现

首先要区别AMQP协议mandatory和immediate标志位的作用。

mandatory和immediate是AMQP协议中basic.pulish方法中的两个标志位,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。具体区别在于:
1. mandatory标志位
当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者;当mandatory设为false时,出现上述情形broker会直接将消息扔掉。
2. immediate标志位
当immediate标志位设置为true时,如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。

具体的代码参考请参考参考资料1.

下面是用rabbitmq-c实现publish消息确认的代码:

/* * * gcc -o amqp_sendstring amqp_sendstring.c utils.c -I/usr/local/rabbitmq-c/include -L/usr/local/rabbitmq-c/lib -lrabbitmq * */#include <stdlib.h>#include <stdio.h>#include <string.h>#include <sys/time.h>#include <unistd.h>#include <stdint.h>#include <amqp_tcp_socket.h>#include <amqp.h>#include <amqp_framing.h>#include "utils.h"int main(int argc, char const *const *argv){  char const *hostname;  int port, status;  char const *exchange;  char const *routingkey;  char const *messagebody;  amqp_socket_t *socket = NULL;  amqp_connection_state_t conn;  if (argc < 6) {    fprintf(stderr, "Usage: amqp_sendstring host port exchange routingkey messagebody\n");    return 1;  }  hostname = argv[1];  port = atoi(argv[2]);  exchange = argv[3];  routingkey = argv[4];  messagebody = argv[5];  conn = amqp_new_connection();  socket = amqp_tcp_socket_new(conn);  if (!socket) {    die("creating TCP socket");  }  status = amqp_socket_open(socket, hostname, port);  if (status) {    die("opening TCP socket");  }  die_on_amqp_error(amqp_login(conn, "/", 0, 131072, 0, AMQP_SASL_METHOD_PLAIN, "guest", "guest"),                    "Logging in");  amqp_channel_open(conn, 1);  die_on_amqp_error(amqp_get_rpc_reply(conn), "Opening channel");  {    amqp_basic_properties_t props;    props._flags = AMQP_BASIC_CONTENT_TYPE_FLAG | AMQP_BASIC_DELIVERY_MODE_FLAG;    props.content_type = amqp_cstring_bytes("text/plain");    props.delivery_mode = 2; /* persistent delivery mode */    amqp_confirm_select(conn, 1);  //在通道上打开Publish确认    die_on_error(amqp_basic_publish(conn,                                    1,                                    amqp_cstring_bytes(exchange),                                    amqp_cstring_bytes(routingkey),                                    0,   //mandatory标志位,消息不能到达队列则返回basic.return                                    0,   //immediate标志位,消息不能到达消费者返回basic.return                                    &props,                                    amqp_cstring_bytes(messagebody)),                 "Publishing");  }  {    /* Publish消息后需要在当前通道上监听返回的信息,来判断消息是否成功投递     * 这里要息根据投递消息的方式来过滤判断几个方法     */    amqp_frame_t frame;    amqp_rpc_reply_t ret;    if (AMQP_STATUS_OK != amqp_simple_wait_frame(conn, &frame)) {      return;    }    if (AMQP_FRAME_METHOD == frame.frame_type) {      amqp_method_t method = frame.payload.method;      fprintf(stdout, "method.id=%08X,method.name=%s\n",        method.id, amqp_method_name(method.id));      switch (method.id) {        case AMQP_BASIC_ACK_METHOD:{          /* if we've turned publisher confirms on, and we've published a message           * here is a message being confirmed           */          {            amqp_basic_ack_t *s;            s = (amqp_basic_ack_t *) method.decoded;            fprintf(stdout, "Ack.delivery_tag=%d\n", s->delivery_tag);            fprintf(stdout, "Ack.multiple=%d\n", s->multiple);          }          break;        case AMQP_BASIC_NACK_METHOD:          /* if we've turned publisher confirms on, and we've published a message           * here is a message not being confirmed           */          {            amqp_basic_nack_t *s;            s = (amqp_basic_nack_t *) method.decoded;            fprintf(stdout, "NAck.delivery_tag=%d\n", s->delivery_tag);            fprintf(stdout, "NAck.multiple=%d\n", s->multiple);            fprintf(stdout, "NAck.requeue=%d\n", s->requeue);          }          break;        case AMQP_BASIC_RETURN_METHOD:          /* if a published message couldn't be routed and the mandatory flag was set           * this is what would be returned. The message then needs to be read.           */          {            amqp_message_t message;            amqp_basic_return_t *s;            char str[1024];            s = (amqp_basic_return_t *) method.decoded;            fprintf(stdout, "Return.reply_code=%d\n", s->reply_code);            strncpy(str, s->reply_text.bytes, s->reply_text.len); str[s->reply_text.len] = 0;            fprintf(stdout, "Return.reply_text=%s\n", str);            ret = amqp_read_message(conn, frame.channel, &message, 0);            if (AMQP_RESPONSE_NORMAL != ret.reply_type) {              return;            }            strncpy(str, message.body.bytes, message.body.len); str[message.body.len] = 0;            fprintf(stdout, "Return.message=%s\n", str);            amqp_destroy_message(&message);          }          break;        case AMQP_CHANNEL_CLOSE_METHOD:          /* a channel.close method happens when a channel exception occurs, this           * can happen by publishing to an exchange that doesn't exist for example           *           * In this case you would need to open another channel redeclare any queues           * that were declared auto-delete, and restart any consumers that were attached           * to the previous channel           */          return;        case AMQP_CONNECTION_CLOSE_METHOD:          /* a connection.close method happens when a connection exception occurs,           * this can happen by trying to use a channel that isn't open for example.           *           * In this case the whole connection must be restarted.           */          return;        default:          fprintf(stderr ,"An unexpected method was received %d\n", frame.payload.method.id);          return;      }    }  }  }  die_on_amqp_error(amqp_channel_close(conn, 1, AMQP_REPLY_SUCCESS), "Closing channel");  die_on_amqp_error(amqp_connection_close(conn, AMQP_REPLY_SUCCESS), "Closing connection");  die_on_error(amqp_destroy_connection(conn), "Ending connection");  return 0;}


参考资料:

1. http://blog.csdn.NET/jiao_fuyou/article/details/21594205

2. http://blog.csdn.net/jiao_fuyou/article/details/21594947

3.  http://my.oschina.Net/moooofly/blog/142095

阅读全文
0 0
原创粉丝点击