如何使用 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 封装一层会省心很多。

相关推荐
凌虚4 小时前
面向 MySQL 用户的 PostgreSQL 快速上手指南
数据库·后端·架构
五阿哥永琪4 小时前
MySQL中操作json的函数!
数据库·mysql·json
来者皆善4 小时前
了解Mysql优化吗?如何优化索引?
数据库·mysql
ai_coder_ai5 小时前
分布式缓存架构设计与实践
分布式·缓存
凡尘——雨落凡尘5 小时前
PHP 线上十大隐形故障复盘:90% 的网站卡顿、502、雪崩,都是这些细节导致的
redis·nginx·php·session
赤壁小虾6 小时前
【渲染流水线】[逐片元阶段]-[透明度测试]以UnityURP为例
java·前端·数据库
MC皮蛋侠客6 小时前
SQLAlchemy 系列(七):高级建模与高效写入——批量 DML、方言与扩展
数据库·python
奈斯先生Vector7 小时前
把 Midjourney 二次编辑做成生产系统:customId 能力令牌、Action Graph 与 WebUI 精修工作台
数据库·人工智能·架构·aigc·音视频·midjourney
Lethehong8 小时前
双擎并驱·全链路并行:KFS让TB级异构增量同步秒级到达
数据库
MC皮蛋侠客8 小时前
SQLAlchemy 系列(八):AsyncIO、并发与 Web 生命周期——让每个并发任务持有自己的 Session
数据库·python