详解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

相关推荐
源代码•宸5 小时前
goframe框架签到系统项目开发(每日签到添加积分和积分记录、获取当月最大连续签到天数、发放连续签到奖励积分、实现签到日历详情接口)
数据库·经验分享·redis·中间件·golang·dao·goframe
斯普信云原生组5 小时前
Linux 平台 Redis Insight 安装卸载与常见问题
linux·运维·redis
小画家~6 小时前
第四十三:redis 查找所有KEY应用方法
数据库·redis·bootstrap
攻心的子乐6 小时前
redis 使用Pipelined 管道命令批量操作 减少网络操作次数
数据库·redis·缓存
蜂蜜黄油呀土豆10 小时前
Redis 数据结构详解:从底层实现到应用场景
数据结构·redis·跳表·zset
a努力。10 小时前
哈罗骑行Java面试被问:Redis的持久化策略对比
java·redis·面试
Yu_iChan10 小时前
苍穹外卖Day6 缓存菜品与缓存套餐功能
redis·缓存
iVictor11 小时前
Redis 7.0 新特性之maxmemory-clients:限制客户端内存总使用量
redis
李少兄1 天前
一文搞懂什么是关系型数据库?什么是非关系型数据库?
数据库·redis·mysql·mongodb·nosql