首页 > 数据库 >利用Redis Stream实现一个可靠的消息队列

利用Redis Stream实现一个可靠的消息队列

时间:2024-07-17 12:54:43浏览次数:19  
标签:读取 Stream 队列 Redis id 消息 key ID mystream

Redis Streams 简介

Redis Streams 是 Redis 5.0 引入的一种新数据结构,专门用于处理日志和消息流数据。它结合了多种数据结构的优点,提供了高效的消息存储和消费机制。Redis Streams 可以用于实时数据处理、事件驱动的系统、日志聚合和消息队列等场景。

主要特点

  1. 持久化:redis的publish/subscribe功能发送的消息是无法持久话的,stream消息存储在 Redis 中,并可以持久化到磁盘,确保数据不会丢失。
  2. 消费者组:支持多消费者组,每个组内可以有多个消费者,实现消息的并发处理。
  3. 阻塞读取:消费者可以阻塞等待新消息,实现实时处理。
  4. 灵活的消息确认机制:消费者处理完消息后需要确认(ACK),确保消息不会丢失。
  5. 高效的内存使用:采用压缩存储,优化内存使用。

使用场景

  1. 实时日志处理:如日志聚合、监控和告警系统。
  2. 消息队列:分布式系统中的消息传递和处理。
  3. 事件溯源:记录和重放系统中的事件。
  4. 数据管道:流数据处理和传输,如实时分析和 ETL。

核心概念

  1. 流(Stream):消息的有序集合,每个消息都有一个唯一的 ID。
  2. 消息(Entry):由多个字段组成的键值对。
  3. 消息 ID:每条消息都有一个唯一的 ID,由时间戳和序列号组成。
  4. 消费者组(Consumer Group):用于实现消息的并发处理,每个组内有多个消费者。
  5. 待处理消息(Pending Entry List, PEL):存储每个消费者组中未确认处理的消息。

队列结构图

创建流并添加消息

XADD

将消息添加到流中。

XADD stream-name [ID or *] field1 value1 [field2 value2 ...]
  • stream-name: 流的名称。
  • ID or *: 消息的 ID。使用 * 表示自动生成唯一 ID。
  • field1 value1 [field2 value2 ...]: 消息的字段和值。

示例:

XADD mystream * sensor-id 1234 temperature 19.8

读取消息

XRANGE

获取指定范围内的消息。

XRANGE stream-name start end [COUNT count]
  • stream-name: 流的名称。
  • start: 起始 ID。- 表示最小 ID。
  • end: 结束 ID。+ 表示最大 ID。
  • COUNT count: (可选)返回的最大消息数量。

示例:

XRANGE mystream - +
XREVRANGE

逆序获取指定范围内的消息。

XREVRANGE stream-name end start [COUNT count]
  • stream-name: 流的名称。
  • end: 结束 ID。+ 表示最大 ID。
  • start: 起始 ID。- 表示最小 ID。
  • COUNT count: (可选)返回的最大消息数量。

示例:

XREVRANGE mystream + -
XREAD

XREAD 命令是 Redis 中用于从一个或多个流(stream)读取数据的命令。它是一种阻塞或非阻塞的读取操作,可以用于构建消息队列、事件处理等场景。以下是 XREAD 命令的详细讲解:

基本语法
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
参数说明
  • COUNT count:可选参数,用于限制返回的条目数。如果不指定,默认会返回尽可能多的条目。
  • BLOCK milliseconds:可选参数,用于阻塞读取。如果指定,命令会在没有新条目可读时阻塞指定的毫秒数。如果不指定,命令会立即返回现有的条目(如果有)。
  • STREAMS key [key ...]:指定要读取的一个或多个流的键。
  • ID [ID ...]:指定每个流的起始 ID,表示从哪个 ID 之后开始读取。

示例

假设我们有两个流 mystream1mystream2,可以使用以下命令从这两个流中读取数据:

XREAD COUNT 2 STREAMS mystream1 mystream2 0 0

这条命令会从 mystream1mystream2 中读取数据,并且每个流最多读取 2 条消息。0 表示从流的开头开始读取。

阻塞读取示例

使用阻塞读取时,如果没有新消息,命令会阻塞指定的毫秒数:

XREAD BLOCK 5000 STREAMS mystream1 0

这条命令会在 mystream1 中阻塞最多 5 秒钟,等待新消息的到来。

读取特定 ID 之后的消息

可以指定从某个特定 ID 之后开始读取消息:

XREAD STREAMS mystream1 1526569495631-0

这条命令会从 mystream1 中读取 ID 为 1526569495631-0 之后的消息。

返回结果格式

XREAD 命令的返回结果是一个包含流数据的列表,每个流的数据以包含流名和条目的元组表示。条目是一个列表,包含 ID 和消息字段-值对。例如:

1) 1) "mystream1"
   2) 1) 1) "1526569495631-0"
         2) 1) "field1"
            2) "value1"
            3) "field2"
            4) "value2"
注意事项
  • XREAD 命令支持从多个流中读取数据,这在构建复杂的消息处理逻辑时非常有用。
  • 使用 BLOCK 参数时要小心,以避免不必要的长时间阻塞,尤其是在高负载的生产环境中。

通过 XREAD 命令,Redis 提供了强大的流数据处理能力,可以满足各种实时数据处理和消息队列的需求。

消费者操作

XGROUP CREATE

创建消费组。消费组用于管理消费者和队列读取记录。Stream中的消费组有两个特点:

  1. 从资源结构上说消费者从属于一个消费组
  2. 一个队列可以拥有多个消费组。不同消费组之间读取队列互不干扰

语法格式:

XGROUP [CREATE key groupname id-or-$] [SETID key groupname id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername]
  • key:队列名称,如果不存在就创建
  • groupname:组名
  • id: $表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略

命令使用:

为队列mystream创建一个消费组 mqGroup,从第一个消息开始读

127.0.0.1:6379> XGROUP CREATE mystream mqGroup 0
OK
XREADGROUP

读取队列的消息。在读取消息时需要指定消费者,只需要指定名字,不用预先创建。

语法格式:

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds]
  [NOACK] STREAMS key [key ...] id [id ...]
  • group:消费组名
  • consumer:消费者名
  • count:读取数量
  • BLOCK milliseconds:阻塞读以及阻塞毫秒数。默认非阻塞。和XREAD类似
  • key:队列名
  • id:消息ID。ID可以填写特殊符号>,表示未被组内消费的起始消息

命令使用:
创建消费者c1和c2,各读取一条消息

127.0.0.1:6379> XREADGROUP GROUP mqGroup c1 COUNT 1 STREAMS mystream >
            
127.0.0.1:6379> XREADGROUP GROUP mqGroup c2 count 1 streams mystream >

可以进行组内消费的基本原理是,STREAM类型会为每个组记录一个最后读取的消息ID(last_delivered_id),这样在组内消费时,就可以从这个值后面开始读取,保证不重复消费。

消费组消费时,还有一个必须要考虑的问题,就是若某个消费者,消费了某条消息,但是并没有处理成功时(例如消费者进程宕机),这条消息可能会丢失,因为组内其他消费者不能再次消费到该消息了

XPENDING

为了解决组内消息读取但处理期间消费者崩溃带来的消息丢失问题,Stream 设计了 Pending 列表,用于记录读取但并未确认完毕的消息。
语法格式:

XPENDING key group [[IDLE min-idle-time] start end count [consumer]]
  • key:队列名
  • group: 消费组名
  • start:开始值,-表示最小值
  • end:结束值,+表示最大值
  • count:数量

命令使用:

首先查看队列中的消息数量有3个,然后查看已读取未处理的消息有两个。

127.0.0.1:6379> xlen mystream
(integer) 3

127.0.0.1:6379> xpending mystream mqGroup
1) (integer) 2
2) "1721185317453-0"
3) "1721185327505-0"
4) 1) 1) "c1"
      2) "1"
   2) 1) "c2"
      2) "1"

队列中一共三条信息,有两条被消费但未处理完毕,也就是上面XREADGROUP消费的两条。一个是消费者c1,另一个是c2。

获取未确认的详细信息

127.0.0.1:6379> xpending mystream mqGroup - + 10
XACK

对于已读取未处理的消息,使用命令 XACK 完成告知消息处理完成
XACK 命令确认消费的信息,一旦信息被确认处理,就表示信息被完善处理。

语法格式:

XACK key group id [id ...]
  • key: stream 名
  • group:消费组
  • id:消息ID

命令使用:

确认消息1674985213802-0

127.0.0.1:6379> XACK mystream mqGroup 1674985213802-0
(integer) 1
127.0.0.1:6379>
XCLAIM

某个消费者读取了消息但没有处理,这时消费者宕机或重启等就会导致该消息失踪。那么就需要该消息转移给其他的消费者处理,就是消息转移。XCLAIM来实现消息转移的操作。

语法格式:

XCLAIM key group consumer min-idle-time id [id ...] [IDLE ms]
  [TIME unix-time-milliseconds] [RETRYCOUNT count] [FORCE] [JUSTID]
  [LASTID id]
  • key: 队列名称
  • group :消费组
  • consumer:消费组里的消费者
  • min-idle-time 最小时间。空闲时间大于min-idle-time的消息才会被转移成功
  • id:消息的ID

转移除了要指定ID外,还需要指定min-idle-time,min-idle-time是最小空闲时间,该值要小于消息的空闲时间,这个参数是为了保证是多于多长时间的消息未处理的才被转移。比如超过24小时的处于pending未xack的消息要进行转移
同时min-idle-time还有一个功能是能够避免两个消费者同时转移一条消息。被转移的消息的IDLE会被重置为0。假设两个消费者都以2min来转移,第一个成功之后IDLE被重置为0,第二个消费者就会因为min-idle-time大与空闲时间而是失败。

命令使用:
目前未确认的消息

127.0.0.1:6379> xpending mystream mqGroup - + 10
1) 1) "1721185327505-0"
   2) "c2"
   3) (integer) 263196
   4) (integer) 1

id: 1721185327505-0
空闲时间:263196,单位ms
读取次数:1

将c2未处理的消息转移给c1。

127.0.0.1:6379> XCLAIM mystream mqGroup c1 3600000 1721185327505-0

查看未确认的消息
消息已经从c2转移给c1,IDLE重置,读取次数加1。转移之后就可以继续处理这条消息。

127.0.0.1:6379> xpending mystream mqGroup - + 10
1) 1) "1721185327505-0"
   2) "c1"
   3) (integer) 63196
   4) (integer) 2

通常转移操作的完整流程是:

  1. 先用xpending命令找出所有未确认的消息
  2. 再用xclaim命令转移所有未确认消息

XINFO

Stream提供了XINFO来实现对服务器信息的监控

查看队列信息

127.0.0.1:6379> xinfo stream mystream
 

消费组信息

127.0.0.1:6379> xinfo groups mystream

消费者组成员信息

127.0.0.1:6379> xinfo consumers mystream mqGroup

完整 Java 示例代码

生产者(Producer)
import redis.clients.jedis.Jedis;
import java.util.Map;

public class Producer {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        for (int i = 1; i <= 10; i++) {
            String messageId = jedis.xadd("mystream", null, Map.of(
                "sensor-id", String.valueOf(i),
                "temperature", String.valueOf(20 + i)
            ));
            System.out.println("Added message with ID: " + messageId);
        }
        
        jedis.close();
    }
}
消费者(Consumer)
import redis.clients.jedis.Jedis;
import redis.clients.jedis.StreamEntryID;
import redis.clients.jedis.StreamEntry;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;

public class Consumer {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        
        while (true) {
            List<Map.Entry<String, List<StreamEntry>>> streams = jedis.xreadGroup(
                "mygroup", "consumer1", 1, 0, false, new AbstractMap.SimpleEntry<>("mystream", StreamEntryID.UNRECEIVED

_ENTRY));
            
            if (streams != null && !streams.isEmpty()) {
                for (Map.Entry<String, List<StreamEntry>> stream : streams) {
                    for (StreamEntry entry : stream.getValue()) {
                        String id = entry.getID().toString();
                        Map<String, String> fields = entry.getFields();
                        
                        System.out.println("Processing message with ID: " + id);
                        fields.forEach((key, value) -> System.out.println(key + ": " + value));
                        
                        // 模拟消息处理
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                        
                        // 确认消息已处理
                        jedis.xack("mystream", "mygroup", new StreamEntryID(id));
                        System.out.println("Acknowledged message with ID: " + id);
                    }
                }
            }
        }
    }
}

通过这些示例代码和命令,您可以创建一个基于 Redis Streams 的消息队列系统,处理和管理实时数据流。

标签:读取,Stream,队列,Redis,id,消息,key,ID,mystream
From: https://blog.csdn.net/2401_85380274/article/details/140491343

相关文章

  • redistemplate介绍与演示
    redistemplate是一个用于在Redis中存储和检索数据的模板库。它提供了一组简单易用的函数和接口,使开发者能够更方便地使用Redis进行数据操作。使用redistemplate,你可以实现以下功能:存储和检索数据:redistemplate提供了存储和检索各种类型数据的函数,包括字符串、哈希表、列表、......
  • 69、Flink 的 DataStream Connector 之 Kafka 连接器详解
    1.概述Flink提供了Kafka连接器使用精确一次(Exactly-once)的语义在Kafkatopic中读取和写入数据。目前还没有Flink1.19可用的连接器。2.KafkaSourcea)使用方法KafkaSource提供了构建类来创建KafkaSource的实例。以下代码片段展示了如何构建KafkaSource来消......
  • Spring Boot+Redis 分布式锁:模拟抢单
    如何删除锁模拟抢单动作(10w个⼈开抢)jedis的nx⽣成锁对于java中想操作redis,好的⽅式是使⽤jedis,⾸先pom中引⼊依赖:<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency>对于分布式锁的⽣成通常......
  • redis学习-10(集群)
    数据部分rediscluster采用哈希分区规则,具体为虚拟槽分区,使用分散度好的哈希函数分到一个大范围的整数,每个节点负责一定数量的槽。slot=CRC16(key)&16383特点:解耦数据和节点之间的关系;节点自身维护槽的映射关系,不需要客户端和代理服务维护槽分区元数据;支持节点、槽、键之间的映......
  • Stream流求和
    Stream流对List<Object>和Set<Object>求和泛型为Integer、Long、Double、BigDecimal的求和使用reduce+orElseIntegersum=scores.stream().reduce(Integer::sum).orElse(0);Longsum=scores.stream().reduce(Long::sum).orElse(0L);Doublesum=scores.stream().r......
  • 异步任务队列
    #周朱张孙宋刘陈"胡王周朱谢周朱刘庄谢.黄"#周朱张孙宋刘陈"./宋周_胡王周朱谢周朱刘庄谢.黄"//#周朱张孙宋刘陈"孙周李袁王郭宋董陈朱.黄"#周朱张孙宋刘陈"赵陈罗曾庄朱罗.黄"#周朱张孙宋刘陈"欧陈朱刘陈郭姜邓曾.黄"#周朱张孙宋刘陈"邓周杨杨蒋胡赵.黄"#周朱张孙宋......
  • Redis - 数据结构与底层实现
    一、Redis数据结构Redis支持五种主要数据结构:字符串(String)、列表(List)、哈希表(Hashe)、集合(Set)和有序集合(SortedSet)。这些数据结构为开发者提供了灵活的数据操作方式,满足了不同场景下的数据存储需求。字符串(Strings):最基本的数据类型,可以包含任何数据,如数字、字符串、二进制数据......
  • 说说RabbitMQ延迟队列实现原理?
    使用RabbitMQ和RocketMQ的人是幸运的,因为这两个MQ自身提供了延迟队列的实现,不像用Kafka的同学那么苦逼,还要自己实现延迟队列。当然,这都是题外话,今天咱们重点来聊聊RabbitMQ延迟队列的实现原理,以及RabbitMQ实现延迟队列的优缺点有哪些?很多人知道使用RabbitMQ是可......
  • 【AI】DeepStream(16):deepstream_image_decode_app-MJPEG编解码器的使用
    【AI】AI学习目录汇总1、简介deepstream-test1:演示各种DeepStream插件构建GStreamer管道。从文件中获取视频、解码、批处理,然后进行对象检测,最后在屏幕上渲染框。deepstream_image_decode_app示例是在deepstream-test1示例之上,增加如下功能:在管道pipe中使用多个......
  • Redis实战篇之秒杀优化(基于黑马程序员Redis讲解视频总结)
    1.秒杀优化-异步秒杀思路我们来回顾一下下单流程当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤1、查询优惠卷2、判断秒杀库存是否足够3、查询订单4、校验是否是一人一单5、扣减库存6、创建订单在这六步操作中,又......