rabbitmq

Docker安装

注意获取镜像的时候要获取management版本的,不要获取last版本的,management版本的才带有管理界面

获取rabbitmq镜像

docker pull rabbitmq:management

查看rabbitmq镜像

docker search rabbitmq:management 

运行rabbitmq镜像

#方式一:默认guest用户,密码也是guest
   docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management
   # 进入容器执行命令
   docker container exec -it rabbitmq  bash
   


#方式二:设置用户名和密码
   docker run -d 
     --name my-rabbitmq 
     -p 5672:5672 -p 15672:15672 
     -v /data:/var/lib/rabbitmq 
     --hostname my-rabbitmq-host 
     -e RABBITMQ_DEFAULT_VHOST=my_vhost 
     -e RABBITMQ_DEFAULT_USER=admin 
     -e RABBITMQ_DEFAULT_PASS=admin 
     --restart=always 
     rabbitmq:management

img

参数说明:

  -d:后台运行容器
   -name:指定容器名
   -p:指定服务运行的端口(5672:应用访问端口;15672:控制台Web端口号)
   -v:映射目录或文件,启动了一个数据卷容器,数据卷路径为:/var/lib/rabbitmq,再将此数据卷映射到住宿主机的/data目录
   --hostname:主机名(RabbitMQ的一个重要注意事项是它根据所谓的 “节点名称” 存储数据,默认为主机名)
   -e:指定环境变量;(RABBITMQ_DEFAULT_VHOST:默认虚拟机名;RABBITMQ_DEFAULT_USER:默认的用户名;RABBITMQ_DEFAULT_PASS:默认用户名的密码)
   --restart=always:当Docker重启时,容器能自动启动   
   rabbitmq:management:镜像名
   
   注1:RABBITMQ_DEFAULT_VHOST=my_vhost,my_vhost名字请记好,在之后的编程中要用到,
        如果启动时没指定,默认值为/

常用操作命令

命令行与管控台-基础操作

rabbitmqctl stop_app:关闭应用
rabbitmqctl start_app:启动应用
rabbitmqctl status:节点状态
rabbitmqctl add_user username password:添加用户
rabbitmqctl list_users:列出所有用户
rabbitmqctl delete_user username:删除用户
rabbitmqctl clear_permissions -p vhostpath username:清除用户权限
rabbitmqctl list_user_permissions username:列出用户权限
rabbitmqctl change_password username newpassword:修改密码
rabbitmqctl set_permissions -p vhostpath username “.*” “.*” “.*”
rabbitmqctl add_vhost vhostpath:创建虚拟主机
rabbitmqctl list_vhosts:列出所有虚拟主机
rabbitmqctl list_permissions -p vhostpath:列出虚拟主机上所有权限
rabbitmqctl delete_vhost vhostpath:删除虚拟主机
rabbitmqctl list_queues:查看所有队列信息
rabbitmqctl -p vhostpath purge_queue blue:清除队列里的消息

命令行与管控台-高级操作

rabbitmqctl reset:移除所有数据,要在rabbitmqctl stop_app之后使用
rabbitmqctl join_cluster <clustermode> [--ram]:组成集群命令
rabbitmqctl cluster_status:查看集群状态
rabbitmqctl change_cluster_node_type disc | ram:修改集群节点的存储形式
rabbitmqctl forget_cluster_node {--offline} 忘记节点 (摘除节点)
rabbitmqctl rename_cluster_node oldnode1 newnode1 [oldnode2] [newnode2...] (修改节点名称)

进入RabbitMQ管理平台进行相关操作在浏览器中查看

输入宿主机的ip+Rabbitmq的端口(15672)

img

默认密码账号是guest,输入登陆进去ok

img

或者如下操作安装rabbitmq也行

docker run -d --hostname my-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3.8.0-beta.4-management
打开浏览器访问ip:15672,默认账号密码都是guest.

RabbitMQ的使用

基于RabbitMQ是工作在‘192.168.174.129:15672’上

消息分发(基础版)

img

这就是RabbitMQ最简单的工作模式,p为生产者(Producer),生产者发送message给queue,queue再把消息发送至消费者c(Customer)

# 生产者至队列
import pika
connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129')) #建立链接
channel = connect.channel()
channel.queue_declare(queue='hello') #定义一个队列,队列的名字叫‘hello'
channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='hello world!')
print("[x] Sent 'hello world!'")
connect.close()

运行了代码后我们可以进入容器查看消息队列
docker container exec -it rabbitmq bash
rabbitmqctl list_queues

img

# 消费者
import pika
connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
# 注意:这里队列名我们在生产者里已经定义了过了,但我们在实际工作中,我们并不能确定是生产者还是消费者先一步运行,如果队列名没有定义的话运行时候是会报错的
channel.queue_declare(queue='hello')

def callback(ch,method,properties,body):
    print('--->', ch, method, properties)
    print("[x] Received %r"%body) #收到的消息是二进制

# ch是connect.channel的内存对象地址
# method是包含的发送的信息

# 当消息来临时,消费者会执行回调函数callback
# pika == 1.1.0 如下写法
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)
# channel.basic_consume(callback, queue='hello', no_ack=True)

print('[*] Waiting for message. To exit press ctrl+c')
channel.start_consuming()

公平分发(workqueue)

img

import pika,sys

connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello')
message = ' '.join(sys.argv[1:]) or "Hello World."
channel.basic_publish(exchange='', routing_key='hello', body=message)
print('send %s'%message)
connect.close()

# cmd 中执行 python 脚本名.py 发送消息
import time,pika

connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello')

print(' [*] Waiting for messages. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    time.sleep(body.count(b'.'))
    print(" [x] Done")
    ch.basic_ack(delivery_tag = method.delivery_tag)

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

启动多个消费者,再用生产者发送消息,可以发现,消息是被公平的依次被分发给各个消费者的(Fair dispatch),这种分发的方式叫轮询。

注意
在现实情况中,因为有可能每个消费者处理信息的能力不一样,有的处理的快有的处理的慢,如果按公平分发的话有可能导致负载不平衡,旱的旱死、涝的涝死。为避免这种情况发生我们需要设置一下,让处理快的分发的消息多点,处理慢的少分发点。channel.basic_qos(prefetch_count=1),用这个语句限制了消费者待处理信息的个数,如果消费者待处理消息还有一条没处理就不给它分发消息了,如果消费者没有待处理消息就给它分发消息。

import pika,sys
connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello')
message = ' '.join(sys.argv[1:]) or "Hello World."
channel.basic_publish(exchange='', routing_key='hello', body=message) #消息持久化
print('send %s'%message)
connect.close()
import time,pika
connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello')
print(' [*] Waiting for messages. To exit press CTRL+C')
def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    time.sleep(body.count(b'.'))
    print(" [x] Done")

channel.basic_qos(prefetch_count=1)  #限制消费者待处理任务个数
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)
channel.start_consuming()

消息确认message acknowledgments

现在考虑下这种情形:消费者在处理消息时需要较长的时间,在这时把这个消费者kill掉,正在处理的消息和已经接收但未被处理的消息就丢失了。这应该是不允许的,我们可不希望有数据丢失,就需要将这些任务重新发送给其他正常工作的消费者。

为了保证任务不丢失,RabbitMQ支持使用message acknowledgments,消费者在完成任务后会给RabbitMQ发送个消息,告诉他活已经干完了,RabbitMQ就会把这个任务给释放掉。而当出现消费者宕机、掉线等情况时,RabbitMQ会重新把这个任务发送给其他的消费者。

往回看上面提到的auto_ack这个值默认的是False,RabbitMQ不会主动销毁消息。等待消费者在完成消息后手动确认在销毁。如果为True会自动销毁消息,也就是分发完消息后直接销毁消息,不会进行确认。

import pika

connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello')
message = "Hello World."
channel.basic_publish(exchange='', routing_key='hello', body=message)
print('send %s'%message)
connect.close()
import time,pika

connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello')

print(' [*] Waiting for messages. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print('要接受消息了~~')
    time.sleep(30)
    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)
channel.start_consuming()

这样,当一个消费者宕机了,RabbitMQ就会直接把任务交给下一个消费者。

消息持久化

刚才通过了消息确认,我们保证了消费者在掉线的时候任务不丢失,可是还有一个问题,如果RabbitMQ如果断掉(或者服务重启)了,里面的任务(包括所有queue和exchange依旧会丢失)这时候我们可以用到——消息持久化Message durability

# 如果队列hello已存在你再去持久化会报错
channel.queue_declare(queue='hello',durable=True)#将队列持久化(只保存了队列)

channel.basic_publish(exchange='', routing_key='hello', body=message,
                      properties=pika.BasicProperties(delivery_mode=2))#保持消息持久化

必须同时将队列和消息持久化,可以保证RabbitMQ服务在重启后任务还存在。

注意几点:

  1. 如果只持久化了消息,服务重启后消息丢失

  2. 如果只持久化了队列,服务重启后队列还在,但消息丢失

  3. 在持久化队列的时候要保持生产者和消费者的一致性

import pika

connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello', durable=True) #将队列持久化(只保存了队列)
message = "Hello World."
channel.basic_publish(exchange='', routing_key='hello', body=message, properties=pika.BasicProperties(delivery_mode=2)) #保持消息持久化
print('send %s'%message)
connect.close()
import time,pika

connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.queue_declare(queue='hello', durable=True) #注意这里也要持久化

print(' [*] Waiting for messages. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print('要接受消息了~~')
    time.sleep(30)
    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)
channel.start_consuming()

发布/订阅(publish/subscribe)

我们在前面两部分将的都是将消息由生产者到消费者之间通过queue传递,现在将引入一个新的成员:exchange。

其实生产者在发送的时候是不知道消息要发送给那个queue的,甚至他都不知道消息是由queue接收的。实际上生产者只是把message发送给了exchange。至于message后续的处理都是由exchange决定的。

img

就像图上标示的,exchange在sender和queue之间起到了转呈的作用。

按照工作方式,我们将exchange分成了fanout、direct、topic和headers四种类型。

  • fanout:所有绑定到这个exchange的队列都接收消息

  • direct:通过routingKey和exchange决定的那个唯一的queue可以接收消息

  • topic:所有符合routingKey(可以是表达式)的routingKey所绑定的queue可以接收消息

    • 表达式说明:# 表示一个或多个字符,* 表示任何字符

    ​ 列:#.a会匹配a.a,aa.a,aaa.a等。*.a会匹配a.a,b.a,c.a等

    ​ 注:使用RoutingKey为#,Exchange Type为topic的时候相当于使用fanout

  • headers:通过headers来决定把消息发送给哪些queue。

注意:前面我们还没有引入exchange这个概念,就用了默认的exchange设置exchange="",空的字符串表示了默认的exchange或名字是空的,那exchange就把消息发送给routing_key指定的queue里(前提是这个queue是存在的),在声明了exchange以后,我们就可以用这个exchange发送消息了

fanout

import pika
connect = pika.BlockingConnection(pika.ConnectionParameters(host='192.168.174.129'))
channel = connect.channel()

#定义一个exchange,名字随便起一个‘logs’,类型声明为fanout
channel.exchange_declare(exchange='logs', exchange_type='fanout')

message = 'info: Hello World!'

#注意这里我们没有定义队列,因为在广播的时候是不用固定具体的哪个queue的
channel.basic_publish(exchange='logs', #使用的exchange名称
                      routing_key='', #使用的队列名称
                      body=message)  #消息内容

print('[x] sent %s' % message)
connect.close()
import pika
connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.exchange_declare(exchange='logs',exchange_type='fanout')

#exclusive 唯一的,为True时不指定queue名的化随机生成一个queue,在断开连接后把queue销毁,相当于生成一个随机queue
result = channel.queue_declare(queue='', exclusive=True)   
                                                    
channel.queue_bind(exchange='logs', queue=result.method.queue) #绑定的是exchange对应的queue

print('waiting for logs.')
def callback(ch,method,preproteries,body):
    print('get data:%r' % body)
channel.basic_consume(queue=result.method.queue, on_message_callback=callback, auto_ack=True)
channel.start_consuming()

img

注意:这个订阅——发布的模型就像电台和收音机一样,如果customer下线了是收不到信息的,消息也是在线发送的,并不会保存。

direct

在fanout的发布订阅模式下,接收端是对信息一股脑的照单全收,而在direct模式下,接收端可以对信息进行一定原则的过滤,只接收自己感兴趣的信息

img

# direct_publisher.py
import pika, sys
connect = pika.BlockingConnection(pika.ConnectionParameters(host='192.168.174.129'))
channel = connect.channel()

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

serverity = sys.argv[1] if len(sys.argv)>1 else 'info'
message = ' '.join(sys.argv[2:]) or "Hello World."


channel.basic_publish(exchange='direct_logs',
                      routing_key=serverity,  #消息的分类
                      body=message)

print('[x] sent %r:%r' % (serverity, message))
connect.close()

#启动脚本发送信息如下所示
python direct_publisher.py info 123
python direct_publisher.py error 456
python direct_publisher.py warning 789
# direct_consumer.py
import pika, sys
connect = pika.BlockingConnection(pika.ConnectionParameters('192.168.174.129'))
channel = connect.channel()
channel.exchange_declare(exchange='direct_logs',exchange_type='direct')


result = channel.queue_declare(queue='', exclusive=True)   
queue_name = result.method.queue
                                                    
servrities = sys.argv[1:]    #获取所有的关键字
if not servrities:
    sys.stderr.write('Usage: %s [info] [warning] [error]
' % sys.argv[0])
    sys.exit(1)   #关键字不存在打印提示后退出
    
print('recived:%s' % servrities)

for servrity in servrities:  #循环绑定
    channel.queue_bind(exchange='direct_logs',
                       queue=queue_name,
                       routing_key=servrity)

print('waiting for logs.')
def callback(ch,method,preproteries,body):
    print('[x] %r:%r' % (method.routing_key, body))
channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()

# 启动两个terminal,分别如下启动
python direct_consumer.py info error warning
python direct_consumer.py error

topic

direct的模式实现了初步的消息过滤,topic比direct更加强大,它可以实现更加细致的消息过滤,比如我在获取info的前提下还要知道哪些message是RabbitMQ发来的,哪些是Redis发来的

img

# topic_publisher.py
import pika
import sys
 
connection = pika.BlockingConnection(pika.ConnectionParameters(host='192.168.174.129'))
channel = connection.channel()
 
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
 
routing_key = sys.argv[1] if len(sys.argv) > 1 else 'anonymous.info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='topic_logs', routing_key=routing_key, body=message)
print(" [x] Sent %r:%r" % (routing_key, message))
connection.close()

#发送
python topic_publisher.py info.11 duziele
python topic_publisher.py mysql.warning.11 slug
python topic_publisher.py mysql.error alzy
# topic_customer.py
import pika
import sys
 
connection = pika.BlockingConnection(pika.ConnectionParameters(host='192.168.174.129'))
channel = connection.channel()
 
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
 
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
 
binding_keys = sys.argv[1:]
if not binding_keys:
    sys.stderr.write("Usage: %s [binding_key]...
" % sys.argv[0])
    sys.exit(1)
 
for binding_key in binding_keys:
    channel.queue_bind(exchange='topic_logs', queue=queue_name, routing_key=binding_key)
 
print(' [*] Waiting for logs. To exit press CTRL+C')
 
def callback(ch, method, properties, body):
    print(" [x] %r:%r" % (method.routing_key, body))
 
channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)
 
channel.start_consuming()

# 启动
python topic_customer.py info.*    # 可以接收以info.开头的所有消息  如info.123 abc  接收到abc
python topic_customer.py *.warning.*   # 中间包含.warning.的消息  如123.warning.345 abc  接收到abc
python topic_customer.py *.error msql.* # 可以接收多个
python topic_customer.py #    # #表示获取所有消息

RPC

远程过程调用

img

# RPC client
import pika
import uuid

class FibonacciRpcClient(object):
    def __init__(self):
        self.connection=pika.BlockingConnection(pika.ConnectionParameters(host='192.168.174.129'))
        self.channel = self.connection.channel()
        result = self.channel.queue_declare(queue='', exclusive=True)
        self.callback_queue = result.method.queue  #声明请求响应回执的queue

        self.channel.basic_consume(queue=self.callback_queue, on_message_callback=self.on_response, auto_ack=True) #监听回执queue

    def on_response(self,ch,method,props,body):  #callback_queue的回调函数
        print(body)
        if self.corr_id == props.correlation_id:
            self.response = body

    def call(self,n):
        self.response = None
        # 设置一个唯一的ID,在callback的队列里通过查看属性来判断他对应哪个请求
        self.corr_id = str(uuid.uuid4())
        self.channel.basic_publish(exchange='', routing_key='rpc_queue',
                                   # reply_to 指定返回队列 correlation_id 携带唯一id
                                   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()   #事件驱动,非阻塞版的start_consuming
        return int(self.response)


fibonacci_rpc = FibonacciRpcClient()
n = input('>>>')
print('[x] Requesting fib(%s)' % n)
response = fibonacci_rpc.call(n)
print(response)
# RPC server
import pika

connect = pika.BlockingConnection(pika.ConnectionParameters(host='192.168.174.129'))
channel = connect.channel()

channel.queue_declare(queue='rpc_queue')

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)
    print(response)

    ch.basic_publish(exchange='', routing_key=props.reply_to, # propos 获取返回队列名
                     properties=pika.BasicProperties(correlation_id=props.correlation_id), # 获取客户端发送过来的唯一id,并回传回去
                     body = str(response))
    print('send over')
    ch.basic_ack(delivery_tag=method.delivery_tag)   #确认client接收到消息


channel.basic_consume(queue='rpc_queue', on_message_callback=on_request)

print('[x]Awaitiong RPC requests')
channel.start_consuming()

RPC工作流程:

  1. client启动,声明一个匿名的callback queue

  2. 建立RPC请求,请求里除了消息还包含两个参数:a.replay_to(告诉server响应的结论callback的队列里)

                       b.correlation_id:每个请求都被赋予一个独一无二的值

  1. 请求被发送给RPC_queue

  2. server等待queue里的消息,一旦出现请求,server响应请求并把结论发送给通过replay_to要求的queue里

  3. client在callback_queue里等待数据,一旦消息出现,他将correlation进行比对,如果相同就获取请求结果。

原文地址:https://www.cnblogs.com/chenwenyin/p/13586713.html