Rabbitmq入门到精通

1 消息队列介绍

1.1 介绍

消息队列就是基础数据结构中的“先进先出”的一种数据机构。想一下,生活中买东西,需要排队,先排的人先买消费,就是典型的“先进先出”。

1.2 MQ解决什么问题

MQ是一直存在,不过随着微服务架构的流行,成了解决微服务之间问题的常用工具。

两个服务相互调用,由于不在同一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。

有两种方式调用:restful(http协议),rpc(远程过程调用),不管用rpc或者restful来通信,都涉及到同步,异步。

1.2.1 应用解耦

以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。

当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,用户感受不到物流系统的故障。提升系统的可用性。

1.2.2 流量削峰

举个栗子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。

使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这样做,有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。

1.2.3 消息分发

多个服务对数据感兴趣,只需要监听同一类消息即可处理,即发布订阅。

例如A产生数据,B对数据感兴趣。如果没有消息的队列,A每次处理完需要调用一下B服务。过了一段时间C对数据也感性,A就需要改代码,调用B服务,调用C服务。只要有服务需要,A服务都要改动代码。很不方便。

有了消息队列后,A只管发送一次消息,B对消息感兴趣,只需要监听消息。C感兴趣,C也去监听消息。A服务作为基础服务完全不需要有改动。

1.2.4 异步消息

有些服务间调用是异步的,例如A调用B,B需要花费很长时间执行,但是A需要知道B什么时候可以执行完,以前一般有两种方式,A过一段时间去调用B的查询api查询(轮询)。或者A提供一个callback api,B执行完之后调用api通知A服务。这两种方式都不是很优雅。

使用消息队列,可以很方便解决这个问题,A调用B服务后,只需要监听B处理完成的消息,当B处理完成后,会发送一条消息给MQ,MQ会将此消息转发给A服务。这样A服务既不用循环调用B的查询api,也不用提供callback api。同样B服务也不用做这些操作。A服务还能及时的得到异步处理成功的消息。


1.3 常见消息队列及比较

RabbitMQ 与 Kafka 的比较:

# rabbitmq:吞吐量小(几百万条数据没问题),有消息确认机制(确认消费完了再删,kafka拿走就没了),适合订单等对消息可靠性有要求的数据。
# kafka:吞吐量高(大数据相关),注重高吞吐量,不注重消息的可靠性(适用电商记录巨量的日志信息),数据量特别大。

结论:

Kafka在于分布式架构,RabbitMQ基于AMQP协议来实现,RocketMQ/思路来源于kafka,改成了主从结构,在事务性可靠性方面做了优化。广泛来说,电商、金融等对事务性要求很高的,可以考虑RabbitMQ和RocketMQ,对性能要求高的可考虑Kafka。

 

2 Rabbitmq安装

官网:https://www.rabbitmq.com/getstarted.html

2.1 服务端原生安装

原生安装,开启web管理界面需要去配置文件中进行配置

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

# 启动RabbitMQ,yum安装自动加入系统服务systemctl
systemctl start rabbitmq-server

2.2 服务端Docker安装

# docker官网拉取镜像,rabbitmq:management版本自动开启了web管理界面
docker pull rabbitmq:management

# docker启动容器
# 跟redis和mysql一样,rabbitmq也需要有用户操作数据,e环境变量配置用户和密码  
# 端口映射,5672是rabbitmq的默认端口,15672是web管理界面的端口
docker run -di --name Myrabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:managemen

 2.3 客户端安装

pip3 install pika

2.4 设置用户和密码

命令行设置用户和密码,也可以在web管理页面设置

# 创建用户名和密码
rabbitmqctl add_user lqz 123

# 设置用户为administrator角色,为用户打上tag
rabbitmqctl set_user_tags lqz administrator

# 设置权限
rabbitmqctl set_permissions -p "/" root ".*" ".*" ".*"

# 重启rabbiMQ服务
systemctl restart rabbitmq-server
 
# 然后可以使用刚创建的用户远程连接rabbitmq server。

 

3 基于Queue实现生产者消费者模型

import Queue
import threading

message = Queue.Queue(10)

def producer(i):
    while True:
        message.put(i)

def consumer(i):
    while True:
        msg = message.get()

for i in range(12):
    t = threading.Thread(target=producer, args=(i,))
    t.start()

for i in range(10):
    t = threading.Thread(target=consumer, args=(i,))
    t.start()

 

4 基本使用(生产者消费者模型)

对于RabbitMQ来说,生产和消费不再针对内存里的一个Queue对象,而是某台服务器上的RabbitMQ Server实现的消息队列。

生产者

import pika

# 拿到连接对象
# 无密码
# connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200'))
# 有密码
credentials = pika.PlainCredentials("admin","admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200',credentials=credentials))

# 拿到channel对象,类似mysql的cousor对象
channel = connection.channel()
# 声明一个队列(创建一个队列)
channel.queue_declare(queue='hello') # 指定队列名字为hello

channel.basic_publish(exchange='',
                      routing_key='hello',  # 消息队列名称
                      body='hello world')   # 往'hello'队列放入一个字符串,通过web管理界面可以查看
print(" Sent 'Hello World!'")
# 关闭连接
connection.close()

消费者

import pika, sys, os

def main():
    # connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.0.0.200'))  没密码
    credentials = pika.PlainCredentials("admin", "admin")
    connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
    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=True) 
    print(' [*] Waiting for messages. To exit press CTRL+C')
    
    # 消费者启动后夯住,队列中一旦放入东西,就从里面消费
    channel.start_consuming()

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print('Interrupted')
        try:
            sys.exit(0)
        except SystemExit:
            os._exit(0)

 

5 消息安全之ack(消息确认机制)

生产者代码不用改动,消费者代码需要如下调整:

def main():
    # connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.0.0.200'))
    credentials = pika.PlainCredentials("admin", "admin")
    connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
    channel = connection.channel()

    channel.queue_declare(queue='hello')

    def callback(ch, method, properties, body):
        print(" [x] Received %r" % body)
        # 真正的消息处理完了(消费成功了),再发确认
        ch.basic_ack(delivery_tag=method.delivery_tag)
    ## auto_ack=False,不会自动回复确认消息,消息会一直存在队列
    ## auto_ack=True,自动回复确认消息,队列收到确认,就会自动把消费过的消息删除,消费成功与失败队列是不知道的
    channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=False)

    channel.start_consuming()

if __name__ == '__main__':
    main()

 

6 消息安全之durable(持久化)

之前声明的队列如果没有配置持久化参数,后面加上参数也无法持久化。

生产者

import pika

credentials = pika.PlainCredentials("admin","admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200',credentials=credentials))
channel = connection.channel()
# 声明一个队列(创建一个队列),durable=True支持队列持久化,队列必须是新的才可以
channel.queue_declare(queue='hello_new',durable=True)

channel.basic_publish(exchange='',
                      routing_key='hello_new', 
                      body='hello world',
                      properties=pika.BasicProperties(
                          delivery_mode=2,  # make message persistent,支持消息持久化
                          ),
                      )
connection.close()

消费者

import pika, sys, os

def main():
    credentials = pika.PlainCredentials("admin", "admin")
    connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
    channel = connection.channel()

    channel.queue_declare(queue='hello_new',durable=True)

    def callback(ch, method, properties, body):
        print(" [x] Received %r" % body)
        ch.basic_ack(delivery_tag=method.delivery_tag)

    channel.basic_consume(queue='hello_new', on_message_callback=callback, auto_ack=False)

    channel.start_consuming()

if __name__ == '__main__':
    main()

 

7 闲置消费

正常情况如果有多个消费者,是按照顺序第一个消息给第一个消费者,第二个消息给第二个消费者,第三个消息给第一个消费者(假如两个消费者),轮着来

但是可能第一个消息的消费者处理消息很耗时,一直没结束,就可以让第二个消费者优先获得闲置的消息

生产者代码不用改动,消费者代码需要如下调整:

### 消费者1
import pika, sys, os

def main():
    credentials = pika.PlainCredentials("admin", "admin")
    connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
    channel = connection.channel()

    channel.queue_declare(queue='hello')

    def callback(ch, method, properties, body):
        import time
        time.sleep(50)  # 第一个消费者模拟消费耗时,第二个消费者闲置,一旦队列有消息都会被第二个消费者获取
        print(" [x] Received %r" % body)
        ch.basic_ack(delivery_tag=method.delivery_tag)

    channel.basic_qos(prefetch_count=1)  ##### 配置这段代码,谁闲置谁获取,没必要按照顺序一个一个来
    channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=False)
    channel.start_consuming()

if __name__ == '__main__':
    main()


###消费者2
import pika, sys, os

def main():
    credentials = pika.PlainCredentials("admin", "admin")
    connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
    channel = connection.channel()

    channel.queue_declare(queue='hello')

    def callback(ch, method, properties, body):
        print(" [x] Received %r" % body)
        ch.basic_ack(delivery_tag=method.delivery_tag)

    channel.basic_qos(prefetch_count=1)  ##### 配置这段代码,谁闲置谁获取,没必要按照顺序一个一个来
    channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=False)
    channel.start_consuming()

if __name__ == '__main__':
    main()

 

8 发布订阅

启动一个消费就会监听一个队列,生产者把消息传给exchange,通过exchange传递给每一个队列,从而实现发布一条消息,被多个消费者订阅。应用于社交网络,比如共同关注的人发布了动态,订阅者都能收到消息。

发布者

import pika

credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
channel = connection.channel()

# 声明队列不用指定队列名字queue_declare,而是指定exchange
channel.exchange_declare(exchange='logs', exchange_type='fanout')

message = "info: Hello World!"
channel.basic_publish(exchange='logs', routing_key='', body=message)
print(" [x] Sent %r" % message)
connection.close()

订阅者(启动多次,会创建出多个队列,都绑定到了同一个exchange上)

import pika

credentials = pika.PlainCredentials("admin","admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200',credentials=credentials))
channel = connection.channel()

# exchange='logs', exchange(秘书)的名称
# exchange_type='fanout' , 秘书工作方式:将消息发送给所有的队列
channel.exchange_declare(exchange='logs',exchange_type='fanout')

# 没有指定队列名称,默认随机生成一个队列
result = channel.queue_declare(queue='',exclusive=True)
queue_name = result.method.queue
print(queue_name)

# 让exchange和queque进行绑定.
channel.queue_bind(exchange='logs',queue=queue_name)
print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)

channel.basic_consume(queue=queue_name,on_message_callback=callback,auto_ack=True)

channel.start_consuming()

 

9 发布订阅高级

9.1 Routing(按关键字匹配)

指定routing_key关键字,朝指定的订阅者发布消息

发布者

import pika

credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
channel = connection.channel()

# exchange工作方式设置为direct
channel.exchange_declare(exchange='logs2', exchange_type='direct')

message = "info: Hello World!"
channel.basic_publish(exchange='logs2', routing_key='m1', body=message) # 指定朝m1关键字的队列发布消息
print(" [x] Sent %r" % message)
connection.close()

订阅者

订阅者1只绑定了m2关键字,订阅者2绑定了m1, m2关键字,如发布者 routing_key只向m1关键字队列发布消息,则订阅者1收不到消息。

##### 订阅者1
import pika

credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
channel = connection.channel()

channel.exchange_declare(exchange='logs2', exchange_type='direct')

result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
print(queue_name)

channel.queue_bind(exchange='logs2', queue=queue_name, routing_key='m2')
print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)

channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()



##### 订阅者2
import pika

credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
channel = connection.channel()

channel.exchange_declare(exchange='logs2', exchange_type='direct')

result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
print(queue_name)

channel.queue_bind(exchange='logs2', queue=queue_name, routing_key='m1')
channel.queue_bind(exchange='logs2', queue=queue_name, routing_key='m2')
print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)

channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()

 9.2 Topic(按关键字模糊匹配)

发布者

import pika

credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
channel = connection.channel()

# exchange工作方式设置为topic
channel.exchange_declare(exchange='logs3', exchange_type='topic')

message = "info: Hello World!"
channel.basic_publish(exchange='logs3', routing_key='lqz.dd', body=message)
print(" [x] Sent %r" % message)
connection.close()

订阅者

# 号表示后面可以跟任意字符,* 号表示后面只能跟一个单词,如routing_key='lqz.dd',则两个订阅者都能匹配;如routing_key='lqz.dd.com'则只有订阅者2能够匹配

##### 订阅者1 
import pika

credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
channel = connection.channel()

channel.exchange_declare(exchange='logs3', exchange_type='topic')

result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
print(queue_name)

channel.queue_bind(exchange='logs3', queue=queue_name, routing_key='lqz.*')
print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)

channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()


##### 订阅者2 
import pika

credentials = pika.PlainCredentials("admin", "admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
channel = connection.channel()

channel.exchange_declare(exchange='logs3', exchange_type='topic')

result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
print(queue_name)

channel.queue_bind(exchange='logs3', queue=queue_name,routing_key='lqz.#')
print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)

channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()

 

10 基于rabbitmq实现rpc

客户端调用服务端的一个方法,发送任务id和内容到队列中,服务端获取后,调用本地方法执行并把结果返回到队列,客户端再从队列获取结果。

服务端

import pika

credentials = pika.PlainCredentials("admin","admin")
connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200',credentials=credentials))
channel = connection.channel()

# 监听任务队列
channel.queue_declare(queue='rpc_queue')

# 服务端本地定义个fib函数(计算斐波那契数列的方法)
def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

def on_request(ch, method, props, body):
    n = int(body)
    print(" [.] fib(%s)" % n)
    response = fib(n)
    # props.reply_to        要放结果的队列
    # props.correlation_id  任务id
    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)
    
channel.basic_qos(prefetch_count=1)
channel.basic_consume( queue='rpc_queue', on_message_callback=on_request,)
print(" [x] Awaiting RPC requests")
channel.start_consuming()

客户端

import pika
import uuid

class FibonacciRpcClient(object):
    def __init__(self):
        self.credentials = pika.PlainCredentials("admin", "admin")
        self.connection = pika.BlockingConnection(pika.ConnectionParameters('10.0.0.200', credentials=credentials))
        self.channel = self.connection.channel()

        # 随机生成一个消息队列(用于接收结果)
        result = self.channel.queue_declare(queue='',exclusive=True)
        self.callback_queue = result.method.queue

        # 监听消息队列中是否有值返回,如果有值则执行 on_response 函数(一旦有结果,则执行on_response)
        self.channel.basic_consume(queue=self.callback_queue, on_message_callback=self.on_response, auto_ack=True)

    def on_response(self, ch, method, props, body):
        if self.corr_id == props.correlation_id:
            self.response = body

    def call(self, n):
        self.response = None
        self.corr_id = str(uuid.uuid4())
        # 客户端给服务端发送一个任务: 任务id = corr_id / 任务内容 = '30' / 用于接收结果的队列名称
        self.channel.basic_publish(exchange='',
                                   routing_key='rpc_queue',               # 服务端接收任务的队列名称
                                   properties=pika.BasicProperties(
                                         reply_to = self.callback_queue,  # 用于接收结果的队列
                                         correlation_id = self.corr_id,   # 任务ID
                                         ),
                                   body=str(n))
        while self.response is None:
            self.connection.process_data_events()

        return self.response

fibonacci_rpc = FibonacciRpcClient()
print(" [x] Requesting fib(30)")
response = fibonacci_rpc.call(30)  # 外界看上去,就像调用本地的call()函数一样
print(" [.] Got %r" % response)

 

posted @ 2022-11-06 18:07  不会钓鱼的猫  阅读(124)  评论(0编辑  收藏  举报