AWS FIFO SQS删除已消费消息后重发相同内容,发送成功但队列无对应消息
问题背景
- 业务场景:使用FIFO队列存储7条消息,配套独立应用需要严格按照固定顺序无限轮询处理消息,标准处理流程为:读取当前消息 → 完成对应业务处理 → 删除已处理的消息 → 将相同消息重新投递到队列队尾,所有消息按此流程循环运转。
- 预期表现:应用可持续轮询拉取消息,严格遵循队列既定顺序完成业务操作。
- 实际异常:首次读取并删除消息后,重新向同一队列发送相同消息时,即便
sendMessage接口返回成功,重发的消息也无法被后续拉取到。
复现过程
- 前置队列配置:
- 创建
Test_Queue.fifo作为业务生产队列 - 创建
Test_Queue_DLQ.fifo作为死信队列,通过RedrivePolicy配置为业务队列的异常流转目标 - 两个队列均开启
ContentBasedDeduplication(内容去重)属性,配置10分钟可见性超时、最大消息保留周期
- 创建
- 实际操作步骤:
- 首次向
Test_Queue.fifo发送内容为Test_Message的消息,接口返回有效MessageId - 长轮询拉取队列中的消息,遍历拉取结果逐个执行删除操作,删除接口返回有效MessageId
- 删除操作完成后,再次向同一队列重发相同内容的消息,接口返回有效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
相关产品推荐
相关产品推荐

