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)开始。它代码简单,易于理解和维护,并且性能出色,能够满足绝大多数异步处理场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少年攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值