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

相关推荐
FL4m3Y4n13 分钟前
MySQL缓存策略
数据库·mysql·缓存
wsx_iot14 分钟前
TDengine学习
数据库·学习·tdengine
不吃香菜的小趴菜34 分钟前
mysql数据库打包与导入
数据库·mysql
野犬寒鸦36 分钟前
Redis复习记录day1
服务器·开发语言·数据库·redis·缓存
njidf37 分钟前
实战:用Python开发一个简单的区块链
jvm·数据库·python
Rick199343 分钟前
慢SQL优化
数据库·python·sql
IvorySQL1 小时前
速抢!HOW 2026 早鸟票最后 1 天,解锁开源数据库盛会全亮点
数据库·postgresql·开源
IvorySQL1 小时前
PostgreSQL 技术日报 (3月30日)|内核补丁密集更新,REPACK 并发与索引预读取进展
数据库·postgresql·开源
Nyarlathotep01131 小时前
Redis的内存回收和对象共享
redis·后端
NineData1 小时前
从业务库到实时分析库,NineData构建MySQL到SelectDB同步链路
数据库·mysql·程序员