什么是消息队列
- 在项目架构中消息队列(Message queue) 简称 MQ, 应用场景非常多, 它是一个异步框架。
- 一般项目做大了之后,会将http-server中的一些业务逻辑放到MQ中, 后端独立处理这些消息。
消息队列的场景
1 ) 同步业务处理存在的问题
拿用户注册来说,首先要进行数据库操作,之后可能会发短信,发邮件等,之后再通知注册成功,而每一步花费时间会非常多,如果按照正常流程下来,用户体验将会非常的糟糕,而且在单次请求上花费的处理时间越长,那么服务器的并发性能就越低, 这就是同步进行业务处理存在的问题。
2 ) 异步业务的好处
如果换成异步处理,用户提交信息到数据库, 然后将用户注册这一消息放到消息队列中, 紧接着去通知用户注册成功, 在用户开心的看到自己很快地完成注册成功后, 其实发短信, 邮件通知等这些业务逻辑还没有开始处理, 接着我们可以在后端处理相应的业务逻辑, 通过一个单独的后台服务从消息队列和数据库中获取相应的数据, 来进行后续的操作如发短信和邮件通知。这样用户的体验将会非常的好,而且有效的提高了服务器的性能。
3 ) 消息队列的高级应用
除了在一些异步场景外, 还可以将MQ使用在应用程序的解耦和中。比如, 我们有三个系统, 订单系统, 库存系统, 风控系统。在没有消息队列的情况下,我们会在订单系统中,对库存系统和风控系统的逻辑进行相应的调用来进行通信。在这种情况下, 一旦库存系统或风控系统进行了变更和修改, 会涉及订单系统的修改, 因为程序存在很多耦合。如果使用了消息队列, 订单系统只需要往消息队列中放上一条消息,如创建了一个新的订单, 或变更了一个订单的状态, 那么, 我们库存系统和风控系统或其他系统将会间接地与订单系统产生联系, 中间的桥梁就是消息队列。这时候各方系统需要变更都不会互相产生影响, 于是成功的解耦了。还有一些应用场景比如分布式的事务,用到MQ的地方也比较多。
常见的消息队列软件
-
ActiveMQ
-
RabbitMQ
-
Kafka
-
Redis
-
…
RabbitMQ的介绍
- 它是一个消息中间件, 对于高并发的处理比较好, 比较稳定, 淘宝就使用RabbitMQ来作为自己的消息队列。
- 它也是一个消息代理, 它的工作就是接收和转发消息。
- 可以想象一下邮局业务,RabbitMQ就扮演了邮箱、邮局、邮递员的角色。
- 它使用的协议是AMQP(Advanced Message Queuing Protocol)协议, 高级消息队列协议。
- 是个提供统一消息服务的应用标准高级消息队列协议。
- 是应用层协议的一个开放标准, 为面向消息的中间件设计。
- 凡是使用这种协议的任何编程语言,任何产品,任何客户端和中间件,相互之间都可以进行通信。
- RabbitMQ的结构如下:
- 生产者producer(产生消息的业务服务器)创建消息,然后发布到代理服务器(RabbitMQ)
- 消费者consumer(用于处理消息的后端服务器)连上代理服务器, 订阅监听消息
- 信道channel连接上服务器之后, 需要建议通讯信道(一个连接上可以建立多条信道)
- 队列queue是个巨大的消息缓冲区, 多个生产者(producers)可以把消息发送给同一个队列,同样,多个消费者(consumers)也能够从同一个队列(queue)中获取数据
- 交换机exchange, 生产者不能直接将消息发到队列中,而是将消息发送到交换机上,根据交换机路由键(routing key)和交换机类型, 决定投递到哪个队列,交换机有4种类型
- direct(关键字类型, 根据消息中不同的关键字将消息发送给不同的队列)
- fanout(广播)
- topic(模糊匹配类型)
- header与其他3个不同,允许匹配AMQP消息的header而不是路由键, 不常用
- MQ的使用过程大致如下:
- 客户端连接到消息队列服务器, 打开一个channel
- 客户端声明一个exchange, 并设置相关属性
- 客户端声明一个queue, 并设置相关属性
- 客户端使用routing key, 在exchange和queue之间建立好绑定关系
- 客户端投递消息到exchange
RabbitMQ的安装
- $
apt update
- $
apt install -y rabbitmq-server
RabbitMQ的服务管理
- $
service rabbitmq-server stop
- $
service rabbitmq-server start
- $
service rabbitmq-server restart
启动RabbitMQ的web管理界面
- $
rabbitmq-plugins enable rabbitmq_management
- 在浏览器中访问:
http://服务器ip地址:15672
就可以看到它的管理界面 - 要想登录进去还需要创建用户和密码
- 添加用户 $
rabbitmqctl add_user <username> <password>
如:rabbitmqctl add_user jack 111111
- 此时还不能登录,在RabbitMQ中有比较复杂的权限系统
- 为刚创建的用户指定角色 $
rabbitmqctl set_user_tags <username> <tag>
如:rabbitmqctl set_user_tags jack administrator
- 再次登录即可进入
- 添加用户 $
- 在这个web界面中有四个重要的页签
- Connections: 应用程序与MQ建立的连接
- Channels: 在建立的连接上创建的信道
- Exchanges: 在MQ内部的交换机, 内部连接着队列
- Queues: 与交换机进行连接的队列, 内部存入很多的消息
- 整个过程是这样的,应用程序与MQ建立连接后, 在连接上创建通讯的信道, 然后应用程序把数据发送到交换机上, 交换机收到消息后存放到它认为应该放到的队列中去
RabbitMQ的应用示例
1 ) 安装扩展
- 在python中操作RabbitMQ的时候需要安装一个扩展 $
pip3 install pika
2 ) 发送消息示例
- 创建文件 buy.py, 开始进行代码编写
import pika # 用户名与密码 user_pwd = pika.PlainCredentials('jack', '111111') # 创建连接 这个ip地址为自己的服务器地址, 此处作为举例 connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.55.66', credentials=user_pwd)) # 在连接上创建一个信道 channel = connection.channel() # 声明交换机exchange, 名字为exchange1 channel.exchange_declare(exchange='exchange1', # 名称 exchange_type='direct', # 交换机类型, 默认值为direct durable=True) # 持久化 # 声明一个队列queue1, 设置队列持久化, 注意不要跟已存在的队列重名,否则报错 channel.queue_declare('queue1', durable=True)
- 开始执行代码:$
python3 buy.py
出现错误信息:... failed: timeout
- 连接失败的原因是 jack 这个用户的权限依然不够(之前给了一个administrator的角色)
- 在RabbitMQ中还有一个概念叫虚拟主机(vhost), 虚拟主机可以在一个RabbitMQ中为多个不同的应用程序提供服务
- 指定jack用户对于根虚拟主机有一个相应的权限: $
rabbitmqctl set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
- 如:
rabbitmqctl set_permissions -p / jack ".*" ".*" ".*"
- -p 表示指定哪个虚拟目录, 我们这里指定根目录 /, 指定用户为jack, 指定配置, 写, 读的权限全开
- 如:
- 再次执行,无报错即成功了!在Web管理界面Exchanges的页签中出现了exchange1, 在Queue页签中出现了queue1
- 点击进入exchange1中发现Bindings中未绑定任何队列,这时候往交换机上发消息队列是收不到的, 我们需要给他做一个绑定的关系
- 继续修改buy.py文件
# 紧接着上面的代码 # 路由的键 routing_key = 'info' # 需要根据实际情况设置名称 # 将交换机、队列、关键字绑定在一起, 使消费者只能根据关键字从不同队列中取消息 channel.queue_bind(exchange='exchange1', queue='queue1', routing_key=routing_key)
- 再次执行代码$
python3 buy.py
,刷新web管理页面发现exchange1已经通过了一个info的路由key绑定了队列queue1 - 以后只要在交换机exchange1中发一个带有info的信息过来,然后交换机就会自动的将消息发送到队列queue1中,尝试一下, 继续编写buy.py:
# 接着上面的代码编写 # 设置消息持久化, 将要发送的消息属性标记为2, 表示该消息要持久 # 如果服务器重启,消息还未处理,它应该被保存下来,重启后会接着处理 properties = pika.BasicProperties(delivery_mode=2) message = 'hello world' channel.basic_publish(exchange='exchange1', # 指明用于发布消息的交换机 routing_key=routing_key, # 绑定关键字,即将message与关键字info绑定, 明确将消息发送到哪个关键字队列中 body=message, properties=properties) # 关闭连接 connection.close()
- 再次执行该文件$
python3 buy.py
- 刷新后发现队列Queues中有一条新的消息
3 ) 取消息示例
-
创建一个文件task.py
import pika import time # 用户名与密码 user_pwd = pika.PlainCredentials('jack', '111111') # 创建连接 ip地址为RabbitMQ的服务器地址 connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.55.66'), credentials=user_pwd)) # 在连接上创建一个信道 channel = connection.channel() # 声明交换机exchange, 名字为exchange1 # 如果已有交换机可以不声明, 为了防止先运行消费者时, 没有交换机, 则创建一下, 已经创建时declare什么也不做 # 此处因为已经有了交换机,不声明也可以, 下面代码可以不用 ''' channel.exchange_declare(exchange='exchange1', # 名称 exchange_type='direct', # 交换机类型, 默认值为direct durable=True) # 持久化 ''' # 声明一个队列queue1,设置队列持久化 # 其实队列已经有了,也可以不声明队列,和交换机一样,下面代码可以不用 ''' channel.queue_declare('queue1', durable=True) ''' # 将交换机、队列、关键字绑定在一起,使消费者只能根据关键字从不同队列中取消息 # 其实绑定关系已经确认,也不需要再次绑定队列了,下面代码可以不用 ''' routing_key = 'info' channel.queue_bind(exchange='exchange1', queue='queue1', routing_key=routing_key) ''' # 定义回调函数, 接收消息 def callback(ch, method, properties, body): print("[消费者] %r:%r" % (method.routing_key, body)) time.sleep(3) # 模拟耗时 # 一定要做消息确认,除非channel.basic_consume(no_ack=True) # 不然后果很严重, 消息在你程序退出后就会重新发送 # 如果它不能释放没响应的消息, RabbitMQ就会占用越来越多的内存 ch.basic_ack(delivery_tag=method.delivery_tag) # print(type(body)) # <class 'bytes'> # 退出 if str(body, encoding="utf-8") == "quit": channel.basic_cancel(consumer_tag=consumer_tag) channel.stop_consuming() # 在同一时刻,不要发送超过1条消息给一个工作者(worker), 直到它已经处理了上一条消息并且做出了响应 # 这行代码很关键, 比如一个消费者积攒了很多消息处理不过来, 而另一个消费者可能已经没有消息可以处理了,就会造成资源的浪费 # 这样可以让多个消费者共同分担协作处理, 提高效率 channel.basic_qos(prefetch_count=1) consumer_tag = channel.basic_consume(callback, queue='queue1') # 循环等待接收消息 channel.start_consuming() # 关闭连接 connection.close()
-
执行task.py文件, $
python3 task.py
发现消息已经被消费掉了 -
在实际项目中,我们会在服务器始终开一个终端运行着task.py程序, 它就会一直保持建立的连接, 等待有新的消息来处理
-
当消息比较多的时候,就会按队列进行一个一个的异步化处理, 如果我们想要提速进行处理, 可以开多个消费者, 也就是task.py
RabbitMQ在实际项目中的应用
1 ) 安装celery
- 关于celery, 它是一个并行分布式框架, 我们在这里只说如何在项目中应用它来处理MQ
- $
pip3 install celery
- 注意celery从4的版本已经不支持windows了, 如果windows上要使用请使用3的版本(任何版本都可)
- 如:$
pip install celery==3.*
- 如:$
2 ) 项目应用举例
- 编写同步的业务逻辑代码web.py程序
import time from backend import send_sms # 导入即将编写的异步程序 def save(): # 模拟保存数据操作 print('save') # 调用保存业务逻辑 save() # 调用发短信的方法,延迟发送,这里的delay方法是`@celery.task`装饰器提供的 send_sms.delay()
- 编写backend.py异步程序
from celery import Celery import time celery = Celery(broker='amqp://jack:[email protected]/') # 这是celery提供的一个装饰器 @celery.task def send_sms() # 模拟发短信的异步操作 time.sleep(5) print('sms')
- 将backend.py程序首先开一个终端运行起来:$
celery -A backend worker
- 开始执行业务操作程序 $
python3 web.py
, 发现没有任何的卡顿,直接执行完成,而在backend终端中过会儿也发现有了输出sms的信息 - 在web管理页面下同时也可发现celery自己创立的交换机和队列等一系列信息