Redis系列---【如何使用redis实现消息队列?】
在只有 Redis 和 Spring Boot 的情况下,利用 Redis 实现一个可靠的异步消息队列是非常常见的做法。Redis 提供了多种数据结构可以用来模拟消息队列,其中最经典和最常用的是 List 结构。
这里将为您提供一个完整、可直接使用的方案,包含生产者和消费者的实现。
核心思想
我们将使用 Redis 的 List 数据结构来作为队列。
- 生产者 (Producer):当需要发送异步消息时,调用
LPUSH
或RPUSH
命令将消息(通常是序列化后的 JSON 字符串)推入 List 的一端。这个操作非常快,几乎是瞬时的,不会阻塞主业务流程。 - 消费者 (Consumer):在应用的一个或多个后台线程中,使用
BRPOP
或BLPOP
命令进行阻塞式地等待。当 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)开始。它代码简单,易于理解和维护,并且性能出色,能够满足绝大多数异步处理场景。