基于Redisson的高性能延迟队列架构设计与实现

引言

延迟队列,简单来说就是让消息在指定时间后才被消费的队列。比如订单超时取消、消息延迟推送、定时任务调度等场景,都需要用到这种定时触发的机制。

起因是这样的。去年刚进公司时小组组织了一次技术竞赛,主题就是基于Redis的延迟队列实现方案。组里四个人各自设计方案、写代码,最终我的方案幸运地被选中并整合到了公司内部Core包中。

现在这套延迟队列方案已经稳定运行在我们的直播、竞拍等多个项目中,支撑着各种延迟业务场景。

至于为什么不用现成的RabbitMQ?原因很简单------当时领导定的方案 Orz。

简单说说它有哪些特性:

  • 多 Topic 支持,可按业务隔离逻辑队列;
  • 高并发消费能力,适应突发任务量;
  • 失败重试机制,避免任务丢失;
  • 灵活的队列消费注册与监听接口,便于业务对接;
  • 延迟任务追加与幂等控制,可自定义策略合并任务;

了解了这些之后,我们来盘点整体方案。

架构设计

核心设计原则

  1. 单一职责原则:生产者只负责消息发送,消费者只负责消息处理,观察者负责监控
  2. 开闭原则:通过接口抽象,支持不同业务场景的消费逻辑扩展
  3. 依赖倒置原则:业务层依赖抽象接口,而非具体实现
  4. 高内聚低耦合:各组件职责清晰,相互依赖最小化

整体架构图

graph TB A[业务系统] --> B[DelayQueueProducer] B --> C[Redisson延迟队列] C --> D[DelayQueueConsumerFactory] D --> E[DelayQueueConsumer实现类] E --> F[业务处理逻辑] G[DelayQueueListener] --> H[监控告警] C -.-> I[Redis集群] E --> J[重试机制] J --> K[死信队列] style A fill:#e1f5fe style C fill:#f3e5f5 style I fill:#fff3e0

系统组件设计

消息实体设计

消息实体是整个系统的数据载体,需要包含完整的生命周期信息:

java 复制代码
public class DelayMessage {
    private String topic;           // 消息主题
    private String messageId;       // 唯一标识
    private Object payload;         // 业务数据
    private long delayTime;        // 延迟时间(毫秒)
    private int retryCount;        // 当前重试次数
    // ... 省略其他字段和方法
}

设计要点

  • 使用UUID确保消息ID全局唯一
  • 支持泛型payload,适应不同业务场景
  • 内置重试机制相关字段,支持失败重试

生产者设计

生产者采用静态工厂模式,提供简洁的API接口:

java 复制代码
public class DelayQueueProducer {
    private static final RedissonClient redissonClient = RedissonConfig.getClient();
    private static final ObjectMapper objectMapper = new ObjectMapper();

    public static <T> boolean sendDelayMessage(String topic, T payload, long delaySeconds) {
        DelayMessage message = new DelayMessage();
        message.setTopic(topic);
        message.setPayload(payload);
        message.setDelayTime(System.currentTimeMillis() + delaySeconds * 1000);
        
        // 获取延迟队列并发送消息
        RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(
            redissonClient.getBlockingQueue(QUEUE_PREFIX + topic));
        
        // ... 序列化和发送逻辑
        return true;
    }
}

亮点

  • 静态方法设计,无需实例化,降低使用复杂度
  • 支持泛型,类型安全
  • 内置JSON序列化,简化使用

消费者接口设计

消费者接口采用策略模式,支持不同的业务处理逻辑:

java 复制代码
public interface DelayQueueConsumer<T> {
    /**
     * 处理延迟消息
     * @param message 消息内容
     * @return 处理结果
     */
    ConsumeResult consume(T message);
    
    /**
     * 获取消费的数据类型
     */
    Class<T> getMessageType();
    
    /**
     * 获取订阅的topic
     */
    String getTopic();
}

消费者工厂设计

核心

工厂模式在延迟队列系统中发挥着至关重要的作用,它不仅封装了队列获取和消息处理的复杂逻辑,更是整个系统的控制中枢:

java 复制代码
public class DelayQueueConsumerFactory {
    private final Map<String, DelayQueueConsumer<?>> consumers = new ConcurrentHashMap<>();
    private final ExecutorService executorService;
    private final RedissonClient redissonClient;
    
    // 工厂不仅创建,更要管理整个生命周期
    public synchronized <T> DelayQueueConsumer<T> createConsumer(String topic, 
                                                               Class<T> messageType,
                                                               MessageHandler<T> handler) {
        if (consumers.containsKey(topic)) {
            return (DelayQueueConsumer<T>) consumers.get(topic);
        }
        
        DelayQueueConsumer<T> consumer = new DelayQueueConsumer<>(topic, messageType, handler, this);
        consumers.put(topic, consumer);
        
        // 启动消费线程
        executorService.submit(consumer);
        
        return consumer;
    }
}

解决程序重启后的队列连接问题

这里有一个非常重要的技术细节,也是生产环境中经常遇到的问题:

java 复制代码
public class DelayQueueConsumer<T> implements Runnable {
    private final RBlockingQueue<T> blockingQueue;
    private final RDelayedQueue<T> delayedQueue;
    
    public DelayQueueConsumer(String topic, Class<T> messageType, 
                             MessageHandler<T> handler, 
                             DelayQueueConsumerFactory factory) {
        this.blockingQueue = factory.getRedissonClient().getBlockingQueue(topic);
        
        // 这里是关键:解决程序重启后blockingQueue无法获取队列的问题
        this.delayedQueue = factory.getRedissonClient().getDelayedQueue(blockingQueue);
        
        this.messageType = messageType;
        this.handler = handler;
    }
    
    @Override
    public void run() {
        Thread.currentThread().setName("DelayQueue-Consumer-" + topic);
        
        while (!Thread.currentThread().isInterrupted()) {
            try {
                T message = blockingQueue.take(); // 阻塞等待消息
                processMessage(message);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } catch (Exception e) {
                log.error("消费消息异常", e);
            }
        }
    }
}

为什么需要显式调用getDelayedQueue?

这是一个容易被忽略但极其重要的技术点:

  • Redisson的DelayedQueue和BlockingQueue是关联关系
  • 程序重启后,如果只获取BlockingQueue,延迟队列的定时转移机制可能失效
  • 通过getDelayedQueue(blockingQueue)确保延迟队列与阻塞队列的关联关系正确建立
  • 这保证了即使程序重启,之前放入延迟队列的消息仍能在指定时间后正确转移到阻塞队列

生命周期管理

消费者工厂不仅负责创建,更承担着完整的生命周期管理职责:

java 复制代码
public class DelayQueueConsumerFactory {
    private final Map<String, DelayQueueConsumer<?>> consumers = new ConcurrentHashMap<>();
    private final ExecutorService executorService;
    private volatile boolean shutdown = false;
    
    // 优雅关闭机制
    public void shutdown() {
        shutdown = true;
        
        // 停止接受新任务
        executorService.shutdown();
        
        try {
            // 等待现有任务完成
            if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
            Thread.currentThread().interrupt();
        }
        
        // 清理资源
        consumers.clear();
    }
    
    // 健康检查
    public Map<String, Boolean> getConsumerStatus() {
        Map<String, Boolean> status = new HashMap<>();
        consumers.forEach((topic, consumer) -> {
            status.put(topic, consumer.isRunning());
        });
        return status;
    }
}

消息处理逻辑

在消费者内部,消息处理逻辑经过了精心设计:

java 复制代码
private void processMessage(T message) {
    String traceId = generateTraceId();
    MDC.put("traceId", traceId);
    
    try {
        // 消息反序列化校验
        if (message == null) {
            log.warn("接收到空消息,跳过处理");
            return;
        }
        
        // 执行业务处理
        handler.handle(message);
        log.info("消息处理成功: {}", message);
        
    } catch (Exception e) {
        log.error("消息处理失败: {}", message, e);
        // 这里可以实现重试逻辑或死信队列
        handleProcessFailure(message, e);
    } finally {
        MDC.remove("traceId");
    }
}

强扩展性

通过工厂模式,可以轻松支持不同类型的消费策略:

java 复制代码
public class DelayQueueConsumerFactory {
    
    // 支持批量消费
    public <T> BatchDelayQueueConsumer<T> createBatchConsumer(String topic, 
                                                             Class<T> messageType,
                                                             BatchMessageHandler<T> handler,
                                                             int batchSize) {
        // 实现批量消费逻辑
    }
    
    // 支持优先级消费
    public <T> PriorityDelayQueueConsumer<T> createPriorityConsumer(String topic, 
                                                                   Class<T> messageType,
                                                                   PriorityMessageHandler<T> handler) {
        // 实现优先级消费逻辑
    }
    
    // 支持有序消费
    public <T> OrderedDelayQueueConsumer<T> createOrderedConsumer(String topic, 
                                                                 Class<T> messageType,
                                                                 OrderedMessageHandler<T> handler) {
        // 实现有序消费逻辑
    }
}

核心流程设计

消息发送流程

sequenceDiagram participant App as 业务应用 participant Producer as DelayQueueProducer participant Redis as Redis延迟队列 participant Listener as 消息监听器 App->>Producer: sendDelayMessage(topic, payload, delay) Producer->>Producer: 创建DelayMessage对象 Producer->>Producer: JSON序列化 Producer->>Redis: offer(message, delay, TimeUnit) Redis-->>Producer: 返回发送结果 Producer->>Listener: 触发发送事件 Producer-->>App: 返回发送状态

消息消费流程

flowchart TD A[消费者启动] --> B[阻塞获取消息] B --> C{消息是否存在} C -->|否| B C -->|是| D[检查重复消费标记] D --> E{是否重复消费} E -->|是| F[丢弃消息] E -->|否| G[设置消费标记] G --> H[反序列化消息] H --> I[调用业务处理逻辑] I --> J{处理是否成功} J -->|成功| K[清理消费标记] J -->|失败| L{是否超过重试次数} L -->|否| M[延迟重试] L -->|是| N[进入死信队列] K --> B M --> B N --> B F --> B

高可用性设计

在实现主要的流程后,我们需要考虑一些实际生产环境中会遇到的问题。理想很丰满,现实很骨感,系统总会出现各种意想不到的情况。

下面几个机制值得花点时间好好进行打磨。

防重复消费机制

重复消费是个很常见的问题。

比如网络抖动导致消息重发,或者消费者处理完消息后由于网络原因没有及时确认,都可能造成同一条消息被处理多次。

我们的解决方案是在消费前先获取分布式锁:

java 复制代码
private void processMessageWithLock(DelayMessage message) {
    String lockKey = CONSUME_LOCK_PREFIX + message.getMessageId();
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 尝试获取锁,30秒等待时间,300秒持有时间
        if (lock.tryLock(30, 300, TimeUnit.SECONDS)) {
            try {
                // 处理消息的业务逻辑
                handleMessage(message);
                log.info("消息处理成功: {}", message.getMessageId());
            } catch (Exception e) {
                log.error("消息处理失败: {}", message.getMessageId(), e);
                // 处理失败时的重试逻辑
                handleProcessFailure(message, e);
            } finally {
                // 无论成功还是失败,都要释放锁
                lock.unlock();
            }
        } else {
            // 获取锁失败,说明其他消费者正在处理或已处理过这条消息
            log.warn("获取消息锁失败,可能存在重复消费: {}", message.getMessageId());
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        log.error("获取锁被中断: {}", message.getMessageId());
    }
}

这里用到了Redisson锁。整个流程是这样的:

  1. 基于消息ID生成锁的key,保证同一条消息使用同一把锁
  2. 尝试获取锁,如果获取成功就处理消息
  3. 处理完成后在finally块中释放锁,确保锁一定会被释放
  4. 如果获取锁失败,说明其他消费者正在处理这条消息,直接跳过

参数说明:

  • 第一个30秒是等待锁的时间,如果30秒内获取不到锁就放弃
  • 第二个300秒是锁的持有时间,防止消费者宕机后锁一直不释放

这样设计的好处是既防止了重复消费,又确保了锁资源能够正确释放。

失败重试策略

消息处理失败是不可避免的,可能是网络问题、数据库连接问题、业务逻辑异常等等。简单粗暴的立即重试并不可取,所以我们采用指数退避的策略:

java 复制代码
private long calculateDelayTime(int retryCount) {
    // 指数退避:1s, 2s, 4s, 8s...
    return (long) Math.pow(2, retryCount) * 1000;
}

这个算法的好处是重试间隔会越来越长:第一次失败后等1秒重试,第二次失败后等2秒,第三次等4秒,以此类推。这样既给了系统恢复的时间,又避免了频繁重试对系统造成额外压力。

在实际使用中,我们应该根据不同的异常类型制定不同的重试策略。

比如网络超时可以快速重试,而业务逻辑错误可能就不需要重试了。

死信队列处理

有些消息可能因为各种原因始终无法处理成功,比如数据格式错误、依赖的外部服务长期不可用等。如果一直重试下去,不仅浪费系统资源,还可能影响其他正常消息的处理。

这时候死信队列就派上用场了:

java 复制代码
private void sendToDeadLetterQueue(DelayMessage message) {
    RQueue<String> deadLetterQueue = redissonClient
        .getQueue(DEAD_LETTER_PREFIX + message.getTopic());
    // ... 省略序列化和发送逻辑
}

当消息重试次数超过设定的阈值(比如3次或5次),系统会自动将消息转移到死信队列中。

进入死信队列后的消息,可以根据实际的业务需求来做处理,比如下面这些常见的处理措施:

  • 人工介入处理
  • 定期分析找出共同问题
  • 在问题修复后重新投递到正常队列

这样既保证了系统的稳定运行,又不会丢失业务数据。

监控与观察者模式

消息生命周期监控

stateDiagram-v2 [*] --> Created: 消息创建 Created --> Delayed: 进入延迟队列 Delayed --> Consuming: 开始消费 Consuming --> Success: 消费成功 Consuming --> Retrying: 消费失败 Retrying --> Consuming: 重试消费 Retrying --> DeadLetter: 超过重试次数 Success --> [*] DeadLetter --> [*]

监听器接口设计

java 复制代码
public interface DelayQueueListener {
    void onMessageSent(DelayMessage message);
    void onMessageConsumed(DelayMessage message, ConsumeResult result);
    void onMessageRetry(DelayMessage message, int retryCount);
    void onMessageFailed(DelayMessage message, Throwable cause);
}

性能优化策略

列一些可能用到的优化策略。

连接池优化

  • 使用Redisson的连接池配置,合理设置最大连接数

序列化优化

  • 使用Jackson的优化配置,减少序列化开销
  • 如有必要,可以考虑使用Protobuf等二进制序列化方案

并发处理优化

  • 消费者使用线程池并行处理消息
  • 根据业务场景去调整线程池大小

内存优化

  • 及时清理过期的消费标记
  • 结合Redis的TTL机制自动清理数据

使用示例

定义消费者

java 复制代码
@Component
public class OrderTimeoutConsumer implements DelayQueueConsumer<OrderInfo> {
    @Override
    public ConsumeResult consume(OrderInfo orderInfo) {
        try {
            // 业务处理逻辑:取消超时订单
            orderService.cancelOrder(orderInfo.getOrderId());
            return ConsumeResult.SUCCESS;
        } catch (Exception e) {
            log.error("处理订单超时失败", e);
            return ConsumeResult.RETRY;
        }
    }
    
    @Override
    public Class<OrderInfo> getMessageType() {
        return OrderInfo.class;
    }
    
    @Override
    public String getTopic() {
        return "order.timeout";
    }
}

发送延迟消息

java 复制代码
// 发送30分钟延迟的订单超时消息
OrderInfo orderInfo = new OrderInfo(orderId, userId);
DelayQueueProducer.sendDelayMessage("order.timeout", orderInfo, 30 * 60);

写在最后

从去年到现在,这套延迟队列方案已经在生产环境稳定运行了一年多。期间经历了各种流量冲击和异常场景的考验,总体来说还是比较抗打的。

当然,这个方案也不是完美的。比如在极高并发场景下,Redis的性能可能会成为瓶颈;再比如分布式锁的开销在某些业务场景下可能显得有点重。但对于大部分中小型项目来说,这套方案的复杂度和性能平衡点还是不错的。

有朋友可能会问,为什么不直接用RabbitMQ的延迟插件或者Kafka的时间轮?说实话,如果重新选型的话,我可能真的会考虑这些成熟的方案。但既然已经造了这个轮子,而且用得还不错,那就继续打磨下去吧。

后续如果有时间,我打算把这套代码整理一下开源到GitHub上,让更多的同学能够参考和使用。毕竟技术这东西,分享出来才有价值。如果你们在使用过程中遇到什么问题,或者有更好的优化建议,欢迎在评论区讨论。

最后,如果这篇文章对你有帮助,麻烦点个赞支持一下,您的每一个赞都是对原创作者最大的鼓励~

技术路上,我们一起前行。

相关推荐
Moonbit4 分钟前
MoonBit Pearls Vol.03:01背包问题
后端·算法·编程语言
干了这杯柠檬多5 分钟前
使用maven-shade-plugin解决es跨版本冲突
java·elasticsearch·maven
Proxbj12 分钟前
MQTT解析
java
南囝coding20 分钟前
这个仓库堪称造轮子的鼻祖,建议看看!
前端·后端
JuiceFS29 分钟前
3000 台 JuiceFS Windows 客户端性能评估
后端·云原生·云计算
埃泽漫笔33 分钟前
Spring 的 ioc 控制反转
java·spring·ioc
太阳之神aboluo37 分钟前
SpringCloud (4) 分布式事务
java·spring·spring cloud
Cosolar38 分钟前
下一代 Python Web 框架?FastAPI 全面解析与实战对比
后端·python
Noii.1 小时前
Mybatis的应用及部分特性
java·数据库·mybatis
Warren981 小时前
Java异常讲解
java·开发语言·前端·javascript·vue.js·ecmascript·es6