关于 Python 系列其他文章的传送门
-
《 Python 基础语法 》
-
《 Python 进阶:网络、多线程、异步、数据库、WEB 》
-
《 Python 和 RabbitMQ 的那点事 》 您当前所在位置
-
《 Python 和 Redis 的那点事 》
-
《 Python 爬虫基础和包 XPath、BeautifulSoup4、PhantomJS、Selenium 》
-
《 边爬豆瓣,边学习 Python 爬虫框架 Scrapy 》
-
《 用了 Scrapy-Redis 分布式爬虫后,一个能打的都没有! 》
-
《 全栈开发入门 JavaScript、React 和 Django 》
-
《 Django REST framework 的那点事 》

Python 和 RabbitMQ 的那点事
RabbitMQ 是由 LShift 提供的一个 Advanced Message Queuing Protocol ( AMQP ) 的开源协议实现,由以高性能、健壮以及可伸缩性出名的 Erlang 语言写成,因此也是继承了这些优点。
优点:很成熟,久经考验,应用广泛;文档详细,客户端丰富,几乎常用语言都有 RabbitMQ 的开发库。
docker 启动 RabbitMQ
1 2 3 4 5 6 7 8 9 10
| # 启动 RabbitMQ # 设置管理员账户 # rabbitmq:3-management 表示启动 WEB 管理插件 docker run -d --hostname my-rabbit --name this-rabbit \ -e RABBITMQ_DEFAULT_USER=monster \ -e RABBITMQ_DEFAULT_PASS=123qwe \ -p 5672:5672 \ -p 15672:15672 \ -p 25672:25672 \ rabbitmq:3-management
|
默认端口:
5672:协议端口;
15672:+10000 为 WEB 管理端口;
25672:+20000 为 内部节点和客户端工具通信用。
非 docker 时:
- 插件管理:
# rabbitmq-plugins list:列出所有可用插件;
# rabbitmq-plugins enable rabbitmq_management:启动 WEB 管理插件。
- 用户管理:
rabbitmqctl add_user username password:添加用户;
rabbitmqctl delete_user username:删除用户;
rabbitmqctl change_password username newpassword:更改密码;
rabbitmqctl set_user_tags username tag:设置权限 Tags,其实就是分配组。
原理及应用:
https://www.rabbitmq.com/getstarted.html
名词解释
- Server:服务器,接收客户端连接,实现消息队列及路由功能的进程(服务),也称为消息代理。注意:客户端包括生产者和消费者;
- Connection:网络物理连接;
- Channel:一个连接允许多个客户端连接;
- Exchange:交换器,接收生产者发来的消息,决定如何路由给服务器中的队列。常用的类型有:
- direct( point-to-point ): 直接;
- topic( publish-subscribe ): 话题订阅;
- fanout( multicast ): 扇出。;
- Message:消息;
- Message Queue:消息队列,数据的存储载体;
- Bind:绑定,建立消息队列和交换器之间的关系,也就是说交换器拿到数据,把什么样的数据送给哪个队列;
- Virtual Host:虚拟主机,一批交换机、消息队列和相关对象的集合。为了多用户互不干 扰,使用虚拟主机分组交换机、消息队列;
- Topic:主题、话题;
- Broke:可等价为 Server。

pika 库
Pika 是纯 Python 实现的支持 AMQP 协议的库。
应用示例
RabbitMQ 支持多种应用模型,下面举例各种应用如何通过 pika 库使用 RabbitMQ。
先通过 ip:15672 WEB 管理界面创建测试的虚拟主机 test,之后的举例都使用它。
1. 队列应用

这种模式就是最简单的生产者消费者模型,消息队列就是一个 FIFO(先进先出) 的队列。
生产者 send.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import pika import time
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel()
channel.queue_declare(queue='hello')
for i in range(40): channel.basic_publish(exchange='', routing_key='hello', body=bytes(f'data{i:02}', encoding='utf8'))
print("[x] Sent 'Hello world!'") connection.close()
|
消费者 receive.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import pika
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel()
channel.queue_declare(queue='hello')
def callback(ch, method, properties, body): print(" [x] Received %r" % body)
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=False)
print(' [*] waiting for messages. To exit press CTRL+C') channel.start_consuming()
|
2. 轮询队列

这种工作模式是一种竞争工作方式,对某一个消息来说,通过轮询的方式,多个消费者只能有一个拿走它。
把 recieve.py ,再启动一个消费者,就有两个消费者。
注意:虽然上面的图中没有画出 exchange,但其实也用到了缺省的 exchange
3. 发布、订阅模式 Publish/Subscribe

订阅者和消费者之间有一个 exchange(交换机),生产者把消息发送到 exchange,由 exchange 来决定如何发送到 queue。
消费者和生产者谁都可以去创建 queue,谁创建都行。
生产者 send.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import pika import time
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel()
channel.exchange_declare(exchange='logs', exchange_type='fanout')
for i in range(40): channel.basic_publish(exchange='logs', routing_key='', body=bytes("pc-data{: 02}".format(i), encoding='utf8')) time.sleep(1)
print("[x] Sent 'Hello world!'") connection.close()
|
消费者 receive.py:
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
| import pika
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel()
channel.exchange_declare(exchange='logs', exchange_type='fanout')
q1 = channel.queue_declare('myQ1', exclusive=True) q2 = channel.queue_declare('myQ2', exclusive=True) q1name = q1.method.queue q2name = q2.method.queue
print(q1name, q2name)
channel.queue_bind(exchange='logs', queue='myQ1') channel.queue_bind(exchange='logs', queue='myQ2')
def callback(ch, method, properties, body): print(f" [x] Received {method} {body}")
channel.basic_consume('myQ1', on_message_callback=callback, auto_ack=True) channel.basic_consume('myQ2', on_message_callback=callback, auto_ack=True)
print(' [*] waiting for messages. To exit press CTRL+C') channel.start_consuming()
|
上面的代码,如果先启动生产者会怎么样呢?
因为生产者没有绑定 queue,如果先启动生产者,那么当生产者把消息发给路由器后,没有 queue 来接收,这部分数据就丢失了。
可以把 exchange 想象成交换机,只负责转发数据,没人接收就丢弃
4. 路由 Routing

路由其实就是数据经过 exchange 的时候,通过匹配规则,对数据进行过滤,决定数据的去向。
而 fanout 不做数据过滤。
生产者 send.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import pika import time import random
exchange_name = 'color' colors = ('orange', 'black', 'green')
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel() channel.exchange_declare(exchange=exchange_name, exchange_type='direct') for i in range(40): rk = colors[random.randint(0, 2)] msg = "pc-data{:02}".format(i) channel.basic_publish(exchange=exchange_name, routing_key=rk, body=bytes(msg, encoding='utf8')) print(rk, msg) time.sleep(2)
connection.close()
|
消费者 receive.py:
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
| import pika
exchangename = 'color' colors = ('orange', 'black', 'green')
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel()
channel.exchange_declare(exchange=exchangename, exchange_type='direct')
q1 = channel.queue_declare('myQ1', exclusive=True) q2 = channel.queue_declare('myQ2', exclusive=True) q1name = q1.method.queue q2name = q2.method.queue
print(q1name, q2name)
channel.queue_bind(exchange=exchangename, queue='myQ1', routing_key=colors[0]) channel.queue_bind(exchange=exchangename, queue='myQ2', routing_key=colors[1]) channel.queue_bind(exchange=exchangename, queue='myQ2', routing_key=colors[2])
def callback(ch, method, properties, body): print(f" [x] Received {method.routing_key} {body}")
channel.basic_consume('myQ1', on_message_callback=callback, auto_ack=True) channel.basic_consume('myQ2', on_message_callback=callback, auto_ack=True)
print(' [*] waiting for messages. To exit press CTRL+C') channel.start_consuming()
|
之后在 WEB 管理界面中,点击 Exchanges => 选择 color 交换机 => 就可以看到 Routing key 绑定信息。
5. 话题 Topic

Topic 的 routing_key 必须使用 . 点号分割的单词组成!!!
支持使用通配符:比如:'phone.*', '*.red'
* 表示严格的一个单词;
# 表示 0 个或者多个单词。
如果 queue 绑定的 routing_key 只是一个 #,那么这个 queue 其实可以接收所有的消息,类似于 fanout。
如果没有使用任何通配符,效果类似于 direct。
生产者 send.py:
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
| import pika import time import random
exchange_name = 'products' topics = ('phone.*', '*.red') product_type = ('phone', 'pc', 'tv') colors = ('red', 'green', 'blue')
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel()
channel.exchange_declare(exchange=exchange_name, exchange_type='topic')
for i in range(40): rk = f'{product_type[random.randint(0,2)]}.{colors[random.randint(0, 2)]}' msg = "{} {:02}".format(rk, i) channel.basic_publish(exchange=exchange_name, routing_key=rk, body=bytes(msg, encoding='utf8')) print(msg) time.sleep(0.2)
connection.close()
|
消费者 receive.py:
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
| import pika
exchange_name = 'products' topics = ('phone.*', '*.red') producttype = ('phone', 'pc', 'tv') colors = ('red', 'green', 'blue')
params = pika.URLParameters('amqp://monster:123qwe@192.168.50.30:5672/test') connection = pika.BlockingConnection(params) channel = connection.channel()
channel.exchange_declare(exchange=exchange_name, exchange_type='topic')
q1 = channel.queue_declare('myQ1', exclusive=True) q2 = channel.queue_declare('myQ2', exclusive=True) q1name = q1.method.queue q2name = q2.method.queue
print(q1name, q2name)
channel.queue_bind(exchange=exchange_name, queue='myQ1', routing_key=topics[0]) channel.queue_bind(exchange=exchange_name, queue='myQ2', routing_key=topics[1])
def callback(ch, method, properties, body): print(f" [x] {body}")
channel.basic_consume('myQ1', on_message_callback=callback, auto_ack=True) channel.basic_consume('myQ2', on_message_callback=callback, auto_ack=True)
print(' [*] waiting for messages. To exit press CTRL+C') channel.start_consuming()
|
Topic 其实就是更加高级的路由,支持模式匹配而已。
消息队列的作用
- 系统间解耦;
- 解决生产者、消费者速度匹配。
由于稍微上规模的项目都会分层、分模块开发,模块间或系统间尽量不要直接耦合,需要开放公共接口供别的模块或系统调用,而调用可能触发并发问题,为了缓冲和解耦,往往采用中间件技术。
RabbitMQ 只是消息中间件中的一种应用程序