基于redisson实现延迟队列功能实战

背景:

最近有个需求是跟第三方对接事故报案,事故报案单状态是由kafka来实现发布订阅的。具体是这样的,事故报案双方都可以发起,如果是第三方发起的事故报案,我们接到kafka推送的状态后,需要创建一张事故报案单,如果是我们发起的事故报案,第三方也需要创建一张事故报案单。因为我们创建事故报案单后,还有后续流程要走,导致当消费该报案单第一个状态的时候,事故报案单还没有创建完成,其结果就是该消息没有被成功消费,导致事故报案单缺少了第一个状态节点。因为状态消息实时性要求不高,所以我们决定用延时队列来解决这个问题。同时为了保证服务重启后,或者服务器宕机时,保存在延时队列中的数据不会丢失,最终选择使用Redisson中的延时队列。

在此先不讨论其实现原理,我们先来实现这个延时队列。

1.引入依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>2.8.2</version>
        </dependency>

2.添加配置 (默认可以直接注入,不用再自行配置。由于依赖版本不同,自行配置可能会报错)

@Configuration
public class RedissonConfiguration {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        SingleServerConfig singleServerConfig = config.useSingleServer();
        String addr = host+":"+port;
        singleServerConfig.setAddress(addr);
        return Redisson.create(config);
    }
}

3.定义延时队列名称枚举类 (后续可以添加其它延时队列名称)

/**
 * 延时队列名称枚举
 */
public enum DelayQueueNameEnum {

    DELAY_QUEUE_TYPE("DELAY_QUEUE_TYPE");

    private String name;

    DelayQueueNameEnum(String name){}
}

4.延时任务执行器接口,业务实现该接口,可以实现自己的业务逻辑

/**
 * 延时任务执行器接口
 * @param 
 */
public interface RedisDelayQueueExecute<T> {


    /**
     * 回调方法
     * @param t
     */
    void execute(T t);
}

5.具体业务实现

@Component(value = "ADelayQueueExecuteImpl")
public class ADelayQueueExecuteImpl implements RedisDelayQueueExecute<Map>{

    @Autowired
    private AService aService;

    @Async
    @Override
    public void execute(Map obj) {
        try {
	        // 延时任务处理实现
            aService.processStatus(obj);
        } catch (Exception e) {
            log.error(e.getMessage(),e);
        }
    }
}

6.延时队列服务类

/**
 * redis延迟队列服务
 */
@Slf4j
@Component
public class RedisDelayJobService {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 添加到消息队列
     * @param value 添加到队列的对象
     * @param delay 延迟时间
     * @param timeUnit 时间单位
     * @param queueName 队列名称
     * @param 
     */
    public <T> void addQueue(T value , long delay, TimeUnit timeUnit,String queueName){
        try {
            log.info("添加到延时队列【{}】【{}】【{}】",value,delay,queueName);
            RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueName);
            RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
            delayedQueue.offer(value,delay,timeUnit);
        } catch (Exception e) {
            log.error("添加到延时队列失败:"+e.getMessage(),e);
            throw new RuntimeException("添加到延时队列失败");
        }
    }


/**
* 取值
*/
    public <T> T take(String queueName) throws InterruptedException {
        RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueName);
        T value  = (T) blockingDeque.take();
        return value;
    }
}

7.将满足条件的kafka消息添加到延时队列,只需要调用addQueue()方法即可

8.取出延时任务,处理该延时任务


@Slf4j
@Component
public class RedisDelayQueueRunner implements ApplicationRunner {

	/**
	* 注入延时任务执行器,指定具体业务实现类
	*/
    @Resource(name = "ADelayQueueExecuteImpl")
    private RedisDelayQueueExecute ADelayQueueExecute;

    @Autowired
    private RedisDelayJobService redisDelayJobService;

    @Override
    public void run(ApplicationArguments args) {
        String queueName = DelayQueueNameEnum.DELAY_QUEUE_TYPE.name();
        //DESC 开启新的线程(常驻)处理延迟队列
        new Thread(()->{
            while (true){
                try {
                    Map value = redisDelayJobService.take(queueName);
                    if (value!=null){
                        ADelayQueueExecute.execute(value);
                    }
                } catch (InterruptedException e) {
                    log.error("延迟队列取值中断异常:"+e.getMessage(),e);
                }
            }
        }).start();
    }
}

到此就实现了延时任务的处理。如有错误之后,欢迎指正。

记实战过程中的疑问

在测试过程中发现一个问题,当延时任务从队列中取出之后,在处理该延时任务时(线程未结束),会阻塞延时队列的添加操作。

希望有大神能够解答。

你可能感兴趣的:(并发编程,延时队列,Redisson)