You need to enable JavaScript to run this app. AWS FIFO SQS删除已消费消息后重发相同内容,发送成功但队列无对应消息
优惠活动
大模型
产品
解决方案
定价
更多

AWS FIFO SQS删除已消费消息后重发相同内容,发送成功但队列无对应消息

问题背景
  • 业务场景:使用FIFO队列存储7条消息,配套独立应用需要严格按照固定顺序无限轮询处理消息,标准处理流程为:读取当前消息 → 完成对应业务处理 → 删除已处理的消息 → 将相同消息重新投递到队列队尾,所有消息按此流程循环运转。
  • 预期表现:应用可持续轮询拉取消息,严格遵循队列既定顺序完成业务操作。
  • 实际异常:首次读取并删除消息后,重新向同一队列发送相同消息时,即便sendMessage接口返回成功,重发的消息也无法被后续拉取到。
复现过程
  • 前置队列配置:
    • 创建Test_Queue.fifo作为业务生产队列
    • 创建Test_Queue_DLQ.fifo作为死信队列,通过RedrivePolicy配置为业务队列的异常流转目标
    • 两个队列均开启ContentBasedDeduplication(内容去重)属性,配置10分钟可见性超时、最大消息保留周期
  • 实际操作步骤:
    1. 首次向Test_Queue.fifo发送内容为Test_Message的消息,接口返回有效MessageId
    2. 长轮询拉取队列中的消息,遍历拉取结果逐个执行删除操作,删除接口返回有效MessageId
    3. 删除操作完成后,再次向同一队列重发相同内容的消息,接口返回有效MessageId
  • 观测到的异常现象:
    • AWS控制台中业务队列的「可用消息」「In-flight消息」计数均为0
    • 重发的消息既没有出现在业务队列中,也没有进入关联的死信队列
问题根因
  • 触发异常的核心规则是FIFO队列的内容去重窗口期机制:当队列开启ContentBasedDeduplication属性时,SQS会基于消息内容生成固定的去重Token,在默认5分钟的去重窗口期内,会对相同Token的消息做去重拦截。
  • 关键认知偏差:去重窗口期的标记缓存不会因为消息被主动删除而提前失效——哪怕此前发送的消息已经被成功从队列中移除,只要还在5分钟窗口期内,相同内容、相同消息组ID的新消息都会被判定为重复请求。
  • 上述拦截逻辑不会触发接口报错,反而会返回合法的MessageId,但实际不会将消息写入队列,这也是重发消息“消失”的直接原因。
解决方案
  • 方案一:取消队列的ContentBasedDeduplication属性配置,每次发送消息时手动传入唯一的MessageDeduplicationId。循环重发消息时,每次生成不同的去重ID,即可跳过窗口期的去重校验。
  • 方案二:如果必须保留内容去重能力,可以在重发消息时对消息体追加无业务影响的字段(比如自增版本号、发送时间戳),让SQS生成的去重Token和历史记录不一致,避免被判定为重复消息。
  • 方案三:等待5分钟去重窗口期完全结束后,再重发相同内容的消息(仅适用于对实时性要求较低的场景)。
问题复现代码
public class SQSIssue {

    @Test
    void sqsMessageAbsenceIssueTest() {
        AmazonSQS amazonSQS = AmazonSQSClientBuilder.standard().withEndpointConfiguration(new AwsClientBuilder
                .EndpointConfiguration("https://sqs.us-east-2.amazonaws.com", "us-east-2"))
                .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(
                        "<accessKey>", "<secretKey>"))).build();

        //create queue
        String queueUrl = createQueues(amazonSQS);
        String message = "Test_Message";
        String groupId = "Group1";

        //Sending message -> "Test_Message"
        sendMessage(amazonSQS, queueUrl, message, groupId);
        //Reading the message and deleting using message.getReceiptHandle()
        readAndDeleteMessage(amazonSQS, queueUrl);
        //Reposting the same message into the queue -> "Test_Message"
        sendMessage(amazonSQS, queueUrl, message, groupId);

        ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest()
                .withQueueUrl(queueUrl)
                .withWaitTimeSeconds(5)
                .withMessageAttributeNames("All")
                .withVisibilityTimeout(30)
                .withMaxNumberOfMessages(10);
        ReceiveMessageResult receiveMessageResult = amazonSQS.receiveMessage(receiveMessageRequest);
        //Here I am expecting the message presence in the queue as I recently reposted the same message into the same queue after the message deletion
        Assertions.assertFalse(receiveMessageResult.getMessages().isEmpty());
    }

    private void readAndDeleteMessage(AmazonSQS amazonSQS, String queueUrl) {
        ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest()
                .withQueueUrl(queueUrl)
                .withWaitTimeSeconds(5)
                .withMessageAttributeNames("All")
                .withVisibilityTimeout(30)
                .withMaxNumberOfMessages(10);
        ReceiveMessageResult receiveMessageResult = amazonSQS.receiveMessage(receiveMessageRequest);
        receiveMessageResult.getMessages().forEach(message -> amazonSQS.deleteMessage(queueUrl, message.getReceiptHandle()));
    }

    private String createQueues(AmazonSQS amazonSQS) {
        String queueName = "Test_Queue.fifo";
        String deadLetterQueueName = "Test_Queue_DLQ.fifo";

        //Creating DeadLetterQueue
        CreateQueueRequest createDeadLetterQueueRequest = new CreateQueueRequest()
                .addAttributesEntry("FifoQueue", "true")
                .addAttributesEntry("ContentBasedDeduplication", "true")
                .addAttributesEntry("VisibilityTimeout", "600")
                .addAttributesEntry("MessageRetentionPeriod", "262144");
        createDeadLetterQueueRequest.withQueueName(deadLetterQueueName);
        CreateQueueResult createDeadLetterQueueResult = amazonSQS.createQueue(createDeadLetterQueueRequest);
        GetQueueAttributesResult getQueueAttributesResult = amazonSQS.getQueueAttributes(
                new GetQueueAttributesRequest(createDeadLetterQueueResult.getQueueUrl())
                        .withAttributeNames("QueueArn"));
        String deadLetterQueueArn = getQueueAttributesResult.getAttributes().get("QueueArn");

        //Creating Actual Queue with DeadLetterQueue configured
        CreateQueueRequest createQueueRequest = new CreateQueueRequest()
                .addAttributesEntry("FifoQueue", "true")
                .addAttributesEntry("ContentBasedDeduplication", "true")
                .addAttributesEntry("VisibilityTimeout", "600")
                .addAttributesEntry("MessageRetentionPeriod", "262144");

        createQueueRequest.withQueueName(queueName);
        String reDrivePolicy = "{\"maxReceiveCount\":\"5\", \"deadLetterTargetArn\":\""
                + deadLetterQueueArn + "\"}";
        createQueueRequest.addAttributesEntry("RedrivePolicy", reDrivePolicy);
        CreateQueueResult createQueueResult = amazonSQS.createQueue(createQueueRequest);
        return createQueueResult.getQueueUrl();
    }

    private void sendMessage(AmazonSQS amazonSQS, String queueUrl, String message, String groupId) {
        SendMessageRequest sendMessageRequest = new SendMessageRequest()
                .withQueueUrl(queueUrl)
                .withMessageBody(message)
                .withMessageGroupId(groupId);
        SendMessageResult sendMessageResult = amazonSQS.sendMessage(sendMessageRequest);
        Assertions.assertNotNull(sendMessageResult.getMessageId());
    }
}

内容的提问来源于stack exchange,提问作者Prasanth Rajendran

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.07 08:54:10