消息队列rabbitmq


为什么用消息队列

举例

比如在一个企业里,技术老大接到boss的任务,技术老大把这个任务拆分成多个小任务,完成所有的小任务就算搞定整个任务了。
那么在执行这些小任务的时候,可能有一个环节很费时间,并且优先级很低,推迟完成也不影响整个任务运转,那么技术老大就会将这个很费时间,且不重要的任务,丢给他的小弟去解决,自己继续完成其他任务。

转化为计算机思想

那个技术老大就是一个 程序系统,那个小弟就是消息队列。
当程序系统发现某些任务耗费时间且优先级较低,迟点完成也不影响整个任务,就把这个任务丢给消息队列。

场景

在程序系统中,例如外卖系统,订单系统,库存系统,优先级较高
发红包,发邮件,发短信,app消息推送等任务优先级很低,很适合交给消息队列去处理,以便于程序系统更快的处理其他请求。

消息队列工作流程

消息队列一般有三个角色:
队列服务端
队列生产者
队列消费者
消息队列工作流程就如同一个流水线,有产品加工,一个输送带,一个打包产品
输送带就是 不停运转的消息队列服务端
加工产品的就是 队列生产者
在传输带结尾打包产品的 就是队列消费者

队列产品

RabbitMQ
Erlang编写的消息队列产品,企业级消息队列软件,支持消息负载均衡,数据持久化等。

ZeroMQ 
saltstack软件使用此消息,速度最快。

Redis
key-value的系统,也支持队列数据结构,轻量级消息队列

Kafka
由Scala编写,目标是为处理实时数据提供一个统一、高通量、低等待的平台

一个app系统消息队列工作流程

消费者,一个后台进程,不断的去检测消息队列中是否有消息,有消息就取走,开启新线程去处理业务,如果没有一会再来

消息队列作用

1)程序解耦

允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

2)冗余:

消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。

许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。

3)峰值处理能力:

(大白话,就是本来公司业务只需要5台机器,但是临时的秒杀活动,5台机器肯定受不了这个压力,我们又不可能将整体服务器架构提升到10台,那在秒杀活动后,机器不就浪费了吗?因此引入消息队列)

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。

如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。

使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

4)可恢复性:

系统的一部分组件失效时,不会影响到整个系统。

消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

5)顺序保证:

在大多使用场景下,数据处理的顺序都很重要。

大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka保证一个Partition内的消息的有序性)

6)缓冲:

有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。

7)异步通信:

很多时候,用户不想也不需要立即处理消息。比如发红包,发短信等流程。

消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

 

rabbitMQ

1. 你了解的消息队列

生活里的消息队列,如同邮局的邮箱,
如果没邮箱的话,
邮件必须找到邮件那个人,递给他,才玩完成,那这个任务会处理的很麻烦,很慢,效率很低

但是如果有了邮箱,
邮件直接丢给邮箱,用户只需要去邮箱里面去找,有没有邮件,有就拿走,没有就下次再来,这样可以极大的提升邮件收发效率!
rabbitmq是一个消息代理,它接收和转发消息,可以理解为是生活的邮局。
你可以将邮件放在邮箱里,你可以确定有邮递员会发送邮件给收件人。
概括:
rabbitmq是接收,存储,转发数据的。
官方教程:http://www.rabbitmq.com/tutorials/tutorial-one-python.html

消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。

消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。

2. 公司在什么情况下会用消息队列?

1.电商订单

想必同学们都点过外卖,点击下单后的业务逻辑可能包括:检查库存、生成单据、发红包、短信通知等,如果这些业务同步执行,完成下单率会非常低,如发红包,短信通知等不必要的流程,异步执行即可

此时使用MQ,可以在核心流程(扣减库存、生成订单记录)等完成后发送消息到MQ,快速结束本次流程。消费者拉取MQ消息时,发现红包、短信等消息时,再进行处理

场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口

 

消费者1.py

路由关键字是sb,alex

# -*- coding: utf-8 -*-
# __author__ = "maple"
import pika
?
credentials = pika.PlainCredentials("root","123")
connection = pika.BlockingConnection(pika.ConnectionParameters('123.206.16.61',credentials=credentials))
channel = connection.channel()
?
# exchange='m1',exchange(秘书)的名称
# exchange_type='fanout' , 秘书工作方式将消息发送给所有的队列
channel.exchange_declare(exchange='m2',exchange_type='direct')
?
# 随机生成一个队列,队列退出时,删除这个队列
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
?
# 让exchange和queque进行绑定,只要
channel.queue_bind(exchange='m2',queue=queue_name,routing_key='alex')
channel.queue_bind(exchange='m2',queue=queue_name,routing_key='sb')
?
?
def callback(ch, method, properties, body):
  print("消费者接受到了任务: %r" % body)
?
channel.basic_consume(callback,queue=queue_name,no_ack=True)
?
channel.start_consuming()
?
?
?

消费者2.py

路由关键字sb

# -*- coding: utf-8 -*-
# __author__ = "maple"
import pika
?
credentials = pika.PlainCredentials("root","123")
connection = pika.BlockingConnection(pika.ConnectionParameters('123.206.16.61',credentials=credentials))
channel = connection.channel()
?
# exchange='m1',exchange(秘书)的名称
# exchange_type='fanout' , 秘书工作方式将消息发送给所有的队列
channel.exchange_declare(exchange='m2',exchange_type='direct')
?
# 随机生成一个队列
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
?
# 让exchange和queque进行绑定.
channel.queue_bind(exchange='m2',queue=queue_name,routing_key='sb')
?
?
def callback(ch, method, properties, body):
  print("消费者接受到了任务: %r" % body)
?
channel.basic_consume(callback,queue=queue_name,no_ack=True)
?
channel.start_consuming()

生产者.py

发送消息给匹配的路由,sb或者alex

# -*- coding: utf-8 -*-
# __author__ = "yugo"
?
?
import pika
credentials = pika.PlainCredentials("root","123")
connection = pika.BlockingConnection(pika.ConnectionParameters('123.206.16.61',credentials=credentials))
channel = connection.channel()
?
# 路由模式的交换机会发送给绑定的key和routing_key匹配的队列
channel.exchange_declare(exchange='m2',exchange_type='direct')
# 发送消息,给有关sb的路由关键字
channel.basic_publish(exchange='m2',
                    routing_key='sb',
                    body='aaaalexlaolelaodi')
?
connection.close()

RPC之远程过程调用

将一个函数运行在远程计算机上并且等待获取那里的结果,这个称作远程过程调用(Remote Procedure Call)或者 RPC。

RPC是一个计算机通信协议。

比喻

将计算机服务运行理解为厨师做饭,厨师想做一个小葱拌豆腐,厨师需要洗小葱、切豆腐、调汁、凉拌。他一个人完成所有的事,如同古老的集中式应用,一台计算机做所有的事。
?
制作小葱拌豆腐{
  厨师>洗小葱>切豆腐>凉拌
}

rpc应用场景

而如今,饭店做大了,有钱了,专职分工来干活,不再是厨师单打独斗,备菜师傅准备小葱、豆腐,切菜师傅切小葱、豆腐,厨师只负责调味,完成食品。
?
制作小葱拌豆腐{
  备菜师>洗菜
  切菜师>切菜
  厨师>调味
}

此时一件事好多人在做,厨师就得和其他人沟通,通知备菜、洗菜师傅的这个动作就是远程过程调用(RPC)。

这个过程在计算机系统中,一个电商的下单过程,涉及物流、支付、库存、红包等多个系统,多个系统又在多个服务器上,由不同的技术团队负责,整个下单过程,需要所有团队进行远程调用。

下单{
  库存>减少库存
  支付>扣款
  红包>减免红包
  物流>生成订单
}

到底什么是rpc

rpc指的是在计算机A上的进程,调用另外一台计算机B的进程,A上的进程被挂起,B上的被调用进程开始执行后,产生返回值给A,A继续执行。
调用方可以通过参数将信息传递给被调用方,而后通过返回结果得到信息,这个过程对于开发人员来说是透明的。
?
如同厨师一样,服务员把菜单给后厨,厨师告诉洗菜人,备菜人,开始工作,完成工作后,整个过程对于服务员是透明的,他完全不用管后厨是怎么把菜做好的。

由于服务在不同的机器上,远程调用必经网络通信,调用服务必须写一坨网络通信代码,很容易出错且很复杂,因此就出现了RPC框架。

阿里巴巴的 Dubbo     java
新浪的 Motan java
谷歌的 gRPC 多语言
Apache thrift 多语言

rpc封装了数据的序列化,反序列化,以及传输协议

python实现RPC

利用RabbitMQ构建一个RPC系统,包含了客户端和RPC服务器,依旧使用pika模块

Callback queue 回调队列

一个客户端向服务器发送请求,服务器端处理请求后,将其处理结果保存在一个存储体中。而客户端为了获得处理结果,那么客户在向服务器发送请求时,同时发送一个回调队列地址reply_to

Correlation id 关联标识

一个客户端可能会发送多个请求给服务器,当服务器处理完后,客户端无法辨别在回调队列中的响应具体和那个请求时对应的。为了处理这种情况,客户端在发送每个请求时,同时会附带一个独有correlation_id属性,这样客户端在回调队列中根据correlation_id字段的值就可以分辨此响应属于哪个请求。

客户端发送请求:某个应用将请求信息交给客户端,然后客户端发送RPC请求,在发送RPC请求到RPC请求队列时,客户端至少发送带有reply_to以及correlation_id两个属性的信息
?
服务器端工作流: 等待接受客户端发来RPC请求,当请求出现的时候,服务器从RPC请求队列中取出请求,然后处理后,将响应发送到reply_to指定的回调队列中
?
客户端接受处理结果: 客户端等待回调队列中出现响应,当响应出现时,它会根据响应中correlation_id字段的值,将其返回给对应的应用

过程
1.启动rpc客户端,等待接收数据到来,来了之后就进行处理,再将结果丢进队列
2.启动rpc服务端,发起请求
 

rpc_server.py


import pika
import uuid
class FibonacciRpcClient(object):
def __init__(self):
# 客户端启动时,创建回调队列,会开启会话用于发送RPC请求以及接受响应
# 建立连接,指定服务器的ip地址
self.connection = pika.BlockingConnection(pika.ConnectionParameters(
host='192.168.119.10'))
# 建立一个会话,每个channel代表一个会话任务
self.channel = self.connection.channel()

# 声明回调队列,再次声明的原因是,服务器和客户端可能先后开启,该声明是幂等的,多次声明,但只生效一次
#exclusive=True 参数是指只对首次声明它的连接可见
#exclusive=True 会在连接断开的时候,自动删除
result = self.channel.queue_declare(exclusive=True)
# 将次队列指定为当前客户端的回调队列
self.callback_queue = result.method.queue
# 客户端订阅回调队列,当回调队列中有响应时,调用`on_response`方法对响应进行处理;
self.channel.basic_consume(self.on_response, no_ack=True,
queue=self.callback_queue)


# 对回调队列中的响应进行处理的函数
def on_response(self, ch, method, props, body):
if self.corr_id == props.correlation_id:
self.response = body

# 发出RPC请求
# 例如这里服务端就是一个切菜师傅,菜切好了,需要传递给洗菜师傅,这个过程是发送rpc请求
def call(self, n):
# 初始化 response
self.response = None
# 生成correlation_id 关联标识,通过python的uuid库,生成全局唯一标识ID,保证时间空间唯一性
self.corr_id = str(uuid.uuid4())
# 发送RPC请求内容到RPC请求队列`s14rpc`,同时发送的还有`reply_to`和`correlation_id`
self.channel.basic_publish(exchange='',
routing_key='s14rpc',
properties=pika.BasicProperties(
reply_to=self.callback_queue,
correlation_id=self.corr_id,
),
body=str(n))
while self.response is None:
self.connection.process_data_events()
return int(self.response)

# 建立客户端
fibonacci_rpc = FibonacciRpcClient()

# 发送RPC请求,丢进rpc队列,等待客户端处理完毕,给与响应
print("发送了请求sum(99)")
response = fibonacci_rpc.call(99)

print("得到远程结果响应%r" % response)
 

?
?

rpc_client.py


import pika
# 建立连接,服务器地址为localhost,可指定ip地址
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='192.168.119.10'))
# 建立会话
channel = connection.channel()
# 声明RPC请求队列
channel.queue_declare(queue='s14rpc')

# 模拟一个进程,例如切菜师傅,等着洗菜师傅传递数据
def sum(n):
n+=100
return n
# 对RPC请求队列中的请求进行处理


def on_request(ch, method, props, body):
print(body,type(body))
n = int(body)
print(" 正在处理sum(%s)" % n)
# 调用数据处理方法
response = sum(n)
# 将处理结果(响应)发送到回调队列
ch.basic_publish(exchange='',
# reply_to代表回复目标
routing_key=props.reply_to,
# correlation_id(关联标识):用来将RPC的响应和请求关联起来。
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)
channel.basic_consume(on_request, queue='s14rpc')
print("等待接收rpc请求")


#开始消费
channel.start_consuming()