定时任务实现(RabbitMQ 延迟队列)
前言
其实rabbit 没有现成可用的延迟队列,但是可以利用其两个重要特性来实现之:1、Time To Live(TTL)消息超时机制;2、Dead Letter Exchanges(DLX)死信队列。
先理解一个概念:
rabbit 中一个消息是有死亡状态的,它会被发送到一个指定的队列中,这个队列是一个普通的队列,根据他的功能,我们叫他死信队列。
当发生下面的情况时,消息会被发送到死信队列:
- 消息被消费者接收,并且标记了reject或者nack,拒绝或者未消费成功。
- 队列设定了消息存活时间,超过存活时间未被消费,会自动发送到死信队列。
- 队列满了,再被分发到队列的消息,会被发送到死信队列。
延迟队列原理
RabbitMQ可以针对Queue设置x-expires 或者 针对Message设置 x-message-ttl,来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
RabbitMQ消息的过期时间有两种方法设置。
- 通过队列(Queue)的属性设置,队列中所有的消息都有相同的过期时间。(本次延迟队列采用的方案)
- 对消息单独设置,每条消息TTL可以不同。
如果同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就成为死信(dead letter)
死信队列
RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
- x-dead-letter-exchange:出现死信(dead letter)之后将dead letter重新发送到指定exchange
- x-dead-letter-routing-key:出现死信(dead letter)之后将dead letter重新按照指定的routing-key发送
队列中出现死信(dead letter)的情况有:
- 消息或者队列的TTL过期。(延迟队列利用的特性)
- 队列达到最大长度
- 消息被消费端拒绝(basic.reject or basic.nack)并且requeue=false
综合上面两个特性,将队列设置TTL规则,队列TTL过期后消息会变成死信,然后利用DLX特性将其转发到另外的交换机和队列就可以被重新消费,达到延迟消费效果。
如图:
理解了概念就知道是使用rabbit 的死信队列 做定时任务了。
示例:(这里使用的是消息过期时间,根据实际情况,也可以换成队列过期时间)
PS: 以下是基于消息或基于队列实现
新建一个rabbitmq 包:
__init__.py
# -*- coding: utf-8 -*- import pika from .config import * class RabbitConnection(object): """ 实例化 Rabbitmq 连接对象, 默认读取config 中的配置 """ def __init__(self, host=None, port=None, user=None, password=None): self.connection = None self.host = host if host else RABBIT_HOST self.port = port if port else RABBIT_PORT self.user = user if user else RABBIT_USER self.password = password if password else RABBIT_USER_PASSWORD def get_connection(self): # mq用户名和密码 credentials = pika.PlainCredentials(self.user, self.password) # 虚拟队列需要指定参数 virtual_host,如果是默认的可以不填。 connection = pika.BlockingConnection( pika.ConnectionParameters(host=self.host, port=self.port, credentials=credentials) ) self.connection = connection return connection.channel()
config.py
# -*- coding: utf-8 -*- RABBIT_USER = 'admin' RABBIT_USER_PASSWORD = '123456' RABBIT_HOST = 'localhost' RABBIT_PORT = '5672' # 死信消息队列 dead_queue_name = "dead_queue" # 交换机 dead_exchange = 'dead_exchange' # 路由key dead_routing_key = 'dead_routing' # 延迟队列声明 delay_queue_name = "delay_queue" delay_exchange = 'delay_exchange' delay_routing_key = 'delay_routing'
生产者
# -*- coding: utf-8 -*- import pika import datetime from rabbitmq import RabbitConnection from rabbitmq.config import * def send_task(channel, timeout): # 声明死信队列 channel.exchange_declare(exchange=dead_exchange, exchange_type="direct") channel.queue_declare(queue=dead_queue_name) channel.queue_bind(queue=dead_queue_name, exchange=dead_exchange, routing_key=dead_routing_key) # 延迟队列声明 # 死信转发参数 arguments = { # 通过x-message-ttl 指定整个队列所有消息过期时间, 单位毫秒 # "x-message-ttl": timeout, 'x-dead-letter-exchange': dead_exchange, 'x-dead-letter-routing-key': dead_routing_key } # 指定交换机 channel.exchange_declare(exchange=delay_exchange, durable=True, exchange_type="direct") # 如果指定的queue不存在,则会创建一个queue,如果已经存在 则不会做其他动作,官方推荐,每次使用时都可以加上这句 channel.queue_declare(queue=delay_queue_name, durable=False, arguments=arguments) # 队列绑定交换机 channel.queue_bind(queue=delay_queue_name, exchange=delay_exchange, routing_key=delay_routing_key) # 发送信息 channel.basic_publish( exchange=delay_exchange, routing_key=delay_routing_key, body="Hello world.", # 通过expiration指定 单条消息过期时间, 单位毫秒 properties=pika.BasicProperties(delivery_mode=2, expiration=str(timeout)) ) return channel if __name__ == "__main__": rabbit = RabbitConnection() channel = rabbit.get_connection() # 3秒过期, 单位:毫秒 print(datetime.datetime.now()) channel = send_task(channel, 3000) # 关闭连接 rabbit.connection.close()
消费者:
# -*- coding: utf-8 -*- import datetime from rabbitmq import RabbitConnection from rabbitmq.config import * def dead_queue(channel): def callback(ch, method, properties, body): # 回调函数, 处理消息队列中的消息 print("Receive msg: ", body) print(datetime.datetime.now()) # 设置手动应答 ch.basic_ack(delivery_tag=method.delivery_tag) # 声明交换机 channel.exchange_declare(exchange=dead_exchange, durable=False, exchange_type='direct') # 声明队列, 如果指定的queue不存在,则会创建一个queue, 如果已经存在 则不会做其他动作 # 官方推荐, 每次使用时都可以加上这句, 这样生产者和消费者就没有必要的先后启动顺序了 channel.queue_declare(queue=dead_queue_name, durable=False) # 建立绑定关系(交换机、队列、路由key) channel.queue_bind(exchange=dead_exchange, queue=dead_queue_name, routing_key=dead_routing_key) # prefetch_count表示接收的消息数量,当我接收的消息没有处理完(用basic_ack标记消息已处理完毕)之前不会再接收新的消息了 # 还有就是这个设置必须在basic_consume之上,否则不生效 # 这种情况必须关闭自动应答ack,改成手动应答。 # 使用basicQos(prefetch_count=1) 限制每次只发送不超过1条消息到同一个消费者,消费者必须手动反馈告知队列,才会发送下一个. channel.basic_qos(prefetch_count=1) # 告诉rabbitmq,用callback来接收消息 channel.basic_consume( dead_queue_name, callback, # 指定为False,表示取消自动应答,交由回调函数手动应答 auto_ack=False ) return channel if __name__ == '__main__': rabbit = RabbitConnection() channel = rabbit.get_connection() consuming = dead_queue(channel) print('开始监听') try: consuming.start_consuming() except KeyboardInterrupt: consuming.stop_consuming() rabbit.connection.close() print('close')
看了上面还是模糊: 点击前往原著
消息阻塞
上面的存在一个问题, expiration 设置表示消息在队列中存活的时长, 当我们的消息存活时长不确定时,会出现消息阻塞,如:前一个消息过期时间为10s, 后一个消息过期时长为3s, 那么你会看到,明明第二个消息已经过期了还是没有加入到死信队列中, 而是等第一个消息过期后它才一起进入死信队列。要解决这个问题得引入rabbitmq 延迟队列插件:
安装插件
打开地址 我的是3.10,在下载插件前一定要确认好对应版本
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.10.0
这是所有
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
下载后放到安装目录plugins下
如:~\RabbitMQ Server\rabbitmq_server-3.9.14\plugins\
进入安装目录的sbin目录执行
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
重启mq后
service rabbitmq-server restart 或 rabbitmq-server restart
进入管理页面,查看是否安装成功
看到 x-delayed-message 说明安装成功, 创建交换机时要注意加上 arguments = {‘x-delayed-type’: 'direct'}, 值可以是direct 或 topic 否则会创建失败
如图:
正确得创建:
生产者
# -*- coding: utf-8 -*- import pika from rabbitmq import RabbitConnection from rabbitmq.config import * def send_task(channel, request_id, timeout): # 延迟队列声明 arguments = { # 通过x-message-ttl 指定整个队列所有消息过期时间, 单位毫秒 # "x-message-ttl": timeout, 'x-delayed-type': 'direct', } # 指定交换机类型,否则报错 exchange_arguments = { 'x-delayed-type': 'direct' } channel.exchange_declare( exchange=delay_exchange, durable=False, exchange_type=delay_exchange_type, arguments=exchange_arguments ) # 如果指定的queue不存在,则会创建一个queue,如果已经存在 则不会做其他动作,官方推荐,每次使用时都可以加上这句 channel.queue_declare(queue=delay_queue_name, durable=False, arguments=arguments) # 队列绑定交换机 channel.queue_bind(queue=delay_queue_name, exchange=delay_exchange, routing_key=delay_routing_key) # 发送信息 channel.basic_publish( exchange=delay_exchange, routing_key=delay_routing_key, body=request_id, # 一定要加上 headers 配置, 否则交换机不会生效 properties=pika.BasicProperties(delivery_mode=2, headers={'x-delay': timeout}) ) return channel if __name__ == "__main__": rabbit = RabbitConnection() channel = rabbit.get_connection() # 3秒过期, 单位:毫秒 channel = send_task(channel, '延迟3s', 3000) channel = send_task(channel, '延迟10s', 10000) channel = send_task(channel, '延迟30s', 30000) channel = send_task(channel, '延迟5s', 5000) print(123456) # 关闭连接 rabbit.connection.close()
PS: headers 设置, 为消息进入交换机后延迟timeout 后在下发到队列得声明,
消费者
# -*- coding: utf-8 -*- import requests from rabbitmq import RabbitConnection from rabbitmq.config import * def dead_queue(channel): def callback(ch, method, properties, body): # 回调函数, 处理消息队列中的消息 # 设置手动应答 ch.basic_ack(delivery_tag=method.delivery_tag) print(f'执行定时任务request_id[{body}]返回结果') # 声明交换机 exchange_arguments = { 'x-delayed-type': 'direct' } channel.exchange_declare( exchange=delay_exchange, durable=False, exchange_type=delay_exchange_type, arguments=exchange_arguments, ) # 声明队列 channel.queue_declare(queue=delay_queue_name, durable=False) # 建立绑定关系(交换机、队列、路由key) channel.queue_bind(exchange=delay_exchange, queue=delay_queue_name, routing_key=delay_routing_key) # prefetch_count表示接收的消息数量,当我接收的消息没有处理完(用basic_ack标记消息已处理完毕)之前不会再接收新的消息了 # 还有就是这个设置必须在basic_consume之上,否则不生效 # 这种情况必须关闭自动应答ack,改成手动应答。 # 使用basicQos(prefetch_count=1) 限制每次只发送不超过1条消息到同一个消费者,消费者必须手动反馈告知队列,才会发送下一个. channel.basic_qos(prefetch_count=1) # 告诉rabbitmq,用callback来接收消息 channel.basic_consume( delay_queue_name, callback, # 指定为False,表示取消自动应答,交由回调函数手动应答 auto_ack=False ) return channel if __name__ == '__main__': rabbit = RabbitConnection() channel = rabbit.get_connection() consuming = dead_queue(channel) print('开始监听') try: consuming.start_consuming() except KeyboardInterrupt: consuming.stop_consuming() rabbit.connection.close() print('close')
加上死信队列版
生产者
# -*- coding: utf-8 -*- import pika from rabbitmq import RabbitConnection from rabbitmq.config import * def send_task(channel, request_id, timeout): # # 声明死信队列 exchange_arguments = { 'x-delayed-type': 'direct' } channel.exchange_declare( exchange=dead_exchange, durable=False, exchange_type=delay_exchange_type, arguments=exchange_arguments ) channel.queue_declare(queue=dead_queue_name, durable=False) channel.queue_bind(queue=dead_queue_name, exchange=dead_exchange, routing_key=dead_routing_key) # 延迟队列声明 # 死信转发参数 arguments = { # 通过x-message-ttl 指定整个队列所有消息过期时间, 单位毫秒 "x-message-ttl": 1, 'x-delayed-type': 'direct', 'x-dead-letter-exchange': dead_exchange, 'x-dead-letter-routing-key': dead_routing_key } # 指定交换机 exchange_arguments = { 'x-delayed-type': 'direct' } channel.exchange_declare( exchange=delay_exchange, durable=False, exchange_type=delay_exchange_type, arguments=exchange_arguments ) # 如果指定的queue不存在,则会创建一个queue,如果已经存在 则不会做其他动作,官方推荐,每次使用时都可以加上这句 channel.queue_declare(queue=delay_queue_name, durable=False, arguments=arguments) # 队列绑定交换机 channel.queue_bind(queue=delay_queue_name, exchange=delay_exchange, routing_key=delay_routing_key) # 发送信息 channel.basic_publish( exchange=delay_exchange, routing_key=delay_routing_key, body=request_id, # 通过expiration指定 单条消息过期时间, 单位毫秒 properties=pika.BasicProperties(delivery_mode=2, headers={'x-delay': timeout})
# 上面通过队列设置了过期时间就不再使用expiration设置消息过期了 # 这个是消息过期得设置模板 # properties=pika.BasicProperties(delivery_mode=2, expiration=str(timeout), headers={'x-delay': timeout}) ) return channel if __name__ == "__main__": rabbit = RabbitConnection() channel = rabbit.get_connection() # 3秒过期, 单位:毫秒 channel = send_task(channel, '延迟3s', 3000) channel = send_task(channel, '延迟10s', 10000) channel = send_task(channel, '延迟30s', 30000) channel = send_task(channel, '延迟5s', 5000) print(123456) # 关闭连接 rabbit.connection.close()
消费者
# -*- coding: utf-8 -*- import requests from rabbitmq import RabbitConnection from rabbitmq.config import * def dead_queue(channel): def callback(ch, method, properties, body): # 回调函数, 处理消息队列中的消息 # 设置手动应答 ch.basic_ack(delivery_tag=method.delivery_tag) print(f'执行定时任务request_id[{body}]返回结果') # 声明交换机 exchange_arguments = { 'x-delayed-type': 'direct' } channel.exchange_declare( exchange=dead_exchange, durable=False, exchange_type=delay_exchange_type, arguments=exchange_arguments, ) # 声明队列, 如果指定的queue不存在,则会创建一个queue, 如果已经存在 则不会做其他动作 # 官方推荐, 每次使用时都可以加上这句, 这样生产者和消费者就没有必要的先后启动顺序了 channel.queue_declare(queue=dead_queue_name, durable=False) # 建立绑定关系(交换机、队列、路由key) channel.queue_bind(exchange=dead_exchange, queue=dead_queue_name, routing_key=dead_routing_key) # prefetch_count表示接收的消息数量,当我接收的消息没有处理完(用basic_ack标记消息已处理完毕)之前不会再接收新的消息了 # 还有就是这个设置必须在basic_consume之上,否则不生效 # 这种情况必须关闭自动应答ack,改成手动应答。 # 使用basicQos(prefetch_count=1) 限制每次只发送不超过1条消息到同一个消费者,消费者必须手动反馈告知队列,才会发送下一个. channel.basic_qos(prefetch_count=1) # 告诉rabbitmq,用callback来接收消息 channel.basic_consume( dead_queue_name, callback, # 指定为False,表示取消自动应答,交由回调函数手动应答 auto_ack=False ) return channel if __name__ == '__main__': rabbit = RabbitConnection() channel = rabbit.get_connection() consuming = dead_queue(channel) print('开始监听') try: consuming.start_consuming() except KeyboardInterrupt: consuming.stop_consuming() rabbit.connection.close() print('close')
总结:
延迟队列实现方式主要有三种:
一、基于队列, 适用于所有任务过期时间一致的。
二、基于消息, 适用于所有消息过期时间都是递增的(后一个过期时长 > 前一个)
三、基于交换机, 适用于消息过期时长不确定的,(时间不确定,上面两个都会造成消息阻塞)
centos 7 安装
MQ 3.10
安装Erlang
curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash
yum install erlang -y
查看版本信息
erl -version
安装rabbitmq
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh | sudo bash
yum install rabbitmq-server -y
配置rabbitmq
# 开机启动 systemctl enable rabbitmq-server.service# 启动rabbitmqsystemctl start rabbitmq-server.service
# 开启
service rabbitmq-server start
# 添加admin,并赋予administrator权限
添加admin用户,密码设置为admin。
sudo rabbitmqctl add_user admin admin
赋予权限
sudo rabbitmqctl set_user_tags admin administrator
赋予virtual host中所有资源的配置、写、读权限以便管理其中的资源
sudo rabbitmqctl set_permissions -p / admin '.*' '.*' '.*'
四、启动web管理工具(rabbitmq_management)
sudo rabbitmq-plugins enable rabbitmq_management
# 重启rabbitmq
service rabbitmq-server restart
基本命令
//启动命令 sudo service rabbitmq-server start //关闭 sudo service rabbitmq-server stop //重启 sudo service rabbitmq-server restart