消费中的幂等处理

一、何为幂等处理

幂等处理(Idempotent Processing) 指的是一种处理方式,使得对资源的多次请求或操作与单次请求或操作具有相同的效果。换句话说,执行多次操作与只执行一次操作的结果是一样的。幂等处理在分布式系统、网络请求、数据库操作等场景中非常重要,它有助于确保系统的健壮性和数据的一致性。

        幂等处理主要用于处理消息重复消费的问题。由于网络问题、消费者处理失败、消费者宕机等原因,消息队列中的消息可能会被重新投递给消费者,导致消费者重复消费同一条消息。为了避免这种情况导致的数据不一致或其他问题,消费者需要对消息进行幂等处理。

实现幂等处理的方式有多种,具体取决于应用场景和业务需求。以下是一些常见的实现方式:

  1. 唯一ID + 去重表:为每个消息分配一个唯一的ID,并在消费消息时将该ID存入去重表中。在消费下一条消息之前,先检查该消息的ID是否已经在去重表中存在。如果存在,则说明该消息已经被消费过,直接跳过;否则,将ID加入去重表并处理消息。
  2. 数据库事务:利用数据库的事务特性来实现幂等处理。在消费消息时,首先检查数据库中是否存在与消息相关的记录。如果存在,则说明该消息已经被处理过,直接结束事务;否则,处理消息并将结果存入数据库,提交事务。
  3. 状态机:将业务处理流程建模为状态机,每个状态都有对应的处理逻辑。在消费消息时,根据当前状态和处理逻辑来判断是否需要处理该消息。如果已经处于某个状态,则说明该消息已经被处理过,直接跳过。

需要注意的是,幂等处理并不意味着每个操作都必须产生相同的结果。在某些情况下,多次操作可能会产生不同的结果,但这些结果对于系统来说是可以接受的或者是不影响系统状态的。因此,在设计幂等处理方案时,需要根据具体的应用场景和业务需求来权衡各种因素。

二、幂等处理方式

1、Python+Redis实现幂等处理

在Python中实现幂等处理通常依赖于特定的业务逻辑和存储机制。以下是一个简单的例子,展示了如何使用Redis(一个内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理)来实现幂等处理。

假设你有一个消息队列,消费者从队列中取出消息后需要执行某些操作,但你不希望因为消息的重复消费而多次执行这些操作。

实现幂等处理:

import redis   
  
# 连接到Redis  
redis_client = redis.Redis(host='localhost', port=6379, db=0)  
  
# 假设你的消息是一个包含唯一ID和数据的字典  
def process_message(message_id, message_data):  
    # 这里是你的业务逻辑代码  
    print(f"Processing message with ID: {message_id}, data: {message_data}")  
    # ...(其他处理逻辑)  
  
def is_message_processed(message_id):  
    # 检查Redis中是否已经存在该消息的ID  
    return redis_client.exists(message_id)  
  
def mark_message_as_processed(message_id):  
    # 在Redis中设置一个键来表示该消息已经被处理过  
    # 这里我们使用SET命令并设置过期时间(可选),以防万一  
    redis_client.set(message_id, 'processed', ex=3600)  # ex=3600表示键在1小时后过期  
  
# 模拟从消息队列中获取消息  
def get_message_from_queue():  
    # 在实际应用中,这里会从消息队列中获取消息  
    # 这里我们只是模拟获取一个包含ID和数据的消息  
    return {  
        'id': 'unique_message_id_123',  
        'data': {'key': 'value'}  
    }  
  
# 消费者逻辑  
def consumer():  
    message = get_message_from_queue()  
    message_id = message['id']  
    message_data = message['data']  
  
    # 检查消息是否已经被处理过  
    if not is_message_processed(message_id):  
        # 如果没有被处理过,则执行处理逻辑并标记为已处理  
        process_message(message_id, message_data)  
        mark_message_as_processed(message_id)  
    else:  
        print(f"Message with ID {message_id} has already been processed.")  
  
# 运行消费者  
consumer()

在上面的例子中,我们使用Redis来存储已经处理过的消息的ID。当消费者从队列中获取到消息时,它会首先检查Redis中是否存在该消息的ID。如果不存在,则执行处理逻辑并将ID存入Redis;如果存在,则说明该消息已经被处理过,直接跳过。

请注意,这只是一个简单的示例,实际应用中可能需要考虑更多的因素,比如并发处理、错误处理、Redis的持久化配置等。

扩展:并发处理

在并发处理中,我们通常使用多线程或异步处理来同时处理多个消息。但在这个简单的例子中,为了保持清晰,我们可以使用线程锁(虽然在实际的高性能系统中,这可能不是最佳选择)

import threading  
  
# 线程锁  
lock = threading.Lock()  
  
# ... 其他代码保持不变 ...  
  
# 并发消费者逻辑(模拟)  
def concurrent_consumer(queue):  
    while True:  
        message = queue.get()  # 假设queue是一个线程安全的队列实现  
        with lock:  # 确保同一时间只有一个线程处理消息  
            process_and_mark_message(message)  
  
# 处理消息并标记为已处理的函数(合并了process_message和mark_message_as_processed)  
def process_and_mark_message(message):  
    try:  
        message_id = message['id']  
        message_data = message['data']  
  
        # 检查消息是否已经被处理过(这里省略了Redis连接等细节)  
        if not is_message_processed(message_id):  
            # 执行处理逻辑  
            process_message(message_id, message_data)  
            # 标记消息为已处理  
            mark_message_as_processed(message_id)  
        else:  
            print(f"Message with ID {message_id} has already been processed.")  
    except Exception as e:  
        # 错误处理逻辑  
        handle_error(e, message_id)  
  
# ... 其他代码保持不变 ...

扩展:Redis的持久化配置

Redis的持久化配置通常是在Redis的配置文件(redis.conf)中进行的。有两种主要的持久化方式:RDB(快照)和AOF(追加文件)。

  • RDB:在指定的时间间隔内,将内存中的数据集快照写入磁盘。可以通过save指令来配置。
  • AOF:将写命令追加到文件中,并在Redis重启时重新执行这些命令来恢复数据。可以通过appendonly yesappendfsync等指令来配置。

conf配置化

# RDB持久化配置  
save 900 1  
save 300 10  
save 60 10000  
  
# AOF持久化配置  
appendonly yes  
appendfsync everysec  
  
# 其他相关配置...

请注意,这些配置是Redis服务器级别的配置,而不是在Python代码中直接设置的。你需要根据你的需求和环境来配置Redis服务器。

此外,如果你使用的是云服务或托管的Redis解决方案,那么这些配置可能通过控制面板或API进行管理,而不是直接编辑配置文件。

2、python + mq 实现消费幂等处理

在使用Python和消息队列(MQ)实现消费幂等处理时,通常我们会利用一个外部存储系统(如Redis、数据库等)来跟踪已经处理过的消息。以下是一个简化的步骤和示例代码,展示了如何使用RabbitMQ作为消息队列和Redis作为去重存储系统来实现消费幂等性。

步骤

  1. 生产者:生产者发送消息到RabbitMQ队列,并为每条消息分配一个唯一的ID。
  2. 消费者
    • 消费者从RabbitMQ队列中接收消息。
    • 消费者检查Redis中是否已经存在该消息的ID。
    • 如果不存在,则处理消息,并将消息ID存储到Redis中。
    • 如果存在,则忽略该消息。

生产者

import pika  
import uuid  
  
# 建立到RabbitMQ的连接  
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))  
channel = connection.channel()  
  
# 声明队列,确保队列存在  
channel.queue_declare(queue='task_queue', durable=True)  
  
# 生成一个唯一的消息ID和消息体  
message_id = str(uuid.uuid4())  
message_body = f'Hello, this is message with ID: {message_id}'  
  
# 发送消息,并设置消息的properties(包括message_id)  
channel.basic_publish(exchange='', routing_key='task_queue', body=message_body,  
                      properties=pika.BasicProperties(  
                          delivery_mode=2,  # 使得消息持久化  
                          message_id=message_id  # 设置消息ID  
                      ))  
print(f" [x] Sent {message_body}")  
  
# 关闭连接  
connection.close()

消费者

import pika  
import redis   
  
# 建立到Redis的连接  
redis_client = redis.Redis(host='localhost', port=6379, db=0)  
  
# 建立到RabbitMQ的连接  
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))  
channel = connection.channel()  
  
# 声明队列,确保队列存在  
channel.queue_declare(queue='task_queue', durable=True)  
  
def callback(ch, method, properties, body):  
    message_id = properties.message_id  
      
    # 检查Redis中是否已经存在该消息的ID  
    if not redis_client.exists(message_id):  
        try:  
            # 处理消息(这里只是打印出来)  
            print(f" [x] Received {body}")  
            # 这里是处理消息的业务逻辑  
            # ...  
              
            # 标记消息为已处理(存储到Redis)  
            redis_client.set(message_id, 'processed', ex=3600)  # ex设置过期时间,例如1小时  
              
        except Exception as e:  
            # 处理异常(例如记录日志、发送警报等)  
            print(f"Error processing message {message_id}: {e}")  
            # 注意:在这里,我们可能需要决定是否重新排队消息(basic_nack)或丢弃它(不执行任何操作)  
    else:  
        print(f" [x] Message with ID {message_id} has already been processed.")  
      
    # 确认消息已被处理(RabbitMQ的基本确认模式)  
    ch.basic_ack(delivery_tag=method.delivery_tag)  
  
# 设置消费者标签(consumer_tag),并开启基本确认模式(basic_qos)  
channel.basic_qos(prefetch_count=1)  
channel.basic_consume(queue='task_queue', on_message_callback=callback, auto_ack=False)  
  
print(' [*] Waiting for messages. To exit press CTRL+C')  
channel.start_consuming()

在这个示例中,我们使用了RabbitMQ的基本确认模式(basic_ack)来确保消息在消费者成功处理后才会从队列中移除。同时,我们使用Redis的existsset命令来检查和处理消息ID。请注意,set命令中的ex参数设置了键的过期时间,这样即使消息处理失败,也不会永远在Redis中留下痕迹。

另外,请注意处理异常时的逻辑。在实际应用中,你可能需要决定在出现异常时是否重新排队消息(使用basic_nack)或完全丢弃它(不执行任何操作)。这取决于你的业务需求和错误处理策略。

  • 7
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当我们使用 RabbitMQ 作为消息队列时,消费者需要处理重复消费的情况。实现幂等处理可以避免重复消费带来的问题。以下是一个示例,演示如何使用 Redis 实现 RabbitMQ 消费者的幂等处理: 1. 在消费者应用引入 Redis 的依赖。 2. 在消费处理消息前,先查询 Redis 是否已经处理过该消息。如果已经处理过,则不再处理;如果未处理,则处理消息,并将消息处理的标识存储到 Redis 。 示例代码: ```python import redis import pika # Redis 配置 redis_host = 'localhost' redis_port = 6379 redis_db = 0 redis_key_prefix = 'rabbitmq_msg_processed:' # 连接 Redis redis_conn = redis.Redis(host=redis_host, port=redis_port, db=redis_db) # RabbitMQ 配置 rabbitmq_host = 'localhost' rabbitmq_port = 5672 rabbitmq_user = 'guest' rabbitmq_password = 'guest' rabbitmq_exchange = 'example_exchange' rabbitmq_queue = 'example_queue' rabbitmq_routing_key = 'example_routing_key' # 连接 RabbitMQ credentials = pika.PlainCredentials(rabbitmq_user, rabbitmq_password) connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbitmq_host, port=rabbitmq_port, credentials=credentials)) channel = connection.channel() # 声明 exchange 和 queue channel.exchange_declare(exchange=rabbitmq_exchange, exchange_type='direct', durable=True) channel.queue_declare(queue=rabbitmq_queue, durable=True) channel.queue_bind(queue=rabbitmq_queue, exchange=rabbitmq_exchange, routing_key=rabbitmq_routing_key) # 定义消息处理函数 def callback(ch, method, properties, body): msg_id = properties.message_id redis_key = redis_key_prefix + msg_id # 查询 Redis 是否已经处理过该消息 if not redis_conn.exists(redis_key): # 处理消息 print('Received message: %r' % body) # 将消息处理的标识存储到 Redis redis_conn.set(redis_key, 1) print('Processed message: %r' % body) else: print('Message already processed: %r' % body) ch.basic_ack(delivery_tag=method.delivery_tag) # 消费消息 channel.basic_qos(prefetch_count=1) channel.basic_consume(queue=rabbitmq_queue, on_message_callback=callback) print('Waiting for messages...') channel.start_consuming() ``` 在上述示例,我们定义了一个 Redis 的连接对象 `redis_conn`,并在消息处理函数 `callback` 使用该对象查询并存储消息处理的标识。如果消息处理过,则不再处理;如果未处理,则处理消息并存储标识。这样可以保证每条消息只被处理一次,避免了重复消费的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值