RabbitMQ-消费者(Consumer)

作者介绍:简历上没有一个精通的运维工程师。请点击上方的蓝色《运维小路》关注我,下面的思维导图也是预计更新的内容和当前进度(不定时更新)。

图片

中间件,我给它的定义就是为了实现某系业务功能依赖的软件,包括如下部分:

Web服务器

代理服务器

ZooKeeper

Kafka

RabbitMQ(本章节)

上个小节我们通过生产者代码,向RabbitMQ的交换机发送消息,本小节我们就通过消费者代码去读取队列里面数据,以下代码基于DeepSeek生成。

我们可以和生产者的RabbitMQ配置对比下,看下有什么差距。

import pika
import json
import logging
import signal
import sys
from datetime import datetime
import os

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger("mq_consumer")

# RabbitMQ 配置
RABBITMQ_HOST = 'localhost'
RABBITMQ_PORT = 5672
RABBITMQ_USER = 'guest'
RABBITMQ_PASS = 'guest'
RABBITMQ_VHOST = '/'
QUEUE_NAME = 'sensor_queue'  # 要消费的队列名称

# 文件存储配置
OUTPUT_DIR = 'mq_data'  # 输出目录
FILE_PREFIX = 'sensor_data'  # 文件前缀
MAX_FILE_SIZE = 10 * 1024 * 1024  # 10MB 最大文件大小
MAX_MESSAGES_PER_FILE = 1000  # 每个文件最大消息数

# 全局变量
running = True
current_file = None
message_count = 0
file_message_count = 0
file_size = 0

def signal_handler(sig, frame):
    """处理Ctrl+C信号,优雅地停止程序"""
    global running
    logger.info("Received shutdown signal. Stopping consumer...")
    running = False

def setup_output_dir():
    """创建输出目录"""
    if not os.path.exists(OUTPUT_DIR):
        os.makedirs(OUTPUT_DIR)
        logger.info(f"Created output directory: {OUTPUT_DIR}")
    else:
        logger.info(f"Using existing output directory: {OUTPUT_DIR}")

def get_next_filename():
    """生成下一个文件名"""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    return os.path.join(OUTPUT_DIR, f"{FILE_PREFIX}_{timestamp}.json")

def open_new_file():
    """打开新文件并写入起始标记"""
    global current_file, file_message_count, file_size
    filename = get_next_filename()

    try:
        current_file = open(filename, 'a', encoding='utf-8')
        current_file.write('[\n')  # 开始JSON数组
        file_message_count = 0
        file_size = 2  # 初始大小([和\n)
        logger.info(f"Opened new output file: {filename}")
        return True
    except Exception as e:
        logger.error(f"Failed to open file {filename}: {str(e)}")
        return False

def close_current_file():
    """关闭当前文件并写入结束标记"""
    global current_file
    if current_file and not current_file.closed:
        try:
            # 移除最后一个逗号(如果有)
            if file_message_count > 0:
                current_file.seek(current_file.tell() - 2)
                current_file.truncate()
                current_file.write('\n')

            current_file.write(']')  # 结束JSON数组
            current_file.close()
            logger.info(f"Closed output file: {current_file.name}")
        except Exception as e:
            logger.error(f"Error closing file: {str(e)}")
        finally:
            current_file = None

def write_message_to_file(message):
    """将消息写入文件"""
    global current_file, file_message_count, file_size

    # 如果文件未打开或达到限制,创建新文件
    if current_file is None or current_file.closed:
        if not open_new_file():
            return False

    # 检查是否需要创建新文件
    if (file_message_count >= MAX_MESSAGES_PER_FILE or 
        file_size >= MAX_FILE_SIZE):
        close_current_file()
        if not open_new_file():
            return False

    # 格式化消息
    try:
        message_json = json.dumps(message, ensure_ascii=False)

        # 添加逗号分隔符(除了第一条消息)
        if file_message_count > 0:
            message_json = ',\n' + message_json
        else:
            message_json = '\n' + message_json

        # 写入文件
        current_file.write(message_json)
        current_file.flush()  # 确保立即写入磁盘

        # 更新计数
        file_message_count += 1
        file_size += len(message_json.encode('utf-8'))

        return True
    except Exception as e:
        logger.error(f"Error writing message to file: {str(e)}")
        return False

def setup_rabbitmq_connection():
    """建立RabbitMQ连接"""
    try:
        credentials = pika.PlainCredentials(RABBITMQ_USER, RABBITMQ_PASS)
        parameters = pika.ConnectionParameters(
            host=RABBITMQ_HOST,
            port=RABBITMQ_PORT,
            virtual_host=RABBITMQ_VHOST,
            credentials=credentials,
            heartbeat=600,
            blocked_connection_timeout=300
        )

        connection = pika.BlockingConnection(parameters)
        channel = connection.channel()

        # 声明队列(确保存在)
        channel.queue_declare(
            queue=QUEUE_NAME,
            durable=True,
            passive=True  # 被动检查队列是否存在
        )

        # 设置QoS(公平分发)
        channel.basic_qos(prefetch_count=1)

        logger.info(f"Connected to RabbitMQ. Consuming from queue: {QUEUE_NAME}")
        return connection, channel

    except Exception as e:
        logger.error(f"Failed to connect to RabbitMQ: {str(e)}")
        return None, None

def message_callback(ch, method, properties, body):
    """消息处理回调函数"""
    global message_count

    try:
        # 解析消息
        message = json.loads(body)
        message_count += 1

        # 添加接收元数据
        message['_meta'] = {
            'received_at': datetime.utcnow().isoformat() + "Z",
            'delivery_tag': method.delivery_tag,
            'exchange': method.exchange,
            'routing_key': method.routing_key
        }

        # 写入文件
        if write_message_to_file(message):
            # 确认消息
            ch.basic_ack(delivery_tag=method.delivery_tag)

            # 每10条消息打印一次摘要
            if message_count % 10 == 0:
                logger.info(f"Processed {message_count} messages | "
                           f"Last sensor: {message.get('sensor_id', 'N/A')} | "
                           f"File: {current_file.name if current_file else 'N/A'}")
        else:
            # 写入失败,拒绝消息(重新入队)
            ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
            logger.error(f"Message processing failed. Requeued: {method.delivery_tag}")

    except json.JSONDecodeError:
        logger.error(f"Invalid JSON message: {body[:100]}...")
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

    except Exception as e:
        logger.error(f"Error processing message: {str(e)}")
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

def main():
    """主函数,消费消息并保存到文件"""
    global running

    # 注册信号处理器
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    # 设置输出目录
    setup_output_dir()

    # 建立RabbitMQ连接
    connection, channel = setup_rabbitmq_connection()
    if not channel:
        logger.error("Exiting due to connection failure")
        return

    try:
        # 开始消费消息
        channel.basic_consume(
            queue=QUEUE_NAME,
            on_message_callback=message_callback,
            auto_ack=False  # 手动确认消息
        )

        logger.info("Starting message consumer")
        logger.info(f"Saving messages to directory: {OUTPUT_DIR}")
        logger.info("Press Ctrl+C to stop")

        # 开始消费循环
        while running:
            try:
                # 处理事件
                connection.process_data_events(time_limit=1)  # 1秒超时
            except pika.exceptions.ConnectionClosedByBroker:
                logger.warning("Connection closed by broker. Reconnecting...")
                connection, channel = setup_rabbitmq_connection()
                if channel:
                    channel.basic_consume(
                        queue=QUEUE_NAME,
                        on_message_callback=message_callback,
                        auto_ack=False
                    )
                else:
                    time.sleep(5)
            except Exception as e:
                logger.error(f"Error in consumer loop: {str(e)}")
                time.sleep(1)

    except Exception as e:
        logger.error(f"Critical error: {str(e)}")

    finally:
        # 清理资源
        close_current_file()  # 确保文件正确关闭

        if connection and connection.is_open:
            try:
                connection.close()
                logger.info("RabbitMQ connection closed")
            except Exception:
                pass

        logger.info(f"Consumer stopped. Total messages processed: {message_count}")

if __name__ == "__main__":
    main()
#消费者日志
2025-06-18 23:52:35,264 - INFO - Created channel=1
2025-06-18 23:52:35,266 - INFO - Connected to RabbitMQ. Consuming from queue: test01
2025-06-18 23:52:35,267 - INFO - Starting message consumer
2025-06-18 23:52:35,267 - INFO - Saving messages to directory: mq_data
2025-06-18 23:52:35,267 - INFO - Press Ctrl+C to stop
2025-06-18 23:52:35,267 - INFO - Opened new output file: mq_data/sensor_data_20250618_235235.json

​​​​​​从下图我们看到消息减少了一半,并且还可以看到消费速度。

图片

从队列来看,我们我们只配置了一个队列,所以另外一个队列的数据还在。

图片

运维小路

一个不会开发的运维!一个要学开发的运维!一个学不会开发的运维!欢迎大家骚扰的运维!

关注微信公众号《运维小路》获取更多内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值