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 发布订阅
想一对多广播?用 PUBLISH 和 SUBSCRIBE:
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 封装一层会省心很多。