2種方式用redis實現延時隊列

第一種:採用redisson

依賴:

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.10.5</version>
        </dependency>

配置:

spring.redis.host=47.98.182.201
spring.redis.port=7007
spring.redis.password=124
spring.redis.database=2

添加隊列:

package com.citydo.faceadd.redis;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisDelayedQueue {

    @Autowired
    RedissonClient redissonClient;

    /**
     * 添加隊列
     *
     * @param t        DTO傳輸類
     * @param delay    時間數量
     * @param timeUnit 時間單位
     * @param <T>      泛型
     */
    public <T> void addQueue(T t, long delay, TimeUnit timeUnit, String queueName) {
        log.info("添加隊列{},delay:{},timeUnit:{}" + queueName, delay, timeUnit);
        RBlockingQueue<T> blockingFairQueue = redissonClient.getBlockingQueue(queueName);
        RDelayedQueue<T> delayedQueue = redissonClient.getDelayedQueue(blockingFairQueue);
        delayedQueue.offer(t, delay, timeUnit);
        delayedQueue.destroy();
    }

}

監聽隊列:

package com.citydo.faceadd.redis;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 初始化隊列監聽
 */
@Component
@Slf4j
public class RedisDelayedQueueInit implements ApplicationContextAware {

    @Autowired
    RedissonClient redissonClient;

    /**
     * 獲取應用上下文並獲取相應的接口實現類
     *
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, RedisDelayedQueueListener> map = applicationContext.getBeansOfType(RedisDelayedQueueListener.class);
        map.entrySet().stream().forEach(taskEventListenerEntry->{
            String listenerName = taskEventListenerEntry.getValue().getClass().getName();
            startThread(listenerName, taskEventListenerEntry.getValue());
        });
    }

    /**
     * 啓動線程獲取隊列*
     *
     * @param queueName                 queueName
     * @param redisDelayedQueueListener 任務回調監聽
     * @param <T>                       泛型
     * @return
     */
    private <T> void startThread(String queueName, RedisDelayedQueueListener redisDelayedQueueListener) {
        RBlockingQueue<T> blockingFairQueue = redissonClient.getBlockingQueue(queueName);
        //由於此線程需要常駐,可以新建線程,不用交給線程池管理
        Thread thread = new Thread(() -> {
            log.info("啓動監聽隊列線程" + queueName);
            while (true) {
                try {
                    T t = blockingFairQueue.take();
                    log.info("監聽隊列線程{},獲取到值:{}", queueName, JSON.toJSONString(t));
                    new Thread(() -> redisDelayedQueueListener.invoke(t)).start();
                } catch (Exception e) {
                    log.info("監聽隊列線程錯誤,", e);
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException ex) {
                        log.error("線程出現異常,", ex);
                    }
                }
            }
        });
        thread.setName(queueName);
        thread.start();
    }

}

隊列事件監聽接口,需要實現這個方法:

package com.citydo.faceadd.redis;

/**
 * 隊列事件監聽接口,需要實現這個方法
 *
 * @param <T>
 */
public interface RedisDelayedQueueListener<T> {
    /**
     * 執行方法
     *
     * @param t
     */
    void invoke(T t);
}

參數:

package com.citydo.faceadd.redis;

import lombok.Data;

import java.io.Serializable;

/**
 * @author nick
 */
@Data
public class TaskBodyDTO implements Serializable {

    private String name;

    private String body;

}

延時執行代碼:

package com.citydo.faceadd.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 監聽器
 */
@Component
@Slf4j
public class TestListener implements RedisDelayedQueueListener<TaskBodyDTO> {

    @Override
    public void invoke(TaskBodyDTO taskBodyDTO) {
        //這裏調用你延遲之後的代碼
        log.info("執行...." + taskBodyDTO.getBody() + "===" + taskBodyDTO.getName());
    }
}

測試接口:

package com.citydo.faceadd.redis;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @author nick
 */
@RestController
@RequestMapping(value = "/test")
public class RedisTestController {

    @Autowired
    private RedisDelayedQueue redisDelayedQueue;

    @GetMapping("/redis")
    private void test(){
        TaskBodyDTO taskBody = new TaskBodyDTO();
        taskBody.setBody("測試DTO,3秒之後執行");
        taskBody.setName("測試DTO,3秒之後執行");
        //添加隊列3秒之後執行
        redisDelayedQueue.addQueue(taskBody, 10, TimeUnit.SECONDS, TestListener.class.getName());
        taskBody.setBody("測試DTO,10秒之後執行");
        taskBody.setName("測試DTO,10秒之後執行");
        //添加隊列10秒之後執行
        redisDelayedQueue.addQueue(taskBody, 20, TimeUnit.SECONDS, TestListener.class.getName());
        taskBody.setBody("測試DTO,20秒之後執行");
        taskBody.setName("測試DTO,20秒之後執行");
        //添加隊列30秒之後執行
        redisDelayedQueue.addQueue(taskBody, 30, TimeUnit.SECONDS, TestListener.class.getName());
    }
}

第二種方式:採用redis實現

依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>

配置:

spring.redis.host=47.98.182.201
spring.redis.port=7007
spring.redis.password=124
spring.redis.database=2

核心代碼:

package com.citydo.faceadd.redis;

import com.alibaba.fastjson.JSON;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;

import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

/**
 * @author nick
 */
public class RedisDelayedQueueT<T> {

    private static final AtomicInteger COUNTER = new AtomicInteger(1);

    private volatile boolean started = false;

    private String queueKey;

    private Consumer<T> handler;

    private Class<T> classOfT;

    private StringRedisTemplate stringRedisTemplate;

    /**
     * @param queueKey 隊列鍵值
     * @param classOfT 元素的類型
     * @param handler  處理器,如果元素到了時間,需要做的處理
     */
    public RedisDelayedQueueT(String queueKey, Class<T> classOfT, Consumer<T> handler) {
        this.queueKey = queueKey;
        this.handler = handler;
        this.classOfT = classOfT;
    }

    /**
     * 往該延時隊列中放入數據,達到指定時間時處理
     *
     * @param value    數據
     * @param deadLine 截止時間戳,單位是毫秒
     * @return 是否執行成功
     */
    public boolean putForDeadLine(T value, long deadLine) {
        if (value == null) {
            return false;
        }
        long current = System.currentTimeMillis();
        if (deadLine < current) {
            throw new IllegalArgumentException(String.format("deadline: %d 小於當前時間: %d !", deadLine, current));
        }
        if (stringRedisTemplate == null) {
            throw new IllegalStateException("請設置stringRedisTemplate!");
        }
        String json = JSON.toJSONString(value);
        Boolean flag = stringRedisTemplate.opsForZSet().add(queueKey, json, deadLine);
        return Boolean.TRUE.equals(flag);
    }

    /**
     * 往該延時隊列中放入數據,指定時間後執行
     *
     * @param value       數據
     * @param delayedTime 需要延長的時間,單位是毫秒
     * @return 是否執行成功
     */
    public boolean putForDelayedTime(T value, long delayedTime) {
        return putForDeadLine(value, System.currentTimeMillis() + delayedTime);
    }

    /**
     * 清除隊列中的數據
     */
    public void clear() {
        stringRedisTemplate.opsForZSet().removeRangeByScore(queueKey, Double.MIN_VALUE, Double.MAX_VALUE);
    }

    /**
     * 驗證隊列是否存在 true 存在  false 不存在
     */
    public Boolean verify() {
        Long value = stringRedisTemplate.opsForZSet().zCard(queueKey);
        return value != null ? value > 0 : false;
    }


    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        if (this.stringRedisTemplate == null && !started) {
            this.stringRedisTemplate = stringRedisTemplate;
            Worker worker = new Worker();
            worker.setName("delayed-queue-task-" + queueKey + "-" + COUNTER.getAndIncrement());
            worker.start();
            started = true;
        }
    }

    class Worker extends Thread {

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                long current = System.currentTimeMillis();
                Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                        .rangeByScoreWithScores(queueKey, 0, current, 0, 1);
                if (CollectionUtils.isNotEmpty(typedTuples)) {
                    ZSetOperations.TypedTuple<String> next = typedTuples.iterator().next();
                    if (next.getScore() != null && next.getScore() < current) {
                        Long removedCount = stringRedisTemplate.opsForZSet().remove(queueKey, next.getValue());
                        // 只有一個線程可以刪除成功,代表拿到了這個需要處理的數據
                        if (removedCount != null && removedCount > 0) {
                            handler.accept(JSON.parseObject(next.getValue(), classOfT));
                        }
                    }
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(10L + ThreadLocalRandom.current().nextInt(10));
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            started = false;
        }
    }

}

測試接口:

package com.citydo.faceadd.redis;

import com.alibaba.fastjson.JSON;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping(value = "/test")
public class RedisDelayedQueueTController {

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    private RedisDelayedQueueT<String> redisDelayedQueue = new RedisDelayedQueueT<>("test-queue", String.class, this::head);
    /**
     * 異步執行方法
     * @param t
     * @param <T>
     */
    @Async
    public <T> void head(T t) {
        System.out.println("執行方法"+ JSON.toJSONString(t));
    }


    @GetMapping("/redisT")
    public void addTest(){
        redisDelayedQueue.setStringRedisTemplate(stringRedisTemplate);
        redisDelayedQueue.putForDelayedTime("測試延時定時任務",5000);
    }
}

參考:葉飛
參考:https://www.wncode.cn/detail/24.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章