Redis 是如何实现消息队列的?

Redis中消息队列的四种实现方式: List 方式、ZSet 方式、发布订阅者模式、Stream 方式
其中发布订阅者模式不支持消息持久化、而其他三种方式支持持久化,并且Stream方式支持消费者确认

list 方式

List类型实现的方式最为简单和直接
通过lpush、rpop 存入和读取实现消息队列的,如下图所示:
在这里插入图片描述
lpush可以把最新的消息存储到消息队列(List 集合)的首部
rpop可以读取消息队列的尾部,这样就实现了先进先出,如下图所示:
在这里插入图片描述

127.0.0.1:6379> lpush myMq "test1" "test2" "test3"
(integer) 3
127.0.0.1:6379> rpop myMq 
"test1"
127.0.0.1:6379> lpush myMq "test4"
(integer) 3
127.0.0.1:6379> rpop myMq
"test2"
127.0.0.1:6379> rpop myMq
"test3"
127.0.0.1:6379> rpop myMq
"test4"
127.0.0.1:6379> rpop myMq
(nil)
127.0.0.1:6379> 

lpush 可以一次放入多个element ,但是rpop一次却只能获取一个最先存放进来的数据.
List 做为消息队列的优缺点

  • 优点 : 使用List实现消息队列的优点是消息可以被持久化
    List可以借助Redis本身的持久化功能,AOF或者是RDB或混合持久化的方式用于把数据保存至磁盘,这样当Redis重启之后,消息不会丢失
    • 缺点, 但使用List同样存在一-定的问题 比如消息不支持重复消费、没有按照主题订阅的功能、不支持消费消息确认等.

ZSet 方式

ZSet实现消息队列的方式和List类似,利用zadd和zrangebyscore来实现存入和读取消息的
但ZSet的实现方式更为复杂一些,因为ZSet多了一个分值(score) 属性,我们可以使用它来实现更多的功能比如用它来存储时间戳,以此来实现延迟消息队列等.
ZSet同样具备持久化的功能,List 存在的问题它也同样存在,使用ZSet还不能存储相同元素的值
因为它是有序集合,有序集合的存储元素值是不能重复的
但分值可以重复,也就是说当消息值重复时,只能存储一条信息在ZSet中.

Redis 2.0之后新增了专门的发布和订阅的类型

  • 发布消息 publish channel “message”
  • 订阅消息 subscribe channel

在这里插入图片描述

发布订阅模式的优点很明显,但同样存在以下3个问题:
●无法持久化保存消息
如果Redis服务器宕机或重启,那么所有的消息将会丢失.
●发布订阅模式是“发后既忘”的工作模式
如果有订阅者离线重连之后就不能消费之前的历史消息

●不支持消费者确认机制,稳定性不能得到保证
例如当消费者获取到消息之后,还没来得及执行就宕机了
因为没有消费者确认机制,Redis 就会误以为消费者已经执行了
因此就不会重复发送未被正常消费的消息了
这样整体的Redis稳定性就被没有办法得到保障了

Stream类型

使用Stream的xadd和xrange来实现消息的存入和读取了
并且Stream提供了xack手动确认消息消费的命令,使用命令如下:

在这里插入图片描述
在这里插入图片描述
●早期版本中比较常用的实现消息队列的方式是List、 ZSet和发布订阅者模式
Stream方式实现消息队列属于附加题,属于面试中的加分项.

public class RedisListMq {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> bConsumer()).start();
        producer();
    }

    /**
     *一直读取,数据为空时 浪费资源
     */
    public static void consumer() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        while (true) {
            String myMq = jedis.rpop("test");
            if (StrUtil.isNotEmpty(myMq)) {
                System.out.println("test = " + myMq);
            }
        }
    }

    /**
     * 阻塞读取
     */
    public static void bConsumer() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        while (true) {
            for (String mq : jedis.brpop(0, "test")) {
                if (StrUtil.isNotEmpty(mq)) {
                    //会打印队列名称
                    //mq => test
                    //mq => hello word!
                    //mq => test
                    //mq => hello java!
                    //mq => test
                    //mq => hello python!
                    System.out.println("mq => " + mq);
                }
            }
        }
    }

    public static void producer() throws InterruptedException {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        Thread.sleep(1000);
        jedis.lpush("test","hello word!");
        Thread.sleep(2000);
        jedis.lpush("test","hello java!");
        Thread.sleep(3000);
        jedis.lpush("test","hello python!");
    }
}

需要注意的是在List中需要使用brpop来读取消息,而不是rpop
这样可以解决没有任务时,while -直循环浪费系统资源的问题
使用jedis 实现stream

brpop中的b是blocking的意思,表示阻塞读
也就是当队列没有数据时,它会进入休眠状态,当有数据进入队列之后
它才会“苏醒”过来执行读取任务,这样就可以解决while循环一直执行消耗系统资源的问题了

maven 中的jedis的依赖版本如下

<!--     jedis redis 的另一种客户端   -->
        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>

这个版本勉强可以实现功能,但是会报错链接超时,一直没找到合适的版本


public class StreamGroupExample {
    public static final String STREAM_KEY="myMq";
    public static final String GROUP_NAME="g1   ";
    public static final String CONSUMER_NAME="c1";
    public static final String CONSUMER2_NAME="c2";


    public static void main(String[] args) {
        //生产者
        producer();
        // 创建分组
        createGroup(STREAM_KEY,GROUP_NAME);
        // 消费者1
        new Thread(()->consumer1()).start();
        // 消费者2
        new Thread(()->consumer2()).start();
    }

    private static void consumer2() {
        Jedis jedis = new Jedis("127.0.0.1", 6379,100000);
        while (true) {
            Map.Entry<String, StreamEntryID> entry = new AbstractMap.SimpleImmutableEntry<>(STREAM_KEY,

                    StreamEntryID.UNRECEIVED_ENTRY);

            // 阻塞读取一条消息(最大阻塞时间120s)
            List<Map.Entry<String, List<StreamEntry>>> list = jedis.xreadGroup(GROUP_NAME, CONSUMER2_NAME, 1,

                    120 * 1000, true, entry);

            if (list!=null && list.size()==1) {
                // 读取到消息
                Map<String, String> content = list.get(0).getValue().get(0).getFields(); // 消息内容

                System.out.println("Consumer 2 读取到消息 ID:" + list.get(0).getValue().get(0).getID() +

                        " 内容:" + JSONUtil.toJsonStr(content));
            }
        }
    }

    private static void consumer1() {
        Jedis jedis = new Jedis("127.0.0.1", 6379,100000);
        while (true) {
            Map.Entry<String, StreamEntryID> entry = new AbstractMap.SimpleImmutableEntry<>(STREAM_KEY,

                     StreamEntryID.UNRECEIVED_ENTRY);

            // 阻塞读取一条消息(最大阻塞时间120s)
            Map<String, StreamEntryID> map = new HashMap<>();
            map.put(entry.getKey(),entry.getValue());

            // 阻塞读取一条消息(最大阻塞时间120s)
            List<Map.Entry<String, List<StreamEntry>>> list = jedis.xreadGroup(GROUP_NAME, CONSUMER_NAME, 1,

                    120 * 1000, true, entry);
            if (list!=null && list.size()==1) {
                // 读取到消息
                Map<String, String> content = list.get(0).getValue().get(0).getFields(); // 消息内容

                System.out.println("Consumer 1 读取到消息 ID:" + list.get(0).getValue().get(0).getID() +

                        " 内容:" + JSONUtil.toJsonStr(content));
            }
        }
    }

    /**
     * 创建分组
     * @param streamKey
     * @param groupName
     */
    private static void createGroup(String streamKey, String groupName) {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.xgroupCreate(streamKey,groupName,new StreamEntryID(),true);
    }

    private static void producer() {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        Map<String, String> map = new HashMap<>();
        map.put("data","redis");

        StreamEntryID streamEntryID = jedis.xadd(STREAM_KEY,  null, map);
        System.out.println("添加id成功 => " + streamEntryID);
        map.put("data","java");
        StreamEntryID streamEntryID2 = jedis.xadd(STREAM_KEY,  null, map);
        System.out.println("添加id成功 => " + streamEntryID2);
    }
}

jedis.xreadGroup()方法的第五个参数noAck表示是否自动确认消息
如果设置true收到消息会自动确认(ack)消息,否则需要手动确认

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值