1.概述
Redis作为一种高效的内存数据库,提供了多种消息队列实现方案来满足不同场景的需求。本文将详细介绍Redis三种消息队列方案:List、Pub/Sub和Streams,并分析其差异,同时通过JAVA代码进行消费案例实现,帮助大家更好的理解和选择。
2.Redis消息队列实现方案
2.1 List消息队列
2.1.1 原理

Redis可以通过lpush/rpop、rpush/lpop来实现简单的消息队列,List本身是一种双向链表结构。通过这种方式实现起来比较简单,但存在一个致命风险:消费者需要不断地轮询RPOP或LPOP命令以获取新消息,会增加不必要的CPU资源占用。为了解决这个问题,Redis提供了BLPOP和BRPOP命令,它是一种阻塞式的读取方式。 LPOP 命令如果指定的弹出列表不存在或者为空,则直接返回,而 BLPOP 命令如果弹出列表不存在或者为空,则会一直等待,直到超时返回或队列中被插入新元素,再弹出返回。这种方式避免了消费者程序的无谓轮询,节省了CPU资源。
2.1.2 JAVA代码实现
java
/**
* @Description:
* @Author: ChengLiang
* @Date: 2025/9/4 16:05
*/
@Slf4j
public class ListHandleThread implements Runnable{
private NameableExecutor nameableExecutor;
private static final String LIST_KEY="list#key";
//线程中断flag
private boolean isInterrupted = false;
public ListHandleThread () {
this.nameableExecutor = ExecutorUtils.create("list-consumer", 1, 20);
}
@Override
public void run() {
while (!isInterrupted) {
try {
//从redis获取消费数据,绑定accountId与businessCode
String message = (String) RedisUtils.rPop(LIST_KEY);
if (StringUtils.isBlank(message)) {
try {
Thread.sleep(1000);
log.debug("未获取到缓存中数据");
} catch (InterruptedException e) {
log.error("线程休眠发生异常:", e);
}
continue;
}
log.info("获取list消费数据入参{}", message);
nameableExecutor.execute(() -> {
// todo 处理消息逻辑
});
} catch (Exception e) {
log.error("List数据消费异常:", e);
}
}
}
}
消费者开启子线程不断轮询队列,获取队列中的数据,获取完成后放入线程池中处理,这是目前List作为消息队列的通常用法。这种方法的缺点在于:占用较多的CPU资源,且当redis连接异常时,会产生大量的error日志,瞬间造成磁盘压力巨大,可能导致服务器宕机风险。
2.2 Pub/Sub消息队列
2.2.1 原理

Pub/Sub(发布/订阅)功能提供了一种基于channel的消息通信机制,发布者将消息发送到指定的channel,而订阅者则从channel接收并处理这些消息。这些特性使得Pub/Sub非常适合用于实现广播型消息系统,如实时聊天、实时推送、实时广播等场景。
Pub/Sub不支持消息的持久化,一旦Redis宕机重启,宕机前未被处理的消息将会全部丢失。并且Pub/Sub的消息是单向的,订阅者无法向生产者确认消息状态,这是 Pub/Sub 最大的问题,如果订阅者离线,或者处理消息的速度跟不上发布者的速度,那么消息就会丢失。由于Pub/Sub的消息处理是基于内存的,如果消息量过大,可能会消耗大量的内存资源。
2.2.2 代码实现
1.配置Redis序列化方式和连接工厂
java
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.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 设置键的序列化器为StringRedisSerializer,使用UTF-8编码
redisTemplate.setKeySerializer(new StringRedisSerializer(StandardCharsets.UTF_8));
redisTemplate.setValueSerializer(new StringRedisSerializer(StandardCharsets.UTF_8));
// 设置哈希键的序列化器
redisTemplate.setHashKeySerializer(new StringRedisSerializer(StandardCharsets.UTF_8));
// 设置哈希值的序列化器
redisTemplate.setHashValueSerializer(new StringRedisSerializer(StandardCharsets.UTF_8));
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
return container;
}
}
2.创建消息监听器
java
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
@Component
public class RedisMessageSubscriber implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
System.out.println("Received message: " + new String(message.getBody()));
}
}
3.注册监听器
java
import com.eckey.lab.service.util.RedisMessageSubscriber;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisListenerConfig {
@Autowired
private RedisMessageSubscriber redisMessageSubscriber;
private static final String CHANNEL_TOPIC = "channel";
@Bean
public ApplicationListener<ContextRefreshedEvent> contextRefreshedEventApplicationListener() {
return new ApplicationListener<ContextRefreshedEvent>() {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
RedisMessageListenerContainer container = event.getApplicationContext()
.getBean(RedisMessageListenerContainer.class);
container.addMessageListener(redisMessageSubscriber, new ChannelTopic(CHANNEL_TOPIC));
}
};
}
}
4.消息发送
java
@Slf4j
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisUtil redisUtil;
private static final String Channel = "channel";
@PostMapping("/publish")
public Response publishMessage(@RequestParam String message) {
log.info("message:{}", message);
redisUtil.pubToChannel(Channel, message);
return Response.builder().code(Constants.ResponseCode.SUCCESS.getCode()).info(Constants.ResponseCode.SUCCESS.getInfo()).build();
}
}
5.测试结果
请求数据:

日志打印结果:

上述测试代码验证了Redis通过Pub/Sub(发布订阅)方式实现消息发送和接收,实际上也可采用@RedisListener实现指定channel消息监听,大家也可去尝试下。
2.3 Stream消息队列
2.3.1 原理
Redis 5.0版本开始引入了Stream类型,该类型专为消息队列设计,提供一种丰富的消息队列操作命令,包括消息的插入、更新、读取和删除等。 Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

基于stream实现的消息队列主要有4个角色:
stream: 一个流是一个按时间排序的日志,可以不断地追加新的消息。
last delivered ID: 每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标last_delivered_id 往前移动。
consumer group :一个消费者组包含多个消费者,共同维护一个消息队列。共同维护一个消息队列。队列中的消费者会被不同的消费者消费,一个消息只会被一个消费者消费,提高消息处理的速度。同时消费者组会维护一个标识,这个标识始终指向最后一个被处理的消息,即使消费者宕机重启,再次读取还是从标识处读取消息,保证队列中的每个消息至少被消费一遍。
pending entries list (PEL):消费者的状态变量,消费者获取到消息后,就被存入pending_list集合,这个集合是专门维护已经执行但还未确认的信息。当消息被处理后,通过XACK来完成确认后,才从pending_list中移除。如果执行过程中发生异常被捕获,线程就会开始去读取pending_list中的信息,直到pending_list中的信息为空,线程再返回去读取消息队列中的数据去执行。
2.3.2 代码实现
StreamConfig配置类文件,主要是配置Redis stream连接配置、订阅主题等。
java
import com.eckey.lab.service.util.stream.StreamMessageListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.data.redis.stream.Subscription;
import java.time.Duration;
@Slf4j
@Configuration
public class RedisStreamConfig {
@Autowired
private StreamMessageListener listenerMessage;
@Value("${stream.key}")
private String streamKey;
@Value("${group.key}")
private String groupKey;
@Bean
public StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ?> streamMessageListenerContainerOptions() {
return StreamMessageListenerContainer
.StreamMessageListenerContainerOptions
.builder()
.pollTimeout(Duration.ofSeconds(1))
.build();
}
@Bean
public StreamMessageListenerContainer streamMessageListenerContainer(RedisConnectionFactory factory,
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ?> streamMessageListenerContainerOptions) {
StreamMessageListenerContainer listenerContainer = StreamMessageListenerContainer.create(factory,
streamMessageListenerContainerOptions);
try {
factory.getConnection()
.xGroupCreate(streamKey.getBytes(), streamKey, ReadOffset.from("0-0"), true);
} catch (RedisSystemException exception) {
log.warn(exception.getCause().getMessage());
}
listenerContainer.start();
return listenerContainer;
}
/**
* 订阅者,消费组group1,收到消息后自动确认
*
* @param streamMessageListenerContainer
* @return
*/
@Bean
public Subscription subscription(StreamMessageListenerContainer streamMessageListenerContainer) {
Subscription subscription = streamMessageListenerContainer.receiveAutoAck(
Consumer.from(groupKey, "name1"),
StreamOffset.create(streamKey, ReadOffset.lastConsumed()),
listenerMessage
);
return subscription;
}
}
Redis监听者代码如下,实现StreamListener接口:
java
import com.eckey.lab.service.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class StreamMessageListener implements StreamListener<String, MapRecord<String, String, String>> {
@Autowired
private RedisUtil redisUtil;
@Override
public void onMessage(MapRecord<String, String, String> message) {
log.info("message id:{},stream:{},body:{}", message.getId(), message.getStream(), message.getValue());
redisUtil.ack(message.getStream(), "group1", String.valueOf(message.getId()));
}
}
RedisUtil工具类补充部分如下,具体代码可查看前几篇博客:
java
public String streamCreateGroup(String key, String group){
return redisTemplate.opsForStream().createGroup(key, group);
}
/**
* 消费组信息
* @param key
* @param group
* @return
*/
public StreamInfo.XInfoConsumers streamConsumers(String key, String group){
return redisTemplate.opsForStream().consumers(key, group);
}
/**
* 确认已消费
* @param key
* @param group
* @param recordIds
* @return
*/
public Long streamAck(String key, String group, String... recordIds){
return redisTemplate.opsForStream().acknowledge(key, group, recordIds);
}
public String streamAdd(String key, Map<String, Object> content){
return Objects.requireNonNull(redisTemplate.opsForStream().add(key, content)).getValue();
}
/**
* 删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度
* 消息存储在stream的节点下,删除时仅对消息做删除标记,当一个节点下的所有条目都被标记为删除时,销毁节点
* @param key
* @param recordIds
* @return
*/
public Long streamDel(String key, String... recordIds){
return redisTemplate.opsForStream().delete(key, recordIds);
}
/**
* 消息长度
* @param key
* @return
*/
public Long streamLen(String key){
return redisTemplate.opsForStream().size(key);
}
/**
* 从开始读
* @param key
* @return
*/
public List<MapRecord<String, Object, Object>> streamRead(String key){
return redisTemplate.opsForStream().read(StreamOffset.fromStart(key));
}
/**
* 从指定的ID开始读
* @param key
* @param recordId
* @return
*/
public List<MapRecord<String, Object, Object>> streamRead(String key, String recordId){
return redisTemplate.opsForStream().read(StreamOffset.from(MapRecord.create(key, new HashMap<>(1)).withId(RecordId.of(recordId))));
}
发布测试工具类如下:
java
package com.eckey.lab.paymallweb.controller;
import com.eckey.lab.common.constants.Constants;
import com.eckey.lab.common.response.Response;
import com.eckey.lab.service.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisUtil redisUtil;
@Value("${stream.key}")
private String streamKey;
@PostMapping("/stream")
public Response stream(@RequestParam String message) {
log.info("message:{}", message);
Map hashMap = new HashMap();
hashMap.put("message", message);
redisUtil.streamAdd(streamKey,hashMap);
return Response.builder().code(Constants.ResponseCode.SUCCESS.getCode()).info(Constants.ResponseCode.SUCCESS.getInfo()).build();
}
}
测试结果如下:


2.4 三种消息队列对比
| 属性/队列 | List | Pub/Sub | Streams |
|---|---|---|---|
| 阻塞式消费 | 支持 | 支持 | 支持 |
| 发布/订阅 | 不支持 | 支持 | 支持 |
| 重复消费 | 不支持 | 不支持 | 支持 |
| 持久化 | 支持 | 不支持 | 支持 |
| 消息堆积 | 内存持续增长 | 缓冲区溢出,消费者被强制下线 | 可控队列最大长度 |
3.小结
1.Redis Stream提供了一个轻量级、高性能且功能丰富的消息队列实现,解决了使用List作为队列时CPU占用问题、Pub/Sub作为消息队列时无法持久化等问题;
2.Redis Stream虽然提供了持久化能力,但消息主要存储在内存中,适用于消息量较小、对事物要求简单的场景,复杂场景仍需选择专业消息队列(如Kafka、RabbitMQ等);
3.建议在吞吐量小于10w QPS,且对消息的一致性要求不高的场景使用stream。
4.参考文献
1.https://www.runoob.com/redis/redis-stream.html
2.https://www.cnblogs.com/neolshu/p/19120364(stream底层原理)
3.https://medium.com/redis-with-raphael-de-lio/understanding-redis-streams-33aa96ca7206