自定义同步、非同步延时队列【java】

文章展示了如何使用Java实现一个延时队列,包括一个非同步和一个同步阻塞版本。队列基于环形数组实现,具有添加、删除和检查元素的方法。同步版本使用ReentrantLock和Condition来处理阻塞和唤醒等待的线程。测试代码创建了生产者和消费者线程,模拟添加带延迟的消息并消费它们。
// 延时队列的关键抽象方法
import java.util.concurrent.TimeUnit;

public interface MyDelayed {

    long getDelay(TimeUnit unit);
}
/**
  * 队列的抽象接口
  */
public interface MyQueue<E> {

    boolean add(E e) throws Exception;

    E take() throws Exception;

    E peek();

    void clear();

    boolean isEmpty();

    int getSize();

}
// 自定义环形数组,线程安全由上游保证
public class RingArray<E> {
    private Object[] element;
    private int head, tail, size, capacity;

    RingArray(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        this.head = -1;
        this.tail = 0;
        element = new Object[capacity];
    }

    public void addToTail(E ele) {
        if(size < capacity) {
            head = (head + 1) == capacity ? 0 : head + 1;
            element[head] = ele;
            size ++;
        }
    }

    public E removeFromTail() {
        E ele = null;
        if(size > 0) {
            ele = (E)element[tail];
            element[tail] = null;
            tail = (tail + 1) == capacity ? 0 : tail + 1;
            size --;
        }
        return ele;
    }

    public void clear() {
        for (int i = 0; i < capacity; i ++) element[i] = null;
    }

    public E peek() {
        return (E) element[tail];
    }

    public int getSize() {
        return size;
    }

    public void print() {
        for (int i = 0; i < capacity; i ++) {
            if(element[i] != null) System.out.print(element[i] + ", ");
        }
        System.out.println();
    }

    @Override
    public String toString() {
        return this.size + " from " + this.head + " to " + this.tail;
    }

}
// 延时队列的抽象类,提供了一些模板方法
import java.util.concurrent.locks.ReentrantLock;

public abstract class MyAbstractQueue<E> implements MyQueue<E> {

    private final RingArray<E> array;
    private final int capacity;

    private final ReentrantLock lock = new ReentrantLock();

    public MyAbstractQueue(int capacity) {
        this.capacity = capacity;
        array = new RingArray(capacity);
    }

    public RingArray<E> getArray() {
        return array;
    }

    public ReentrantLock getLock() {
        return lock;
    }

    public int getCapacity() {
        return capacity;
    }

    @Override
    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return array.peek();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void clear() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            array.clear();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public boolean isEmpty() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return getSize() == 0;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public int getSize() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return array.getSize();
        } finally {
            lock.unlock();
        }
    }

}
// 一个非同步的延时队列
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class MyDelayQueueWithoutSyn<E extends MyDelayed> extends MyAbstractQueue<E> {

    public MyDelayQueueWithoutSyn(int capacity) {
        super(capacity);
    }

    public boolean add(E e) {
        final ReentrantLock lock = getLock();
        lock.lock();
        try {
            getArray().addToTail(e);
        } finally {
            lock.unlock();
        }
        return true;
    }

    public E take() {
        final ReentrantLock lock = getLock();
        lock.lock();
        try {
            E first = peek();
            if(first == null) {
                return null;
            } else {
                for (;;) {
                    if (first.getDelay(TimeUnit.NANOSECONDS) <= 0) {
                        return getArray().removeFromTail();
                    }
                }
            }
        } finally {
            lock.unlock();
        }
    }

}
//同步阻塞延时对列实现
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class MyDelayQueueWithSyn<E extends MyDelayed> extends MyAbstractQueue<E> {

    private Condition fullCondition = getLock().newCondition();
    private Condition emptyCondition = getLock().newCondition();

    public MyDelayQueueWithSyn(int capacity) {
        super(capacity);
    }

    public boolean add(E e) throws Exception {
        final ReentrantLock lock = getLock();
        lock.lock();
        try {
            while (getSize() == getCapacity()) {
                fullCondition.await();
            }
            getArray().addToTail(e);
            emptyCondition.signalAll();
        } finally {
            lock.unlock();
        }
        return true;
    }

    public E take() throws Exception {
        final ReentrantLock lock = getLock();
        lock.lock();
        try {
            while (getSize() == 0) {
                emptyCondition.await();
            }
            fullCondition.signalAll();
            for (;;) {
                if (peek().getDelay(TimeUnit.NANOSECONDS) <= 0) {
                    return getArray().removeFromTail();
                }
            }
        } finally {
            lock.unlock();
        }
    }


}
// 测试方法
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
 * 自定义延时队列
 *      同步延时队列
 *      非同步延时队列
 */
public class MyDelayQueueTest {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        //MyDelayQueueWithoutSyn queue = new MyDelayQueueWithoutSyn(10);
        MyDelayQueueWithSyn queue = new MyDelayQueueWithSyn(10);
        ExecutorService producer = Executors.newFixedThreadPool(2, new MyThreadFactory());
        for(int i = 0; i < 100; i ++) {
            final int index = i;
            int mod = index % 2;
            int random = new Random().nextInt(5);
            final long deadLine = mod == 0 ? 250 * random : 500 * random;
            producer.submit(() -> {
                Message<Integer> m = new Message<>(index, deadLine);
                try {
                    queue.add(m);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
        }

        ExecutorService consumer = Executors.newSingleThreadExecutor();
        consumer.submit(() -> {
            while (!queue.isEmpty()) {
                try {
                    System.out.println("get => " + queue.take());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });

        producer.shutdown();
        consumer.shutdown();

        System.out.println(System.currentTimeMillis() - start);
    }

    static class MyThreadFactory implements ThreadFactory {

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setDaemon(true);
            return thread;
        }

    }

    static class Message<T> implements MyDelayed {

        long deadLine;

        T t;

        Message(T t, long deadLine) {
            this.t = t;
            setDeadLine(deadLine);
        }

        public void setDeadLine(long deadLine) {
            this.deadLine = deadLine + System.currentTimeMillis();
        }

        public long getDeadLine() {
            return deadLine;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return deadLine - System.currentTimeMillis();
        }

        @Override
        public String toString() {
            return "Message{" +
                    "deadLine=" + deadLine +
                    ", t=" + t +
                    '}';
        }
    }
}

<think>好的,我现在需要帮用户补充死信队列和延迟队列的方案调研,讲清楚现有方案以及选择的原因。首先,用户的需求是扩展EventCenter组件,所以需要结合当前使用的技术栈(Kafka为主,Local为辅助)来分析。 首先,死信队列部分。用户当前使用Kafka作为消息队列,所以需要调研Kafka及其他中间件的死信队列实现,比如RabbitMQ、RocketMQ,然后对比它们的优缺点,特别是与现有技术栈的适配性。 对于Kafka,原生不支持死信队列,通常需要自定义实现,比如使用消费者拦截器或者Confluent的DLQ组件。RabbitMQ有原生的DLX支持,但用户可能没有使用RabbitMQ,所以改造成本高。RocketMQ有死信队列,但同样技术栈不匹配。所以重点比较Kafka的自定义方案和Confluent的方案,考虑License成本和开发复杂度。 然后是延迟队列。用户需要支持灵活的延迟时间,比如30分钟,所以需要调研Redis ZSet、RocketMQ延迟消息、时间轮等方案。RocketMQ的延迟等级固定,可能不够灵活;Redis ZSet可以支持任意延迟,且用户可能已有Redis集群,复用资源;时间轮需要修改中间件源码,开发难度大。所以需要比较这些方案的优缺点,特别是与现有技术栈的兼容性和实现成本。 接下来,需要整理每个方案的实现方式、优缺点、适配性,然后总结选择的原因。比如死信队列选择Kafka自定义拦截器,因为技术栈匹配,灵活可控;延迟队列选择Redis ZSet,因为支持任意延迟,复用现有资源,成本低。 需要注意用户提到的现有系统架构,比如EventCenter的Kafka模块,所以适配性是关键。同时,用户可能关注开发成本、维护成本、性能影响等因素。需要详细说明每个方案的具体实现步骤,以及为什么其他方案不适合。 最后,确保结构清晰,分点说明死信队列和延迟队列的调研,每个部分包括现有方案、对比分析、选择原因。 </think>### 死信队列与延迟队列技术方案调研(补充完善版) --- ## 一、死信队列方案调研与选型 ### 1.1 现有主流死信队列实现方案 #### 1.1.1 **RabbitMQ:死信交换器(DLX)** **实现方式**: RabbitMQ通过绑定“死信交换器(Dead Letter Exchange, DLX)”实现死信队列。当消息满足以下条件时,会被自动路由到DLX对应的死信队列: - 消费者显式拒绝消息(`basic.reject`或`basic.nack`)且未设置重新入队; - 消息超时(设置队列或消息的TTL); - 队列达到最大长度(`x-max-length`或`x-max-length-bytes`)。 **配置示例**: ```java // 定义普通队列时绑定DLX Map<String, Object> args = new HashMap<>(); args.put("x-dead-letter-exchange", "dlx_exchange"); // 死信交换器名称 args.put("x-dead-letter-routing-key", "dlq_key"); // 死信路由键 channel.queueDeclare("normal_queue", true, false, false, args); ``` **优缺点**: - 优点:原生支持,配置简单(仅需声明DLX参数),无需额外开发; - 缺点:依赖RabbitMQ中间件,与VMS当前Kafka技术栈不兼容(需引入新中间件,改造成本高);消息重试需手动配置(如通过`x-max-retries`插件)。 #### 1.1.2 **RocketMQ:消费者组死信队列** **实现方式**: RocketMQ为每个消费者组自动创建死信队列(名称格式:`%DLQ%+消费者组名`)。消息消费失败后,RocketMQ默认重试16次(间隔递增:10s→30s→...→2h),重试耗尽后自动存入死信队列。 **流程示例**: ```mermaid graph TD A[消息发送至Topic] --> B[消费者消费失败] B --> C[RocketMQ自动重试(16次)] C --> D[重试耗尽] D --> E[消息存入%DLQ%+消费者组名] ``` **优缺点**: - 优点:原生支持自动重试与死信存储,无需额外开发; - 缺点:死信队列无法自动消费(需人工导出后重新投递);与VMS当前Kafka技术栈不兼容(需迁移消息队列,改造成本高)。 #### 1.1.3 **Kafka:自定义死信主题(Dead Letter Topic)** **实现方式**: Kafka原生不支持死信队列,需通过以下两种方式实现: - **方案1:自定义消费者拦截器** 在Kafka Consumer端实现`ConsumerInterceptor`,拦截消费失败的消息,手动发送至独立死信Topic(如`vms_dlq_topic`)。需结合重试逻辑(如最大3次),重试耗尽后触发死信投递。 ```java public class DeadLetterInterceptor implements ConsumerInterceptor<String, Event> { private KafkaProducer<String, Event> dlqProducer; @Override public ConsumerRecords<String, Event> onConsume(ConsumerRecords<String, Event> records) { records.forEach(record -> { try { // 业务处理逻辑 eventHandler.handle(record.value()); } catch (Exception e) { // 记录失败信息并发送至DLQ ProducerRecord<String, Event> dlqRecord = new ProducerRecord<>("vms_dlq_topic", record.key(), record.value()); dlqProducer.send(dlqRecord); } }); return records; } } ``` - **方案2:Confluent Platform DLQ组件** Confluent提供内置DLQ功能,通过配置`dead.letter.topic.name`指定死信Topic,失败消息自动转发。需引入Confluent依赖(如`confluent-kafka`),并注意License成本(企业版需付费)。 **优缺点**: - 优点(方案1):完全自主可控,兼容现有Kafka技术栈(无需引入新中间件);可灵活集成重试策略; - 优点(方案2):配置简单(仅需设置死信Topic名称),自动转发; - 缺点(方案1):需开发拦截器逻辑,需处理Producer与Consumer的线程隔离; - 缺点(方案2):依赖Confluent组件,需评估License成本(如VMS使用社区版Kafka,可能无法直接集成)。 --- ### 1.2 死信队列方案选型结论 **最终选择:Kafka自定义消费者拦截器方案** **原因**: 1. **技术栈适配性**:VMS当前使用Kafka作为消息队列(`nbu-eventcenter-port-kafka`模块),自定义拦截器可无缝集成至现有架构,无需迁移中间件; 2. **成本与可控性**:Confluent DLQ需引入第三方组件并可能产生License费用,而自定义拦截器仅需开发少量代码(如拦截器、重试逻辑),成本更低且自主可控; 3. **功能扩展性**:可灵活记录失败详情(如异常堆栈、重试次数),支持与VMS监控系统(如Prometheus)集成,实现死信堆积报警。 --- ## 二、延迟队列方案调研与选型 ### 2.1 现有主流延迟队列实现方案 #### 2.1.1 **RocketMQ:预设延迟等级** **实现方式**: RocketMQ支持“延迟消息”,发送时指定延迟等级(共18级,如1s、5s、10s、30s、1m…2h)。消息发送至Broker后,会被暂存至“延迟队列”,到期后转发至目标Topic供消费者消费。 **配置示例**: ```java Message msg = new Message("topic", "tag", "content".getBytes()); msg.setDelayTimeLevel(5); // 对应延迟等级5(如30s) producer.send(msg); ``` **优缺点**: - 优点:原生支持,无需额外开发;基于RocketMQ高并发能力,吞吐量高; - 缺点:仅支持预设延迟等级(无法自定义如3分钟、30分钟);与VMS当前Kafka技术栈不兼容(需迁移中间件,改造成本高)。 #### 2.1.2 **Redis:有序集合(ZSet)** **实现方式**: 利用Redis ZSet的`score`字段存储消息到期时间戳(如当前时间+延迟时间),通过定时任务扫描ZSet(`ZRANGEBYSCORE`)获取到期消息并触发消费。 **核心步骤**: 1. **消息存储**:生产者发送延迟消息时,将消息体与到期时间戳存入ZSet: ```bash ZADD vms_delay_queue <到期时间戳> <消息ID> ``` 2. **扫描触发**:定时任务(如每1秒)执行`ZRANGEBYSCORE vms_delay_queue 0 <当前时间戳>`,获取到期消息,调用EventCenter发送至目标Topic; 3. **异常处理**:若发送失败,重新设置消息的到期时间戳(如当前时间+10s),重新插入ZSet。 **优缺点**: - 优点:支持任意延迟时间(精度毫秒级);可复用VMS已有的Redis集群(如缓存使用的Redis),无需引入新中间件;分布式友好(通过Redis主从复制保障高可用); - 缺点:需开发定时扫描逻辑(需处理并发扫描、消息去重);依赖Redis持久化配置(如AOF/RDB)保障消息不丢失。 #### 2.1.3 **时间轮算法(Time Wheel)** **实现方式**: 时间轮是一种高效的延迟任务调度算法(类似时钟轮盘),将时间划分为多个槽位(如每个槽位1秒),消息按到期时间插入对应槽位。轮盘转动时(每秒移动一个槽位),触发当前槽位的消息处理。 **典型应用**:Kafka的延时生产(`KafkaDelayedMessage`)、Netty的`HashedWheelTimer`。 **优缺点**: - 优点:时间复杂度O(1),高吞吐量下延迟低; - 缺点:需深度集成中间件源码(如修改Kafka客户端),开发难度大;VMS当前使用Kafka社区版,无原生时间轮支持。 --- ### 2.2 延迟队列方案选型结论 **最终选择:Redis ZSet方案** **原因**: 1. **延迟灵活性**:VMS需支持设备事件定时删除(如30分钟)、任务延迟触发(如5分钟重试)等自定义延迟需求,Redis ZSet支持任意延迟时间(毫秒级精度),而RocketMQ预设等级无法覆盖; 2. **技术栈复用**:VMS可能已部署Redis集群(如用于缓存),可直接复用现有资源,避免引入新中间件(如RocketMQ)的改造成本; 3. **实现复杂度**:时间轮需修改中间件源码(如Kafka客户端),开发难度高;而Redis ZSet仅需开发定时扫描逻辑(可通过Spring的`@Scheduled`实现),代码复杂度低; 4. **可靠性保障**:通过Redis AOF持久化(`appendfsync everysec`)和主从复制,可保障消息在Redis宕机时不丢失(仅需配置主从同步策略)。 --- ## 三、总结 | 功能模块 | 主流方案 | 选型方案 | 核心原因 | |--------------|-------------------------|---------------------------|--------------------------------------------------------------------------| | 死信队列 | RabbitMQ DLX、RocketMQ DLQ、Kafka自定义/Confluent DLQ | Kafka自定义消费者拦截器 | 兼容现有Kafka技术栈,自主可控,无额外中间件成本 | | 延迟队列 | RocketMQ延迟消息、Redis ZSet、时间轮算法 | Redis ZSet | 支持任意延迟时间,复用现有Redis资源,实现复杂度低 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

x_pengcheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值