Redis也能做消息队列!Spring Boot实战:从List到Stream的优雅实现

0 阅读6分钟

在微服务架构中,消息队列是系统解耦、削峰填谷的核心组件。但引入Kafka、RocketMQ等重型中间件往往意味着额外的运维成本和复杂度。如果你的业务量级适中,Redis完全可以承担起消息队列的职责,而且Spring Boot提供了优雅的集成方式。

本文将带你深入Redis实现消息队列的三种方式:List、Pub/Sub、Stream,从原理到Spring Boot实战,从坑点到最佳实践,帮你选型并落地。无需额外运维,轻松实现消息队列功能!


为什么选择Redis做消息队列?

在微服务架构中,消息队列的作用不言而喻:异步处理、流量削峰、系统解耦。但引入专业MQ(如Kafka、RabbitMQ)也带来了运维成本、学习曲线和资源开销。

而Redis作为业务系统标配的中间件,如果能在其基础上实现消息队列,无疑可以降低系统复杂度。Redis 5.0推出的Stream数据结构,更是让Redis在消息队列领域的成熟度大大提升。

适用场景

  • ✅ 异步任务(如发送邮件、短信)
  • ✅ 订单超时关闭(延迟队列)
  • ✅ 日志收集
  • ✅ 内部系统解耦(QPS < 1万)

Redis消息队列的三种实现方式(Spring Boot实战)

Redis实现消息队列经历了三个阶段:List → Pub/Sub → Stream。每种方式各有优劣,下面逐一剖析。


1. List:生产者-消费者模式(Spring Boot实现)

原理

利用Redis的List数据结构,生产者使用LPUSH将消息推入队列,消费者使用RPOP(或BRPOP)从队列中弹出消息。

Spring Boot实现

@Component
public class ListQueueProducer {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    public ListQueueProducer(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    public void send(String message) {
        redisTemplate.opsForList().leftPush("order_queue", message);
    }
}
@Component
public class ListQueueConsumer {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    public ListQueueConsumer(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @Scheduled(fixedDelay = 500)
    public void receive() {
        String message = redisTemplate.opsForList().rightPop("order_queue");
        if (message != null) {
            // 处理消息
            System.out.println("收到消息:" + message);
        }
    }
}

优缺点

优点缺点
✅ 实现简单❌ 消息丢失风险高(无ACK机制)
✅ 低内存占用❌ 不支持消费组,单消费者
✅ 低延迟❌ 队列阻塞时CPU消耗高

适用场景

简单异步任务,允许少量消息丢失,且无需广播的场景。


2. Pub/Sub:广播模式(Spring Boot实现)

原理

Pub/Sub是Redis的发布订阅模型。生产者将消息发布到某个频道,所有订阅该频道的消费者都会收到消息。

Spring Boot实现

@Component
public class PubSubProducer {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    public PubSubProducer(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    public void send(String message) {
        redisTemplate.convertAndSend("order_channel", message);
    }
}
@Component
public class PubSubConsumer {
    
    @RedisListener(channels = "order_channel")
    public void receive(String message) {
        // 处理消息
        System.out.println("收到消息:" + message);
    }
}

优缺点

优点缺点
✅ 实时性强❌ 无消息持久化(消费者不在线则丢失)
✅ 实现简单❌ 无法回溯历史消息
✅ 低内存占用❌ 无法保证消息可靠性

适用场景

实时通知、广播场景,对可靠性要求不高的场景。


3. Stream:真正可靠的消息队列(Redis 5.0+,Spring Boot实现)

原理

Stream是Redis 5.0引入的数据结构,专为消息队列设计,提供了持久化、消费组、消息确认、消息回溯等特性。

Spring Boot实现

@Component
public class StreamProducer {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    public StreamProducer(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    public String send(Map<String, String> fields) {
        // * 表示由Redis自动生成消息ID
        return redisTemplate.opsForStream().add("order_stream", fields);
    }
}
@Component
public class StreamConsumer {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    public StreamConsumer(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    @PostConstruct
    public void init() {
        // 确保消费组存在(从头开始消费)
        try {
            redisTemplate.opsForStream().createGroup("order_stream", "group1", StreamOffset.fromStart());
        } catch (Exception e) {
            // 消费组已存在时忽略
        }
    }
    
    @Scheduled(fixedDelay = 500)
    public void consume() {
        // 从消费组中读取消息
        List<StreamMessage<String, String>> messages = redisTemplate.opsForStream().read(
            Consumer.from("group1", "consumer1"),
            StreamReadOptions.empty().count(10).block(Duration.ofMillis(0)),
            StreamOffset.create("order_stream", StreamOffset.FromStart.START)
        );
        
        if (messages != null && !messages.isEmpty()) {
            for (StreamMessage<String, String> message : messages) {
                Map<String, String> fields = message.getValue();
                String messageId = message.getId();
                
                // 处理消息
                System.out.println("收到消息:" + fields);
                
                // 确认消息(防止丢失)
                redisTemplate.opsForStream().acknowledge("order_stream", "group1", messageId);
            }
        }
    }
}

优缺点

优点缺点
✅ 消息持久化❌ 内存占用较高(需定期修剪)
✅ 消息确认(XACK)❌ 需要处理Pending队列
✅ 支持消费组❌ 复杂度中高
✅ 消息回溯能力

适用场景

需要可靠消息、消费组、消息回溯的业务,如订单处理、任务调度等。


三种方案对比

特性ListPub/SubStream
消息持久化
消息确认
消费组
消息回溯
内存占用较高(需定期修剪)
复杂度中高
推荐场景简单异步任务实时广播可靠消息、复杂业务

生产实践:使用Stream的完整案例

场景:订单超时自动关闭

假设我们需要在订单创建30分钟后自动关闭未支付的订单。

1. 添加延时消息

// 生产者:添加延时消息
Map<String, String> msg = new HashMap<>();
msg.put("order_id", "123");
msg.put("execute_time", String.valueOf(System.currentTimeMillis() + 30 * 60 * 1000));
streamProducer.send(msg);

2. 消费者处理逻辑

@Scheduled(fixedDelay = 5000)
public void processDelayedMessages() {
    List<StreamMessage<String, String>> messages = redisTemplate.opsForStream().read(
        Consumer.from("delay_group", "delay_consumer"),
        StreamReadOptions.empty().count(10).block(Duration.ofMillis(0)),
        StreamOffset.create("order_delay_stream", StreamOffset.FromStart.START)
    );
    
    if (messages != null && !messages.isEmpty()) {
        for (StreamMessage<String, String> message : messages) {
            Map<String, String> fields = message.getValue();
            long executeTime = Long.parseLong(fields.get("execute_time"));
            
            if (System.currentTimeMillis() >= executeTime) {
                // 执行超时关闭
                closeOrder(fields.get("order_id"));
                // 确认消息
                redisTemplate.opsForStream().acknowledge("order_delay_stream", "delay_group", message.getId());
            } else {
                // 未到时间,重新放回队列
                redisTemplate.opsForStream().add("order_delay_stream", fields);
                redisTemplate.opsForStream().acknowledge("order_delay_stream", "delay_group", message.getId());
                redisTemplate.opsForStream().delete("order_delay_stream", message.getId());
            }
        }
    }
}

3. 监控Pending队列(防止消息堆积)

@Scheduled(fixedDelay = 60000)
public void checkPendingMessages() {
    List<StreamPendingEntry> pending = redisTemplate.opsForStream().pending("order_stream", "group1");
    
    if (pending != null && !pending.isEmpty()) {
        // 将超时未处理的消息分配给其他消费者
        for (StreamPendingEntry entry : pending) {
            redisTemplate.opsForStream().claim("order_stream", "group1", "new_consumer", 60000, entry.getId());
        }
    }
}

总结与选型建议

Redis消息队列的选择,取决于你的业务需求:

场景推荐方案说明
简单异步任务List实现最简单,但丢失消息风险高
实时广播Pub/Sub不支持持久化,适合对可靠性要求不高的场景
可靠消息、复杂业务Stream支持持久化、ACK、消费组,是生产环境首选

重要提醒

  1. 消息幂等性:无论哪种方案,都要考虑消息幂等性(因为可能重复消费)。
  2. 监控:监控队列长度、消费者健康状态,防止积压。
  3. 内存管理:Stream会持久化所有消息,需根据业务设置消息最大长度或定期修剪。
  4. 高吞吐量:对于极高吞吐量(QPS > 1万)的场景,仍建议使用专业MQ(如Kafka)。

结语

Redis作为消息队列,凭借其轻量级、低延迟和简单运维的优势,已经成为众多中小型项目的首选方案。Spring Boot的优雅集成让Redis消息队列的实现变得如此简单。

"不要为了解决一个问题而引入另一个问题。"
用好Redis,让消息队列不再成为你的负担。

希望本文能帮助你快速上手并避开常见坑点。如果这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多开发者受益!

关注我,持续输出后端干货,一起成长!