Java轻量延迟重试队列实现

curl --retry 5 --retry-delay 2 -o curl 超时与重试 阅读详情

背景

很多开放平台都使用Webhook的方式进行外部系统数据同步或者通知,对于Webhook请求的对外发送不进行重试显然有点说不过去。使用简单的while一个条件去重试N次好像达不到什么效果,只能是说有重试而已,而使用消息队列中间件好像依赖又太重,于是索性自己动手写一个抽象范型延迟重试队列组件。

1. 实现思路及考虑

  • 按照重试间隔(RetryDelaySeconds)& 重试持续结束时间(RetryTask.retryEndTime)进行重试以支持较长的重试周期(例如,保障一定可跨天的持续24小时的重试);
  • 通过限制延迟队列容量上限的方式避免重试任务积压较多时发生OOM以及落盘和恢复的时间不至于太长;
  • 重试队列的消费使用重试任务哈希值(RetryTask.retryHash)得到的线程进行执行以避免不同业务之间重试任务的互相影响以及相同业务重试任务的串行执行;
  • 如果接受重试任务不落盘(程序意外重启时直接丢弃没有达到最大重试持续时间的任务)则可以不实现:flushTask() 和 recoverTask() 方法;

2. 代码实现

2.1 重试队列组件代码

2.1.1 RetryTask

package retry;

import lombok.Data;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * RetryTask
 *
 * @author chenx
 */
@Data
public class RetryTask implements Delayed {

    /**
     * expireTime
     */
    protected long expireTime;

    /**
     * retryHash
     */
    protected int retryHash;

    /**
     * retryEndTime
     */
    protected long retryEndTime;

    @Override
    public long getDelay(TimeUnit unit) {
        long diff = this.expireTime - System.currentTimeMillis();
        return unit.convert(diff, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed other) {
        long diff = this.getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
        return Long.compare(diff, 0);
    }
}

2.1.2 RetryQueue

package retry;

import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * RetryQueue
 * <p>
 * 1、按照重试间隔(RetryDelaySeconds)& 重试持续结束时间(RetryTask.retryEndTime)进行重试以支持较长的重试周期(例如,保障一定可跨天的持续24小时的重试);
 * 2、通过限制延迟队列容量上限的方式避免重试任务积压较多时发生OOM以及落盘和恢复的时间不至于太长;
 * 3、重试队列的消费使用重试任务哈希值(RetryTask.retryHash)得到的线程进行执行以避免不同业务之间重试任务的互相影响以及相同业务重试任务的串行执行;
 *
 * @author chenx
 */
public abstract class RetryQueue<T extends RetryTask> {

    private static final Logger LOGGER = LoggerFactory.getLogger(RetryQueue.class);

    private final int maxQueueSize;
    private final int processExecutorPoolSize;

    private boolean isStart = false;
    private DelayQueue<T> queue;
    private ExecutorService takeExecutor;
    private ExecutorService[] taskProcessExecutors;

    protected RetryQueue(int maxQueueSize, int processExecutorPoolSize) {
        this.maxQueueSize = maxQueueSize;
        this.processExecutorPoolSize = processExecutorPoolSize;

        this.queue = new DelayQueue<>();
        this.takeExecutor = Executors.newSingleThreadExecutor();
        this.taskProcessExecutors = new ExecutorService[this.processExecutorPoolSize];
        for (int i = 0; i < this.processExecutorPoolSize; i++) {
            this.taskProcessExecutors[i] = Executors.newSingleThreadExecutor();
        }
    }

    /**
     * process(重试任务处理)
     *
     * @param task
     */
    public abstract void process(T task);

    /**
     * flushTask(重试任务落盘)
     *
     * @param taskList
     */
    public abstract void flushTask(List<T> taskList);

    /**
     * recoverTask(重试任务恢复)
     *
     * @return
     */
    public abstract List<T> recoverTask();

    /**
     * startup
     */
    public void startup() {
        if (this.isStart) {
            LOGGER.warn("<[RetryQueue startup already!]>");
            return;
        }

        this.isStart = true;

        // 落盘任务恢复
        this.recoverTaskProcess();

        // take重试任务
        this.takeExecutor.execute(() -> {
            while (this.isStart) {
                try {
                    T task = this.queue.take();
                    this.onRetryTaskTaken(task);
                } catch (InterruptedException ex) {
                    LOGGER.warn("<[RetryQueue.startup() interrupted!]>");
                    Thread.currentThread().interrupt();
                } catch (Exception ex) {
                    LOGGER.error("retryQueue.startup() error!", ex);
                }
            }
        });
    }

    /**
     * shutdown
     */
    public void shutdown() {
        if (!this.isStart) {
            LOGGER.warn("<[RetryQueue stop already!]>");
            return;
        }

        this.isStart = false;
        this.takeExecutor.shutdownNow();
        for (int i = 0; i < this.processExecutorPoolSize; i++) {
            this.taskProcessExecutors[i].shutdownNow();
        }

        this.flushTask(this.getFlushTaskList());
    }

    /**
     * enqueue
     *
     * @param task
     */
    public void enqueue(T task) {
        try {
            if (task.getRetryEndTime() < System.currentTimeMillis()) {
                LOGGER.warn("<[Discarded an expired RetryTask: {}]>", task);
                return;
            }

            if (this.queue.size() >= this.maxQueueSize) {
                LOGGER.warn("<[queue.size() >= maxQueueSize! maxQueueSize: {}]>", this.maxQueueSize);
                return;
            }

            if (!this.queue.offer(task)) {
                LOGGER.error("<[RetryQueue is full!]>");
                return;
            }

            LOGGER.info("<[RetryQueue.enqueue() done, queueSize: {}, task: {}]>", this.queue.size(), task);
        } catch (Exception ex) {
            LOGGER.error("RetryQueue.enqueue() error!", ex);
        }
    }

    /**
     * onRetryTaskTaken
     *
     * @param task
     */
    private void onRetryTaskTaken(T task) {
        try {
            if (Objects.isNull(task)) {
                LOGGER.warn("<[RetryTask is null!]>");
                return;
            }

            this.getExecutor(task).execute(() -> this.process(task));
        } catch (Exception ex) {
            LOGGER.error("RetryQueue.onRetryTaskTaken() error!", ex);
        }
    }

    /**
     * getExecutor
     *
     * @param task
     * @return
     */
    private ExecutorService getExecutor(T task) {
        if (Objects.isNull(task)) {
            throw new RuntimeException("<[RetryTask is null!]>");
        }

        int hashCode = task.getRetryHash();
        if (hashCode == Integer.MIN_VALUE) {
            hashCode = 0;
        }

        return this.taskProcessExecutors[Math.abs(hashCode) % this.processExecutorPoolSize];
    }

    /**
     * recoverTaskProcess
     */
    private void recoverTaskProcess() {
        List<T> recoverTaskList = this.recoverTask();
        LOGGER.warn("<[recoverTask size is: {}]>", (CollectionUtils.isEmpty(recoverTaskList) ? 0 : recoverTaskList.size()));
        if (CollectionUtils.isEmpty(recoverTaskList)) {
            return;
        }

        for (T task : recoverTaskList) {
            this.enqueue(task);
        }
    }

    /**
     * getFlushTaskList
     *
     * @return
     */
    private List<T> getFlushTaskList() {
        List<T> list = new ArrayList<>();
        Iterator<T> iterator = this.queue.iterator();
        while (iterator.hasNext()) {
            list.add(iterator.next());
        }

        LOGGER.warn("<[flushTaskList size is: {}]>", list.size());

        return list;
    }
}
  • 备注:
    虽然DelayQueue提供了drainTo()方法,但是DelayQueue仍然无法快速全部取出队列里的元素,因此getFlushTaskList()方法中使用queue.iterator()进行遍历取获取队列中的全部元素用于重试任务落盘;

2.2 测试代码

2.2.1 FooRetryQueue

package retry;

import java.util.List;

/**
 * FooRetryQueue
 *
 * @author chenx
 */
@SuppressWarnings("all")
public class FooRetryQueue extends RetryQueue<FooRetryQueue.FooRetryTask> {

    private static final int MAX_QUEUE_SIZE = 10000;

    private static final int PROCESS_EXECUTOR_POOL_SIZE = 64;

    private FooRetryQueue() {
        super(MAX_QUEUE_SIZE, PROCESS_EXECUTOR_POOL_SIZE);
    }

    /**
     * getInstance
     *
     * @return
     */
    public static FooRetryQueue getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public void process(FooRetryTask task) {
        FooService.getInstance().sendMessage(task.getMsgId(), task.getRetryEndTime());
    }

    @Override
    public void flushTask(List<FooRetryTask> taskList) {

    }

    @Override
    public List<FooRetryTask> recoverTask() {
        return null;
    }

    /**
     * SingletonHolder
     */
    private static class SingletonHolder {
        public static final FooRetryQueue INSTANCE = new FooRetryQueue();
    }

    /**
     * FooRetryTask
     */
    public static class FooRetryTask extends RetryTask {

        private String msgId;

        public String getMsgId() {
            return this.msgId;
        }

        public void setMsgId(String msgId) {
            this.msgId = msgId;
        }

        @Override
        public String toString() {
            return "FooRetryTask{" +
                    "msgId='" + msgId + '\'' +
                    ", expireTime=" + expireTime +
                    ", retryHash=" + retryHash +
                    ", retryEndTime=" + retryEndTime +
                    '}';
        }
    }
}

2.2.2 FooService

package retry;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * FooSendService
 *
 * @author chenx
 */
@SuppressWarnings("all")
public class FooService {

    private static final Logger LOGGER = LoggerFactory.getLogger(FooService.class);

    /**
     * 重试延迟时间(建议走配置)
     */
    private static final int RETRY_DELAY_SECONDS = 5;

    /**
     * 重试最大持续时间(建议走配置)
     */
    private static final int MAX_RETRY_DURATION = 15;

    private FooService() {
        // just do nothing
    }

    /**
     * getInstance
     *
     * @return
     */
    public static FooService getInstance() {
        return SingletonHolder.INSTANCE;
    }

    /**
     * startup
     */
    public void startup() {
        FooRetryQueue.getInstance().startup();
    }

    /**
     * shutdown
     */
    public void shutdown() {
        FooRetryQueue.getInstance().shutdown();
    }

    /**
     * sendMessage
     *
     * @param msgId
     * @param retryEndTime
     */
    public void sendMessage(String msgId, long retryEndTime) {
        boolean isRetry = false;
        try {
            // 模拟消息发送失败
            isRetry = true;
            LOGGER.info("sendMessage() failed! msgId: {}", msgId);
        } finally {
            if (isRetry) {
                FooRetryQueue.FooRetryTask task = new FooRetryQueue.FooRetryTask();
                task.setExpireTime(System.currentTimeMillis() + RETRY_DELAY_SECONDS * 1000);
                task.setRetryHash(msgId.hashCode());
                task.setRetryEndTime(retryEndTime <= 0 ? System.currentTimeMillis() + MAX_RETRY_DURATION * 1000 : retryEndTime);
                task.setMsgId(msgId);

                FooRetryQueue.getInstance().enqueue(task);
            }
        }
    }

    /**
     * SingletonHolder
     */
    private static class SingletonHolder {
        public static final FooService INSTANCE = new FooService();
    }

    public static void main(String[] args) throws InterruptedException {
        FooService.getInstance().startup();
        FooService.getInstance().sendMessage("msgId-1", -1L);

        // 模拟服务停止
        Thread.sleep((MAX_RETRY_DURATION + RETRY_DELAY_SECONDS) * 1000);
        FooService.getInstance().shutdown();
    }
}

3. 执行结果

FooService中的main方法执行结果如下,从执行结果可以看出确实可以按照预期进行重试:最多持续MAX_RETRY_DURATION秒,每RETRY_DELAY_SECONDS秒进行一次重试。
在这里插入图片描述

另外需要补充说明是,如果大家希望对重试任务进行落盘则需要:
1:实现void flushTask(List taskList) 和 List recoverTask() 这2个方法即可,方式可以是写本地临时文件或者持久到DB中。
2:服务需要支持优雅停机(关于java优雅停机的实现一搜一大把),并且需要在停机阶段调用RetryQueue.shutdown()方法。

4. 重试任务落盘打样

以下是一个采用本地临时文件方式落盘的打样,示例中使用了自定义紧凑序列化+零拷贝读写本地文件的方式,目的是降低重试任务落盘及恢复的时间。

  • WebhookRetryTask (重试任务实体)
public class WebhookRetryTask extends RetryTask {

    /**
     * webhookType
     */
    private WebhookType webhookType;

    /**
     * webhookRequest
     */
    private Object webhookRequest;

    public WebhookRetryTask() {

    }

    public WebhookRetryTask(long retryEndTime,
                            int retryHash,
                            WebhookType webhookType,
                            Object webhookRequest) {
        this.retryEndTime = retryEndTime;
        this.retryHash = retryHash;

        this.webhookType = webhookType;
        this.webhookRequest = webhookRequest;
    }

    public WebhookType getWebhookType() {
        return this.webhookType;
    }

    public void setWebhookType(WebhookType webhookType) {
        this.webhookType = webhookType;
    }

    public Object getWebhookRequest() {
        return this.webhookRequest;
    }

    public void setWebhookRequest(Object webhookRequest) {
        this.webhookRequest = webhookRequest;
    }

    @Override
    public String toString() {
        return GsonUtils.toJson(this);
    }
}
  • WebhookRetryQueue (重试队列实现)
@SuppressWarnings({"squid:S6548", "squid:S1075"})
public class WebhookRetryQueue extends RetryQueue<WebhookRetryTask> {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebhookRetryQueue.class);

    private static final int MAX_QUEUE_SIZE = 50000;
    private static final int SCHEDULED_EXECUTOR_POOL_SIZE = 128;
    private static final String WEBHOOK_RETRY_QUEUE_RECOVER_FILE_PATH = "/tmp/WebhookRetryQueue_recoverFile.data";

    private WebhookRetryQueue() {
        super(MAX_QUEUE_SIZE, SCHEDULED_EXECUTOR_POOL_SIZE);
    }

    /**
     * getInstance
     */
    public static WebhookRetryQueue getInstance() {
        return WebhookRetryQueue.SingletonHolder.INSTANCE;
    }

    @Override
    public void process(WebhookRetryTask task) {
        try {
            if (task.getWebhookType().equals(WebhookType.MESSAGE_WEBHOOK)) {
                MessageWebhookProto request = (MessageWebhookProto) task.getWebhookRequest();
                SpringContextUtils.getBean(WebhookService.class).sendMessageWebhook(request, task.getRetryEndTime());
            } else if (task.getWebhookType().equals(WebhookType.MESSAGE_STATUS_UPDATE)) {
                MessageStatusWebhookProto request = (MessageStatusWebhookProto) task.getWebhookRequest();
                SpringContextUtils.getBean(WebhookService.class).sendMessageStatusWebhook(request, task.getRetryEndTime());
            }
        } catch (Exception ex) {
            LOGGER.error("WebhookRetryQueue.process() error!", ex);
        }
    }

    @Override
    public void flushTask(List<WebhookRetryTask> taskList) {
        try {
            if (CollectionUtils.isEmpty(taskList)) {
                LOGGER.warn("WebhookRetryQueue.flushTask() done, taskList is empty!");
                return;
            }

            byte[] data = WebhookRetryTaskCodec.serialize(taskList);
            File recoverFile = this.getRecoverFile();
            WebhookRetryTaskCodec.writeFileData(recoverFile, data);
        } catch (Exception ex) {
            LOGGER.error("WebhookRetryQueue.flushTask() error!", ex);
        }
    }

    @Override
    public List<WebhookRetryTask> recoverTask() {
        try {
            File file = new File(WEBHOOK_RETRY_QUEUE_RECOVER_FILE_PATH);
            if (!file.exists()) {
                return Collections.emptyList();
            }

            byte[] date = WebhookRetryTaskCodec.readFileData(file);
            
            return WebhookRetryTaskCodec.deserialize(date);
        } catch (Exception ex) {
            LOGGER.error("WebhookRetryQueue.recoverTask() error!", ex);
        }

        return Collections.emptyList();
    }

    /**
     * getRecoverFile
     * <p>
     * 由于服务更新会导致服务物理根目录发生变化,因此往/tmp/目录下写落盘文件是一种可行的选择。
     * @return
     * @throws IOException
     */
    private File getRecoverFile() throws IOException {
        File file = new File(WEBHOOK_RETRY_QUEUE_RECOVER_FILE_PATH);
        if (!file.exists()) {
            Path path = Paths.get(WEBHOOK_RETRY_QUEUE_RECOVER_FILE_PATH);
            Files.createDirectories(path.getParent());
            Files.createFile(path);
            LOGGER.warn("WebhookRetryQueue.createFile() done, filePath: {}", file.getAbsolutePath());
        }

        return file;
    }

    private static class SingletonHolder {
        public static final WebhookRetryQueue INSTANCE = new WebhookRetryQueue();
    }
}
  • WebhookRetryTaskCodec(重试任务序列化)
public class WebhookRetryTaskCodec {

    private WebhookRetryTaskCodec() {
        // just do nothing
    }

    /**
     * serialize
     *
     * @param taskList
     * @return
     */
    public static byte[] serialize(List<WebhookRetryTask> taskList) throws IOException {
        if (CollectionUtils.isEmpty(taskList)) {
            return new byte[0];
        }

        try (ByteArrayOutputStream out = new ByteArrayOutputStream();
             DataOutputStream dos = new DataOutputStream(out)) {
            dos.writeInt(taskList.size());

            for (WebhookRetryTask entry : taskList) {
                dos.writeLong(entry.getRetryEndTime());
                dos.writeInt(entry.getRetryHash());
                dos.writeInt(entry.getWebhookType().getCode());

                byte[] webhookRequestData = null;
                if (entry.getWebhookType().getCode() == WebhookType.MESSAGE_WEBHOOK.getCode()) {
                    MessageWebhookProto request = (MessageWebhookProto) entry.getWebhookRequest();
                    webhookRequestData = ProtoStuffUtil.serialize(request);
                } else if (entry.getWebhookType().getCode() == WebhookType.MESSAGE_STATUS_UPDATE.getCode()) {
                    MessageStatusWebhookProto request = (MessageStatusWebhookProto) entry.getWebhookRequest();
                    webhookRequestData = ProtoStuffUtil.serialize(request);
                }

                int webhookRequestDataLength = ArrayUtils.isEmpty(webhookRequestData) ? 0 : webhookRequestData.length;
                dos.writeInt(webhookRequestDataLength);
                dos.write(webhookRequestData);
            }

            return out.toByteArray();
        }
    }

    /**
     * deserialize
     *
     * @param data
     * @return
     */
    public static List<WebhookRetryTask> deserialize(byte[] data) throws IOException {
        if (ArrayUtils.isEmpty(data)) {
            return Collections.emptyList();
        }

        try (ByteArrayInputStream in = new ByteArrayInputStream(data);
             DataInputStream inputStream = new DataInputStream(in)
        ) {
            int taskListSize = inputStream.readInt();
            if (taskListSize <= 0) {
                return Collections.emptyList();
            }

            List<WebhookRetryTask> taskList = new ArrayList<>(taskListSize);
            for (int i = 0; i < taskListSize; i++) {
                long retryEndTime = inputStream.readLong();
                int retryHash = inputStream.readInt();
                int webhookType = inputStream.readInt();
                int webhookRequestDataLength = inputStream.readInt();
                byte[] webhookRequestData = new byte[webhookRequestDataLength];

                WebhookRetryTask task = new WebhookRetryTask();
                task.setRetryEndTime(retryEndTime);
                task.setRetryHash(retryHash);
                task.setWebhookType(WebhookType.getByCode(webhookType));

                if (webhookRequestDataLength <= 0) {
                    taskList.add(task);
                    continue;
                }

                // webhookRequest反序列化
                if (inputStream.read(webhookRequestData) != webhookRequestDataLength) {
                    throw new OpenApiRuntimeException("read webhookRequestData failed!");
                }

                if (webhookType == WebhookType.MESSAGE_WEBHOOK.getCode()) {
                    MessageWebhookProto request = ProtoStuffUtil.deserialize(webhookRequestData, MessageWebhookProto.class);
                    task.setWebhookRequest(request);
                } else if (webhookType == WebhookType.MESSAGE_STATUS_UPDATE.getCode()) {
                    MessageStatusWebhookProto request = ProtoStuffUtil.deserialize(webhookRequestData, MessageStatusWebhookProto.class);
                    task.setWebhookRequest(request);
                }

                taskList.add(task);
            }

            return taskList;
        }
    }

    /**
     * writeData
     *
     * @param destFile
     * @param data
     * @throws IOException
     */
    public static void writeFileData(File destFile, byte[] data) throws IOException {
        if (Objects.isNull(destFile) || !destFile.exists()) {
            throw new OpenApiRuntimeException("destFile not existed!");
        }

        if (ArrayUtils.isEmpty(data)) {
            throw new OpenApiRuntimeException("data is empty!");
        }

        try (FileOutputStream fos = new FileOutputStream(destFile);
             FileChannel destChannel = fos.getChannel();
             ByteArrayInputStream srcInput = new ByteArrayInputStream(data);
             ReadableByteChannel srcChannel = Channels.newChannel(srcInput)
        ) {
            destChannel.transferFrom(srcChannel, 0, data.length);
        }
    }

    /**
     * readFileData
     *
     * @param srcFile
     * @return
     * @throws IOException
     */
    public static byte[] readFileData(File srcFile) throws IOException {
        if (Objects.isNull(srcFile) || !srcFile.exists()) {
            throw new OpenApiRuntimeException("srcFile not existed!");
        }

        try (FileInputStream fis = new FileInputStream(srcFile);
             FileChannel srcFileChannel = fis.getChannel();
             ByteArrayOutputStream out = new ByteArrayOutputStream();
             WritableByteChannel destChannel = Channels.newChannel(out)
        ) {
            srcFileChannel.transferTo(0, srcFile.length(), destChannel);
            return out.toByteArray();
        }
    }
}
使用 Redis Stream 结合 Redission DelayedQueue 实现延迟消息队列(已上线生产) *** redis stream 消息监听器基类*/@Data@Slf4j@Component@Overridelog.info("--- redis stream 接收到消息 --- message :{}", message);try {// 消息消费// 消息确认/*** 具体的消费逻辑* @param streamName 主题* @param message 消息体*//*** 消息确认机制。 阅读详情

相关推荐

java中使用redis及redisson客户端实现轻量级消息队列、延迟消息队列

3、如果业务并发量较大,消息过多可能会导致内存占用过大从而影响 redis 的响应速度,毕竟 redis 的主功能是用作缓存的。2、只是需要把消息队列绑定到延迟队列上,然后生产者端通过延迟队列发送,延迟时间到了之后直接路由到绑定的消息队列中。4、不过还是建议使用专业的消息队列中间件,例如:RabbitMQ、RocketMQ、PulsarMQ 等。2、如果数据量不是特别大,对消息数据可靠性要求不高、允许消息容错的业务,可以使用此方案。1、已经集成了redis服务,这里服务端版本为:Redis 7.0.8。

hkl_Forever的博客 580

axios请求失败、请求超时重新发送请求

axios请求超时,重新发送请求,拦截非网络错误的请求直接返回错误信息

weixin_47437528的博客 1万+

K8s入门教程(一)

本教程提供了Kubernetes(K8s)的入门指南,涵盖了从基础概念到实战操作的全面内容。首先介绍了Kubernetes的定义和核心功能,如自动化部署、弹性伸缩、自我修复等。接着,详细讲解了如何在macOS上安装Minikube和kubectl工具,以及如何使用这些工具创建和管理Pod、Deployment和Service等核心资源。教程还包括了实战案例,如部署Nginx应用,并介绍了高级特性如滚动更新和自动伸缩。最后,提供了常见问题的解决方案,帮助用户快速上手和解决实际使用中的问题。

2401_82505179的博客 1690

要做重试机制,就只能选择 DelayQueue ?其实 RabbitMQ 它上它也行!

关于做重试机制,我们不但可以选择 JDK 自带的 DelayQueue,还能借用 RabbitMQ 的消息 ttl 和死信队列来完成~

Howinfun的博客 796

安装keil5(MDK)及导入pack包教程

首先说明的是我安装的Keil版本为Keil V5.29.0.0 1、安装软件 右键管理员权限运行安装包 设置安装路径以及pack的存放路径 随意输入First Name和E-mail 安装驱动 完成软件安装,然后可以先关闭弹出的Pack Install(一会再添加) 2、激活一下软件 打开桌面的Keil File → License Management(我之前注册过了) 右...

hrx-@@ 10万+

Zephyr、FreeRTOS 和 RT-Thread

你是…更适合用嵌入式新手,刚接触 RTOSFreeRTOS(入门简单)IoT 项目开发,需要 BLE/WiFi/MQTTZephyr(协议全)想快速构建可部署的产品Zephyr(模块统一)只做单个任务或简单状态机控制FreeRTOS(轻量)做平台化、持续集成、自动测试Zephyr(CI 支持强)

is0815的博客 2247

vue中retry的使用--解决axios请求超时的问题

1、使用场景 (1)后端接口请求超时。 (2)后端接口报错。 (3)弱网状态下,重新请求。 2、实现步骤 axios中的使用 以下代码都是在api.js中进行操作: (1)先进行axios的配置: //设置全局的请求次数,请求的间隙 axios.defaults.retry = 3; // 请求次数 axios.defaults.retryDelay = 2000; //请求间隙 (2)设置拦截器 axios.interceptors.response.use((res) => { // 拦截器

weixin_44490109的博客 2500

Quartus Prime v18.1 standard-QuartusPrimeV18.1标准版安装过程

Quartus Prime v18.1 standard安装破解过程 下载方式1:官网下载: https://fpgasoftware.intel.com/18.1/?edition=standard&platform=windows 下载方式2:可以从我的网盘下载安装包(如果链接未时效的话): 链接:https://pan.baidu.com/s/1bq4_FcNPSflU1zGJv__rvA 提取码:o0in ----------------------------------.

Alonger1988的博客 2万+

Java实现重试机制

关键词:线程、线程池、队列、任务调度 1、重试工具类 package com.iretry; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskS

春风化雨 3807

Ubuntu中交叉编译armadillo库

网上关于交叉编译armdillo库比较少,借鉴了一些但是在前几天编译时总是磕磕绊绊,于是写一个详细的编译过程。交叉编译armdillo库包含两个步骤:交叉编译依赖库和交叉编译armdillo库。

weixin_39813867的博客 1879

华为AR3260路由器NAT回流配置实战:从WEB界面到命令行的完整避坑指南

本文详细解析了华为AR3260路由器NAT回流(Hairpin NAT)的配置原理与实战方法。针对内网用户无法通过公网地址访问内部服务器的常见问题,文章不仅剖析了WEB界面配置的常见报错原因,更提供了清晰、可靠且一劳永逸的命令行(CLI)配置方案与排错指南,帮助企业网络工程师彻底解决内外网访问不一致的难题。

u6v7w8x的博客 874

广州24h健身房系统开发公司排名,异步消息队列处理订单

第一梯队为健身专项高并发开发团队,熟练运用RabbitMQ、RocketMQ等异步消息队列处理订单业务,实现下单、支付、权限、通知、日志全链路异步解耦,具备订单重试、死信处理、消息防丢、幂等去重能力,可平稳承接广州商圈门店高峰期高并发订单,订单异常率极低,适配连锁品牌规模化运营。广州24h自助健身房用户下单具备时段集中、瞬时并发高、订单类型多样、业务联动复杂的特点,黄金时段大量用户集中预约、购卡、续费,同步订单处理架构的短板会被快速放大,衍生出各类运营与用户体验问题,是本地多数健身房系统的共性技术痛点。

小马总的博客 116

栅极驱动电路设计(一)

SiC驱动电路设计需满足更高标准,重点包括:驱动电压需18~20V开通、-3~-5V关断;栅极电阻优选5~10Ω以平衡开关性能与可靠性;必须集成欠压保护(阈值11~15V)和快速短路保护。多电平驱动控制通过动态调节栅压和驱动强度优化性能,如前期弱驱动抑制振荡,后期强驱动降低损耗。以NCE120ED65VTP4为例,栅压从18V提升至20V可使导通损耗降低15%。相比传统方案,有源驱动(含数字分段和模拟闭环控制)能更灵活地提升开关速度与效率。

嵌入式老牛的博客 453

Java 重试机制详解

重试机制是一种容错设计模式,在分布式系统和网络通信中尤为重要。当操作失败时(例如网络请求超时、数据库连接失败等),系统会自动重新尝试执行该操作,直到成功或达到预定的重试次数上限。在Java应用程序中,特别是涉及外部服务调用、数据库操作或文件IO等不可靠操作时,合理的重试机制可以:一个完善的重试机制通常包含以下几个关键要素:并非所有失败的操作都适合重试。一般来说,以下场景适合实施重试机制:不适合重试的场景:最基本的重试机制是使用循环来实现: 2.2 带延迟的重试 实际应用中,通常需要在重试之间添加一定的延迟,

yk_dflkg的博客 2111

ROS局部路径规划器插件teb_local_planner流程梳理(上)

在我之前的文章《ROS导航包Navigation中的 Movebase节点路径规划相关流程梳理》中已经介绍过Move_base节点调用局部路径规划器插件的接口函数是computeVelocityCommands,接下来,我们就从这个函数入手梳理一下teb_local_planner功能包的工作流程。

慕羽★的博客 5165

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Java 中间件:RabbitMQ 延迟队列(死信交换机实现)

本文介绍了如何利用RabbitMQ的死信交换机机制实现延迟队列功能。主要内容包括: 延迟队列的概念及应用场景,如订单超时处理、消息重试等 RabbitMQ通过TTL和死信交换机组合实现延迟队列的原理 关键概念解析:TTL、死信、死信交换机和路由键 实现流程:消息先进入延迟队列,TTL到期后转为死信并路由到业务队列 环境准备和Spring Boot项目搭建指南 核心组件定义:延迟交换机、延迟队列、死信交换机和业务队列 文章通过清晰的流程图和代码示例,展示了如何从零开始构建一个基于RabbitMQ的延迟队列系统

千淘万漉虽辛苦,吹尽狂沙始到金 2万+

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

基于redis实现的轻量级延迟队列

来源:juejin.cn/post/ 7339801868473466918???? 欢迎加入小哈的星球,你将获得:专属的项目实战 /1v1 提问/Java 学习路线 /学习打卡/每月赠书/社群讨论新项目:《从零手撸:仿小红书(微服务架构)》正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,点击查看项目介绍;...

犬小哈 189

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

基于 Redis 实现的轻量级延迟队列

imstfy使用namespace做业务隔离,每个业务一个[token] ,需要向服务端申请,如果服务端开启了basic auth accounts验证,调用接口的时候 需要传入配置文件的账号,例如上面的配置,调用的时候需要传入”change.me“lmstfy是依赖[redis]实现的延迟队列,所以首先要安装redis,redis的安装教程可以谷歌搜一搜, 只需要注意一点,redis的持久化,使用。服务器内部会有Timer,每隔一秒检查ZSet中是否有到期的消息,有则将消息移到就绪队列供消费者消费;

犬小哈 153

Redis 实现延迟队列的方案总结

需要注意的是,生产环境中不推荐使用此方案。原因在于 Redis 的过期策略采用的是惰性删除和定期删除相结合的方式,Redis 并不保证 Key 在过期时会被立即删除。这种方法比监听过期 Key 更合理一些,但仍存在一些缺陷,比如无重试机制、延迟时间固定化(依赖定时任务的执行时间),并且不适用于大规模的延迟任务。3. 监听过期 Key:当 Key 过期时,Redis 会将过期通知推送给监听者,从而触发延迟任务的执行。2. 设置 Key 的过期时间:在业务逻辑中,给需要延迟执行的任务设置一个过期时间。

2201_75783970的博客 1091

Java实现Redis延迟队列:从原理到高可用架构

在现代分布式系统中,延迟队列是一种至关重要的组件。它允许我们将消息或任务放入队列,直到指定的延迟时间到达后才被消费。这种机制广泛应用于订单超时自动取消、支付后定时发送通知、任务重试等场景。虽然RabbitMQ和RocketMQ等专业消息中间件都支持延迟消息,但在很多轻量级场景下,利用Redis来实现延迟队列是一个更简单、高效的选择。本教程将带你深入理解Redis延迟队列的原理,并手把手教你用Java实现一个高可用的延迟队列。

0xDevNull的博客 642

springboot kafka 实现延时队列_kafka延迟队列

②适用于轻量级的任务调度和消息通知场景,适合短期延迟任务,不适合长期任务,例如订单超时未支付等。①没有原生的延迟队列功能,需要使用topic和消费者组来实现,实现延迟队列需要额外的开发工作。①RabbitMQ的延迟队列是通过RabbitMQ的插件实现的,易于部署和使用。例如电商平台、社交软件等。①RocketMQ的延迟队列是RocketMQ原生支持的,易于使用和部署。②RocketMQ的延迟队列需要保证消息的顺序,可能会导致消息延迟。适用于大规模的数据处理,实时性要求较高的,高吞吐量的消息处理场景。

2401_86400247的博客 2272

2026 轻量级消息队列 Redis Stream

核心角色:消息 ID(时间戳-序号)、Stream 队列、消费者组(Consumer Group)、消费者(Consumer)、PEL(Pending Entry List)。日志型、持久化、有序、支持多播+ACK的轻量级消息队列,Redis 5.0 引入,底层由 Radix Tree + listpack 实现,单节点可 10w+ TPS。内存控制 XTRIM MAXLEN ~ 100000 或 redis.conf 中 stream-node-max-entries 双重控制,防止堆积爆炸。

Y717310的博客 1132

订单到期关闭如何实现(延迟场景、延时队列、定时推送、限时抢购)

订单30分钟未支付自动取消怎么实现日常开发中,我们经常遇到这种业务场景,如:外卖订单超 30 分钟未支付,则自动取订单;用户注册成功 15 分钟后,发短信息通知用户等等。这就延时任务处理场景。在电商,支付等系统中,一设都是先创建订单(支付单),再给用户一定的时间进行支付,如果没有按时支付的话,就需要把之前的订单(支付单)取消掉。这种类以的场景有很多,还有比如到期自动收货,超时自动退款,下单后自动发送短信等等都是类似的业务问题。

刘皇叔说Java的博客 2736

RabbitMQ消息确认机制+延时队列

RabbitMQ之TTL(JAVA设置TTL) 大白话讲解RabbitMQ消息可靠传输保障 消息确认机制 死信队列 应用RabbitMQ可靠性传输机制实现 Redis 缓存的实时更新 深入分析RabbitMQ消息异常处理,及延迟队列在缓存架构中的应用 RabbitMQ消息确认机制 生产者消息确认机制 当消息发...

Lzc的博客 1976

用 Redis 做一个可靠的延迟队列

点击上方“芋道源码”,选择“设为星标”管她前浪,还是后浪?能浪的浪,才是好浪!每天 10:33更新文章,每天掉亿点点头发...源码精品专栏原创 | Java 2021超神之路,很肝~中文详细注释的开源项目RPC 框架 Dubbo 源码解析网络应用框架 Netty 源码解析消息中间件 RocketMQ 源码解析数据库中间件 Sharding-JDBC 和 MyCAT ...

芋艿V 246

RabbitMQ RPC超时与死信队列实战:消息补偿、重试与监控方案

在分布式系统设计中,服务间通信的可靠性往往取决于消息中间件的深度使用。RabbitMQ作为主流的消息队列,不仅承担异步解耦的职责,其RPC模式更让服务调用变得灵活可控。然而,同步调用超时、消息丢失、异常堆积等问题,常常让开发者陷入被动排查的困境。死信队列(DLX)作为消息的“兜底”机制,为处理无法正常消费的消息提供了标准化出口。结合TTL、reply-to、correlationId等核心属性,我们可以构建一套完整的失败补偿与延迟重试链路。通过将RPC调用与死信队列、监控告警深度结合,系统能够自动识别超时消

weixin_33985679的博客 237
上一篇: JAVA版网易云音乐格式转换器
下一篇: 【手撸IM】消息ID设计与实现
BossFriday
博客等级 码龄23年 6080粉丝 38原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

BossFriday

原创不易,请给作者打赏或点赞!

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

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

打赏作者

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

抵扣说明:

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

余额充值