Redis系列---【如何使用redis实现消息队列?】

在只有 Redis 和 Spring Boot 的情况下,利用 Redis 实现一个可靠的异步消息队列是非常常见的做法。Redis 提供了多种数据结构可以用来模拟消息队列,其中最经典和最常用的是 List 结构。

这里将为您提供一个完整、可直接使用的方案,包含生产者和消费者的实现。

核心思想

我们将使用 Redis 的 List 数据结构来作为队列。

  • 生产者 (Producer):当需要发送异步消息时,调用 LPUSHRPUSH 命令将消息(通常是序列化后的 JSON 字符串)推入 List 的一端。这个操作非常快,几乎是瞬时的,不会阻塞主业务流程。
  • 消费者 (Consumer):在应用的一个或多个后台线程中,使用 BRPOPBLPOP 命令进行阻塞式地等待。当 List 中有新消息时,Redis 会立即将消息弹出给消费者,消费者拿到消息后进行处理。B代表Blocking,这种方式比自己写 while(true) 循环去轮询高效得多,因为它不会空耗 CPU。

方案一:使用 Redis List 实现经典消息队列(推荐)

这是最简单、最直观的实现方式,完全能满足您的需求。

第1步:确保依赖存在

请确保您的 pom.xml 中有 spring-boot-starter-data-redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot 2.3.x 之后,lettuce-core 不再默认包含连接池,如果需要请手动添加 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!-- 用于对象与JSON字符串之间的转换 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

第2步:配置 Redis

application.yml 中配置好 Redis 连接。Spring Boot 会自动为您配置 RedisTemplate

spring:
  redis:
    host: localhost
    port: 6379
    # password: your-password
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

为了能序列化任意对象,我们配置一个使用 JSON 序列化的 RedisTemplate Bean。

package com.yourcompany.project.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用 String 序列化 Key
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        // 使用 Jackson JSON 序列化 Value
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

第3步:创建消息生产者 (Producer)

生产者是一个 Service,它负责将业务数据封装成消息并推入 Redis 队列。

package com.yourcompany.project.producer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class MessageProducer {

    private static final Logger log = LoggerFactory.getLogger(MessageProducer.class);
    
    // 定义队列的Key
    public static final String QUEUE_KEY = "my:async:task:queue";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 发送异步消息到队列
     * @param message 消息对象,可以是任何可序列化的Java对象
     */
    public void sendMessage(Object message) {
        try {
            // 使用 LPUSH 将消息推入列表的左侧
            Long size = redisTemplate.opsForList().leftPush(QUEUE_KEY, message);
            log.info("成功发送消息到队列 '{}',当前队列大小: {}", QUEUE_KEY, size);
        } catch (Exception e) {
            log.error("发送消息到Redis队列失败", e);
        }
    }
}

第4步:创建消息消费者 (Consumer)

消费者需要在后台持续监听队列。一个好的实践是使用一个实现了 ApplicationListener<ApplicationReadyEvent> 的组件来启动一个后台线程,以确保在应用完全启动后再开始消费。

package com.yourcompany.project.consumer;

import com.yourcompany.project.producer.MessageProducer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ApplicationReadyEvent;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Component
public class MessageConsumer implements ApplicationListener<ApplicationReadyEvent> {

    private static final Logger log = LoggerFactory.getLogger(MessageConsumer.class);

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 使用一个单线程的线程池来在后台执行监听任务
    private final ExecutorService executorService = Executors.newSingleThreadExecutor();

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        log.info("应用已启动,开始监听Redis队列...");
        executorService.submit(this::listen);
    }

    private void listen() {
        // 持续监听队列
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 使用 BRPOP 进行阻塞式拉取,超时时间设置为0表示永久阻塞,直到有消息为止
                // 为了能优雅停机,可以设置一个超时时间,例如5秒
                Object message = redisTemplate.opsForList().rightPop(MessageProducer.QUEUE_KEY, 5, TimeUnit.SECONDS);

                if (message != null) {
                    // 成功获取到消息,进行处理
                    handleMessage(message);
                }
            } catch (Exception e) {
                // 如果在阻塞等待时发生异常(如连接断开),记录日志并可能需要重连逻辑
                log.error("监听Redis队列时发生错误", e);
                // 防止因异常导致循环过快,可以短暂休眠
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // 恢复中断状态
                }
            }
        }
    }

    private void handleMessage(Object message) {
        log.info("成功消费消息: {}", message);
        // 在这里写真正的业务逻辑
        // 例如,可以根据 message 的类型进行不同的处理
        // if (message instanceof Order) { ... }
        // if (message instanceof UserRegistration) { ... }
        // 模拟耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        log.info("消息处理完毕。");
    }
}

第5步:如何使用

现在,您可以在任何需要异步处理的地方注入 MessageProducer 并调用其方法。

package com.yourcompany.project.controller;

import com.yourcompany.project.producer.MessageProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class TestController {

    @Autowired
    private MessageProducer messageProducer;

    @GetMapping("/send")
    public String sendMessage(@RequestParam String content) {
        // 创建一个消息对象
        Map<String, Object> message = new HashMap<>();
        message.put("id", UUID.randomUUID().toString());
        message.put("content", content);
        message.put("timestamp", System.currentTimeMillis());

        // 发送到队列
        messageProducer.sendMessage(message);

        return "消息已异步发送,请查看消费者日志。";
    }
}

方案二:使用 Redis Streams(更现代、更强大的队列)

如果您的业务场景更复杂,比如需要消息持久化、消费组(多个消费者共同消费一个队列)、消息确认(ACK) 等特性,那么 Redis 5.0 之后引入的 Streams 是一个更好的选择。它更像一个轻量级的 Kafka。

  • 生产者:使用 XADD 命令添加消息到 Stream。
  • 消费者:创建消费组(XGROUP CREATE),然后使用 XREADGROUP 来读取消息。处理完后用 XACK 确认消息。

这种方式配置稍复杂,但可靠性更高。如果您的需求只是简单的异步解耦,方案一(List)已经完全足够。

总结与建议

特性 Redis List (方案一) Redis Streams (方案二)
易用性 非常简单,符合直觉 中等,概念稍多
可靠性 良好。消息在Redis中,只要Redis不丢数据就不会丢。但如果消费者取出消息后崩溃,消息会丢失。 非常高。支持消费组和ACK,消费者崩溃未ACK的消息可以被重新投递。
功能 基本队列功能 消费组、消息持久化、ACK机制、失败重投
适用场景 简单的异步任务、日志收集、延迟处理等 需要高可靠性的订单处理、事件溯源等复杂场景

对于您“实现异步消息写入队列”的初始需求,我强烈推荐您从方案一(Redis List)开始。它代码简单,易于理解和维护,并且性能出色,能够满足绝大多数异步处理场景。

posted on 2025-07-23 11:51  少年攻城狮  阅读(99)  评论(0)    收藏  举报

导航