阿里云普通Topic实现异步消费的实践
在日常业务开发中,我们时常需要使用消息队列(MQ)来实现系统间的解耦和异步处理。然而,在选择具体的 MQ 方案时,成本往往是需要重点考量的因素之一。以阿里云 MQ 为例,其普通 Topic 和异步 Topic 的收费模式存在显著差异。
本文介绍了在使用阿里云普通 Topic 的基础上,通过自定义异步消费逻辑来替代原生异步 Topic 的实践方法,并分析了这样做的好处。
背景
阿里云普通 Topic 的特点是价格较低且具备基础的消息发布订阅能力,而异步 Topic 专为异步处理场景设计,但价格更高且普通 Topic 与异步 Topic 无法共用。为了减少 Topic 的使用数量,降低系统成本,同时实现异步处理需求,我们选择基于普通 Topic,自行实现异步消费。
技术实现
为了实现上述目标,我们使用了 Java 的 DelayQueue 实现消息的延迟处理,并结合线程池优化消费逻辑,同时加入了失败重试机制。
以下是核心的两个类:
DelayedMessage类 此类实现了Delayed接口,用于定义延迟消息的结构。
public class DelayedMessage implements Delayed {
private final Object message;
private final long startTime;
private int retryCount;
public DelayedMessage(Object message, long delayInMillis) {
this(message, delayInMillis, 0);
}
public DelayedMessage(Object message, long delayInMillis, int retryCount) {
this.message = message;
this.startTime = System.currentTimeMillis() + delayInMillis;
this.retryCount = retryCount;
}
@Override
public long getDelay(TimeUnit unit) {
long diff = startTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(@NotNull Delayed o) {
return Long.compare(this.startTime, ((DelayedMessage) o).startTime);
}
public Object getMessage() {
return message;
}
public int getRetryCount() {
return retryCount;
}
public void incrementRetryCount() {
this.retryCount++;
}
}
DelayQueueService类 此服务类负责管理延迟队列及消息的处理逻辑,使用线程池提升并发处理能力,并支持失败重试。
@Slf4j
@RequiredArgsConstructor
@Service
public class DelayQueueService {
private final DelayQueue<DelayedMessage> delayQueue = new DelayQueue<>();
private final ThreadPoolExecutor executorService = new ThreadPoolExecutor(
QueueConfig.QUEUE_CORE_POOL_SIZE, // 核心线程数
QueueConfig.QUEUE_MAX_POOL_SIZE, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(QueueConfig.QUEUE_QUEUE_CAPACITY), // 队列容量
new CustomizableThreadFactory("DelayQueueService-"), // 自定义线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 饱和策略
);
private volatile boolean shutdown = false;
@PostConstruct
public void init() {
processMessages();
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
}
public void addMessage(Object message, long delayInMillis) {
if (shutdown) {
throw new IllegalStateException("Service is shutting down, cannot add new messages");
}
delayQueue.put(new DelayedMessage(message, delayInMillis));
}
@PreDestroy
public synchronized void shutdown() {
log.info("服务暂停清理队列消息:{}", delayQueue);
shutdown = true;
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
} finally {
cleanup();
}
}
private void processMessages() {
Thread messageProcessingThread = new Thread(() -> {
try {
while (!shutdown || !delayQueue.isEmpty()) {
try {
// 检查线程池队列是否已满
if (executorService.getQueue().size() >= QueueConfig.QUEUE_QUEUE_CAPACITY) {
continue;
}
DelayedMessage delayedMessage = delayQueue.poll(1, TimeUnit.SECONDS);
if (delayedMessage != null) {
executorService.submit(() -> handleMsg(delayedMessage));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("delay queue handle message error: {}", Throwables.getStackTraceAsString(e));
}
}
} finally {
cleanup();
}
});
messageProcessingThread.setDaemon(true);
messageProcessingThread.start();
}
private void cleanup() {
while (!delayQueue.isEmpty()) {
DelayedMessage delayedMessage = delayQueue.poll();
if (delayedMessage != null) {
handleMsg(delayedMessage);
}
}
}
private void handleMsg(DelayedMessage delayedMessage) {
try {
Object message = delayedMessage.getMessage();
log.debug("delay queue handle message: {}", JSON.toJSONString(message));
// 消息发送逻辑实现
} catch (Exception e) {
log.error("[Workflow] delay queue handle message error: {}", Throwables.getStackTraceAsString(e));
if (delayedMessage.getRetryCount() < 3) {
delayedMessage.incrementRetryCount();
delayQueue.put(new DelayedMessage(
delayedMessage.getMessage(),
calculateRetryDelay(delayedMessage.getRetryCount()),
delayedMessage.getRetryCount()
));
log.info("Message requeued for retry. Retry count: {}", delayedMessage.getRetryCount());
} else {
log.error("Message processing failed after 3 attempts. Message: {}",
JSON.toJSONString(delayedMessage.getMessage()));
}
}
}
private long calculateRetryDelay(int retryCount) {
// 使用指数退避策略计算重试延迟
return (long) Math.pow(2, retryCount) * 1000; // 第一次: 1秒, 第二次: 2秒, 第三次: 4秒
}
}
方案优势
-
成本优化
使用普通 Topic 替代异步 Topic,可以显著降低 Topic 使用的成本,尤其在大量使用消息队列的场景下。 -
灵活性提升
自行实现延迟和异步逻辑,能够根据业务需要灵活调整消息的延迟时间和处理方式,而不受制于 MQ 的原生特性。 -
系统解耦
通过延迟队列,生产者和消费者的执行时机完全解耦,进一步提升系统的稳定性和扩展性。
注意事项
-
资源管理
在服务关闭时需确保队列中的消息都已处理,避免消息丢失。 -
队列积压风险
如果消息处理速度跟不上生产速度,可能会导致延迟队列积压,需要通过监控及时发现并处理。 -
异常处理
在消息消费逻辑中加入完善的异常处理机制,避免单条消息处理失败影响后续消息。
总结
通过使用阿里云普通 Topic 并结合 DelayQueue 实现异步消费,不仅有效降低了成本,还提供了更多的灵活性。对于需要大量使用异步队列但预算有限的项目,这种方案不失为一个优雅且实用的选择。