Redis篇--应用篇2--消息队列(List,pub/sub模式,Streams)

使用Redis实现简单的消息队列是一种常见的做法,Redis提供了多种数据结构和命令来支持这种用法。
最常用的方式是利用Redis的列表(List)数据结构,通过LPUSH(左推入)和RPOP(右弹出)来模拟消息的生产和消费。
此外,还可以使用发布/订阅(Pub/Sub)模式或流(Stream)数据结构来实现更复杂的消息队列。

1、List实现消息队列

第一步:创建队列
指定队列的名称,并为该队列实现生产(添加元素)和消费方法(弹出元素)。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class MessageQueueService {

    private static final String QUEUE_NAME = "message_queue";   // 队列名称

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 生产者:向队列中添加消息
    public void sendMessage(String message) {
        redisTemplate.opsForList().leftPush(QUEUE_NAME, message);
        System.out.println("Message sent: " + message);
    }

    // 消费者:从队列中获取并处理消息
    public String receiveMessage() {
        String message = redisTemplate.opsForList().rightPop(QUEUE_NAME);
        if (message != null) {
            System.out.println("Message received: " + message);
        } else {
            System.out.println("No message in the queue.");
        }
        return message;
    }
}

第二步:创建队列的监听方法
这里简单通过定时任务,5秒钟监听一次消息队列的状态,如果队列中存在数据,则开始根据业务消费数据。

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class MessageConsumer {

    @Autowired
    private MessageQueueService messageQueueService;

    @Scheduled(fixedRate = 5000) // 每5秒检查一次队列
    public void consumeMessages() {
        messageQueueService.receiveMessage();
    }
}

第三步:启用调度任务
可以在主方法或配置类中添加该注解。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling   // 地洞调度任务
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2、Pub/Sub模式实现

第一步:定义通道,实现基本逻辑
如:订阅通道,在指定通道内发布消息等。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class PubSubService implements MessageListener {

    private static final String CHANNEL_NAME = "message_channel";    // 目标主题频道

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 发布消息到指定频道
    public void publishMessage(String message) {
        redisTemplate.convertAndSend(CHANNEL_NAME, message);
        System.out.println("Message published to channel " + CHANNEL_NAME + ": " + message);
    }

    // 订阅频道
    public void subscribe() {
        redisTemplate.getConnectionFactory().getConnection().subscribe(this, CHANNEL_NAME);
        System.out.println("Subscribed to channel " + CHANNEL_NAME);
    }

    // 频道订阅后,此处处理接收到的消息
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        System.out.println("Message received from channel " + channel + ": " + body);
    }
}

3、Streams消息队列

创建服务类来实现消息的发布和消费。

import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.api.sync.RedisStreamCommands;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Service
public class StreamService {

    private static final String STREAM_KEY = "message_stream";
    private static final String GROUP_NAME = "consumer_group";
    private static final String CONSUMER_NAME = "consumer_1";

    private final RedisClient redisClient;
    private StatefulRedisConnection<String, String> connection;
    private RedisCommands<String, String> syncCommands;
    private RedisStreamCommands<String, String> streamCommands;
    private ScheduledExecutorService scheduler;

    @Autowired
    public StreamService(RedisClient redisClient) {
        this.redisClient = redisClient;
    }

    @PostConstruct
    public void init() {
        connection = redisClient.connect();
        syncCommands = connection.sync();
        streamCommands = syncCommands;

        // 创建消费者组(如果不存在)
        if (!streamCommands.xinfoGroups(STREAM_KEY).stream().anyMatch(group -> group.get(GROUP_NAME) != null)) {
            streamCommands.xgroupCreate(STREAM_KEY, GROUP_NAME, "0");
        }

        // 启动消费者线程
        scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(this::consumeMessages, 0, 1, TimeUnit.SECONDS);
    }

    // 发布消息到流
    public void publishMessage(String message) {
        Map<String, String> entry = new HashMap<>();
        entry.put("message", message);
        streamCommands.xadd(STREAM_KEY, "*", entry);
        System.out.println("Message published to stream: " + message);
    }

    // 消费消息
    private void consumeMessages() {
        try {
            // 从流中读取消息(阻塞式等待新消息)
            var messages = streamCommands.xreadGroup(GROUP_NAME, CONSUMER_NAME,
                    io.lettuce.core.XReadArgs.StreamOffset.lastConsumed(STREAM_KEY));

            if (messages != null && !messages.isEmpty()) {
                for (var message : messages.get(0).getMessages()) {
                    String msgId = message.getId();
                    String content = message.getFields().get("message");

                    System.out.println("Message received from stream: " + content);

                    // 处理消息后确认
                    streamCommands.xack(STREAM_KEY, GROUP_NAME, msgId);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 关闭资源
    public void shutdown() {
        if (scheduler != null && !scheduler.isShutdown()) {
            scheduler.shutdown();
        }
        if (connection != null && connection.isOpen()) {
            connection.close();
        }
    }
}

4、三种方式对比

(1)、Redis List

实现方式

  • 生产者:使用LPUSH命令将消息推入列表的左端。
  • 消费者:使用RPOP命令从列表的右端弹出消息。为了实现阻塞式消费,可以使用BRPOP命令。

优点

  • 简单易用:实现非常简单,适合快速构建基本的消息队列。
  • 持久化:默认情况下,Redis数据是持久化的,因此即使Redis重启,未处理的消息也不会丢失。
  • 负载均衡:可以通过多个消费者同时从列表中弹出消息来实现简单的负载均衡(但需要额外逻辑确保每个消息只被处理一次)。

缺点

  • 缺乏可靠性:如果消费者在处理消息时失败或崩溃,消息可能会丢失(除非你实现额外的确认机制)。
  • 无消息确认:Redis List没有内置的消息确认机制,无法保证消息一定会被成功处理。
  • 顺序问题:虽然LPUSH和RPOP保证了FIFO(先进先出)顺序,但在高并发情况下可能会出现消息顺序不一致的问题。

适用场景

  • 简单任务队列:适用于不需要复杂功能的简单任务分发场景。
  • 临时性消息传递:适用于对消息持久性和可靠性要求不高的场景。

(2)、Redis Pub/Sub

实现方式

  • 发布者:使用PUBLISH命令向指定频道发布消息。
  • 订阅者:使用SUBSCRIBE命令订阅一个或多个频道,并接收发布到这些频道的消息。

优点

  • 实时通信:非常适合用于实时通信场景,如通知系统、聊天应用等。
  • 广播模式:所有订阅同一频道的客户端都会收到相同的消息,适合广播式消息传递。
  • 轻量级:实现简单,开销较小,适合低延迟的场景。

缺点

  • 不支持持久化:一旦消息被发布出去,如果没有订阅者在线,消息就会丢失。Redis Pub/Sub本身不提供消息持久化功能。
  • 无消息确认:无法保证消息一定会被所有订阅者成功接收,也没有消息确认机制。
  • 不适合高吞吐量:由于每次发布消息时所有订阅者都会立即收到消息,可能导致网络带宽和服务器资源的浪费,特别是在订阅者数量较多的情况下。

适用场景

  • 实时通知:适用于需要实时推送通知的场景,如WebSocket通信、在线聊天等。
  • 事件驱动架构:适用于事件驱动的应用程序,如日志收集、监控系统等。

(3)、Redis Streams

实现方式

  • 生产者:使用XADD命令将消息添加到流中。
  • 消费者:使用XREADGROUP命令从流中读取消息,并通过XACK命令确认消息已被成功处理。未确认的消息会保留在流中,直到其他消费者处理。

优点

  • 持久化:Redis Streams是持久化的,即使Redis重启,未处理的消息也不会丢失。
  • 消息确认:支持消息确认机制(XACK),确保每个消息只会被成功处理一次,即使消费者崩溃也能恢复。
  • 消费者组:支持消费者组(Consumer Group),允许多个消费者共享同一个流,并自动分配消息,确保每个消息只被一个消费者处理。
  • 历史消息:可以保留历史消息,方便调试和审计。还可以通过XRANGE命令查询历史消息。
  • 批量处理:支持一次性读取多个消息(COUNT参数),提高处理效率。
  • 超时处理:可以设置消息的最大处理时间(NOACK选项),如果消费者在规定时间内没有确认消息,Redis会将消息重新分配给其他消费者。

缺点

  • 复杂度较高:相比List和Pub/Sub,Streams的实现和配置稍微复杂一些,特别是涉及到消费者组和消息确认机制。
  • 性能开销:由于提供了更多的功能和保障,Redis Streams的性能开销相对较大,特别是在高并发场景下。

适用场景

  • 可靠的消息队列:适用于需要高可靠性和持久化的消息队列场景,如任务调度、分布式事务等。
  • 分布式系统:适用于分布式系统中的任务分发和协调,确保每个任务只被一个节点处理。
  • 日志和事件流:适用于需要记录和分析大量日志或事件流的场景,如日志聚合、监控系统等。

(4)、总结与选择建议

在这里插入图片描述
选择建议

  • 如果你需要一个简单的、临时性的消息队列,并且对持久性和可靠性要求不高,可以选择Redis List。它实现简单,适合快速开发和小规模应用。
  • 如果你需要实现实时通信,并且消息的持久性和可靠性不是主要关注点,可以选择Redis Pub/Sub。它非常适合用于通知系统、聊天应用等实时场景。
  • 如果你需要一个高可靠、持久化的消息队列,并且希望支持复杂的分布式场景(如任务调度、日志聚合等),Redis Streams 是最佳选择。它提供了丰富的功能和保障,适合构建大型、复杂的分布式系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值