RabbitMQ info

RabbitMQ 简介

  1. AMQP是什么?

    1
    2
    AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计.
    基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制.
  2. AMQP的范围:

    1
    2
    3
    4
    为了完全实现消息中间件的互操作性,需要充分定义*网络协议*和*消息代理服务*的功能语义
    因此,AMQP定义网络协议和代理服务如下:
    * 一套确定的消息交换功能,也就是"高级消息交换协议模型".AMQP模型包括一套用于路由和存储消息的功能模块,以及一套在这些模块之间交换消息的规则.
    * 一个网络线级协议(数据传输格式),客户端应用可以通过这个协议与消息代理和它实现的AMQP模型进行交互通信.
  3. AMQP模型

    1
    2
    3
    4
    5
    6
    在服务器中,三个主要功能模块连接成一个处理链完成预期的功能:
    * "exchange"接收发布应用程序发送的消息,并根据一定的规则将这些消息路由到"消息队列"
    * "message queue"存储消息,直到这些消息被消费者安全处理完为止
    * "binding"定义了exchange 和 message queue 之间的关联,提供路由规则

    一个AMQP服务器类似于邮件服务器,exchange类似于消息传输代理(email里的概念),message queue类似于邮箱.Binding定义了每一个传输代理中的消息路由表,发布者将消息发给特定的传输代理,然后传输代理将这些消息路由到邮箱中,消费者从这些邮箱中取出消息.
  4. AMQP 消息交换体系结构

    1
    2
    3
    4
    5
    6
    * 存储转发(多个消息发送者,单个消息接收者)
    * 分布式事务(多个消息发送者,多个消息接收者)
    * 发布订阅(多个消息发送者,多个消息接收者)
    * 基于内容的路由(多个消息发送者,多个消息接收者)
    * 文件传输队列(多个消息发送者,多个消息接收者)
    * 点对点连接(单个消息发送者,单个消息接收者)
  5. 理解消息通信

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    1. 生产者和消费者
    RabbitMQ在应用程序和服务器之间扮演者路由器的角色.所以当应用程序连接RabbitMQ时,它会决定是发送还是接收
    1. 生产者生产消息,然后发布到代理服务器(RabbitMQ).
    消息分为两部分:
    * 有效载荷:你要传输的数据
    * 标签:它描述了有效载荷,并且RabbitMQ用它来决定谁将获得消息的拷贝
    2. 消费者消费消息,先连接到代理服务器(RabbitMQ),并订阅到队列上
    当消费者接收到消息时,它只得到消息的一部分:有效载荷.在消息的路由过程中,消息的标签并没有随有效载荷一起传递
    生产者创建消息,消费者接收消息.在发布消息或接收消息之前必须先连接RabbitMQ,建立一条信道(channel)
    2. 队列
    AMPQ消息路由必须包含三部分:
    * 路由器: 生产者把消息发送到路由器上
    * 队列: 消息最终到达队列,并被消费者接收
    * 绑定: 决定了消息如何从路由器路由到特定的队列上

    消费者通过两种方式从特定的队列中接收消息:
    * 通过AMQP的base.consume命令订阅(持续订阅)
    * 通过AMQP的base.get命令(单条订阅)
    当至少一个消费者订阅了队列, 消息会立即发送给这些订阅的消费者;如果消息到达了无人订阅的队列,消息会在队列中等待,一旦有消费者订阅该队列,那么队列上的消息会立即发送给消费者.当有多个消费者订阅到同一个队列时,队列收到的消息将以循环的方式发送给消费者,**每条消息只会发送给一个订阅消费者**.
    例如:消费者A和B都订阅了队列send
    1. Message_A发送到队列send
    2. RabbitMQ将消息Message_A发送给消费者A
    3. 消费者A确认收到了消息Message_A
    4. RabbitMQ将消息Message_A从队列send中删除
    5. Message_B发送到队列send
    6. RabbitMQ将消息Message_B发送给消费者B
    7. 消费者B确认收到了消息Message_B
    8. RabbitMQ将消息Message_B从队列send中删除
    注意:
    * 消费者接收到的每一条消息都必须进行确认(通过AMQP的base.ack命令或在订阅队列的时候将auto_ack设置为true).
    * 消费者对消息的确认和告诉生产者消息已经被接收了这两件事情毫无相关.因此,消费者通过确认命令告诉RabbitMQ它已经正确的接收到了消息,同时RabbitMQ才能安全的把消息从队列中移除
    * 如果消费者接收一条消息,然后确认之前从Rabbit处断开连接,RabbitMQ会认为这条消息没有分发,它会从新分发给下一个订阅的消费者
    * 如果你的程序崩溃了,这样做一方面保证消息会被发送给下一个消费者处理;另一方面,如果应用程序有Bug而忘记确认消息,RabbitMQ不会给该消费者发送更多消息,这是因为在上一条消息被确认之前,RabbitMQ会认为这个消费者还没有准备好接收下一条消息

    在收到消息后,如果你想明确拒绝而不是确认收到该消息,你有两个选择:
    * 把消费者从RabbitMQ服务器断开连接,这会导致RabbitMQ会重新将消息发送给另一个消费者
    优点:所有RabbitMQ都支持
    缺点:这样连接/断开的连接方式会增加RabbitMQ的负担
    * RabbitMQ >= 2.0.0版本,使用AMPQ的base.reject命令
    如果把reject的requeue参数设置为true,RabbitMQ会把消息发送给下一个消费者
    如果把reject的requeue参数设置为false,RabbitMQ会立即把消息从队列中移除,而不会把它发送给新的消费者

    创建队列:
    生产者和消费者都能使用AMPQ的queue.declare命令创建队列
    如果消费者在同一条信道上订阅了另一个队列,就无法声明队列了.必须首先取消订阅,将信道设置为"传输"模式
    创建队列时需要指定队列名称,消费者订阅时需要队列名称,在创建绑定时也需要队列名称.如果在创建队列时没有指定队列名称,RabbitMQ会分配一个随机名称并在queue.declare命令的响应中返回
    创建队列有用参数:
    * exclusive
    设置true,队列将变为私有的,此时只有你的应用程序才能够消费队列消息.当你限制一个队列只有一个消费者时有用
    * auto-delete
    当最后一个消费者取消订阅时,队列会自动移除
    * 如果你只想检测队列是否存在,可设置queue.declare的passive选项为true.在该设置下,如果队列存在返回成功;如果队列不存在返回错误

    队列是AMPQ的基础模块:
    * 为消息提供了处所,消息在此等待消费
    * 对负载均衡来说,队列是绝佳方案.只需附加一堆消费者,并让RabbitMQ循环的方式均匀分发消息
    * 队列是RabbitMQ消息的最终点(除非消息进入了黑洞)

    3. 交换器和绑定
    将你想将消息投递给队列时,你通过把消息发送给交换器来完成.然后,根据确定的规则,RabbitMQ会决定消息该投递到哪个队列.这些规则成为路由键
    **队列通过路由键来绑定到交换器.**
    当你把消息发送到代理服务器时,消息将有一个路由键(即便是空的),RabbitMQ也会将其和绑定的路由键进行匹配.如果匹配成功,那么消息将投递到该队列;如果匹配不成功,消息将进入黑洞

    服务器会根据路由键将消息从交换器路由到队列,如何处理投递多个队列呢?
    协议中定义的不通类型交换器发挥了作用:
    * direct
    * fanout
    * topic
    * headers
    每一类实现了不通的路由算法.

    1. headers交换器允许你匹配AMQP消息的header而非路由键,除此之外,headers交换器和direct交换器完全一致,但性能会差很多
    2. direct
    **如果路由键匹配,消息就会被投递到对应的队列**
    服务器必须实现direct类型交换器,包含一个空白字符串名称的默认交换器
    当声明一个队列时,它会自动绑定到默认交换器,并以队列名称作为路由键.
    例如: $channel->baseic_publish($msg,'','queue-name')
    第一个参数表示你想发送的内容
    第二个参数为空,表示指定了默认的交换器
    第三个参数就是路由键,即队列名字
    当默认的direct交换器无法满足应用程序的需求时,你可以声明自己的交换器.只需发送exchange.declare命令并设置合适的参数
    3. fanout
    **会将收到的消息广播到绑定的队列上**
    当你发送一条消息到fanout交换器时,它会把消息投递给所有附加在此交换机的队列
    4. topic
    **允许来自不同源头的消息能够到达同一个队列**

    声明队列msg-inbox-errors,并将其绑定到交换器上
    $channel->queue_bind('msg-inbox-errors','logs-exchange','error.msg-inbox')
    error.msg-inbox表示绑定规则
    4. 虚拟主机和隔离
    每一个RabbitMQ服务器都能创建虚拟消息服务器,我们称为虚拟主机vhost.
    每一个vhost本质上都一个mini版的RabbitMQ服务器,拥有自己的队列,交换器和绑定,更重要的是拥有自己的权限机制
    vhost之于RabbitMQ就像虚拟机之于物理服务器一样:它们通过在各个实例间提供逻辑上的分离,允许你为不同应用程序安全保密的运行数据
    vhost是AMQP概念的基础,你必须在连接时进行制定.默认的vhost:"/"

    AMQP它并没有指定权限控制是在vhost级别还是在服务器端级别实现
    当你在RabbitMQ里面创建一个用户时,用户通常会被指派至少一个vhost,并且只能访问被指派vhost内的队列,交换器和绑定
    注意:vhost之间是绝对隔离的
    vhost和权限控制非常独特,它们是AMQP中唯一无法通过AMQP协议创建的基元.需要通过rabbitmqctl工具创建
    例如:
    * rabbitmqctl add_vhost vhost-name
    * rabbitmqctl delete_vhost vhost-name
    * rabbitmqctl list_vhost
    5. 持久化和你的策略
    默认情况下,重启RabbitMQ服务器后,那些队列和交换器就消失了.原因在于每个队列和交换器的durable属性,该属性为false,它决定了RabbitMQ是否在崩溃或者重启之后重建队列.该属性为true,你就不需要在服务器断电重启后重新创建队列和交换器了

    在发布消息之前,通过把它的"投递模式"(delivery mode)选项设置为2来把消息标记成持久化.到目前为止,消息还只是被表示为持久化的,但是它还必须被发布到持久化交换器中并到达持久化队列中才行.
    如果消息想从RabbitMQ崩溃中恢复,那么消息必须:
    * 把它的投递模式设置为2(持久)
    * 发送到持久化交换器
    * 到达持久化队列

    RabbitMQ确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件.当发布一条持久性消息到交换器时,RabbitMQ会在消息提交到日志文件后才发送响应.记住,之后这条消息如果路由到了非持久队列,它会自动从持久性日志中移除,并且无法从服务器重启中恢复.如果你使用持久性消息,则确保之前提到的持久性消息的那三点必须做到位.一旦你从持久化队列中消费了一条持久性消息,RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集.在你消费持久化消息之前,如果RabbitMQ重启的话,服务器会自动重建交换器和队列,绑定,重播持久性日志文件中消息到合适的队列或者交换器上

    持久化消息能保证RabbitMQ重启后数据的恢复,同时你也要为此付出性能代价.

    和持久化相关的一个概念是AMQP事务.
    在AMQP中,在把信道设置成事务模式后,你通过信道发送那些想要确认的消息,之后还有多个其他AMQP命令.这些命令是执行还是忽略,取决于第一条消息发送是否成功.一旦你发送完所有命令,就可以提交事务了.
    事务填补了生产者发布消息以及RabbitMQ将它们提交到磁盘上这两者之间"最有1英里"的差距

    RabbitMQ事务消耗RabbitMQ性能,RabbitMQ团队拿出最好的方案来保证消息投递:发送方确认模式
    你需要告诉RabbitMQ将信道设置成confirm模式,而且你只能通过重新创建信道来关闭该设置.一旦信道进入confirm模式,所有在信道上发布的消息都会被指派一个唯一的ID号.一旦消息被投递给所有匹配的队列后,信道会发送一个发送方确认模式给生产者应用程序.这使得生产者知晓消息已经安全到达队列了.如果消息和队列是可持久化的,那么确认消息只会在队列将消息写入磁盘后才会发出.发送方确认模式的最大好处是它们是异步的.一旦发布了一条消息,生产者应用程序就可以在等待确认的同时继续发送下一条.当确认消息最终收到的时候,生产者应用的回调方法就会被触发来处理该确认消息.如果RabbitMQ内部错误从而导致了消息的丢失,RabbitMQ会发送一条nack消息.就像发送方确认消息那样,只不过这次说明的消息已经丢失了.同时,由于没有消息回滚的概念,因此发送方确认模式更加轻量级,同时对RabbitMQ代理服务器的性能影响几乎可以忽略不计
    6. 一条消息的一生
    发布者:
    * 连接到RabbitMQ
    * 获取信道
    * 声明交换器
    * 创建消息
    * 发布消息
    * 关闭信道
    * 关闭连接

    脚本:
    esay_install pika
    # -*- coding:utf-8 -*-
    import pika,sys
    # 建立连接
    credentials = pika.PlainCredentials("guest","guest")
    conn_params = pika.ConnectionParameters("localhost",credentials = credentials)
    conn_broker = pika.BlockingConnection(conn_params)
    # 获取信道
    channel = conn_broker.channel()
    # 声明交换器
    channel.exchange_declare(exchange="hello-exchange",type="direct",passive=False,durable=True,auto_delete=False)
    # 创建消息
    msg = sys.argv[1]
    msg_props = pika.baseicProperties()
    msg_props.content_type = "text/plain"
    # 发布消息
    channel.basic_publish(body=msg,exchange="hello-exchange",properties=msg_props,routing_key="hola")

    消费者:
    * 连接到RabbitMQ
    * 获取信道
    * 声明交换器
    * 声明队列
    * 把队列和交换器绑定起来
    * 消费消息
    * 关闭信道
    * 关闭连接

    脚本:
    esay_install pika
    # -*- coding:utf-8 -*-
    import pika,sys
    # 建立连接
    credentials = pika.PlainCredentials("guest","guest")
    conn_params = pika.ConnectionParameters("localhost",credentials = credentials)
    conn_broker = pika.BlockingConnection(conn_params)
    # 获取信道
    channel = conn_broker.channel()
    # 声明交换器
    channel.exchange_declare(exchange="hello-exchange",type="direct",passive=False,durable=True,auto_delete=False)
    # 声明队列
    channel.queue_declare(queue="hello-queue")
    # 通过routing键将队列和交换器绑定起来
    channel.queue_bind(queue="hello-queue",exchange="hello-exchange",routing_key="hola")
    # 定义消息处理函数
    def msg_consumer(channel,method,header,body):
    # 消息确认
    channel.basic_ack(delivery_tag=method.delivery_tag)
    # 停止消费并退出
    if body == "quit":
    channel.basic_cancel(consumer_tag="hello-consumer")
    channel.stop_consuming()
    else:
    print body
    return
    # 订阅消费者
    channel.basic_consume(msg_consumer,queue="hello-queue",consumer_tag="hello-consumer")
    channel.start_consuming()

    7. 使用发送方确认模式来确认投递
    带有确认功能的生产者

    # -*- coding:utf-8 -*-
    import pika,sys
    from pika import spec

    credentials = pika.PlainCredentials("guest","guest")
    conn_params = pika.ConnectionParameters("localhost",credentials = credentials)
    conn_broker = pika.BlockingConnection(conn_params)

    channel = conn_broker.channel()
    #发送方确认模式处理器
    def confirm_handler(frame):
    if type(frame.method) == spec.Confirm.SelectOK:
    print "Channel in 'confirm' mode."
    elif type(frame.method) == spec.Basic.Nack:
    if frame.method.delivery_tag in msg_ids:
    print "Message lost!"
    elif type(frame.method) == spec.Basic.Ack:
    if frame.method.delivery_tag in msg_ids:
    print "Confirm received!"
    msg_ids.remove(frame.method.delivery_tag)
    # 将信道设置为confirm模式
    channel.confirm_delivery(callback=confirm_handler)
    # 重设消息ID追踪器
    msg = sys.argv[1]
    msg_props = pika.baseicProperties()
    msg_props.content_type = "text/plain"
    msg_ids = []
    # 发布消息
    channel.basic_publish(body=msg,exchange="hello-exchange",properties=msg_props,routing_key="hola")
    msg_ids.append(len(msg_ids) + 1)
    channel.close()