背景
在很多业务场景中,我们都会使用消息队列。而消息队列的ACK机制,让我们在处理消息时更有保障。
比如,当consumer从队列中获取到消息,进行消费处理时,服务宕机了。
- ACK机制,就保障这些在预设的超时时间内,未被正常消费的消息,会重新被安排消费。
- 如果服务还在正常处理,但是超过了超时时间,会带来消息重复消费问题。
本文的目标,通过增加消息的监听器,来解决由于消费超时,导致的重复消费问题。
另外,通过组件封装,与大家分享可复用代码的编写思路。进一步理解面向对象设计中的开闭原则。实现的代码片段较为简单,可以随处复制到项目中使用。但是对于逻辑中的超时时间、续期间隔等配置参数,如果复制到新的业务逻辑中,加之代码修改,容易产生BUG。而通过封装,对外提供扩展策略,关闭代码修改,使得复用组件时,可以专注业务逻辑开发。
实现流程
在消息队列中,消息的生命周期包含:
- 发送,由生产者将消息写入到队列
- 接受,由消费者将消息从队列中取走
- 回退,消费异常,重新进行Step.2
- ACK,消费完成,通知消息队列清理消息
解决方案:在Step.2和Step.4的操作过程中,将消息状态提供给消息监听器Listener,通过线程遍历,来完成不可见消息时间的自动更新。
代码实现
本文的技术实现,是基于aws sqs服务
Listener逻辑
private void listen() {
new Thread(() -> {
log.info("start message visibility listener");
while (true) {
for (Map.Entry<String, ObjTime> entry : objTimeMap.entrySet()) {
ObjTime time = entry.getValue();
if ((System.currentTimeMillis()
- time.getTime()) / 1000
+ updateVisibilityInterval > timeout) {
// 重新放入obj对象信息
time.setTime(System.currentTimeMillis());
amazonSQS.changeMessageVisibility(queueUrl,
time.getObj().getSqsReceiptHandle(),
timeout);
messageObjectConverter.log("visibility", time.getObj());
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
使用手册
Maven依赖
说明:依赖并未发布到公共仓库中,可参考附录中项目仓库。
<dependency>
<groupId>com.yanmushi</groupId>
<artifactId>aws-sqs-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
属性配置
核心服务:AwsSqsService
属性 | 是否必须 | 说明 |
---|---|---|
AmazonSQS | 是 | 消息队列客户端 |
QueueName | 是 | 消息队列名称 |
MessageObjectConverter | 是 | 消息与实体对象之间转换器,需要自定义实现 |
Timeout | 否 | 默认:60s。消息不可见时间。 |
UpdateVisibilityInterval | 否 | 默认:5s。到期5s前,延长消息不可见时间 |
IllegalMessageHandler | 否 | 默认:丢弃策略。可自定义扩展 |
遇到问题
- SQS中消息不可见时间最大为12小时,如果过长会被拒绝
- 消息队列中的消息是非期望的
附录
- Gitee项目仓库
- 不错的代码绘图工具PlantUML
消息队列的工作模式
@startuml
消息生产者 -> 队列SQS : 1. 发送
队列SQS -> 消息消费者 : 2. 接收
队列SQS <-- 消息消费者 : 3. 回退(消费异常)
队列SQS <- 消息消费者 : 4. ACK
@enduml
引入Listener处理逻辑
@startuml
队列SQS -> 消息消费者 : 2. 接收
消息消费者 -> 消息Listener : 记录消息及时间
消息Listener -> 消息Listener : 遍历消息,更新将到期消息
队列SQS <-- 消息消费者 : 3. 回退(消费异常)
队列SQS <- 消息消费者 : 4. ACK
消息消费者 -> 消息Listener : 移除消息
@enduml