详解Redis消息队列的三种实现方案

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

4.https://www.cnblogs.com/ljbguanli/p/19406684

相关推荐
流星白龙7 小时前
【Redis】2.Redis重大版本
数据库·redis·junit
流星白龙7 小时前
【Redis】7.Hash表
数据库·redis·哈希算法
流星白龙8 小时前
【Redis】4.基本全局命令
数据库·redis·缓存
流星白龙11 小时前
【Redis】3.Redis安装与命令行客户端
数据库·redis·缓存
Season45015 小时前
Redis命令 (generic即通用命令)
数据库·redis·bootstrap
我不想名字重复16 小时前
redis缓存和数据库数据保持一致
数据库·redis·缓存
流星白龙19 小时前
【Redis】6.计数其他命令
数据库·redis·缓存
流星白龙19 小时前
【Redis】8.List列表
redis
NWU_白杨19 小时前
三种常用的数据存储技术
数据库·redis·mysql·sqlite
我叫黑大帅19 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试