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

相关推荐
辞旧 lekkk3 小时前
【Qt】信号和槽
linux·开发语言·数据库·qt·学习·mysql·萌新
我是唐青枫4 小时前
终于不用手搓两级缓存了!C#.NET HybridCache 详解:L1 L2、标签失效与防击穿实战
redis·缓存·c#·.net
2301_809204704 小时前
JavaScript中严格模式use-strict对引擎解析的辅助.txt
jvm·数据库·python
zjy277774 小时前
mysql如何选择合适的索引类型_mysql索引设计实战
jvm·数据库·python
笨蛋不要掉眼泪5 小时前
Mysql架构揭秘:update语句的执行流程
数据库·mysql·架构
万邦科技Lafite5 小时前
京东item_get接口实战案例:实时商品价格监控全流程解析
java·开发语言·数据库·python·开放api·淘宝开放平台
秋95 小时前
ruoyi项目更换为mysql9.7.0数据库
数据库
Andya_net6 小时前
MySQL | MySQL 8.0 权限管理实践-精确赋予库、表只读等权限
android·数据库·mysql
筑梦之路7 小时前
harbor数据库报错权限异常如何处理——筑梦之路
数据库·harbor
czlczl200209257 小时前
理解 MySQL 行锁:两阶段锁协议与热点更新优化
数据库·mysql