引入
题外话:纪念一下开始写的第一篇博客,之前一直觉得没有很深入的学习,担心会写的很low,很多看的可能因为心急没有真正掌握,或者没有时间及时记录,过了一段时间慢慢就忘记了,才发现学习不能着急,一点一点积累很重要,所以,决定记录一下看过的跑过的,为了梳理一下学习路程,留痕。
为什么会想到从rabbitmq开始,因为工作的项目中遇到了,就这么简单。当发起的请求到达后台,后台处理请求需要花费很长时间,发送请求方又不能一直等待任务完成才返回结果,尤其是在高并发的场景下,很容易造成阻塞。为了让生产方(发送请求方)与消费方(接收请求方)解耦开来,同时在并发请求下不阻塞,选择消息队列作为通信方式。由于目前只实践了rabbitmq相关,就以它为引初探消息队列。
实践
安装-Mac
brew install rabbitmq
安装地址: /usr/local/Cellar/rabbitmq/3.7.9
启动:sbin/rabbitmq-server
管理界面: http://localhost:15672/ 默认用户名密码:guest/guest
命令行管理: rabbitmqctl list_queues等
rabbitmq
rabbitmq有三部分主体,生产者,队列,消费者,生产者负责把消息发送到队列,虽然消息从rabbigmq到应用,但是消息只能在队列中存储,队列只和host的内存的memor以及disk绑定,可以认为是一个很大的缓存。可以有多个生产者和多个消费者。生产者,broker,消费者不需要在同一个主机上,大多数时候他们在不同的主机上。一个应用可以继为生产者又是消费者。
简单的发送接收(定死一个queue name)
虽然是给queue一个确定的名字,但是实际上生产者是发送消息到exchange上,然后exchange再绑定队列把消息发送到队列,消费者监听队列中有消息,进行处理。在这种情况下,指定exchange为空字符串即可(认为是默认的exchange)。同时生产者与消费者需要设置对应的参数。
下面是发送方和接收方都需要写的,新建一个connection,在connection上定义虚拟的channel(实际上channel是复用的TCP连接),然后大部分的接口都是通过channel来操作的。如果在多线程消费或者发送的情况下,每个线程都有一个channel,彼此间独立,TCP要频繁建立关闭连接成本大,channel是轻量级的,但是当channel的流量很大,所有channel都分摊在一个connection上对性能会造成影响,这个时候可以建立多个connection分摊多个channel(具体的调优还没有研究,这块待续)。
//发送方和接收方都需要的
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.queue_declare(queue=queue_name, durable=durable)
所以流程就是定义connection,channel,声明queue, durable参数表明队列是持久化的,当重启rabbitmq服务的时候,队列还在,没有被删除。
# 发送
channel.basic_publish(exchange='',
routing_key=queue_name,
body=str(body))
# 接收
def callback(self, ch, method, properties, body):
print(" [x] Received %r" % body)
channel.basic_consume(callback, queue=queue_name, no_ack=True)
channel.start_consuming()
这里设置ack为不需要手动发送ack,认为收到消息就会自动发送ack,然后队列中的消息会被删除。
关于ack
ack用来保证消息不被丢失,如果consumer收到消息但是还没处理完工作线程就挂了,消息就会丢失。如果consumer挂了(channel关掉,connection关掉,TCP连接丢失),并且没有发送ack, rabbitmq就会知道消息没有被处理,从而不会在队列中删掉,如果有其他的consumer可以监听,这个消息会发送给这个consumer,从而保证了消息不会丢失,没有timeout,所以即使处理耗时很长的任务也没关系。ack必须发送到接收到的相同的channel, 如果尝试使用另一个channel来发送ack会导致channel级别的异常。
# 发送
channel.basic_publish(exchange='',
routing_key=queue_name,
body=body,
properties=pika.BasicProperties(
delivery_mode=2 #消息持久化
))
# 接收
def callback_sendack(ch, method, properties, body): # no_ack=False
print(" [x] Received %r" % body)
ch.basic_ack(delivery_tag=method.delivery_tag)
均分发送
如果这个时候有两个消费者同时监听一个queue,这个时候如果生产者发送消息1,2,3,4,5,最后的接收情况是消费者1收到了消息1,3,5,消费者2收到了消息2,4.
广播(exchange为fanout)
之前是一个消息同时只有一个consumer,现在是发布/订阅模式,多个consumer可以同时消费一个消息,广播,即一个消息可以同时被所有的消费者接收。producer把消息发送给exchange,exchange决定消息是发送给特定的队列,还是好多队列,还是丢弃,规则由exchange类型决定:direct, topic, headers, fanout,fanout 广播模式
logger,一个console能打印,一个log,fanout
关于队列名, 如果需要队列在producer和consumer之间共享,那么指定队列名即可
但是在这种情况下广播消息,所以接收方无法绑定一个队列名
两个步骤:1)获得随机队列名 result= channel.queue_declare() result.method.queue 2)接收方在接收到消息的时候删掉队列 exclusive=True
bindings
exchange和queue的绑定关系,通过他们的binding_key(routing_key,与basic_publish里面的routing_key区别看),direct mode, binding_key与routing_key对应,其他的消息将被丢弃
一个exchange可以绑定多个queue
# 发送
channel.exchange_declare(exchange=exchange_name, exchange_type='fanout')
# 接收
channel.exchange_declare(exchange=exchange_name, exchange_type='fanout')
result = channel.queue_declare(exclusive=True) # connection断开队列会被删掉
queue_name = result.method.queue
channel.bind(exchange=exchange_name, queue=queue_name)
direct mode
# 发送
channel.exchange_declare(exchange=exchange_name, exchange_type='direct')
# 接收
channel.exchange_declare(exchange=exchange_name, exchange_type='direct')
result = channel.queue_declare(exclusive=True) # connection断开队列会被删掉
queue_name = result.method.queue
channel.bind(exchange=exchange_name, queue_name=queue_name, routing_key=routing_key)
rpc mode
# 发送
corr_id = str(uuid.uuid4()) #发送就生成一个corr_id, 保证server返回的消息与发送对应
def call(self, correlation_id, callback_queue, body, routing_key="rpc_queue", exchange_name=""):
response = None
basic_publish(exchange=exchange_name,
routing_key=routing_key,
properties=pika.BasicProperties(
reply_to=callback_queue,
correlation_id=correlation_id,
),
body=str(body))
while response is None:
connection.process_data_events()
return response
response = send.call(30)
# 接收
def on_rpc_mode_callback(self, ch, method, props, body):
print(" [.] received one msg (%s)" % body)
response = body
ch.basic_publish(exchange='',
routing_key=props.reply_to,
properties=pika.BasicProperties(correlation_id=props.correlation_id),
body=str(response))
ch.basic_ack(delivery_tag=method.delivery_tag)
rpc远程调用,发送方发送消息的时候会同时发送reply_to,用于给消费者返回消费发送的队列名,发送方也会发送correlation_id,用于匹配消费者返回消息对应哪个发送方。
有的没的想法
调试的过程中遇到几个问题:
- 如果消息是json类型的,会报错,json.dumps()即可
- callback的时候callback的名字,不要加()
- 设计的时候需要考虑当server挂掉的情况下,重启程序能否正常工作,涉及到数据持久化方案,多线程,多个线程处理多个队列情况下是否会死锁等。具体的设计方案待续。