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

相关推荐
赵渝强老师14 分钟前
【赵渝强老师】使用Oracle可传输的表空间迁移数据
数据库·oracle
IpdataCloud1 小时前
跨区服玩家延迟高怎么办?用IP离线库自动分配到最近服务器
数据库·tcp/ip·github·php·ip
wuqingshun3141592 小时前
MYSQL默认的事务隔离级别是什么?为什么选择这个级别?
数据库·mysql
空中湖3 小时前
电池简史(五):极限之梦与终章——锂硫、锂空气、量子电池,以及你该知道的电池投资与生活指南
数据库·程序人生
蓝胖子酱4 小时前
在 Electron 中用 electron-store 实现加密存储与自定义格式文件
前端·javascript·数据库
snow@li5 小时前
Redis:数据库语法速查表 / 全景梳理
数据库·redis·缓存
Devlive 开源社区5 小时前
Nebula 1.6.0 发布:跨区域桶自动路由、秒传、文本/PDF 预览一次到位
数据库·sql
这个DBA有点耶5 小时前
导个数据中文全变问号?字符集这事儿真没那么简单
数据库·mysql·代码规范
Alluxio5 小时前
Alluxio + Anyscale Ray框架,实现跨区域训练数据读取速度20倍提升
人工智能·分布式·机器学习·缓存·ai
wuqingshun3141595 小时前
MYSQL中如果发生死锁应该如何解决?
数据库·mysql