使用Jedis监听Redis Stream 实现消息队列功能

本文档展示了如何使用Jedis代替SpringBoot,实现RedisStream的消息队列监听功能。通过创建群组和消费者,或者使用xread原生监听,实现多线程监听。文中提供了详细代码示例,包括群组监听避免重复消费和非群组监听的实现,以及创建群组的步骤。此外,还给出了一个简单的数据写入线程,以供演示。
摘要由CSDN通过智能技术生成

简介

之前使用SpringBoot去监听Redis Stream实现了消息队列的功能,本次分享的是使用Jedis来实现同样的功能,而且还可以继续拓展功能,因为Jedis我觉得还是比之前那种方式要灵活。本次实现的监听可以使用多线程去监听。

之前通过SpringBoot实现文章链接:
SpringBoot 中使用Redis Stream 实现消息监听

视频演示

使用Jedis自己实现监听Redis Stream的功能达到消息队列的效果Demo

实现原理

这次实现监听我分为了通过群组和消费者监听和模式使用xread的原生监听,它们的区别就是如果使用的是通过群组和消费者这样的监听可以确保消息只会被同一个消费者消费一次,不会对消息进行重复消费,适合要求数据唯一性的场景,比如入库或者其他的操作。默认的xread实现方式监听有几个线程那么这几个线程会同时收到相同的插入的消息,可以理解为广播的方式去接受消息。

这里主要基于的是Redis Stream中的一下几个命令对应Jedis的方法:

  • xadd:创建群组
  • xread:读取数据
  • xgroup: 创建群组
  • xreadgroup:读取群组消息

读取时主要用到了它们的block属性,将block属性设置为0则表示一直阻塞直到收到新的消息,然后我将这一步骤放到一个轮询中,实现了阻塞收到消息后进入到下一次阻塞,从而实现了监听到效果。

实现代码

本次demo代码很简单,都放到了一个类中,并且只要有redis就可以在修改代码中的配置后直接进行运行,不需要手动创建stream或者群组等操作。

pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>vip.huhailong</groupId>
    <artifactId>JRedisMQ</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <jedis.version>4.2.3</jedis.version>
    </properties>

    <!--  jedis dependency  -->
    <dependencies>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.36</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.7</version>
        </dependency>
    </dependencies>

</project>
实现代码
package jredismq.test;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.StreamEntryID;
import redis.clients.jedis.params.XAddParams;
import redis.clients.jedis.params.XReadGroupParams;
import redis.clients.jedis.params.XReadParams;
import redis.clients.jedis.resps.StreamEntry;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

/**
 * 使用jedis实现监听stream消息
 */
public class JedisStreamMQTest {

    private static final Logger logger = LoggerFactory.getLogger(JedisStreamMQTest.class);

    public static void main(String[] args) {
		//以下内容根据自己的情况进行修改
        String host = "192.168.1.110";
        int port = 6379;
        int timeout = 1000;
        String password = "huhailong";
        int database = 0;

        String streamKeyName = "streamtest";
        String groupName = "testgroup";

        String[]consumerNames = {"huhailong", "xiaohu"};

        String listenerType = "DEFAULT";  //GROUP or DEFAULT

        //创建 redis 连接池实例
        JedisPool pool = new JedisPool(new GenericObjectPoolConfig<>(),host,port,timeout,password,database);

        JedisStreamMQTest test = new JedisStreamMQTest();
        test.createGroup(pool,streamKeyName,groupName); //创建群组

        if("GROUP".equals(listenerType)){
            test.listenerByGroup(pool,streamKeyName,groupName,consumerNames);   //使用群组和消费者监听
        }else{
            test.listenerDefault(pool,streamKeyName);
        }

        new Thread(()->{    //线程3:用于写入stream数据
            Jedis jedis = pool.getResource();
            while(true) {
                try {
                    Thread.sleep(500L);
                    Map<String,String> map = new HashMap<>();
                    map.put("currentTime", LocalDateTime.now().toString());
                    jedis.xadd(streamKeyName,map, XAddParams.xAddParams());
                } catch (Exception e) {
                    logger.error(e.getMessage());
                }
            }
        }).start();
    }

    /**
     * 使用群组和消费者监听,该监听可以确保消息不会重复消费,因为每个组每个用户只会消费一次消息
     * @param keyName   stream 名称
     * @param groupName 群组名称
     * @param consumerNames 消费者名称集合
     */
    private void listenerByGroup(JedisPool pool, String keyName, String groupName, String...consumerNames){
        Map<String, StreamEntryID> entryIDMap = new HashMap<>();
        entryIDMap.put(keyName,StreamEntryID.UNRECEIVED_ENTRY);
        //以下为了演示简单就不用线程池了,直接创建两个线程说明问题
        IntStream.range(0,2).forEach(i->{
            Jedis jedis = pool.getResource();   //创建jedis实例
            new Thread(()->{
                while(true){
                    try{
                        Thread.sleep(500L);
                        //下面的 xreadGroup 方法等同与redis中的xreadgroup命令,将block阻塞时间设置为0表示一直阻塞知道收到消息,然后上面StreamEntryID设置为接收最新值
                        List<Map.Entry<String, List<StreamEntry>>> entries = jedis.xreadGroup(groupName, consumerNames[i], XReadGroupParams.xReadGroupParams().block(0), entryIDMap);
                        logger.info("Thread:-{},result:{}",Thread.currentThread().getName(), JSONObject.toJSONString(entries.get(0).getValue().get(0).getFields()));
//                        jedis.xack(keyName,groupName,entries.get(0).getValue().get(0).getID()); //确认消息
                        jedis.xdel(keyName,entries.get(0).getValue().get(0).getID());   //删除消息
                    } catch (Exception e){
                        logger.error(e.getMessage());
                    }
                }
            }).start();
        });

    }

    /**
     * 不使用群组和消费者的概念读取,多个线程会重复消费数据
     * @param keyName stream 名称
     */
    private void listenerDefault(JedisPool pool, String keyName){
        Map<String, StreamEntryID> entryIDMap = new HashMap<>();
        entryIDMap.put(keyName,StreamEntryID.LAST_ENTRY);
        //以下为了演示简单就不用线程池了,直接创建两个线程说明问题
        IntStream.range(0,2).forEach(i->{
            new Thread(()->{
                Jedis jedis = pool.getResource();   //创建jedis实例
                while(true){
                    try{
                        Thread.sleep(500L);
                        List<Map.Entry<String, List<StreamEntry>>> entries = jedis.xread(XReadParams.xReadParams().block(0), entryIDMap);
                        logger.info("Thread:-{},result:{}",Thread.currentThread().getName(), JSONObject.toJSONString(entries.get(0).getValue().get(0).getFields()));
                        jedis.xdel(keyName,entries.get(0).getValue().get(0).getID());
                    } catch (Exception e){
                        logger.error(e.getMessage());
                    }
                }
            }).start();
        });
    }

    private void createGroup(JedisPool pool, String keyName, String groupName){
        Jedis jedis = pool.getResource();
        try{
            //StreamEntryID 表示创建群组并接收新的消息,这里可以根据自己需要设置,0表示读取所有历史消息,后面的boolean值表示如果stream不存在是否创建stream
            jedis.xgroupCreate(keyName,groupName,StreamEntryID.LAST_ENTRY,true);
        } catch (Exception e){
            //这里捕获异常的原因是可能创建时群组已经存在
            logger.error(e.getMessage());
        }
    }

}

本demo的代码还是比较简单的,大家可以根据自己的需求修改和封装。我也在继续探索使用这种方式封装和完善成一个完整的项目,不依赖于第三方框架例如Spring的项目,这样就可以灵活的使用它了,觉得有用的小伙伴要点赞哟!

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@胡海龙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值