使用Redis实现延时队列

116 阅读2分钟

解题思路

  • 前置条件

项目中使用了reids, 但是有不想集成MQ,可以采用。使用MQ请跳过 使用 redis zset有序集合,zsetRedis提供的一个非常特别的数据结构。每一个value后面会携带一个score。我们可以使用score来存储过期时间,然后以当前时间为条件轮询zset集合。从而达到延时的效果.

实现方式

本文介绍基于SpringBoot的方式实现.

1. 队列配置

可通过配置文件配置是否开启队列模式

package com.sword.starter.cache.redis.config;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * knife4j 属性
 *
 * @author tan
 * @version 1.0 2021-11-12
 */
@Getter
@Setter
@ToString
@ConfigurationProperties(prefix = "bluerosa.redis")
public class RedisProps {

    private Queue queue = new Queue();

    @Getter
    @Setter
    @ToString
    public static class Queue {

        /**
         * 启用队列
         */
        private boolean enable = false;

        /**
         * 轮询间隔
         */
        private long pollingInterval = 1000;
    }

}

2. message 对象

封装消息对象

package com.sword.starter.cache.redis.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * redis queue message, 简单的消息队列实现
 *
 * @author Tan
 * @version 1.0 2022/5/23
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Message {

    /**
     * 消息Id
     */
    private long id;

    /**
     * 延时时间,单位毫秒
     */
    private Long delay;

    /**
     * 消息内容
     */
    private String content;

    /**
     * 主题
     */
    private String topic;
}

3. 消息事件

package com.sword.starter.cache.redis.queue;

import com.sword.starter.cache.redis.domain.Message;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;

/**
 * redis 队列事件
 *
 * @author Tan
 * @version 1.0 2022/5/23
 */
@Getter
public class RedisQueueEvent extends ApplicationEvent {
    public RedisQueueEvent(Message message) {
        super(message);
    }

    public Message getSource() {
        return (Message) source;
    }
}

4. 轮询zset集合(核心)

SpringBoot 项目启动时加入线程,轮询zset集合获取队列值

package com.sword.starter.cache.redis.queue;

import com.alibaba.fastjson.JSON;
import com.sword.starter.cache.redis.config.RedisProps;
import com.sword.starter.cache.redis.domain.Message;
import com.sword.starter.cache.redis.enums.RedisKey;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.Set;

/**
 * redis 延时队列
 *
 * @author Tan
 * @version 1.0 2022/5/23
 */
@Slf4j
@Order(1000)
@RequiredArgsConstructor
@EnableConfigurationProperties(RedisProps.class)
public class RedisQueueRunner implements CommandLineRunner {

    private final TaskExecutor taskExecutor;
    private final StringRedisTemplate stringRedisTemplate;
    private final ApplicationContext applicationContext;
    private final RedisProps redisProps;


    @Override
    public void run(String... args) {
        log.info("start redis delay queue: {}", redisProps);
        taskExecutor.execute(() -> {
            while (redisProps.getQueue().isEnable()) {
                try {
                    long max = System.currentTimeMillis();
                    Set<String> queue = stringRedisTemplate.opsForZSet().rangeByScore(RedisKey.REDIS_QUEUE.getKey(), 0, max);
                    log.debug("get queue: {}", queue);
                    if (CollectionUtils.isEmpty(queue)) {
                        sleep();
                        continue;
                    }
                    queue.forEach(it -> {
                        Message message;
                        if (StringUtils.hasText(it)) {
                            message = JSON.parseObject(it, Message.class);
                        } else {
                            message = null;
                        }
                        log.debug("redis  queue message: {}", message);
                        applicationContext.publishEvent(new RedisQueueEvent(message));
                    });
                    log.debug("remove queue: {}", queue);
                    stringRedisTemplate.opsForZSet().removeRangeByScore(RedisKey.REDIS_QUEUE.getKey(), 0, max);
                } catch (Exception e) {
                    log.error("get redis queue message error", e);
                    sleep();
                }
            }
        });
    }

    private void sleep() {
        try {
            Thread.sleep(redisProps.getQueue().getPollingInterval());
        } catch (InterruptedException e) {
            log.error(" sleep error", e);
        }
    }
}

5. 发送消息模版

package com.sword.starter.cache.redis.template;

import com.sword.starter.cache.redis.domain.Message;

/**
 * redis 队列模板
 *
 * @author Tan
 * @version 1.0 2022/5/23
 */
public interface RedisQueueTemplate {

    /**
     * 发送消息
     *
     * @param message 消息
     */
    void sendMessage(Message message);

}
package com.sword.starter.cache.redis.template.impl;

import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSON;
import com.sword.starter.cache.redis.domain.Message;
import com.sword.starter.cache.redis.enums.RedisKey;
import com.sword.starter.cache.redis.template.RedisQueueTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.Optional;

/**
 * redis 简单的消息队列实现
 *
 * @author Tan
 * @version 1.0 2022/5/23
 */
@Slf4j
@RequiredArgsConstructor
public class RedisQueueTemplateImpl implements RedisQueueTemplate {

    private final StringRedisTemplate stringRedisTemplate;

    @Override
    public void sendMessage(Message message) {
        log.debug("sendMessage: {}", message);
        Assert.notNull(message, "message 不能为空");
        Assert.isTrue(StringUtils.hasText(message.getTopic()), "topic 不能为空");
        Assert.isTrue(StringUtils.hasText(message.getContent()), "content 不能为空");
        message.setId(IdUtil.getSnowflakeNextId());
        stringRedisTemplate.opsForZSet().add(RedisKey.REDIS_QUEUE.getKey(), JSON.toJSONString(message), Optional.ofNullable(message.getDelay()).map(it -> System.currentTimeMillis() + it).orElse(0L));
    }
}

6.监听消息

package com.sword.example.listener;

import com.sword.starter.cache.redis.queue.RedisQueueEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * @author Tan
 * @version 1.0 2022/5/23
 */
@Component
public class ConsumeListener implements ApplicationListener<RedisQueueEvent> {
    @Override
    public void onApplicationEvent(RedisQueueEvent event) {
        System.out.println("消费者接收到消息:" + event.getSource());
    }
}