使用Redis实现简单的消息队列是一种常见的做法,Redis提供了多种数据结构和命令来支持这种用法。
最常用的方式是利用Redis的列表(List)数据结构,通过LPUSH(左推入)和RPOP(右弹出)来模拟消息的生产和消费。
此外,还可以使用发布/订阅(Pub/Sub)模式或流(Stream)数据结构来实现更复杂的消息队列。
1、List实现消息队列
第一步:创建队列
指定队列的名称,并为该队列实现生产(添加元素)和消费方法(弹出元素)。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class MessageQueueService {
private static final String QUEUE_NAME = "message_queue"; // 队列名称
@Autowired
private StringRedisTemplate redisTemplate;
// 生产者:向队列中添加消息
public void sendMessage(String message) {
redisTemplate.opsForList().leftPush(QUEUE_NAME, message);
System.out.println("Message sent: " + message);
}
// 消费者:从队列中获取并处理消息
public String receiveMessage() {
String message = redisTemplate.opsForList().rightPop(QUEUE_NAME);
if (message != null) {
System.out.println("Message received: " + message);
} else {
System.out.println("No message in the queue.");
}
return message;
}
}
第二步:创建队列的监听方法
这里简单通过定时任务,5秒钟监听一次消息队列的状态,如果队列中存在数据,则开始根据业务消费数据。
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MessageConsumer {
@Autowired
private MessageQueueService messageQueueService;
@Scheduled(fixedRate = 5000) // 每5秒检查一次队列
public void consumeMessages() {
messageQueueService.receiveMessage();
}
}
第三步:启用调度任务
可以在主方法或配置类中添加该注解。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 地洞调度任务
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2、Pub/Sub模式实现
第一步:定义通道,实现基本逻辑
如:订阅通道,在指定通道内发布消息等。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class PubSubService implements MessageListener {
private static final String CHANNEL_NAME = "message_channel"; // 目标主题频道
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 发布消息到指定频道
public void publishMessage(String message) {
redisTemplate.convertAndSend(CHANNEL_NAME, message);
System.out.println("Message published to channel " + CHANNEL_NAME + ": " + message);
}
// 订阅频道
public void subscribe() {
redisTemplate.getConnectionFactory().getConnection().subscribe(this, CHANNEL_NAME);
System.out.println("Subscribed to channel " + CHANNEL_NAME);
}
// 频道订阅后,此处处理接收到的消息
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel());
String body = new String(message.getBody());
System.out.println("Message received from channel " + channel + ": " + body);
}
}
3、Streams消息队列
创建服务类来实现消息的发布和消费。
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.api.sync.RedisStreamCommands;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
public class StreamService {
private static final String STREAM_KEY = "message_stream";
private static final String GROUP_NAME = "consumer_group";
private static final String CONSUMER_NAME = "consumer_1";
private final RedisClient redisClient;
private StatefulRedisConnection<String, String> connection;
private RedisCommands<String, String> syncCommands;
private RedisStreamCommands<String, String> streamCommands;
private ScheduledExecutorService scheduler;
@Autowired
public StreamService(RedisClient redisClient) {
this.redisClient = redisClient;
}
@PostConstruct
public void init() {
connection = redisClient.connect();
syncCommands = connection.sync();
streamCommands = syncCommands;
// 创建消费者组(如果不存在)
if (!streamCommands.xinfoGroups(STREAM_KEY).stream().anyMatch(group -> group.get(GROUP_NAME) != null)) {
streamCommands.xgroupCreate(STREAM_KEY, GROUP_NAME, "0");
}
// 启动消费者线程
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(this::consumeMessages, 0, 1, TimeUnit.SECONDS);
}
// 发布消息到流
public void publishMessage(String message) {
Map<String, String> entry = new HashMap<>();
entry.put("message", message);
streamCommands.xadd(STREAM_KEY, "*", entry);
System.out.println("Message published to stream: " + message);
}
// 消费消息
private void consumeMessages() {
try {
// 从流中读取消息(阻塞式等待新消息)
var messages = streamCommands.xreadGroup(GROUP_NAME, CONSUMER_NAME,
io.lettuce.core.XReadArgs.StreamOffset.lastConsumed(STREAM_KEY));
if (messages != null && !messages.isEmpty()) {
for (var message : messages.get(0).getMessages()) {
String msgId = message.getId();
String content = message.getFields().get("message");
System.out.println("Message received from stream: " + content);
// 处理消息后确认
streamCommands.xack(STREAM_KEY, GROUP_NAME, msgId);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 关闭资源
public void shutdown() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
}
if (connection != null && connection.isOpen()) {
connection.close();
}
}
}
4、三种方式对比
(1)、Redis List
实现方式
- 生产者:使用LPUSH命令将消息推入列表的左端。
- 消费者:使用RPOP命令从列表的右端弹出消息。为了实现阻塞式消费,可以使用BRPOP命令。
优点
- 简单易用:实现非常简单,适合快速构建基本的消息队列。
- 持久化:默认情况下,Redis数据是持久化的,因此即使Redis重启,未处理的消息也不会丢失。
- 负载均衡:可以通过多个消费者同时从列表中弹出消息来实现简单的负载均衡(但需要额外逻辑确保每个消息只被处理一次)。
缺点
- 缺乏可靠性:如果消费者在处理消息时失败或崩溃,消息可能会丢失(除非你实现额外的确认机制)。
- 无消息确认:Redis List没有内置的消息确认机制,无法保证消息一定会被成功处理。
- 顺序问题:虽然LPUSH和RPOP保证了FIFO(先进先出)顺序,但在高并发情况下可能会出现消息顺序不一致的问题。
适用场景
- 简单任务队列:适用于不需要复杂功能的简单任务分发场景。
- 临时性消息传递:适用于对消息持久性和可靠性要求不高的场景。
(2)、Redis Pub/Sub
实现方式
- 发布者:使用PUBLISH命令向指定频道发布消息。
- 订阅者:使用SUBSCRIBE命令订阅一个或多个频道,并接收发布到这些频道的消息。
优点
- 实时通信:非常适合用于实时通信场景,如通知系统、聊天应用等。
- 广播模式:所有订阅同一频道的客户端都会收到相同的消息,适合广播式消息传递。
- 轻量级:实现简单,开销较小,适合低延迟的场景。
缺点
- 不支持持久化:一旦消息被发布出去,如果没有订阅者在线,消息就会丢失。Redis Pub/Sub本身不提供消息持久化功能。
- 无消息确认:无法保证消息一定会被所有订阅者成功接收,也没有消息确认机制。
- 不适合高吞吐量:由于每次发布消息时所有订阅者都会立即收到消息,可能导致网络带宽和服务器资源的浪费,特别是在订阅者数量较多的情况下。
适用场景
- 实时通知:适用于需要实时推送通知的场景,如WebSocket通信、在线聊天等。
- 事件驱动架构:适用于事件驱动的应用程序,如日志收集、监控系统等。
(3)、Redis Streams
实现方式
- 生产者:使用XADD命令将消息添加到流中。
- 消费者:使用XREADGROUP命令从流中读取消息,并通过XACK命令确认消息已被成功处理。未确认的消息会保留在流中,直到其他消费者处理。
优点
- 持久化:Redis Streams是持久化的,即使Redis重启,未处理的消息也不会丢失。
- 消息确认:支持消息确认机制(XACK),确保每个消息只会被成功处理一次,即使消费者崩溃也能恢复。
- 消费者组:支持消费者组(Consumer Group),允许多个消费者共享同一个流,并自动分配消息,确保每个消息只被一个消费者处理。
- 历史消息:可以保留历史消息,方便调试和审计。还可以通过XRANGE命令查询历史消息。
- 批量处理:支持一次性读取多个消息(COUNT参数),提高处理效率。
- 超时处理:可以设置消息的最大处理时间(NOACK选项),如果消费者在规定时间内没有确认消息,Redis会将消息重新分配给其他消费者。
缺点
- 复杂度较高:相比List和Pub/Sub,Streams的实现和配置稍微复杂一些,特别是涉及到消费者组和消息确认机制。
- 性能开销:由于提供了更多的功能和保障,Redis Streams的性能开销相对较大,特别是在高并发场景下。
适用场景
- 可靠的消息队列:适用于需要高可靠性和持久化的消息队列场景,如任务调度、分布式事务等。
- 分布式系统:适用于分布式系统中的任务分发和协调,确保每个任务只被一个节点处理。
- 日志和事件流:适用于需要记录和分析大量日志或事件流的场景,如日志聚合、监控系统等。
(4)、总结与选择建议
选择建议
- 如果你需要一个简单的、临时性的消息队列,并且对持久性和可靠性要求不高,可以选择Redis List。它实现简单,适合快速开发和小规模应用。
- 如果你需要实现实时通信,并且消息的持久性和可靠性不是主要关注点,可以选择Redis Pub/Sub。它非常适合用于通知系统、聊天应用等实时场景。
- 如果你需要一个高可靠、持久化的消息队列,并且希望支持复杂的分布式场景(如任务调度、日志聚合等),Redis Streams 是最佳选择。它提供了丰富的功能和保障,适合构建大型、复杂的分布式系统。