防止相同 SQS 消息重复处理

问题

一般来说在我们的系统中,消息处理必须保证幂等性,以防止消息重复处理。在我们的系统中,下面两种情况可能导致相同消息被重复处理:

  1. 调度器和消息生产者:调度器或消息生产者可能会被多次触发,比如时不时有些任务因为超时而被多次触发。
  2. 队列管理 :如果一个 Lambda 实例处理消息超时,另一个实例可能会在 visibility timeout 设置不合适的情况下得到重新处理相同消息的机会。

如果消息被多次处理,我们可能会向客户发送重复的电子邮件和短信,甚至礼品卡都可能重复发送。所以,我们需要一个通用的机制来确保相同消息不会被多次处理。

解决方案

思路很简单:我们将使用 DynamoDB / Redis 缓存来存储消息 ID 和处理状态。当接收到消息时,我们将检查记录以查看是否已处理。如果已处理,我们将丢弃当前消息。如果没有,我们将处理消息并更新缓存。考虑到我们当前的无服务器架构,DynamoDB 是我们的默认选择。

消息处理有三种情况:

  1. 首次处理消息:处理消息。
  2. 消息已处理或正在处理:丢弃消息。
  3. 消息处理失败:重新处理消息。
    为了处理这种情况,我们需要为记录添加锁超时。如果消息在锁超时后仍处于正在处理状态,要能够被再次处理。

实现

  1. 创建 DynamoDB 表 message-processor。这是一个普通表,具有主键 messageId
  2. 实现下面接口的服务:
typescript 复制代码
interface IMessageProcessorService {
  /**
   * Here use DynamoDB message-processor table as the fact store to decide if a message has been seen before
   * @param messageId unique identifier for each message
   * @param lockTimeoutInSeconds how long to lock the message for processing. It gives another chance to reprocess the message if it fails.
   * @returns boolean: true indicates the lock is acquired and should continue the processing.
   *                   false indicates the message is already being processed or being processed by another instance.
   */
  acquireProcessingLock(messageId: string, lockTimeoutInSeconds: number): Promise<boolean>;

  /**
   * Mark the message as processed, preventing it from being processed again
   * @param messageId
   */
  markMessageProcessed(messageId: string): Promise<void>;

  /**
   * Remove record of failed message processing, allowing it to be processed again
   * @param messageId
   */
  releaseProcessingLock(messageId: string): Promise<void>;
}

下面的代码片段展示了如何实现 acquireProcessingLock 方法:(我们使用了内部库简化代码)

typescript 复制代码
await this.store.replace(
  {
    _id: id,
    status: 'PROCESSING',
    timestamp: Date.now(),
  },
  {
    condition: {
      $or: [
        { _id: { $exists: false } }, // insert new record
        {
          $and: [
            { timestamp: { $lt: Date.now() - lockTimeoutInSeconds * 1000 } },
            { status: { $eq: 'PROCESSING' } },
          ],
        },
      ],
    },
  },
);

最后,我们使用一个简单函数封装既有的处理程序:

typescript 复制代码
export const makeHandlerIdempotent = async <T>(
  handler: MessageHandler<T>,
  IdGenerator: (message: T) => string,
  {
    messageProcessorService,
    lockTimeoutInSeconds,
    logger,
  }: {
    logger: ILoggerService;
    messageProcessorService: IMessageProcessorService;
    lockTimeoutInSeconds: number;
  },
): Promise<MessageHandler<T>> => {
  return async (message: T) => {
    const id = IdGenerator(message);
    const acquiredProcessingExclusiveLock = await messageProcessorService.acquireProcessingLock(
      id,
      lockTimeoutInSeconds,
    );
    if (!acquiredProcessingExclusiveLock) {
      logger.info('processMessageIdempotent: message has already been processed', { message });
      return;
    }
    try {
      const result = await handler(message);
      await messageProcessorService.markMessageProcessed(id);
      return result;
    } catch (error) {
      await messageProcessorService.releaseProcessingLock(id);
      throw error;
    }
  };
};

总结

总的来说,防止分布式系统中消息处理似乎是一个常见的需求。在实现过程中,发现一个类似的解决方案 How to prevent duplicate SQS Messages?,解释的也很详细。

bash 复制代码
〉原博客 http://localhost:4000/2024/05/11/zh-cn/How-to-prevent-duplicate-SQS-messages/
相关推荐
程序员爱钓鱼1 天前
Go语言实战案例 — 项目实战篇:图书管理系统(文件存储)
后端·google·go
元闰子1 天前
OLTP上云,哪种架构最划算?·VLDB'25
数据库·后端·云原生
IT_陈寒1 天前
Vite 5.0重磅升级:8个性能优化秘诀让你的构建速度飙升200%!🚀
前端·人工智能·后端
hui函数1 天前
scrapy框架-day02
后端·爬虫·python·scrapy
Moshow郑锴1 天前
SpringBootCodeGenerator使用JSqlParser解析DDL CREATE SQL 语句
spring boot·后端·sql
小沈同学呀1 天前
创建一个Spring Boot Starter风格的Basic认证SDK
java·spring boot·后端
方圆想当图灵1 天前
如何让百万 QPS 下的服务更高效?
分布式·后端
凤山老林1 天前
SpringBoot 轻量级一站式日志可视化与JVM监控
jvm·spring boot·后端
凡梦千华1 天前
Django时区感知
后端·python·django
Chan161 天前
JVM从入门到实战:从字节码组成、类生命周期到双亲委派及打破双亲委派机制
java·jvm·spring boot·后端·intellij-idea