Redis实现消息队列的方式汇总以及代码实现

前言

经常听到很多人讨论,关于「把 Redis 当作队列来用是否合适」的问题。

有些人表示赞成,他们认为 Redis 很轻量,用作队列很方便。也些人则反对,认为 Redis 会「丢」数据,最好还是用「专业」的队列中间件更稳妥。

这篇文章就聊一聊把 Redis 当作队列,究竟是否合适这个问题。我们会从简单到复杂,一步步带你梳理其中的细节,把常用的实现方式展现一遍。

开始前准备

1、添加依赖
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
                <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
                <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
                <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.0</version>
        </dependency>

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
2、添加配置的Bean

避免不方便用软件查看存储的数据

    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
    <span class="token comment">// 使用Jackson2JsonRedisSerialize 替换默认序列化</span>
    <span class="token class-name">Jackson2JsonRedisSerializer</span> jackson2JsonRedisSerializer <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Jackson2JsonRedisSerializer</span><span class="token punctuation">(</span><span class="token class-name">Object</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token class-name">ObjectMapper</span> objectMapper <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ObjectMapper</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    objectMapper<span class="token punctuation">.</span><span class="token function">setVisibility</span><span class="token punctuation">(</span><span class="token class-name">PropertyAccessor</span><span class="token punctuation">.</span><span class="token constant">ALL</span><span class="token punctuation">,</span> <span class="token class-name">JsonAutoDetect<span class="token punctuation">.</span>Visibility</span><span class="token punctuation">.</span><span class="token constant">ANY</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    objectMapper<span class="token punctuation">.</span><span class="token function">enableDefaultTyping</span><span class="token punctuation">(</span><span class="token class-name">ObjectMapper<span class="token punctuation">.</span>DefaultTyping</span><span class="token punctuation">.</span><span class="token constant">NON_FINAL</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    jackson2JsonRedisSerializer<span class="token punctuation">.</span><span class="token function">setObjectMapper</span><span class="token punctuation">(</span>objectMapper<span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">// 设置value的序列化规则和 key的序列化规则</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">setKeySerializer</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">StringRedisSerializer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">//jackson2JsonRedisSerializer就是JSON序列号规则,</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">setValueSerializer</span><span class="token punctuation">(</span>jackson2JsonRedisSerializer<span class="token punctuation">)</span><span class="token punctuation">;</span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">afterPropertiesSet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> redisTemplate<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

具体实现

一、从最简单的开始:List 队列

首先,我们先从最简单的场景开始讲起。

如果你的业务需求足够简单,想把 Redis 当作队列来使用,肯定最先想到的就是使用 List 这个数据类型

因为 List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型。

如果把 List 当作队列,你可以这么来用。

代码实现

生产者端读取:

@RestController
@RequestMapping("/redis01")
public class RedisTest1 {
<span class="token annotation punctuation">@Autowired</span>
<span class="token keyword">private</span> <span class="token class-name">RedisTemplate</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">String</span><span class="token punctuation">,</span> <span class="token class-name">Object</span><span class="token punctuation">&gt;</span></span> redisTemplate<span class="token punctuation">;</span>


<span class="token comment">//LPUSH 发布消息</span>
<span class="token annotation punctuation">@GetMapping</span><span class="token punctuation">(</span><span class="token string">"/set"</span><span class="token punctuation">)</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">set</span><span class="token punctuation">(</span><span class="token class-name">String</span> code<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">opsForList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">leftPush</span><span class="token punctuation">(</span><span class="token string">"code"</span><span class="token punctuation">,</span>code<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// RPOP 拉取消息</span>
<span class="token annotation punctuation">@GetMapping</span><span class="token punctuation">(</span><span class="token string">"/get1"</span><span class="token punctuation">)</span>
<span class="token keyword">public</span> <span class="token class-name">String</span> <span class="token function">get1</span><span class="token punctuation">(</span><span class="token class-name">String</span> key<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    <span class="token class-name">Object</span> code <span class="token operator">=</span>  redisTemplate<span class="token punctuation">.</span><span class="token function">opsForList</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">rightPop</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>code<span class="token operator">!=</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
        <span class="token keyword">return</span> code<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">return</span> <span class="token string">"redis中没数据!"</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

实现模型:
这个模型也非常简单容易理解。 在这里插入图片描述

但这里有个小问题,当队列中已经没有消息了,消费者在执行 RPOP 时,会返回 NULL。
在这里插入图片描述
一般在编写消费者时,会采用一个死循环,这个实现方式就是不断去队列中拉取数据。

    @GetMapping("/get2")
    public String get2(String key) throws InterruptedException {
        while (true){
            Object code = redisTemplate.opsForList().rightPop(key);
            System.out.println(code);
 //            读取到消息,退出,没读到继续循环
            if (code!=null){
                return code.toString();
            }
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

如果此时队列为空,那消费者依旧会频繁拉取消息,这会造成「CPU 空转」,不仅浪费 CPU 资源,还会对 Redis 造成压力。

怎么解决这个问题呢?

也很简单,当队列为空时,我们可以「休眠」一会,再去尝试拉取消息。代码可以修改成这样:

    @GetMapping("/get2")
    public String get2(String key) throws InterruptedException {
        while (true){
            Object code = redisTemplate.opsForList().rightPop(key);
            System.out.println(code);
 //            读取到消息,退出,没读到继续循环
            if (code!=null){
                return code.toString();
            }
            Thread.sleep(2000);
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

这就解决了 CPU 空转问题。

这个问题虽然解决了,但又带来另外一个问题:当消费者在休眠等待时,有新消息来了,那消费者处理新消息就会存在「延迟」。

假设设置的休眠时间是 2s,那新消息最多存在 2s 的延迟。

要想缩短这个延迟,只能减小休眠的时间。但休眠时间越小,又有可能引发 CPU 空转问题。

鱼和熊掌不可兼得。

那如何做,既能及时处理新消息,还能避免 CPU 空转呢?

Redis 是否存在这样一种机制:如果队列为空,消费者在拉取消息时就「阻塞等待」,一旦有新消息过来,就通知我的消费者立即处理新消息呢?

幸运的是,Redis 确实提供了「阻塞式」拉取消息的命令:BRPOP / BLPOP,这里的 B 指的是阻塞(Block)。
在这里插入图片描述
在java中也已经封装好了,调用pop方法时,直接设置一个过期时间就行

    @GetMapping("/get3")
    public String get3(String key) throws InterruptedException {
        Object code = redisTemplate.opsForList().rightPop(key,0, TimeUnit.SECONDS);
        if (code==null){
            return "数据读取超时!";
        }
        return code.toString();
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用 BRPOP 这种阻塞式方式拉取消息时,还支持传入一个「超时时间」,如果设置为 0,则表示不设置超时,直到有新消息才返回,否则会在指定的超时时间后返回 NULL。

这个方案不错,既兼顾了效率,还避免了 CPU 空转问题,一举两得。

注意:如果设置的超时时间太长,这个连接太久没有活跃过,可能会被 Redis Server 判定为无效连接,之后 Redis Server
会强制把这个客户端踢下线。所以,采用这种方案,客户端要有重连机制。

解决了消息处理不及时的问题,你可以再思考一下,这种队列模型,有什么缺点?

我们一起来分析一下:

  1. 不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据
  2. 消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了
    第一个问题是功能上的,使用 List 做消息队列,它仅仅支持最简单的,一组生产者对应一组消费者,不能满足多组生产者和消费者的业务场景。

第二个问题就比较棘手了,因为从 List 中 POP 一条消息出来后,这条消息就会立即从链表中删除了。也就是说,无论消费者是否处理成功,这条消息都没办法再次消费了。

这也意味着,如果消费者在处理消息时异常宕机,那这条消息就相当于丢失了。

针对这 2 个问题怎么解决呢?我们一个个来看。

二、发布订阅模式:Pub/Sub

从名字就能看出来,这个模块是 Redis 专门是针对「发布/订阅」这种队列模型设计的。

它正好可以解决前面提到的第一个问题:重复消费。

即多组生产者、消费者的场景,我们来看它是如何做的。

Redis 提供了 PUBLISH / SUBSCRIBE 命令,来完成发布、订阅的操作。
在这里插入图片描述
依赖继续用前面的就行

1、使用RedisMessageListenerContainer实现订阅
  • 通过实现MessageListener接口来处理接收到的消息。这允许您在Spring应用程序中以更高级的方式处理消息,例如使用依赖注入和其他Spring功能。它还支持基于注解的消息监听器,使消息处理更加简洁和灵活。
  • 该方式是Spring Data Redis库提供的方法,用于在Spring应用程序中使用Redis的发布订阅功能。它需要创建一个MessageListenerContainer对象,并通过调用addMessageListener方法来添加消息监听器。
  • 添加监控器,用于监听通道
    为了便于更加直观的对比测试,我添加了两个
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO  配置监控器
 * @date 2023/7/28 17:10
 */
@Component
public class RedisMessaeListener1 implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
    <span class="token class-name">String</span> channel <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span>message<span class="token punctuation">.</span><span class="token function">getChannel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">String</span> body <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span>message<span class="token punctuation">.</span><span class="token function">getBody</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"监听器1号:消息: "</span> <span class="token operator">+</span> body <span class="token operator">+</span> <span class="token string">" 通道QQ: "</span> <span class="token operator">+</span> channel<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

/########################/

/**

  • @author zhengfuping
  • @version 1.0
  • @description: TODO 配置监控器1
  • @date 2023/8/2 11:24
    */
    @Component
    public class RedisMessaeListener2 implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
    String channel = new String(message.getChannel());
    String body = new String(message.getBody());
    System.out.println("监听器2号:消息: “ + body + ” 通道QQ: " + channel);
    }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 配置订阅可以单和多个
    用于绑定主题(通道)和监听器,该发送是
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO 使用RedisMessageListenerContainer直接注入到bean进行监听
 * @date 2023/7/28 15:43
 */
@Configuration
public class RedisPubSubExample {
<span class="token annotation punctuation">@Autowired</span>
<span class="token keyword">private</span>  <span class="token class-name">RedisMessaeListener1</span> redisMessaeListener1<span class="token punctuation">;</span>
<span class="token annotation punctuation">@Autowired</span>
<span class="token keyword">private</span>  <span class="token class-name">RedisMessaeListener2</span> redisMessaeListener2<span class="token punctuation">;</span>

<span class="token comment">/**
 * 订阅三个频道
 * @author zhengfuping
 * @date 2023/8/2 11:19
 * @param redisConnectionFactory redis线程工厂
 * @return RedisMessageListenerContainer
 */</span>

// @Bean
public RedisMessageListenerContainer subscribeToChannel(RedisConnectionFactory redisConnectionFactory){
RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();

    listenerContainer<span class="token punctuation">.</span><span class="token function">setConnectionFactory</span><span class="token punctuation">(</span>redisConnectionFactory<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">List</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">Topic</span><span class="token punctuation">&gt;</span></span> list <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">PatternTopic</span><span class="token punctuation">(</span><span class="token string">"TEST01"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">PatternTopic</span><span class="token punctuation">(</span><span class="token string">"TEST02"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">PatternTopic</span><span class="token punctuation">(</span><span class="token string">"TEST03"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">/*
     *  redisMessaeListener  消息监听器
     *  list  订阅的主题(可以单个和多个)
     */</span>
    listenerContainer<span class="token punctuation">.</span><span class="token function">addMessageListener</span><span class="token punctuation">(</span>redisMessaeListener1<span class="token punctuation">,</span>list<span class="token punctuation">)</span><span class="token punctuation">;</span>
    listenerContainer<span class="token punctuation">.</span><span class="token function">addMessageListener</span><span class="token punctuation">(</span>redisMessaeListener2<span class="token punctuation">,</span><span class="token keyword">new</span> <span class="token class-name">PatternTopic</span><span class="token punctuation">(</span><span class="token string">"TEST01"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> listenerContainer<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 向指定频道发送消息
    /**
     *  PUBLISH 发送消息到指定频道
     * @author zhengfuping
     * @date 2023/8/2 11:14
     * @param channel 通道
     * @param name  数据
     * @param age
     * @return Object
     */
    @GetMapping("/pub")
    public Object pub(String channel,String name,Integer age) {
        User user = new User(name, age);
        redisTemplate.convertAndSend(channel,user);
        return user;
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述

2、还可以使用redisTemplate实现订阅
  • 该方式是Redis客户端的方法,用于在独立的Redis客户端中直接使用发布订阅功能。它需要创建一个Redis连接对象,并通过调用subscribe方法来订阅一个或多个频道。

  • 如果您只是在独立的Redis客户端中使用发布订阅功能,并且不需要使用Spring的其他功能,则可以选择connection.subscribe

 /**
 * 自行添加订阅
 */
    @GetMapping("/sub")
    public void sub(String channel) {
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    <span class="token comment">/*
     *  MessageListener:监听器,直接使用内部类实现绑定监听可以把数据传递出去
     *  channel 订阅频道
     */</span>
    connection<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span><span class="token punctuation">(</span>message<span class="token punctuation">,</span> pattern<span class="token punctuation">)</span> <span class="token operator">-&gt;</span> <span class="token punctuation">{<!-- --></span>
        <span class="token class-name">String</span> channel1 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span>message<span class="token punctuation">.</span><span class="token function">getChannel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name">String</span> body <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span>message<span class="token punctuation">.</span><span class="token function">getBody</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"subscribe方式监听:消息: "</span> <span class="token operator">+</span> body <span class="token operator">+</span> <span class="token string">" 通道QQ: "</span> <span class="token operator">+</span> channel1<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span> channel<span class="token punctuation">.</span><span class="token function">getBytes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

// connection.close();
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在这里插入图片描述

发送消息

    @GetMapping("/pub")
    public Object pub(String channel,String name,Integer age) {
        User user = new User(name, age);
        redisTemplate.convertAndSend(channel,user);
        return user;
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

最终监听到的结果
在这里插入图片描述

三、、 趋于成熟的队列:Stream

我们来看 Stream 是如何解决上面这些问题的。

我们依旧从简单到复杂,依次来看 Stream 在做消息队列时,是如何处理的?

首先,Stream 通过 XADD 和 XREAD 完成最简单的生产、消费模型:

  • 生产者发布 2 条消息:
// *表示让Redis自动生成消息ID
127.0.0.1:6379> XADD queue * name zhangsan
"1618469123380-0"
127.0.0.1:6379> XADD queue * name lisi
"1618469127777-0"

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 消费者拉取消息:
// 从开头读取5条消息,0-0表示从开头读取
127.0.0.1:6379> XREAD COUNT 5 STREAMS queue 0-0
1) 1) "queue"
   2) 1) 1) "1618469123380-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618469127777-0"
         2) 1) "name"
            2) "lisi"

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

流程图
在这里插入图片描述

具体java代码实现:
  • 先配置监听消息类
@Slf4j
@Component
public class ListenerMessage implements StreamListener<String, MapRecord<String, String, String>> {
<span class="token annotation punctuation">@Override</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">onMessage</span><span class="token punctuation">(</span><span class="token class-name">MapRecord</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">String</span><span class="token punctuation">,</span> <span class="token class-name">String</span><span class="token punctuation">,</span> <span class="token class-name">String</span><span class="token punctuation">&gt;</span></span> entries<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"接受到来自redis的消息"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"message id "</span><span class="token operator">+</span>entries<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"stream "</span><span class="token operator">+</span>entries<span class="token punctuation">.</span><span class="token function">getStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token class-name">System</span><span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"body "</span><span class="token operator">+</span>entries<span class="token punctuation">.</span><span class="token function">getValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 添加工具类,实现初始化
@Component
@Slf4j
public class RedisStreamUtil {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
<span class="token comment">/**
 * @author zhengfuping 添加数据
 * @param streamKey
 * @param map
 * @return RecordId
 */</span>
<span class="token keyword">public</span> <span class="token class-name">RecordId</span> <span class="token function">addStream</span><span class="token punctuation">(</span><span class="token class-name">String</span> streamKey<span class="token punctuation">,</span><span class="token class-name">Map</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">String</span><span class="token punctuation">,</span> <span class="token class-name">Object</span><span class="token punctuation">&gt;</span></span> map<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    <span class="token class-name">RecordId</span> recordId <span class="token operator">=</span> redisTemplate<span class="token punctuation">.</span><span class="token function">opsForStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>streamKey<span class="token punctuation">,</span> map<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> recordId<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">/**
 * 用来创建绑定流和组
 */</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">addGroup</span><span class="token punctuation">(</span><span class="token class-name">String</span> key<span class="token punctuation">,</span> <span class="token class-name">String</span> groupName<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">opsForStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">createGroup</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span>groupName<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">/**
 * 用来判断key是否存在
 */</span>
<span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">hasKey</span><span class="token punctuation">(</span><span class="token class-name">String</span> key<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    <span class="token keyword">if</span><span class="token punctuation">(</span>key<span class="token operator">==</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span><span class="token keyword">else</span><span class="token punctuation">{<!-- --></span>
        <span class="token keyword">return</span> redisTemplate<span class="token punctuation">.</span><span class="token function">hasKey</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

<span class="token punctuation">}</span>
<span class="token comment">/**
 * 用来删除掉消费了的消息
 */</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">delField</span><span class="token punctuation">(</span><span class="token class-name">String</span> key<span class="token punctuation">,</span><span class="token class-name">String</span> recordIds<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    redisTemplate<span class="token punctuation">.</span><span class="token function">opsForStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span>recordIds<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">/**
 * 用来初始化 实现绑定
 */</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">initStream</span><span class="token punctuation">(</span><span class="token class-name">String</span> key<span class="token punctuation">,</span> <span class="token class-name">String</span> group<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    <span class="token comment">//判断key是否存在,如果不存在则创建</span>
    <span class="token keyword">boolean</span> hasKey <span class="token operator">=</span> <span class="token function">hasKey</span><span class="token punctuation">(</span>key<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">if</span><span class="token punctuation">(</span><span class="token operator">!</span>hasKey<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
        <span class="token class-name">Map</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">String</span><span class="token punctuation">,</span><span class="token class-name">Object</span><span class="token punctuation">&gt;</span></span> map <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HashMap</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        map<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span><span class="token string">"field"</span><span class="token punctuation">,</span><span class="token string">"value"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token class-name">RecordId</span> recordId <span class="token operator">=</span> <span class="token function">addStream</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> map<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token function">addGroup</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span>group<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token comment">//把Stream和gropu绑定</span>
        <span class="token function">delField</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span>recordId<span class="token punctuation">.</span><span class="token function">getValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        log<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"stream:{}-group:{} initialize success"</span><span class="token punctuation">,</span>key<span class="token punctuation">,</span>group<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 添加配置类,配置Stream
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO  添加配置类,配置Stream
 */
@Configuration
@Slf4j
public class RedisStreamConfig {
<span class="token annotation punctuation">@Autowired</span>
<span class="token keyword">private</span> <span class="token class-name">RedisStreamUtil</span> redisStream<span class="token punctuation">;</span>
<span class="token annotation punctuation">@Autowired</span>
<span class="token keyword">private</span> <span class="token class-name">ListenerMessage</span> listenerMessage<span class="token punctuation">;</span>


<span class="token annotation punctuation">@Bean</span>
<span class="token keyword">public</span> <span class="token class-name">Subscription</span> <span class="token function">subscription</span><span class="token punctuation">(</span><span class="token class-name">RedisConnectionFactory</span> factory<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>

// 代码中的var是使用了Lombok的可变局部变量。主要是为了方便
// StreamMessageListenerContainer: 消息侦听容器,不能在外部实现。创建后,StreamMessageListenerContainer可以订阅Redis流并使用传入的消息
var options = StreamMessageListenerContainer
.StreamMessageListenerContainerOptions

.builder()
.pollTimeout(Duration.ofSeconds(1))
.build();
redisStream.initStream(“mystream”,“mygroup”); //调用初始化
var listenerContainer = StreamMessageListenerContainer.create(factory,options);

    <span class="token comment">/*
     *  注意这里接受到消息后会被自动的确认,如果不想自动确认请使用其他的创建订阅方式
     * 消费组 consumer group ,它不能为null (Consumer类型)
     * stream offset ,stream的偏移量(StreamOffset 类型)
     * listener 不能为null (StreamListener&lt;K,V&gt; 类型)
     */</span>
    <span class="token keyword">var</span> subscription <span class="token operator">=</span> listenerContainer<span class="token punctuation">.</span><span class="token function">receiveAutoAck</span><span class="token punctuation">(</span><span class="token class-name">Consumer</span><span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span><span class="token string">"mygroup"</span><span class="token punctuation">,</span><span class="token string">"huhailong"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token class-name">StreamOffset</span><span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token string">"mystream"</span><span class="token punctuation">,</span> <span class="token class-name">ReadOffset</span><span class="token punctuation">.</span><span class="token function">lastConsumed</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>listenerMessage<span class="token punctuation">)</span><span class="token punctuation">;</span>

    listenerContainer<span class="token punctuation">.</span><span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">return</span> subscription<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 调用测试
/**
 * @author zhengfuping
 * @version 1.0
 * @description: TODO
 * @date 2023/8/2 16:06
 */
@RestController
@RequestMapping("/redisStream")
public class RedisStreamTest {
<span class="token annotation punctuation">@Autowired</span>
<span class="token keyword">private</span> <span class="token class-name">RedisStreamUtil</span> redisStream<span class="token punctuation">;</span>

<span class="token annotation punctuation">@GetMapping</span><span class="token punctuation">(</span><span class="token string">"add"</span><span class="token punctuation">)</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">add</span><span class="token punctuation">(</span><span class="token class-name">String</span> key<span class="token punctuation">,</span><span class="token class-name">String</span> data<span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
    <span class="token class-name">Map</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token class-name">String</span><span class="token punctuation">,</span> <span class="token class-name">Object</span><span class="token punctuation">&gt;</span></span> map <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HashMap</span><span class="token generics"><span class="token punctuation">&lt;</span><span class="token punctuation">&gt;</span></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    map<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span>

// 添加数据到mystream流中
RecordId recordId = redisStream.addStream(“mystream”, map);
// 删除流中消费了的指定key的数据
redisStream.delField(“mystream”,recordId.getValue());
}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Stream的好处在于可以写入到 RDB 和 AOF 做持久化。
Stream是新增加的数据类型,它与其它数据类型一样,每个写操作,也都会写入到 RDB 和 AOF 中。
我们只需要配置好持久化策略,这样的话,就算 Redis 宕机重启,Stream 中的数据也可以从 RDB 或 AOF 中恢复回来。

总结

好了,总结一下。这篇文章我们从「Redis 能否用作队列」这个角度出发,介绍了 List、Pub/Sub、Stream 在做队列的使用方式,以及它们各自的优劣。

之后又把 Redis 和专业的消息队列中间件做对比,发现 Redis 的不足之处。

最后,我们得出 Redis 做队列的合适场景。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot与Redis结合实现消息队列的方法如下: 1. 首先,确保你的Spring Boot项目中已经引入了Redis的依赖。 2. 创建一个消息发布者类,用于发布消息Redis消息队列中。可以使用RedisTemplate来实现消息的发布。以下是一个示例代码: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class MessagePublisher { @Autowired private RedisTemplate<String, Object> redisTemplate; public void publish(String channel, Object message) { redisTemplate.convertAndSend(channel, message); } } ``` 3. 创建一个消息订阅者类,用于监听Redis消息队列并处理接收到的消息。可以使用@RedisListener注解来实现消息的订阅。以下是一个示例代码: ```java import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import org.springframework.stereotype.Component; @Component public class MessageSubscriber implements MessageListener { @Override public void onMessage(Message message, byte[] pattern) { String channel = new String(message.getChannel()); String body = new String(message.getBody()); // 处理接收到的消息 System.out.println("Received message: " + body + " from channel: " + channel); } } ``` 4. 在需要发布消息的地方,通过调用消息发布者类的publish方法来发布消息。以下是一个示例代码: ```java @Autowired private MessagePublisher messagePublisher; public void sendMessage(String channel, Object message) { messagePublisher.publish(channel, message); } ``` 5. 在需要订阅消息的地方,通过在消息订阅者类的方法上添加@RedisListener注解来监听指定的频道。以下是一个示例代码: ```java @RedisListener(channels = "myChannel") public void handleMessage(String message) { // 处理接收到的消息 System.out.println("Received message: " + message); } ``` 通过以上步骤,你就可以使用Spring Boot与Redis结合实现消息队列了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值