Spring Boot「19」使用 Redis 实现消息队列功能

330 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 03 天,点击查看活动详情

Redis 支持订阅、发布模式的消息传递,这使得其能够作为简单的消息队列使用。 而且,Redis 是单线程的,能够保证 FIFO 队列特性。 今天,我会通过典型的生产者-消费者模型,来演示 Redis 是如何作为消息队列的。

01-生产者

在生产者消费者模型中,生产者负责生产消息对象,并放入到消息队列中。 在前面的文章 Spring Boot「18」Lettuce 原生 API 与 RedisTemplate 对比 中,我介绍了如何使用 SpringRedisTemplate 向某个 channel 上发送消息,即调用 SpringRedisTemplate#convertAndSend 方法。

生产者的代码如下:

@Component
public class MessageProducer {
    private static final Logger LOGGER = LoggerFactory.getLogger(MessageProducer.class);
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ChannelTopic topic;   // 消息队列
    // 向消息队列中发送消息
    public void produce(Object obj) {
        redisTemplate.convertAndSend(topic.getTopic(), obj);
    }

    public Thread startProduceThread() {
        // 每 30s 产生一个 MessageObject 对象,并通过 produce 发送到消息队列中
        Runnable producerTask = () -> {
            while (true) {
                try {
                    produce(MessageObjectFactory.getObject());
                    TimeUnit.SECONDS.sleep(30);
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                    LOGGER.warn("producer exception: ", ie);
                }
            }
        };

        Thread produceThread = new Thread(producerTask, "producer");
        produceThread.start(); // 启动生产者线程,不断地产生消息对象
        return produceThread;
    }
}

注:我在生产者中单独创建了一个生产线程,每隔 30s 就产生一个 MessageObject 对象。

02-消费者

在生产者-消费者模型中,消费者负责从消息队列中读取消息。 当 Redis 作为消息队列时,通过 MessageListener 接受消息。

消费者代码如下:

@Component
public class MessageConsumer implements MessageListener {
    private final static Logger LOGGER = LoggerFactory.getLogger(MessageConsumer.class);

    @Autowired
    private RedisSerializer serializer;   // 对象序列、反序列化器,用来从消息队列中消费消息,并反序列化为对象
    @Override
    public void onMessage(Message message, byte[] pattern) {

        LOGGER.info("从消息队列[{}]收到消息", pattern);
        LOGGER.info("从消息队列[{}]收到消息", message.getChannel());
        LOGGER.info("消息内容消息:{}", message.getBody());
        Object object = serializer.deserialize(message.getBody());
        LOGGER.info("反序列化后的消息:{}", object);

    }
}

装配

定义好生产者、消费者之后,这个模型中的主要元素就凑齐了。 接下来就是把他们组装在一起。

首先,Redis 是通过订阅、发布模式来实现消息队列的,所以我们需要定义一个 topic 作为消息队列。 其次,我们向消息队列中存放的时 MessageObject 对象,所以,需要一个序列化器将对象序列化。 而且,在消费者中,还需要序列化器将收到的内容反序列化为 MessageObject 对象。 之后,就是设置 RedisTemplate 对象,使用我们定义好的序列化器。 最后,就是将消费者订阅到消息队列上。

@Configuration
public class ProducerConsumerConfig {

    @Bean
    ChannelTopic queue() {
        return ChannelTopic.of("mq:pc");  // 消息队列
    }
    @Bean
    RedisSerializer redisSerializer() {
        return new GenericJackson2JsonRedisSerializer();  // 对象序列、反序列化器
    }

    @Bean
    RedisTemplate<String, Object> redisTemplate(@Autowired RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        // 设置 template 使用的序列、反序列化器
        template.setDefaultSerializer(redisSerializer());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(redisSerializer());
        return template;
    }

    @Bean
    RedisMessageListenerContainer redisMessageListenerContainer(@Autowired RedisConnectionFactory connectionFactory, 
                                                                @Autowired Collection<MessageListener> listeners) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        listeners.forEach(e -> container.addMessageListener(e, queue()));  // 向消息队列上注册监听器
        return container;
    }
}

装配好后,我们就可以启动程序了:

@SpringBootApplication
public class ProducerConsumerApplication implements CommandLineRunner {
    @Autowired
    private MessageProducer producer;

    public static void main(String[] args) {
        SpringApplication.run(ProducerConsumerApplication.class);
    }

    @Override
    public void run(String... args) throws Exception {
        Thread produceThread = producer.startProduceThread();
        produceThread.join();           // 主要是为了保证 main 线程不退出
    }
}

04-总结

今天,我用生产者-消费者模型演示了 Redis 如何作为消息队列使用。 不过,Redis 并不是一个成熟的消息队列,消费者不在线时,生产者产生的消息有可能会丢失。 如果要使用完备消息队列功能,建议使用 AMQ、RocketMQ 等成熟的消息队列中间件。