如何使用 Redis实现一个简易消息队列?

Redis 也能做消息队列?没错,RabbitMQ、Kafka 太重的时候,可以考虑用 Redis 临时顶一下。

一、List 实现

Redis 的 List 本质就是个队列,用 LPUSH + RPOP 或者 RPUSH + LPOP 就能实现先进先出的消息队列。

java 复制代码
// 生产者:发消息
stringRedisTemplate.opsForList().leftPush("queue:order", orderId);

// 消费者:取消息
String orderId = stringRedisTemplate.opsForList().rightPop("queue:order");

简单场景够用了,但有个问题:消费者一直 pop 没数据会空转,浪费 CPU。

解决方法是改用 BRPOP,阻塞等待有数据再返回:

java 复制代码
// 取不到就等着,超时返回 null
String orderId = stringRedisTemplate.opsForList().rightPop("queue:order", 
    Duration.ofSeconds(30));

优点:简单,Redis 自带,不需要额外依赖

缺点:不支持消息确认、消息广播,一个消息只能被一个消费者消费

二、Pub/Sub 发布订阅

想一对多广播?用 PUBLISHSUBSCRIBE

java 复制代码
// 生产者:发布消息到频道
stringRedisTemplate.convertAndSend("channel:notice", "用户{0}已注册", userId);

// 消费者:订阅频道
@PostConstruct
public void init() {
    stringRedisTemplate.subscribe((message, channel) -> {
        System.out.println("收到通知:" + new String(message.getBody()));
    }, "channel:notice".getBytes());
}

多个消费者订阅同一个频道,都能收到消息。

问题在于:Pub/Sub 是"发完就不管"的模式。消费者挂了没收到的消息就丢了,不支持消息持久化。所以它适合那种丢了也无所谓的场景,比如实时日志。

三、Stream 更靠谱的方案

Redis 5.0 引入的 Stream 才是正经做消息队列的选择,支持持久化、消息确认、消费组。

3.1 基本操作

java 复制代码
// 生产者:发送消息
stringRedisTemplate.opsForStream().add(
    new Record<String, String>("queue:stream", Map.of(
        "orderId", "1001",
        "amount", "299"
    ))
);

// 消费者:读取消息
List<MapRecord<String, String, String>> records = 
    stringRedisTemplate.opsForStream().read(
        Consumer.from("group1", "consumer1"),
        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(30)),
        StreamOffset.create("queue:stream", ReadOffset.last())
    );

for (MapRecord<String, String, String> record : records) {
    System.out.println("处理消息:" + record.getValue());
    // 确认消息已处理
    stringRedisTemplate.opsForStream().acknowledge("queue:stream", "group1", record.getId());
}

3.2 消费组

消费组是 Stream 的精髓:消息可以被组内多个消费者负载均衡地处理,且每个消息只会投递给组内一个消费者。

java 复制代码
// 1. 创建消费组(从最新消息开始消费)
stringRedisTemplate.opsForStream().createGroup("queue:stream", 
    StreamOffset.fromStart("queue:stream"), "group1");

// 2. 消费者各自读消息
// 消费者A读
List<MapRecord<String, String, String>> recordsA = streamOps.read(
    Consumer.from("group1", "consumerA"),
    StreamReadOptions.empty().count(1),
    StreamOffset.create("queue:stream", ReadOffset.last())
);

// 消费者B读(Stream 会自动分配,不会重复)
List<MapRecord<String, String, String>> recordsB = streamOps.read(
    Consumer.from("group1", "consumerB"),
    StreamReadOptions.empty().count(1),
    StreamOffset.create("queue:stream", ReadOffset.last())
);

3.3 消息堆积怎么办

消费者的处理速度跟不上生产速度时,消息会堆积在 Stream 里。

java 复制代码
// 定期检查堆积情况
Long len = stringRedisTemplate.opsForStream().size("queue:stream");
if (len > 10000) {
    // 告警通知
    alert("消息堆积超过1万,pending列表有" + pendingLen + "条");
}

// 查看 Pending 列表(已投递但未确认的消息)
List<MapRecord<String, String, String>> pending = streamOps.readPending(
    "queue:stream", "group1", 0, 100, false
);

可以加个定时任务,把超时的消息重新投递给其他消费者处理:

java 复制代码
// XPENDING 查看超时未处理的消息,重新投递
stringRedisTemplate.opsForStream().claim("queue:stream", "group1", 
    "consumerB", Duration.ofMinutes(5), StreamRecordId.last());

四、怎么选

方案 适用场景
List 轻量级队列,不要求消息可靠,只求异步解耦
Pub/Sub 实时广播,比如通知、聊天消息
Stream 正式项目,需要消息确认、消费组、消息堆积处理

一般情况我推荐直接上 Stream,API 也不复杂,关键是可靠性有保障。

Stream 唯一的遗憾是 Java 客户端用起来有点啰嗦,用 Redisson 封装一层会省心很多。

相关推荐
像风一样自由202013 分钟前
29.Redis在大模型应用中有哪些用途缓存会话与限流
数据库·人工智能·redis·缓存·大模型·milvus·智能体
robottt6661 小时前
2026国产协作机器人排名与选型指南:越疆、遨博、节卡、珞石解析
大数据·数据库·人工智能
郑州光合科技余经理1 小时前
本地生活系统:多业务订单字段怎么分账本导出
java·开发语言·前端·数据库·uni-app·php·ai编程
滕州市燕猫虎计算机科技工作室个体工商户2 小时前
RabbitMQ和RocketMQ
消息队列·mq
weixin_460443564 小时前
考试监控大屏为什么不能直接查数据库?WebSocket、Redis与实时数据聚合架构设计
数据库·redis·在线考试系统·培训考试系统·煤矿培训考试系统·煤矿在线考试系统·宏远培训考试系统
风哥2号4 小时前
数据库教程FGMT02‑生产环境Linux+Oracle19c安装配置与项目实战
linux·数据库·ffmpeg
九皇叔叔8 小时前
【第二章】Redis基础入门:Redis是什么、为什么快以及核心特性详解
数据库·redis·缓存
云和恩墨9 小时前
删库不跑路,回滚极速触达——zData S“旁路日志”持续保护原理深剖
数据库
MetaLite9 小时前
SpringBoot接口分层规范-外网网关内部服务与参数边界
java·数据库·spring boot
Wang's Blog11 小时前
Java框架快速入门: Spring Security+OAuth2之元注解简化权限表达式
java·数据库·spring