Redis实现消息队列

前言

Redis可以通过发布订阅模式、轮询机制实现消息队列。

由于没有消息持久化与 ACK 的保证,所以,Redis 的发布订阅功能并不可靠。这也就导致了它的应用场景很有限,建议用于实时与可靠性要求不高的场景。


一、Redis发布订阅

​​​​​​1.Redis发布订阅模式实现原理

服务器中维护着一个pubsub_channels字典,所有的频道和订阅关系都存储在这里。字典的键为频道的名称,而值为订阅频道的客户端链表。

1. 当有新的客户端订阅某个频道时,会发生两种情况中的一种:

(1)如果频道已经存在,则新的客户端会添加到pubsub_channels对应频道的链表末尾

(2)如果频道原本不存在,则会为频道创建一个键,该客户端成为链表的第一个元素

2. 当一个客户端退订一个频道的时候:

pubsub_channels对应键的链表会删除该客户端

3. 发送信息

服务器会遍历pubsub_channels中对应键的链表,向每一个客户端发送信息

服务器还维护着一个pubsub_patterns链表,链表的pattern属性记录了被订阅的模式,而client属性记录了订阅模式的客户端

1. 当有新的客户端订阅某个模式的时,会进行如下步骤:

(1)创建一个链表节点,pattern属性记录订阅的模式,client记录订阅模式的客户端

(2)将这个链表节点添加到pubsub_patterns链表中

2. 当一个客户端退订某一个模式的时候:

服务器遍历pubsob_patterns找到对应的pattern同时也是对应该client客户端的节点,将改节点删除

3. 发送信息

服务器遍历pubsub_channels,查找与channels频道相匹配的模式麻将消息发送给订阅了这些模式的客户端。

2.引入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

3.redis配置

public class RedisConfig {
    //Key的过期时间
    private Duration timeToLive = Duration.ofDays(1);

    /**
     * redis模板,存储关键字是字符串,值jackson2JsonRedisSerializer是序列化后的值
     *
     * @param
     * @return org.springframework.data.redis.core.RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //使用StringRedisSerializer来序列化和反序列化redis的key值
        RedisSerializer redisSerializer = new StringRedisSerializer();
        //key
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        //value
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.
                defaultCacheConfig().
                entryTtl(this.timeToLive).			//Key过期时间 1天
                serializeKeysWith(RedisSerializationContext.SerializationPair.
                fromSerializer(new StringRedisSerializer())).
                serializeValuesWith(RedisSerializationContext.SerializationPair.
                        fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

4.redis监听

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

import java.util.concurrent.CountDownLatch;


@Configuration
public class RedisMessageListener {
    /**
     * 创建连接工厂
     *
     * @param connectionFactory
     * @param listenerAdapter
     * @return
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                   MessageListenerAdapter listenerAdapter,
                                                   MessageListenerAdapter listenerAdapterWang,
                                                   MessageListenerAdapter listenerAdapterTest2) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //(不同的监听器可以收到同一个频道的信息)接受消息的频道
        container.addMessageListener(listenerAdapter, new PatternTopic("phone"));

        container.addMessageListener(listenerAdapterWang, new PatternTopic("phone"));

        container.addMessageListener(listenerAdapterTest2, new PatternTopic("phoneTest2"));
        return container;
    }


    /**
     * 绑定消息监听者和接收监听的方法
     *
     * @param receiver
     * @return
     */
    @Bean
    public MessageListenerAdapter listenerAdapter(ReceiverRedisMessage receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }

    @Bean
    public MessageListenerAdapter listenerAdapterWang(ReceiverRedisMessage receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessageWang");
    }
   /**
     * 绑定消息监听者和接收监听的方法
     *
     * @param receiver
     * @return
     */
    @Bean
    public MessageListenerAdapter listenerAdapterTest2(ReceiverRedisMessage receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage2");
    }

    /**
     * 注册订阅者
     *
     * @param latch
     * @return
     */
    @Bean
    ReceiverRedisMessage receiver(CountDownLatch latch) {
        return new ReceiverRedisMessage(latch);
    }


    /**
     * 计数器,用来控制线程
     *
     * @return
     */
    @Bean
    public CountDownLatch latch() {
        return new CountDownLatch(1);//指定了计数的次数 1
    }
}

5.redis消息接收类

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.concurrent.CountDownLatch;


public class ReceiverRedisMessage {
    private CountDownLatch latch;

    @Autowired
    public ReceiverRedisMessage(CountDownLatch latch) {
        this.latch = latch;
    }


    /**
     * 队列消息接收方法
     *
     * @param jsonMsg
     */
    public void receiveMessage(String jsonMsg) {
        log.info("[开始消费REDIS消息队列phone数据...]");
        try {
            System.out.println(jsonMsg);
            log.info("[消费REDIS消息队列phone数据成功.]");
        } catch (Exception e) {
            log.error("[消费REDIS消息队列phone数据失败,失败信息:{}]", e.getMessage());
        }
        latch.countDown();
    }


    public void receiveMessageWang(String jsonMsg) {
        log.info("[开始消费REDIS消息队列phone数据...]");
        try {
            System.out.println(jsonMsg);
            log.info("[消费REDIS消息队列phone数据成功.]");
        } catch (Exception e) {
            log.error("[消费REDIS消息队列phone数据失败,失败信息:{}]", e.getMessage());
        }
        latch.countDown();
    }

    /**
     * 队列消息接收方法
     *
     * @param jsonMsg
     */
    public void receiveMessage2(String jsonMsg) {
        log.info("[开始消费REDIS消息队列phoneTest2数据...]");
        try {
            System.out.println(jsonMsg);
            /**
             *  此处执行自己代码逻辑 操作数据库等
             */

            log.info("[消费REDIS消息队列phoneTest2数据成功.]");
        } catch (Exception e) {
            log.error("[消费REDIS消息队列phoneTest2数据失败,失败信息:{}]", e.getMessage());
        }
        latch.countDown();
    }

}

6.代码测试

  public void redisTest() {
        //这个是测试同一个频道,不同的订阅者收到相同的信息,“phone”也就是topic也可以理解为频道
        redisTemplate.convertAndSend("phone", "223333");
        //这个phoneTest2是另外的一个频道
      //  redisTemplate.convertAndSend("phoneTest2", "34555665");
    }

二、Redis轮询机制

1.原理

   Redis的列表类型键可以用来实现队列,并且支持阻塞式读取,可以实现一个高性能的优先队列, 在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是4294967295。从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。  

2.生产者

  public R saveUserTicket(String phoneNum) {
        redisTemplate.opsForList().leftPush("ticket:Data", phoneNum);
        return R.ok();
    }

3.消费者

 @Scheduled(fixedRate = 1)
    public synchronized void consumer() {
            String message = redisTemplate.opsForList().rightPop("ticket:Data", 5, TimeUnit.SECONDS);
            if (!StringUtils.isEmpty(message)){
           //数据库操作
            }
    }

4.优化

如上述代码,如果此时队列为空,消费者依然会频繁拉取数据,造成CPU空转,不仅占用CPU资源还对Redis造成压力。因此当队列为空时我们可以休眠一段时间,再进行拉取。

实现如下

     @Scheduled(fixedRate = 1)
    public synchronized void consumer() throws InterruptedException {
        long a = redisTemplate.opsForList().size("ticket:Data");
        if (a == 0) {
            TimeUnit.SECONDS.sleep(1);//等待时间
        }
            String message = redisTemplate.opsForList().rightPop("ticket:Data", 5, TimeUnit.SECONDS);
            if (!StringUtils.isEmpty(message)){
           //数据库操作
            }
    }

<think>我们被要求使用Redis实现消息队列。根据引用[1]和引用[2],Redis提供了三种方式:基于List结构、PubSub(发布订阅)和Stream结构。其中,引用[1]和引用[2]都提到,基于Stream的方式是比较完善的消息队列模型,推荐使用。 下面分别介绍这三种方式: ### 1. 基于List结构 Redis的List是一个双向链表,我们可以利用其`LPUSH`(或`RPUSH`)和`BRPOP`(或`BLPOP`)命令实现消息队列。 - 生产者使用`LPUSH`将消息放入队列的左侧(头部)。 - 消费者使用`BRPOP`从队列的右侧(尾部)阻塞地取出消息。 **优点**: - 实现简单,利用Redis已有的数据结构。 - 支持阻塞等待(使用`BRPOP`),避免消费者轮询。 **缺点**: - 不支持多消费者(一个消息只能被一个消费者消费)。 - 没有消息确认机制,如果消费者处理失败,消息就丢失了。 - 没有消息回溯功能。 #### 示例代码(生产者): ```java Jedis jedis = new Jedis("localhost"); // 向队列list1中放入消息 jedis.lpush("list1", "message1"); ``` #### 示例代码(消费者): ```java Jedis jedis = new Jedis("localhost"); // 阻塞地从队列list1中取出消息,0表示无限等待 List<String> result = jedis.brpop(0, "list1"); String message = result.get(1); ``` ### 2. 基于PubSub(发布订阅) Redis的发布订阅模式允许消息被多个消费者接收(广播)。 - 生产者发布消息到指定频道(channel)。 - 消费者订阅频道,接收消息。 **优点**: - 支持一对多的消息发布。 - 实时性较好。 **缺点**: - 消息是即时的,没有持久化。如果消费者不在线,消息就丢失了。 - 不支持消息的存储,因此无法回溯历史消息。 - 没有消息确认机制。 #### 示例代码(生产者): ```java Jedis jedis = new Jedis("localhost"); // 向频道channel1发布消息 jedis.publish("channel1", "message1"); ``` #### 示例代码(消费者): ```java Jedis jedis = new Jedis("localhost"); // 创建订阅对象 jedis.subscribe(new JedisPubSub() { @Override public void onMessage(String channel, String message) { // 处理消息 System.out.println("Received: " + message); } }, "channel1"); ``` ### 3. 基于Stream(推荐) Redis 5.0引入了Stream数据结构,它提供了更完善的消息队列功能: - 支持消息持久化。 - 支持消费者组(Consumer Group):多个消费者可以组成一个组,共同消费一个队列,每条消息只会被组内的一个消费者消费。 - 提供消息确认机制(ACK),确保消息被成功处理。 - 支持消息回溯。 #### 基本命令: - `XADD`:添加消息到流。 - `XREAD`:读取消息(可以阻塞)。 - `XGROUP`:创建消费者组。 - `XREADGROUP`:通过消费者组读取消息。 - `XACK`:确认消息已被处理。 #### 示例代码(生产者): ```java Jedis jedis = new Jedis("localhost"); // 向名为stream1的流中添加消息,消息包含字段key1和value1 jedis.xadd("stream1", StreamEntryID.NEW_ENTRY, Collections.singletonMap("key1", "value1")); ``` #### 示例代码(消费者组): 首先,创建消费者组: ```bash # 在Redis命令行中创建消费者组 XGROUP CREATE stream1 mygroup 0 ``` 然后,在Java中使用消费者组消费: ```java Jedis jedis = new Jedis("localhost"); // 消费者组mygroup中的消费者consumer1读取消息 Map.Entry<String, StreamEntryID> streamQuery = new AbstractMap.SimpleEntry<>("stream1", StreamEntryID.UNRECEIVED_ENTRY); List<Map.Entry<String, List<StreamEntry>>> result = jedis.xreadGroup("mygroup", "consumer1", 1, 0, // 读取1条消息,0表示阻塞直到有消息 streamQuery); // 处理消息 for (StreamEntry entry : result.get(0).getValue()) { System.out.println("Received: " + entry.getFields()); // 确认消息 jedis.xack("stream1", "mygroup", entry.getID()); } ``` ### 总结 - **List结构**:适用于简单的消息队列,单消费者,无持久化要求。 - **PubSub**:适用于广播消息,实时性要求高,但消息可丢失的场景。 - **Stream**:适用于需要持久化、多消费者组、消息确认等高级功能的场景,是最接近专业消息队列实现[^1][^2]。 在实际应用中,根据需求选择合适的方式。对于需要可靠消息队列的场景,推荐使用Stream结构。 ### 相关问题 1. Redis Stream 如何实现消息的持久化和重放? 2. 在 Redis 中,如何监控消息队列的运行状态? 3. Redis 作为消息队列与专业的消息队列(如 RabbitMQ)相比有哪些优缺点?</think>### Redis 实现消息队列的三种方式 Redis 提供了三种主要方式实现消息队列,根据引用[1][2][3]的内容整理如下: #### 1. 基于 List 结构(双向链表) ```bash # 生产者 LPUSH myqueue "message1" # 左侧插入消息 LPUSH myqueue "message2" # 消费者 BRPOP myqueue 0 # 阻塞式右侧弹出(0=无限等待) ``` **特点**: - 支持阻塞式弹出(`BRPOP`) - 消息持久化存储 - 简单高效,适合基本队列需求 - **缺点**:不支持多消费者组[^1][^2] #### 2. 基于 PubSub(发布订阅) ```bash # 消费者订阅 SUBSCRIBE channel1 # 生产者发布 PUBLISH channel1 "message" ``` **特点**: - 实时广播消息 - 支持多消费者 - **缺点**:消息不持久化(离线消费者丢失消息)[^1][^2] #### 3. 基于 Stream(推荐方式) ```bash # 生产者添加消息 XADD mystream * key1 "value1" key2 "value2" # 消费者组消费 XGROUP CREATE mystream mygroup 0 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream > ``` **特点**: - 消息持久化存储 - 支持消费者组(同组内竞争消费) - 消息确认机制(ACK) - 支持消息回溯 - 提供阻塞读取能力[^1][^2] ### 完整 Java 实现示例(使用 Jedis + Stream) ```java import redis.clients.jedis.Jedis; import redis.clients.jedis.StreamEntry; import redis.clients.jedis.StreamEntryID; public class RedisStreamQueue { private static final String STREAM_KEY = "orders"; private static final String GROUP_NAME = "order_processor"; public static void main(String[] args) { // 初始化消费者组 initConsumerGroup(); // 启动生产者线程 new Thread(RedisStreamQueue::producer).start(); // 启动消费者线程 new Thread(RedisStreamQueue::consumer).start(); } private static void initConsumerGroup() { try (Jedis jedis = new Jedis("localhost")) { jedis.xgroupCreate(STREAM_KEY, GROUP_NAME, new StreamEntryID(), true); } } private static void producer() { try (Jedis jedis = new Jedis("localhost")) { int count = 1; while (true) { String order = "订单-" + count++; // 添加消息到流 jedis.xadd(STREAM_KEY, null, Map.of("order_id", order, "amount", String.valueOf(count * 10))); System.out.println("生产: " + order); Thread.sleep(1500); } } catch (Exception e) { e.printStackTrace(); } } private static void consumer() { try (Jedis jedis = new Jedis("localhost")) { while (true) { // 从消费者组读取消息 var result = jedis.xreadGroup( GROUP_NAME, "consumer-1", 1, 0, true, Map.of(STREAM_KEY, ">") ); if (!result.isEmpty()) { for (var stream : result) { for (var entry : stream.getValue()) { System.out.println("消费: " + entry.getFields()); // 处理成功后确认消息 jedis.xack(STREAM_KEY, GROUP_NAME, entry.getID()); } } } Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } } } ``` ### 三种方式对比 | **特性** | **List** | **PubSub** | **Stream** | |------------------|----------|------------|------------| | 消息持久化 | ✓ | ✗ | ✓ | | 阻塞式消费 | ✓ | ✗ | ✓ | | 多消费者组 | ✗ | ✗ | ✓ | | 消息确认(ACK) | ✗ | ✗ | ✓ | | 消息回溯 | ✗ | ✗ | ✓ | | 广播消息 | ✗ | ✓ | ✗ | | 适用场景 | 简单队列 | 实时通知 | 企业级队列 | ### 使用建议 1. **基本需求**:使用 List 结构(`LPUSH`+`BRPOP`) 2. **实时通知**:使用 PubSub(广播场景) 3. **企业级应用**:推荐使用 Stream 结构(支持消费者组和ACK)[^1][^2] Redis Stream 是目前最接近专业消息队列(如 Kafka/RabbitMQ)的实现方式,适合需要消息持久化、消费确认和多消费者组的场景[^3]。 ### 相关问题 1. Redis Stream 如何实现消息的持久化和重放? 2. 在 Redis 中,如何监控消息队列的运行状态? 3. Redis 作为消息队列与专业的消息队列(如 RabbitMQ)相比有哪些优缺点?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值