Jonathan1314

导航

RabbitMQ基本理论

本节内容

一  RabbitMQ介绍

二  RabbitMQ安装配置

三  RabbitMQ的Python实现-pika

  1. 生产者消费者

  2. 工作队列 

  3. 持久化和公平分发

  4. 发布与订阅

  5. RPC

附:概念汇总

 

一  RabbitMQ介绍

 

 

1.  RabbitMQ使用场景

RabbitMQ是一个消息中间件,消息中间件【最主要的作用:信息的缓存区】从应用场景如下:

场景一:系统集成与分布式系统的设计

各个子系统通过消息来对接,这种解决方案也逐步发展成一种架构风格,即“通过消息传递的架构”

举个例子:现在医院有两个科“看病科”和“住院科”在之前他们之间是没有任何关系的,

如果你在“看病课”看完病后注册的信息和资料,

到住院科后还得重新注册一遍?那现在要改革,你看完病后可以直接去住院科那两个系统之间需要打通怎么办?

这里就可以使用我们的消息中间件了。

场景二:异步任务处理结果回调的设计

举个列子:记录日志,假如需要记录系统中所有的用户行为日志,如果通过同步的方式记录日志势必会影响系统的响应速度,

当我们将日志消息发送到消息队列,记录日志的子系统就会通过异步的方式去消费日志信息,这样就不需要同步的写入日志了

场景三:并发请求的压力高可用设计

举个列子:比如电商的秒杀场景。当某一时刻应用服务器或数据库服务器收到大量请求,将会出现系统宕机。

如果能够将请求转发到消息队列,再由服务去消费这些消息将会使得请求变得平稳,提高系统的可用性。

 

2.  消息队列介绍

 

ActiveMQ

RabbitMQ Kafka
所属社区/公司

Apache

Mozilla Public License Apache/LinkedIn
开发语言

Java

Erlang Java
支持的协议

OpenWire、STOMP

REST、XMPP、AMQP

AMQP 仿AMQP
事务 支持

不支持

不支持

集群

支持

支持 支持
负载均衡 支持 支持

支持

动态扩容 不支持 不支持

支持(zk)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

常用的消息中间件来说,感觉Kafka更好些,没错Kafka是大数据时代诞生的消息中间件,但对于目前来说使用最广的还是RabbitMQ

 

Advanced Message Queuing Protocol(AMQP,高级消息队列协议)

是一个标准开放的应用层的消息中间件(Message Oriented Middleware)协议

AMQP定义了通过网络发送的字节流的数据格式,因此兼容性非常好,

任何实现AMQP协议的程序都可以和与AMQP协议兼容的其他程序交互,可以容易做到跨语言、跨平台

 

3.  RabbitMQ和一般的消息传输模式:队列模式&主题模式区别

队列模式:

一个发布者发布消息,下面的接收者按队列顺序接收,比如发布了10个消息,两个接收者A、B,那就是A、B总共会收到10条消息,不重复

 

主题模式:

对于Topic模式,一个发布者发布消息,有两个接收者A、B来订阅,那么发布了10条消息,A、B各收到10条消息

 

RabbitMQ的模式:

生产者生产消息后不直接发送到队列中,而是发到一个交换空间:Exchange,

Exchange会根据Exchange类型和Routing Key来决定发到哪个队列中,详情见发布订阅

 

二  RabbitMQ安装配置

 

 

测试环境

[root@localhost ~]# cat /etc/redhat-release
CentOS Linux release 7.3.1611 (Core)

1.  安装环境

# 安装配置epel源
yum install -y epel-release
# 安装erlang
yum -y install erlang
# 安装RabbitMQ
yum -y install rabbitmq-server

2.  用户配置

# 启动服务
service rabbitmq-server start
# 添加用户
rabbitmqctl add_user admin admin
# 添加管理员权限
rabbitmqctl set_user_tags admin administrator
# 修改密码 
abbitmqctl add_user admin youpassword
# 设置权限
rabbitmqctl  set_permissions  -p  '/'  admin '.' '.' '.'

3.  启动WEB管理

# 启动Web插件
rabbitmq-plugins enable rabbitmq_management
# 删除guest用户
rabbitmqctl delete_user guest
# 重启rabbitmq
service rabbitmq-server restart
# 配置文档
# vim /etc/rabbitmq/rabbitmq.config

4.  查看WEB后台

RabbitMQ默认启动的端口:5672
RabbitMQ默认的WEB端口:15672

http://localhost:15672

 

三  RabbitMQ的Python实现-pika

 

 

1.  生产者消费者

send.py

"""
生产者/发送消息方
"""
import pika

# 远程主机的RabbitMQ Server设置的用户名密码
credentials = pika.PlainCredentials('admin', 'admin')
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.254.143', 5672, '/', credentials))

'''
ConnectionParameters 中的参数:virtual_host 注:
    相当于在rabbitmq层面又加了一层域名空间的限制,每个域名空间是独立的有自己的Echange/queues等
    举个好玩的例子Redis中的db0/1/2类似
'''

# 创建通道
channel = connection.channel()
# 声明队列hello,RabbitMQ的消息队列机制如果队列不存在那么数据将会被丢掉,下面我们声明一个队列如果不存在创建
channel.queue_declare(queue='hello')

# 给队列中添加消息
channel.publish(exchange="",       # 默认时,向routing_key="hello",指定的队列发送消息
                routing_key="hello",
                body="Hello World")
print("向队列hello添加数据结束")
# 缓冲区已经flush而且消息已经确认发送到了RabbitMQ中,关闭通道
channel.close()

receive.py

"""
消费者/接收消息方
"""
import pika

# 远程主机的RabbitMQ Server设置的用户名密码
credentials = pika.PlainCredentials('admin', 'admin')
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.254.143', 5672, '/', credentials))

# 创建通道
channel = connection.channel()
# 声明队列
channel.queue_declare(queue='hello')
"""
你可能会问为什么我们还要声明队列呢? 我们在之前代码里就有了,但是前提是我们已经知道了我们已经声明了代码,
但是我们可能不太确定谁先启动~
所以如果你们100%确定也可以不用声明,但是在很多情况下生产者和消费者都是分离的.所以声明没有坏处
"""


# 订阅的回调函数这个订阅回调函数是由pika库来调用的
def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)

# 定义通道消费者参数
channel.basic_consume(callback,
                      queue='hello',
                      no_ack=True)  # 相当于告诉发送者,我收到了

print(' [*] Waiting for messages. To exit press CTRL+C')
# 开始接收信息,并进入阻塞状态,队列里有信息才会调用callback进行处理。按ctrl+c退出。
channel.start_consuming()

 

2. 工作队列  

举个例子:

写日志同步慢、或者请求量写日志机器扛不住怎么办?异步、并且分担日志记录压力到多台服务器上

工作队列背后主要思想是避免立即执行资源密集型任务时,需要等待其他任务完成。

所以可以把要执行的任务拆分,部分任务封装成任务消息发送到队列。

send.py

"""
生产者/发送方
"""
import pika

# 远程主机的RabbitMQ Server设置的用户名密码
credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.254.143', 5672, '/', credentials))

# 创建通道
channel = connection.channel()
# 声明队列task_queue,RabbitMQ的消息队列机制如果队列不存在那么数据将会被丢掉,下面我们声明一个队列如果不存在创建
channel.queue_declare(queue='task_queue')

# 在队列中添加消息
for i in range(100):
    message = '%s Meassage ' % i or "Hello World!"
    # 发送消息
    channel.basic_publish(exchange='',
                          routing_key='task_queue',
                          body=message,)
    # 发送消息结束
    print(" [x] Sent %r" % message)

# 关闭通道
channel.close()

receive1.py

"""
消费者/接收方
"""
import time
import pika

# 认证信息
credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.254.143", 5672, "/", credentials))
# 建立通道
channel = connection.channel()
# 创建队列
channel.queue_declare("task_queue")


# 订阅的回调函数这个订阅回调函数是由pika库来调用的
def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    print(body.count(b'.'))
    time.sleep(body.count(b'.'))
    print(" [x] Done")

# 定义通道消费者参数
channel.basic_consume(callback,
                      queue="task_queue",
                      no_ack=True)

print(' [*] Waiting for messages. To exit press CTRL+C')
# 开始接收信息,并进入阻塞状态,队列里有信息才会调用callback进行处理。按ctrl+c退出。
channel.start_consuming()

receive2.py

"""
消费者/接收方
"""
import time
import pika

# 认证信息
credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.254.143", 5672, "/", credentials))
# 建立通道
channel = connection.channel()
# 创建队列
channel.queue_declare("task_queue")


# 订阅的回调函数这个订阅回调函数是由pika库来调用的
def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    print(body.count(b'.'))
    time.sleep(body.count(b'.'))
    print(" [x] Done")

# 定义通道消费者参数
channel.basic_consume(callback,
                      queue="task_queue",
                      no_ack=True)

print(' [*] Waiting for messages. To exit press CTRL+C')
# 开始接收信息,并进入阻塞状态,队列里有信息才会调用callback进行处理。按ctrl+c退出。
channel.start_consuming()

默认RabbitMQ按照顺序发送每一个消息,每个消费者会获得相同的数量消息,这种分发消息的方式称之为循环。

 

3.  持久化和公平分发

3.1  消息持久化

在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完就宕机(或出现其他意外)的情况,这种情况下就可能导致消息丢失。

为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,

RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;

如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。

这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。

这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,

这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…

下面是因为我们在消费者端标记了ACK=True关闭了它们,如果你没有增加ACK=True或者没有回执就会出现这个问题

 

生产者需要在发送消息的时候标注属性为持久化

# 在队列中添加消息
for i in range(100):
    message = '%s Meassage '% i or "Hello World!"
    # 发送消息
    channel.basic_publish(exchange='',
                          routing_key='task_queue',
                          body=message,
                          properties=pika.BasicProperties(delivery_mode=2, ))  # 标记属性消息为持久化消息需要客户端应答

    # 发送消息结束,并关闭通道
    print(" [x] Sent %r" % message)

消费者需要发送消息回执

# 订阅回调函数,这个订阅回调函数是由pika库来调用
def callback(ch, method, properties, body):
    """
    :param ch: 通道对象
    :param method: 消息方法
    :param properties: 消息属性
    :param body: 消息内容
    :return: None
    """
    print(" [x] Received %r" % (body,))
    time.sleep(2)
    print(" [x] Done")
    # 发送消息确认,确认交易标识符
    ch.basic_ack(delivery_tag=method.delivery_tag)

我们可以通过命令查看那些消费者没有回复ack确认

# Linux
rabbitmqctl list_queues name messages_ready messages_unacknowledged
# Windows
rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged

3.2  队列持久化

如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),

这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器

已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,

那么我们要用到事务。由于这里仅为RabbitMQ的简单介绍,所以这里将不讲解RabbitMQ相关的事务。

 这里我们需要修改下生产者和消费者设置RabbitMQ消息的持久化  **[生产者/消费者]都需要配置**

channel.queue_declare(queue='task_queue', durable=True)  # 队列持久化

 

3.3  公平分发

默认情况下RabitMQ会把队列里面的消息立即发送到消费者,无论该消费者有多少消息没有应答,也就是说即使发现消费者来不及处理,

新的消费者加入进来也没有办法处理已经堆积的消息,因为那些消息已经被发送给老消费者了

 

在消费者中增加: channel.basic_qos(prefetch_count=1) 

prefetchCount:会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,

则该consumer将block掉,直到有消息ack。 这样做的好处是,如果系统处于高峰期,消费者来不及处理,

消息会堆积在队列中,新启动的消费者可以马上从队列中取到消息开始工作。

工作过程如下:

1. 消费者1接收到消息后处理完毕发送了ack并接收新的消息并处理

2. 消费者2接收到消息后处理完毕发送了ack并接收新的消息并处理

3. 消费者3接收到消息后一直处于消息中并没有发送ack不在接收消息一直等到消费者3处理完毕后发送ACK后再接收新消息

 

4. 发布与订阅

前面我们学工作队列主要任务是把每个任务分配给一个worker[工作者],而把消息发给多个消费者(不同队列中),这个模式称之为“发布订阅”

举个例子我们将创建一个简单的日志系统,包含两个程序第一个是用来发送日志,第二个是用来接收日志,

接收日志的程序每一个副本都将收到消息,**这样我们可以一个接收器用来写入磁盘,一个接收器用来输入到日志**

Exchanges可用的类型很少:**direct, topic, headers, fanout**---4种,我们先看最后一种

 

4.1 Fanout模式

模式特点:

  • 可以理解为广播模式

  • 不需要routing key,消息发送时通过exchange binding进行路由,在这个模式下routing key失去作用

  • 这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定

  • 如果接收到消息的Exchange没有与任何Queue绑定,则消息会被抛弃

send.py

import pika

# 远程主机的RabbitMQ Server设置的用户名密码
credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.254.144", 5672, "/", credentials))

# 创建通道
channel = connection.channel()

# 声明Exchanges
channel.exchange_declare(exchange="logs",
                         exchange_type="fanout")
"""
这里可以看到我们建立连接后,就声明了Exchange,因为把消息发送到一个不存在的Exchange是不允许的,
如果没有消费者绑定这个,Exchange消息将会被丢弃这是可以的因为没有消费者
"""
# 添加消息
for i in range(100):
    message = 'Logs Number: %s' % i
    channel.basic_publish(exchange="logs",  # 将消息发送至Exchange为logs绑定的队列中
                          routing_key="",
                          body=message,)
    print(" [x] Sent %r" % message)

# 关闭通道
connection.close()

receive.py

"""
1. 声明一个exchange,类型为fanout
2. 申明一个随机queue
3. 绑定exchang与queue
"""

# 连接RabbitMQ验证信息
credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.254.144", 5672, "/", credentials))

# 建立通道
channel = connection.channel()

# Bindings Exchanges 接收消息
channel.exchange_declare(exchange='logs',
                         exchange_type="fanout")

# 使用随机队列/并标注消费者断开连接后删除队列
# 不需要维护一个队列
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue


# Queue 与 Exchanges绑定
channel.queue_bind(exchange='logs',
                   queue=queue_name)

print(' [*] Waiting for logs. To exit press CTRL+C')


# 定义回调函数这个回调函数将被pika库调用
def callback(ch, method, properties, body):
    print(" [x] %r" % body)

# 定义接收消息属性
channel.basic_consume(callback,
                      queue=queue_name,
                      no_ack=True)

# 开始接收消息
channel.start_consuming()

测试:现在可以把信息分开记录

日志存入文件

python3 receive.py > fanout_logs.txt

并且想再屏幕也输出,另启一个窗口

python3 receive.py

概念解释

binding

当我们创建了Exchanges和(QUEUE)队列后,我们需要告诉Exchange发送到们的Queue队列中,所需要需要把Exchange和队列(Queue)进行绑定

channel.queue_bind(exchange='logs',
                   queue=result.method.queue)

 

4.2 Direct模式

任何发送到Direct Exchange的消息都会被转发到routing_key中指定的Queue

  •  一般情况可以使用rabbitMQ自带的Exchange:”” (该Exchange的名字为空字符串), 也可以自定义Exchange

  •  这种模式下不需要将Exchange进行任何绑定(bind)操作。当然也可以进行绑定,

            可以将不同的routing_key与不同的queue进行绑定,不同的queue与不同exchange进行绑定

  •  消息传递时需要一个“routing_key”

  •  如果消息中不存在routing_key中绑定的队列名,则该消息会被抛弃

  如果一个exchange 声明为direct,并且bind中指定了routing_key,那么发送消息时需要同时指明该exchange和routing_key

 

简而言之就是:生产者生成消息发送给Exchange, Exchange根据Exchange类型和basic_publish中的routing_key进行消息发送 

消费者:订阅Exchange并根据Exchange类型和binding key(bindings 中的routing key) ,如果生产者和订阅者的routing_key相同,

Exchange就会路由到那个队列

 

举个列子:

我们创建了一个简单的日志系统,我们把消息发给所有的订阅者 在下面的内容中将把特定的消息发给特定的订阅者,

举个例子来说,把error级别的报警写如文件,并把所有的报警打印到屏幕中,进行了路由的规则类似下面的架构

这里也要注意一个routing key 是可以绑定多个队列的

在上面我们已经创建过bindings了类似下面

channel.queue_bind(exchange=exchange_name,
                   queue=queue_name)

 

消费者端:Bindings可以增加routing_key 这里不要和basic_publish中的参数弄混了,我们给它称之为**binding key**

channel.queue_bind(exchange=exchange_name,
                   queue=queue_name,
                   routing_key='black')

binding key的含义依赖于Exchange类型,fanout exchanges类型只是忽略了它

 

4.3 Topic类型

前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求

topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,

也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同

  • routing key为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit

  • binding key与routing key一样也是句点号“. ”分隔的字符串

  • binding key中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)

以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1,

routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配)

routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey

 

5. RPC

MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功

或者失败(甚至连有没有消费者来处理这条消息都不知道)。但实际的应用场景中,我们很可能需要一些同步处理,

需要同步等待服务端将我的消息处理完成后再进行下一步处理。

这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。

5.1 RabbitMQ实现RPC机制

  • 客户端发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14种properties,

        这些属性会随着消息一起发送)中设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)

       和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)

  • 服务器端收到消息并处理

  • 服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性

  • 客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,

         根据执行结果进行后续业务处理

虽然RPC在计算中是一个很常见的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是慢RPC时,问题就出现了。

这样的混淆导致了不可预测的系统,并增加了调试的不必要的复杂性。与简化软件不同,误用的RPC可能导致无法维护的面代码。

记住上面几点问题,考虑下面几个建议

  • - 确保调用的函数是本地的还是远程的

  • - 记录您的系统。明确组件之间的依赖关系

  • - 处理错误案例。当RPC服务器关闭很长时间时,客户端应该如何反应?

如果对RPC有很多疑问,如果可以的话最好使用异步管道 一般来说在RabbitMQ执行RPC是很简单的,客户端发送请求服务端响应消息,

为了接收响应,客户端需要发送一个“回调”队列地址

result = channel.queue_declare(exclusive=True)
callback_queue = result.method.queue

channel.basic_publish(exchange='',
                      routing_key='rpc_queue',
                      properties=pika.BasicProperties(
                            reply_to = callback_queue,
                            ),
                      body=request)

 

AMQP 0-9-1 协议预先定义了14种属性,除了以下几种之外常用其他的不经常用

  • - delivery_mode: 标记消息持久化,值为2的时候为持久化其他任何值都是瞬态的

  • - content_type: 用来描述mime-type编码,举个例子来说经常使用JSON编码的话将此属性设置为:application/json.

  • - reply_to:通常用于命名回调队列

  • - correlation_id:将RCP请求和响应进行关联的id

 

5.2 工作机制

- 当客户端启动会创建一个匿名回调队列

- 在RCP请求Client发送两个属性:reply_to 标记callback队列,correlation_id 每个请求的唯一值,这个请求是被发送到rpc_queue 中

- 这个RPC worker (aka: server)等待请求在这个rpc_queue中,当请求出现它执行任务并将结果返回给客户端,发送到那个队列呢?

就是我们reply_to标记的队列,客户端等待数据返回在这个callback队列,当消息出现 检查correlation_id 属性是否是和请求的如果匹配进行相应

rpc_server.py

"""
RPC/Server端
"""
import pika
# 添加认证信息
credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.254.144", 5672, "/", credentials))

# 添加一个通道
channel = connection.channel()
# 添加一个队列,这个队列在Server就是我们监听请求的队列
channel.queue_declare(queue='rpc_queue')


# 斐波那契计算
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)


#  应答函数,它是我们接受到消息后如处理的函数替代原来的callback
def on_request(ch, method, props, body):
    n = int(body)
    print(" [.] fib(%s)" % n)
    response = fib(n)

    ch.basic_publish(exchange='',
                     # 返回的队列,从属性的reply_to取出来
                     routing_key=props.reply_to,
                     # 添加correlation_id,和Client进行一致性匹配使用的
                     properties=pika.BasicProperties(correlation_id=props.correlation_id),
                     # 相应消息/我们的处理结果
                     body=str(response))
    # 增加消息回执
    ch.basic_ack(delivery_tag=method.delivery_tag)

# 每次预处理的请求个数/这个请求我没有处理完不要给我发了
channel.basic_qos(prefetch_count=1)
# 定义接收通道的属性/定义了callback方法,接收的队列,返回的队列在哪里?on_request 的routing_key=props.reply_to
channel.basic_consume(on_request, queue='rpc_queue')

# 开始接收消息
print(" [x] Awaiting RPC requests")
channel.start_consuming()

这个Server端的代码是非常简单的

- 往常一样我们先创建连接并声明队列 - 我们声明我们的斐波那契额函数,它只假设有效的正整数输入。

(不要期望这个能够为大数据工作,它可能是最慢的递归实现)。

- 我们声明了callback在basic_consume,它是RCP核心请求过来执行callback,它来工作并发送相应

- 我们可能希望运行多个服务器进程。为了在多个服务器上平均分配负载,我们需要设置prefetch_count设置

 

rpc_client.py

"""
RPC/Client端
"""
import pika
import uuid


# 定义菲波那切数列RPC Client类调用RPC Server
class FibonacciRpcClient(object):
    def __init__(self):
        # 添加认证信息
        credentials = pika.PlainCredentials("admin", "admin")
        self.connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.254.144", 5672, "/", credentials))
        # 添加一个通道
        self.channel = self.connection.channel()
        # 生成一个随机队列-定义callback回调队列
        result = self.channel.queue_declare(exclusive=True)
        self.callback_queue = result.method.queue
        # 定义回调的通道属性
        self.channel.basic_consume(self.on_response,  # 回调结果执行完执行的Client端的callback方法
                                   no_ack=True,
                                   queue=self.callback_queue)
        # 这里注意我们并没有直接阻塞的开始接收消息了

    # Client端 Callback方法
    def on_response(self, ch, method, props, body):
        if self.corr_id == props.correlation_id:
            # 定义了self.response = body
            self.response = body

    def call(self, n):
        # 定义了一个普通字段
        self.response = None
        # 生成了一个uuid
        self.corr_id = str(uuid.uuid4())
        # 发送一个消息
        self.channel.basic_publish(exchange='',   # 使用默认的Exchange,根据发送的routing_key来选择队列
                                   routing_key='rpc_queue',  # 消息发送到rpc_queue队列中
                                   # 定义属性
                                   properties=pika.BasicProperties(
                                         reply_to=self.callback_queue,  # Client端定义了回调消息的callback队列
                                         correlation_id=self.corr_id,  # 唯一值用来做什么的?request和callback 匹配用
                                         ),
                                   body=str(n))

        # 开始循环 我们刚才定义self.response=None当不为空的时候停止`
        while self.response is None:
            # 非阻塞的接受消息
            self.connection.process_data_events(time_limit=3)
        return int(self.response)

# 实例化对象
fibonacci_rpc = FibonacciRpcClient()

print(" [x] Requesting fib(30)")
# 发送请求计算菲波那切数列 30
response = fibonacci_rpc.call(30)
print(" [.] Got %r" % response)

- 我们建立一个连接,通道,并声明一个排他的“回调”队列作为回复

- 我们订阅这个callback队列,所以我们可以接收RPC Server消息

- 每个相应回来之后执行非常简单的on_response方法,每个相应来后检查下correlation_id是否是匹配的,保存相应并退出接收循环

- 接下来,我们定义主调用方法——它执行实际的RPC请求。

- 我们定义了uuid,并在消息发送时添加了两个属性:reply_to、correlation_id

 

概念汇总

ConnectionFactory、Connection、Channel

ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。

Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑

ConnectFactory为Connection的制造工厂

Channel是与RabbitMQ打交道的最重要的一个接口,大部分业务操作是在Channel这个接口中完成的,包括

定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等

 

exchange

实际上生产者不会直接把消息放到消息队列里,实际的情况是,生产者将消息发送到Exchange,

由Exchange将消息路由到一个或多个Queue中(或者丢弃)

queue

Queue(队列)是RabbitMQ的内部对象,用于存储消息,

RabbitMQ的消息都只能存储在Queue中,生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费

 

Message acknowledgement

在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,

这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,

RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;

如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。 

这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。

**这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,

这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…**

 

Message durability

如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),

这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,

但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。

由于这里仅为RabbitMQ的简单介绍,所以这里将不讲解RabbitMQ相关的事务。

 

Prefetch count

生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要

与Exchange Type及binding key联合使用才能最终生效。 在Exchange Type与binding key固定的情况下(在正常使用时

一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。

 RabbitMQ为routing key设定的长度限制为255 bytes。

 

Binding

RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。 

在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;生产者将消息发送给Exchange时,

一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。

这个将在Exchange Types章节会列举实际的例子加以说明。

在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。 binding key 并不是在所有情况下都生效,

它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue

 

更多信息请参考:

http://www.cnblogs.com/luotianshuai/p/7469365.html

课程代码请参考:

https://github.com/Jonathan1314/LearnRabbitMQ

 

posted on 2017-10-14 20:02  Jonathan1314  阅读(698)  评论(0编辑  收藏  举报