Redis Stream 消息队列的简单应用

27 篇文章 0 订阅
4 篇文章 0 订阅

Redis Stream 消息队列的简单应用

目标

借助Redis Stream构建一个简单消息队列有以下特点:

  1. 拥有一个不断增长的队列,但支持容量限制自动缩容,避免超出内存限制,并支持配置化
  2. 开启消息监听,支持 consumer group 监听队列
  3. 手动确认与消息重试机制,消息被消费并不出现问题时手动确认消息消费,如果出现异常则通过补偿机制对未处理完成队列(pending list)中的消息进行重试。

Redis Stream 基础概念介绍

基本数据结构

中间最核心的是一个基础的消息队列,队列中的每个节点都有自己的ID(key),这个ID可以用*来让redis自动生成递增ID可以自己实现,但必须保证递增。

周围则是通过消费组(Consumer Group)的形式来读取队列中的消息进行消费,消费组内部维护了一些数据,一个消费组对应多个消费者(Consumer) ,每个消费者都存储自身负责但为确认消费(acknowledge)的消息集合未决队列(Pending_ids).

  • Consumer Group

    消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer)。

  • last_delivered_id

    游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。

  • pending_ids

    消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。

Redis Stream因以下原因变得复杂:

  • 支持阻塞等待生产者向队列中录入新数据
  • 添加新的Consumer group 的概念,与Kafka中的概念相同目标都是允许一组客户消费同一个队列中的不同部分的消息

Stream 基础

如果但看Stream自身的数据结构则于List/Set/Zset等的基础结构非常相似,List中维护了一些复杂的阻塞API例如BLPOP等。因此在基础结构方面Stream并没有什么不同,但提供了更为复杂强大的API。

Stream是append only因此没有基础构造的命令只有XADD用于向流末尾附加数据,若流不存在则创建一个新的流结构。简单的添加命令:

redis5:5>XADD mystream * sensor-id 1234 temperature 19.8
"1618643385666-0"
redis5:5>XRANGE mystream - +
 1)    1)   "1618643385666-0"
  2)      1)    "sensor-id"
   2)    "1234"
   3)    "temperature"
   4)    "19.8"
redis5:5>XLEN mystream
"1"

使用XADD 添加流,第一个参数代表流,第二个参数*则代表让redis自动生成一个递增的序列,如果自己设置必须保证在流数据不断增加的过程中id是递增序列。

可使用XRANGE于XLEN来查看流的内容以及长度

Entry IDs

两部分组成

<millisecondsTime>-<sequenceNumber>

分别代表redis本地的毫秒时间以及当前毫秒内生成数据的序列

如果自定义XADD somestream 0-1 field value则代表最小时间是0,后续不会接收0开头时间戳的命令

从Stream获取数据

类似BLPOP命令,客户端可以阻塞等待新数据,但不同的是Stream支持fan out扇出数据到多个客户端。

但是阻塞访问并不常用。更常用的是单纯的最为一个时间排序的队列并按照时间范围获取数据。在Stream中不同消费者组可看到不同的stream子集,并进行不同的处理。

Stream支持三种查询方式:

范围查询:XRANGE and XREVRANGE

针对特定Stream进行范围查询,指定两个ID(开始/结束)即可

> XRANGE mystream - +
1) 1) 1518951480106-0
   2) 1) "sensor-id"
      2) "1234"
      3) "temperature"
      4) "19.8"
2) 1) 1518951482479-0
   2) 1) "sensor-id"
      2) "9999"
      3) "temperature"
      4) "18.2"

ID后半部分的序列可省略,则变为查询某个时间段的数据:

> XRANGE mystream 1518951480106 1518951480107
1) 1) 1518951480106-0
   2) 1) "sensor-id"
      2) "1234"
      3) "temperature"
      4) "19.8"

在末尾加上COUNT可限定查询条目数量

> XRANGE mystream - + COUNT 2
1) 1) 1519073278252-0
   2) 1) "foo"
      2) "value_1"
2) 1) 1519073279157-0
   2) 1) "foo"
      2) "value_2"

使用符号(可查询排除当前时间戳后的条目

> XRANGE mystream (1519073279157-0 + COUNT 2
1) 1) 1519073280281-0
   2) 1) "foo"
      2) "value_3"
2) 1) 1519073281432-0
   2) 1) "foo"
      2) "value_4"

Stream的查询效率是Olog(N)使用O(M)的效率返回元素,使用XRANGE自身就是一个支持迭代遍历的命令因此不需要XSCAN

可使用XREVRANGE返回stream的最后一个元素

> XREVRANGE mystream + - COUNT 1
1) 1) 1519073287312-0
   2) 1) "foo"
      2) "value_10"
XREAD 监听新元素

类似Redis的Pub/Sub以及阻塞队列,订阅到达Stream 队列的新元素,但与者两者不同的地方在于:

  1. 一个stream拥有多个客户(consumers)等待数据。每个新的数据实例(item),都将默认交付给队列中的每个消费者。这点与阻塞队列(blocking lists)不同,在阻塞队列中,每个consumer将获取一个不同的元素。然而,这个功能与Pub/Sub类似都分发给每个客户
  2. 在Pub/Sub模式中消息被触发后就遗忘,并且无法持久化(stored anyway)。在使用阻塞队列时客户端接收到一条消息,这条消息会从队列中弹出,而stream的工作方式则是根本不同的。所有消息(message)都会被无限制的附加到stream中(除非用户明确要求删除数据实例)。不同客户通过记住接收到的最后一条消息来了解什么是最新消息
  3. stream Consumer Groups提供了一个Pub/Sub或阻塞队列无法实现的控制级别。不同组对于同一流 有手动确认(XACK)的以处理列表,每个group 都只能看到自己的历史处理记录。
> XREAD COUNT 2 STREAMS mystream 0
1) 1) "mystream"
   2) 1) 1) 1519073278252-0
         2) 1) "foo"
            2) "value_1"
      2) 1) 1519073279157-0
         2) 1) "foo"
            2) "value_2"

非阻塞读取,读取指定Stream大于指定ID的消息

> XREAD BLOCK 0 STREAMS mystream $

阻塞性读取 0 代表永不过期,$代表从最后一个开始等待新到达的元素,类似tail -f

通过consumer group 访问流则通过XREADGROUP命令实现

XREAD除了COUNT和BLOCK之外没有其他选项,因此这是一个非常基本的命令,其特定目的是将使用者附加到一个或多个流。 使用使用者组API可以使用更强大的功能来使用流,但是通过消费者组进行读取是通过称为XREADGROUP的不同命令实现的。

Consumer groups

XREAD拥有简单的将消息扇出给不同客户端的能力,但如果N个不同的工作线程同时读取队列并拉取不同的消息处理则无法通过XREAD命令实现。

为实现这一点Stream提供了一个consumer group的概念,有以下特点;

  1. 每个消息都提供给不同consumer,因此用一个consumer group的同一个consumer无法获取两条一样的消息
  2. 由客户端提供区分大小写的consumer 唯一标识,这样即使暂时端开连接,下次监听读取时也可指导接下去要读取什么
  3. consumer group有从未被消费的第一个ID的概念,因此consumer请求消息时可返回 没交付给consumer group的消息
  4. 一条消息要用指定命令确认消费(XACK),然后消息将被移除consumer group
  5. consumer group 追踪每个consumer以处理但未确认的消息,因此每个consumer可只读取交付给自己的历史消息

一个consumer group可认为是stream的一组状态

+----------------------------------------+
| consumer_group_name: mygroup           |
| consumer_group_stream: somekey         |
| last_delivered_id: 1292309234234-92    |
|                                        |
| consumers:                             |
|    "consumer-1" with pending messages  |
|       1292309234234-4                  |
|       1292309234232-8                  |
|    "consumer-42" with pending messages |
|       ... (and so forth)               |
+----------------------------------------+

每个consumer group都有一个last_delivered_id 标识接下去进入consumer group的消息ID应大于该ID。

三个相关命令:

  • XGROUP

    创建、管理、销毁consumer group

  • XREADGROUP

    通过consumer group 读取stream

  • XACK

    消息确认

Creating a consumer group
> XGROUP CREATE mystream mygroup $
OK

通过CREATE并指定 stream 以及 consumer group的唯一标识来创建

通过$指定接收当前最大ID之后的消息,也可用0 代表从头开始接收

还支持自动创建不存在的流通过MKSTREAM

> XGROUP CREATE newstream mygroup $ MKSTREAM
OK

XREADGROUP也支持阻塞读,同时带有两个必须指定的参数:consumer group及consumer的唯一标识

> XREADGROUP GROUP mygroup Alice COUNT 1 STREAMS mystream >
1) 1) "mystream"
   2) 1) 1) 1526569495631-0
         2) 1) "message"
            2) "apple"

上述命令的 COUNT与XRREAD相同如果指定为0 则代表永久阻塞

特殊ID > 读取到目前为止为交付给指定consumer group的消息

如果不带COUNT的从0开始读取则可看到刚刚分配但未交付的信息

> XREADGROUP GROUP mygroup Alice STREAMS mystream 0
1) 1) "mystream"
   2) 1) 1) 1526569495631-0
         2) 1) "message"
            2) "apple"

使用XACK确认

> XACK mystream mygroup 1526569495631-0
(integer) 1
> XREADGROUP GROUP mygroup Alice STREAMS mystream 0
1) 1) "mystream"
   2) (empty list or set)

XACK:将处理过的信息移除能访问的历史记录

切换到另一个角色Bob来读取待消费记录

redis5:1>XREADGROUP GROUP mygroup Bob COUNT 2 STREAMS mystream >
 1)    1)   "mystream"
  2)      1)        1)     "1618458321143-0"
    2)          1)      "message"
     2)      "orange"


   2)        1)     "1618458325417-0"
    2)          1)      "message"
     2)      "strawberry"

可以看到同一个consumer group 中的不同 consumer会读取到同一个stream的不同的message,这样可通过XREADGROUP命令读取未处理的历史记录,或者标记message被处理过。这就提供了从stream中消费message的逻辑与语义的扩展。

请记住:

  • consumer 在第一次使用时自动创建
  • 可以对多个stream key读取但需要在不同stream中创建相同名的 consumer group
  • XREADGROUP是一种*写命令,*因为即使它从流中读取,consumer group也会因读取的副作用被修改(读取后可能确认消息导致group整体的pending list移动),因此只能在主实例上调用它。

一个consumer的实现逻辑 -by ruby

require 'redis'

if ARGV.length == 0
    puts "Please specify a consumer name"
    exit 1
end

ConsumerName = ARGV[0]
GroupName = "mygroup"
r = Redis.new

def process_message(id,msg)
    puts "[#{ConsumerName}] #{id} = #{msg.inspect}"
end

$lastid = '0-0'

puts "Consumer #{ConsumerName} starting..."
check_backlog = true
while true
    # Pick the ID based on the iteration: the first time we want to
    # read our pending messages, in case we crashed and are recovering.
    # Once we consumed our history, we can start getting new messages.
    if check_backlog
        myid = $lastid
    else
        myid = '>'
    end

    items = r.xreadgroup('GROUP',GroupName,ConsumerName,'BLOCK','2000','COUNT','10','STREAMS',:my_stream_key,myid)

    if items == nil
        puts "Timeout!"
        next
    end

    # If we receive an empty reply, it means we were consuming our history
    # and that the history is now empty. Let's start to consume new messages.
    check_backlog = false if items[0][1].length == 0

    items[0][1].each{|i|
        id,fields = i

        # Process the message
        process_message(id,fields)

        # Acknowledge the message as processed
        r.xack(:my_stream_key,GroupName,id)

        $lastid = id
    }
end

这里的消费动作是从历史记录开始的这样可以避免那些之前尝试消费但出现异常的message也可成功被消费。一个message只有被消费确认(acknowledgedXACK)才会被认为消费成功。而待处理消息列表(pending list)的存在则代表消息可被重复消费在一定程度上保证系统的稳定性。但这依赖于redis的持久化机制

message被消费完后列表置空,此时使用特殊ID>来消费新的message

Recovering from permanent failures

通过上述内容可了解参与consumer group的每个consumer都会被分到不同的message去处理。并且从故障中恢复后从pending list中读取待待处理的消息。然而在现实世界,consumers可能永久失败无法恢复,此时pendling message将会如何?

Redis的consumer group提供了一种处理此种情况的特性:将这些永远无法被消费的message改变其所有者并被重新分配给不同的consumer.

使用者检阅pending list并通过特定命令处理特定message,否则server将永久的将这些消息分配给旧consumer

第一步,使用XPENDING命令检阅consumer group的未决条目(pending entities)。这是一个只读命令,可被安全调用并不回改变消息所有权。

XPENDING 只读命令读取数据 两个参数 流名称合consumer group 名称 例如:XPENDING mystream mygroup

redis5:1>XPENDING mystream mygroup
 1)  "2"
 2)  "1618458321143-0"
 3)  "1618458325417-0"
 4)    1)      1)    "Bob"
   2)    "2"

可以看到XPENDING 列表的内容代表消息队列中被某个consumer group监控并且被读取但没有成功消费(未给某个message 发送XACK命令),所以上面说XGROUPREAD是一个写命令,因为这个命令确实会改变consumer group的某些信息,比如当前待读取的信息,以及读取后未处理的信息。

仅输出两个message 分别代表ID较低和较高的,最后则是consumers的列表以及他们拥有的未决消息数Bob有两条待处理消息Alice的单条消息被ACK过了.

通过XPENDING命令携带更多参数来看到更多信息。

XPENDING <key> <groupname> [<start-id> <end-id> <count> [<consumer-name>]]

通过start-id/end-id 可更详细了解情况也可使用-/+

0-0 后面的-0不知道啥含义,0-0就代表开头/所有

redis5:1>XPENDING mystream mygroup - + 10
 1)    1)   "1618458321143-0"
  2)   "Bob"
  3)   "4357478"
  4)   "1"

 2)    1)   "1618458325417-0"
  2)   "Bob"
  3)   "4357478"
  4)   "1"

现在是每条消息的更详细信息:

  1. ID
  2. consumer 名称
  3. 空闲时间,单位:milliseconds (自从上次将消息传递给某个使用者以来已过去的毫秒数) 4357478 ms ~= 1.2h
  4. 被交付数

注意:没人能阻止我们使用XRANGE函数来检查第一条消息的内容

redis5:1>XRANGE mystream 1618458321143 1618458321143
 1)    1)   "1618458321143-0"
  2)      1)    "message"
   2)    "orange"

XRANGE 重复两次ID即可了解某message的信息,假设20小时后Bob无法马上恢复并处理消息此时Alice替代Bob处理消息,此时可声明claim这条消息并且代替Bob恢复处理程序。可使用XCLAIM命令完成此动作

以下命令非常复杂,参数多以复制consumer groups的更改,但我们只需简单使用:

XCLAIM <key> <group> <consumer> <min-idle-time> <ID-1> <ID-2> ... <ID-N>

基本上,我们希望特定Stream key的特定组中更改message的所有权 并分配给指定的consumer。还提供了一个min-idle-time的参数来处理多个客户端尝试获取同一条message的情况

Client 1: XCLAIM mystream mygroup Alice 3600000 1526569498055-0
Client 2: XCLAIM mystream mygroup Lora 3600000 1526569498055-0

但是claim一条message会有副作用:

  1. 重置延迟时间(idle-time)
  2. 增加交付数量计数

因为这两条信息的变更,另一个客户端就无法获取message的使用权,因此避免了消息的重复处理(即使在正常情况不会出现同时处理)

命令执行后的情况:

redis5:1>XCLAIM mystream mygroup Alice 3600000 1618458321143
 1)    1)   "1618458321143-0"
  2)      1)    "message"
   2)    "orange"
redis5:1>XPENDING mystream mygroup
 1)  "2"
 2)  "1618458321143-0"
 3)  "1618458325417-0"
 4)    1)      1)    "Alice"
   2)    "1"

  2)      1)    "Bob"
   2)    "1"

命令成功被Alice获取(声明claim

CLAIM命令的副作用就是会返回该message,但这不是强制性的可使用JUSTID选项以便返回声明成功的消息ID。

该选项可减少交互带宽、或者不对message本体感兴趣实现某种方式扫描历史未决队列(pending list)。

claim可实现一个特性:扫描未决队列(pending list)并将未处理的message分配给另一个consumer。

活跃的消费者可通过STREAM的观察性功能来发现。这是下一章的主题。

Automatic claiming(待考究,6.2后新特性)

Claiming and the delivery counter

通过XPENDING输出获取的每个message的传递次数,两个影响因素:

  • CLAIM命令成功交付给其他consumer
  • XREADGROUP命令读取历史的未决信息(pending message)时

出现故障时,消息被多次传递,但最终都会被处理并确认(XACK)。然而一些特殊message会触发代码中的bug,此时会持续失败。因此有传递尝试的计数器,检测消息因某些原因无法正常处理的次数。因此一旦计数达到自己设定的最大值后可将其放入另一个队列,并给系统管理员发送信息。这是Redis STEAM处理死信(DEAD LETTER)概念的基本方式。

Streams observability

缺乏观察性的系统很难使用。不知道谁在消费消息、什么消息被挂起(pending)、stream中那些consumer group存活,将造成一些都不透明。

为了解决这个问题,STREAM拥有不同方式监控正在发生的事情。之前已经介绍了PENDING命令,它帮助检测正在执行的消息列表、空闲时间、传递次数。

然而想要了解更多消息则需要XINFO命令,该命令可与子命令一起使用观察STREAM以及consumer group

XINFO使用子命令来STREAM以及consumer group的不同状态信息,例如XINFO STREAM报告有关流本身的信息。

redis5:1>XINFO STREAM mystream
 1)  "length"
 2)  "8"
 3)  "radix-tree-keys"
 4)  "1"
 5)  "radix-tree-nodes"
 6)  "2"
 7)  "groups"
 8)  "2"
 9)  "last-generated-id"
 10)  "1618458335300-0"
 11)  "first-entry"
 12)    1)   "1618387989533-0"
  2)      1)    "name"
   2)    "sara"


 13)  "last-entry"
 14)    1)   "1618458335300-0"
  2)      1)    "message"
   2)    "banana"

输出包含:

  1. 流内部的编码方式
  2. 展示流的初始/最后 消息
  3. 输出的另一部分则包含consumer groupsstream关联的数量,一个流有多少个消费者组

如果要查询STREAM内consumer group的更多信息:

redis5:1>XINFO GROUPS mystream

 1)    1)   "name"
  2)   "group55"
  3)   "consumers"
  4)   "0"
  5)   "pending"
  6)   "0"
  7)   "last-delivered-id"
  8)   "0-0"

 2)    1)   "name"
  2)   "mygroup"
  3)   "consumers"
  4)   "2"
  5)   "pending"
  6)   "2"
  7)   "last-delivered-id"
  8)   "1618458325417-0"

如上所见XINFO命令输出了一系列字段值项。因为这是一个观察性命令,因此返回是符合人类阅读的,马上就能理解输出了什么,并且可通过额外的参数(field)获取更多信息,兼容旧客户端。其他效率更高的命令例如XPENDING仅返回值而不带字段(field)名称。

上面示例的输出(使用GROUPS)子命令,通过检测注册到consumer groupconsumers了解特定consumer group的状态信息。

redis5:1>XINFO CONSUMERS mystream mygroup
 1)    1)   "name"
  2)   "Alice"
  3)   "pending"
  4)   "1"
  5)   "idle"
  6)   "2599531"

 2)    1)   "name"
  2)   "Bob"
  3)   "pending"
  4)   "1"
  5)   "idle"
  6)   "10064891"

如果不记得命令语法可通过命令本身获取帮助

redis5:1>XINFO HELP
 1)  "XINFO <subcommand> arg arg ... arg. Subcommands are:"
 2)  "CONSUMERS <key> <groupname>  -- Show consumer groups of group <groupname>."
 3)  "GROUPS <key>                 -- Show the stream consumer groups."
 4)  "STREAM <key>                 -- Show information about the stream."
 5)  "HELP                         -- Print this help."

Differences with Kafka ™ partitions(待补充)

整体参考

程序设计

使用springboot 自带的redis交付组件

主要依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
		<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

配置

spring:
  application:
    name: redis
  redis:
    #默认配置 主
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
    timeout: 5000
    lettuce:
      pool:
        max-active: 100
        max-idle: 30
        min-idle: 20
        max-wait: 500
    # 从
    slave:
      host: 127.0.0.1
      port: 6379
      password:
      database: 0
      timeout: 5000
      lettuce:
        pool:
          max-active: 100
          max-idle: 30
          min-idle: 20
          max-wait: 500

自定义监听器

package priv.yuzuki.redis.config;


import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.stream.StreamListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Map;

/**
 * @program: redis
 * @author: wangzibai01
 * @create: 2021-04-12 15:47
 * @description:
 **/
@Component
@Slf4j
public class StreamMessageListener implements StreamListener<String, MapRecord<String, String, String>> {
// static final Logger LOGGER = LoggerFactory.getLogger(StreamMessageListener.class);

   @Resource
   RedisStreamConfig redisStreamConfig;

   @Resource
   StringRedisTemplate stringRedisTemplate;

   public static int count = 1;

   @Override
   public void onMessage(MapRecord<String, String, String> message) {

      // 消息ID
      RecordId messageId = message.getId();

      // 消息的key和value
      Map<String, String> body = message.getValue();

      log.info("stream message。messageId={}, stream={}, body={}", messageId, message.getStream(), body);

      // 通过RedisTemplate手动确认消息
//    PendingMessagesSummary pendingMessagesSummary = stringRedisTemplate.opsForStream().pending(redisStreamConfig.getStream(), redisStreamConfig.getConsumerGroup());

      try {
         // 模拟失败
         if (count++ %5 == 0){
            log.info("message = " + message);
            throw new RuntimeException("出错啦");
         }
         this.stringRedisTemplate.opsForStream().acknowledge(redisStreamConfig.getConsumerGroup(), message);
      } catch (RuntimeException e) {
         // todo 可发送邮件通知
         e.printStackTrace();
      }

//    stringRedisTemplate.opsForStream().createGroup()
   }
}

使用线程执行监听任务并注入监听器

package priv.yuzuki.redis.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ReadOffset;
import org.springframework.data.redis.connection.stream.StreamOffset;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamMessageListenerContainerOptions;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.ErrorHandler;

import javax.annotation.Resource;
import java.time.Duration;

/**
 * @program: redis
 * @author: wangzibai01
 * @create: 2021-04-12 15:43
 * @description: stream订阅
 **/
@Slf4j
@Component
public class StreamConsumerRunner implements ApplicationRunner, DisposableBean {
//	static final Logger LOGGER = LoggerFactory.getLogger(StreamConsumerRunner.class);

//	@Value("${redis.stream.consumer}")
//	private String consumer;

	@Resource
	RedisConnectionFactory redisConnectionFactory;

	@Resource
	ThreadPoolTaskExecutor threadPoolTaskExecutor;

	@Resource
	StreamMessageListener streamMessageListener;

	@Resource
	StringRedisTemplate stringRedisTemplate;

	@Resource
	RedisStreamConfig redisStreamConfig;

	private StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer;

	@Override
	public void run(ApplicationArguments args) throws Exception {
//		try {
//			StreamInfo.XInfoStream info = stringRedisTemplate.opsForStream().info(redisStreamConfig.getStream());
//		} catch (Exception e) {
//			stringRedisTemplate.opsForStream().add(redisStreamConfig.getStream(),new HashMap<String,String>());
//			e.printStackTrace();
//		}
//		stringRedisTemplate.opsForStream().createGroup(redisStreamConfig.getStream(),redisStreamConfig.getConsumerGroup());

		// 创建配置对象
		StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> streamMessageListenerContainerOptions = StreamMessageListenerContainerOptions
				.builder()
				// 一次性最多拉取多少条消息
				.batchSize(10)
				// 执行消息轮询的执行器
				.executor(this.threadPoolTaskExecutor)
				// 消息消费异常的handler
				.errorHandler(new ErrorHandler() {
					@Override
					public void handleError(Throwable t) {
						// throw new RuntimeException(t);
						t.printStackTrace();
					}
				})
				// 超时时间,设置为0,表示不超时(超时后会抛出异常)
				// 不能设置为0 如果设置为0则不断循环导致redis QPS过高,这是poll模式的延迟时间
				.pollTimeout(Duration.ZERO)
				// 序列化器
				.serializer(new StringRedisSerializer())
				.build();

		// 根据配置对象创建监听容器对象
		StreamMessageListenerContainer<String, MapRecord<String, String, String>> streamMessageListenerContainer = StreamMessageListenerContainer
				.create(this.redisConnectionFactory, streamMessageListenerContainerOptions);

		// 使用监听容器对象开始监听消费(使用的是手动确认方式)
		streamMessageListenerContainer.receive(Consumer.from(redisStreamConfig.getConsumerGroup(), redisStreamConfig.getConsumer()),
				StreamOffset.create(redisStreamConfig.getStream(), ReadOffset.lastConsumed()), this.streamMessageListener);

		this.streamMessageListenerContainer = streamMessageListenerContainer;
		// 启动监听
		this.streamMessageListenerContainer.start();

	}

	@Override
	public void destroy() throws Exception {
		this.streamMessageListenerContainer.stop();
	}
}

自动生产、补偿并携带自动缩容

	@Scheduled(cron = "0 0/1 * * * ?")
	@Async
	public void pendingHandler(){

		Consumer notifier = Consumer.from(redisStreamConfig.getConsumerGroup(), redisStreamConfig.getConsumer());
		List<PendingMessage> pendingMessageList = getPendingMessages(notifier);
		for (int i = 0; i < LIMIT_SIZE; i++) {
			if (!CollUtil.isEmpty(pendingMessageList)){
				for (PendingMessage pendingMessage : pendingMessageList) {
					try {
						// todo 消息消费
						log.info("pendingMessage = " + pendingMessage);
						RecordId messageId = pendingMessage.getId();
						Range<String> range = Range
								.from(Range.Bound.inclusive(messageId.toString()))
								.to(Range.Bound.inclusive(messageId.toString()));
						List<MapRecord<String, Object, Object>> mapRecordList = stringRedisTemplate.opsForStream().range(redisStreamConfig.getStream(), range);
						if (CollUtil.isEmpty(mapRecordList)){
							continue;
						}
						Map<Object, Object> map = mapRecordList.get(0).getValue();
						System.out.println("MessageMap = " + map);
						// 模拟失败
						if (count++ %5 == 0){
							throw new RuntimeException("pending handler 出错啦");
						}
						// 消费
						stringRedisTemplate.opsForStream().acknowledge(redisStreamConfig.getStream(),redisStreamConfig.getConsumerGroup(),pendingMessage.getId());
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
			// 重新获取pending list
			pendingMessageList = getPendingMessages(notifier);
		}
		if (CollUtil.isNotEmpty(pendingMessageList)){
			log.info("wrongList = " + pendingMessageList);
			throw new RuntimeException("pendingList 弥补出错 value:" + pendingMessageList);
		}

		// 容量大小判断
		Long size = stringRedisTemplate.opsForStream().size(redisStreamConfig.getStream());
		if (Objects.nonNull(size) && size>redisStreamConfig.getQueueSize()){
			// 缩容
			stringRedisTemplate.opsForStream().trim(redisStreamConfig.getStream(),redisStreamConfig.getQueueSize()/2);
		}



	}

完整代码 欢迎fork star

踩坑

  1. XREADGROUP 如何读取现在消费到哪里了,好像不行,设定上这个是一个写操作不是读操作,可以整体观测队列情况,使用XINFO
  2. 使用一个队列还是说重试多次失败后丢入另一个队列?重试失败可以是抛出异常的暂时无法修复的代码性异常,暂时再次丢入,如果出现了异常类,会有些message永远无法被消费?会的
  3. 如果开始redis中没有stream 会报错么,会的,需要手动创建/首次允许加判断,不存在则自动创建
  4. 推荐:spring boot 尽可能使用高版本,低版本中不支持Pending list相关功能
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值