SpringCloud Alibaba集成Spring Cloud Stream RocketMQ

RocketMQ介绍

RocketMQ是阿里巴巴旗下一款开源的MQ框架,Java编程语言实现,有非常好完整的生态系统。RocketMQ作为一款纯java、分布式、队列模型的开源消息中间件,支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。

消息队列的好处:

  • 异步
  • 解耦
  • 削峰填谷

RocketMQ架构

系统架构组成:

系统启动流程:

RocketMQ各组件

Producer:消息发布者,⽀持分布式集群部署。Produer 通过 MQ 负载均衡模块选择相应 Broker 中的 queue 进⾏消息投递,投递过程⽀持快速失败,并且低延迟。

Consumer:消息消费的⾓⾊,⽀持分布式集群⽅式部署。⽀持以push推,pull拉两种模式对消息进⾏消费。同时也⽀持集群⽅式和⼴播⽅式的消费,它提供实时消息订阅机制,可以满⾜⼤多数⽤户的需求。

NameServer:NameServer是⼀个⾮常简单的Topic路由注册中⼼,其⾓⾊类似Dubbo中的zookeeper,⽀持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供⼼跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和⽤于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从⽽进⾏消息的投递和消费。NameServer通常也是集群的⽅式部署,各实例间相互不进⾏信息通讯。Broker是向每⼀台NameServer注册⾃⼰的路由信息,所以每⼀个NameServer实例上⾯都保存⼀份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以动态感知Broker的路由的信息。

Broker:主要负责消息的存储、投递和查询以及服务⾼可⽤保证。

RocketMQ集群工作流程

启动NameServer,NameServer起来后监听端⼝,等待Broker、Producer、Consumer连上来,相当于⼀个路由控制中⼼。

Broker启动,跟所有的NameServer保持长连接,定时发送⼼跳包。⼼跳包中包含当前Broker信息(IP+端⼝等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic跟Broker的映射关系。

收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时⾃动创建Topic。

Producer发送消息,启动时先跟NameServer集群中的其中⼀台建⽴长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择⼀个队列,然后与队列所在的Broker建⽴长连接从⽽向Broker发消息。

Consumer跟Producer类似,跟其中⼀台NameServer建⽴长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建⽴连接通道,开始消费消息。

RocketMQ名词解释

Topic

消息主题,一级消息类型,通过 Topic 对消息进行分类。

Message

消息,消息队列中信息传递的载体。

Message ID

消息的全局唯一标识,由消息队列 MQ 系统自动生成,唯一标识某条消息。

Tag

消息标签,二级消息类型,用来进一步区分某个 Topic 下的消息分类。

Producer

消息生产者,也称为消息发布者,负责生产并发送消息。

Producer 实例

Producer 的一个对象实例,不同的 Producer 实例可以运行在不同进程内或者不同机器上。Producer 实例线程安全,可在同一进程内多线程之间共享。

Consumer

消息消费者,也称为消息订阅者,负责接收并消费消息。

Consumer 实例

Consumer 的一个对象实例,不同的 Consumer 实例可以运行在不同进程内或者不同机器上。一个 Consumer 实例内配置线程池消费消息。

Group

一类 Producer 或 Consumer,这类 Producer 或 Consumer 通常生产或消费同一类消息,且消息发布或订阅的逻辑一致。

Group ID

Group 的标识。

队列

每个 Topic 下会由一到多个队列来存储消息。每个 Topic 对应队列数与消息类型以及实例所处地域(Region)相关。

Exactly-Once 投递语义

Exactly-Once 投递语义是指发送到消息系统的消息只能被 Consumer 处理且仅处理一次,即使 Consumer 重试消息发送导致某消息重复投递,该消息在 Consumer 也只被消费一次。

集群消费

一个Topic里有3条消息,一个GroupId有3个consumer实例,每个实例平均分摊消费,每个consumer消费一条。

一个 Group ID 所标识的所有 Consumer 平均分摊消费消息。例如某个 Topic 有 9 条消息,一个 Group ID 有 3 个 Consumer 实例,那么在集群消费模式下每个实例平均分摊,只消费其中的 3 条消息。

广播消费

一个Topic里有三条消息,一个Group有3个consumer实例,每个实例各自消费3条消息,共计消费9条消息。

一个 Group ID 所标识的所有 Consumer 都会各自消费某条消息一次。例如某个 Topic 有 9 条消息,一个 Group ID 有 3 个 Consumer 实例,那么在广播消费模式下每个实例都会各自消费 9 条消息。

定时消息

Producer 将消息发送到消息队列 MQ 服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息。

延时消息

Producer 将消息发送到消息队列 MQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息。

事务消息

消息队列 MQ 提供类似 X/Open XA 的分布事务功能,通过消息队列 MQ 的事务消息能达到分布式事务的最终一致。

顺序消息

消息队列 MQ 提供的一种按照顺序进行发布和消费的消息类型,分为全局顺序消息和分区顺序消息。

全局顺序消息实际上是一种特殊的分区顺序消息,即Topic中只有一个分区,因此全局顺序和分区顺序的实现原理相同。因为分区顺序消息有多个分区,所以分区顺序消息比全局顺序消息的并发度和性能更高。

全局顺序消息

对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。

分区顺序消息

对于指定的一个 Topic,所有消息根据 Sharding Key 进行区块分区。同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。Sharding Key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Message Key 是完全不同的概念。

消息堆积

Producer 已经将消息发送到消息队列 MQ 的服务端,但由于 Consumer 消费能力有限,未能在短时间内将所有消息正确消费掉,此时在消息队列 MQ 的服务端保存着未被消费的消息,该状态即消息堆积。

消息过滤

Consumer 可以根据消息标签(Tag)对消息进行过滤,确保 Consumer 最终只接收被过滤后的消息类型。消息过滤在消息队列 MQ 的服务端完成。

消息轨迹

在一条消息从 Producer 发出到 Consumer 消费处理过程中,由各个相关节点的时间、地点等数据汇聚而成的完整链路信息。通过消息轨迹,您能清晰定位消息从 Producer 发出,经由消息队列 MQ 服务端,投递给 Consumer 的完整链路,方便定位排查问题。

重置消费位点

以时间轴为坐标,在消息持久化存储的时间范围内(默认 3 天),重新设置 Consumer 对已订阅的 Topic 的消费进度,设置完成后 Consumer 将接收设定时间点之后由 Producer 发送到消息队列 MQ 服务端的消息。

死信队列

死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列 MQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明 Consumer 在正常情况下无法正确地消费该消息。此时,消息队列 MQ 不会立刻将消息丢弃,而是将这条消息发送到该 Consumer 对应的特殊队列中。

消息队列 MQ 将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

消息路由

消息路由常用于不同地域之间的消息同步,保证地域之间的数据一致性。消息队列 MQ 的全球消息路由功能依托阿里云优质基础设施实现的高速通道专线,可以高效地实现国内外不同地域之间的消息同步复制。

Spring Cloud Stream介绍

微服务中会经常使用消息中间件,通过消息中间件在服务与服务之间传递消息,例如RabbitMQ、Kafka和RocketMQ,无论使用哪一种消息中间件和服务之间都有一点耦合性,这个耦合性指的是原来使用RabbitMQ,现在要替换为RocketMQ,我们的微服务改动比较大,因为两款消息中间件有一些区别,使用Spring Cloud Stream来整合我们的消息中间件,这样就可以降低微服务和消息中间件的耦合性,做到轻松在不同消息中间件之间切换,然而Spring Cloud Stream官方整合了消息中间件,Spring Cloud Alibaba写了个starter可以支持RocketMQ。

Spring Cloud Stream是一个构建消息驱动微服务的框架,Spring Cloud Stream解决了开发人员无感知的使用消息中间件的问题,因为Spring Cloud Stream对消息中间件的进一步封装,可以做到代码层面对消息中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为rocketmq或者kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。

Stream重要概念

Spring Cloud Stream 内部有几个概念:Binder Bindinginputoutput

在这里插入图片描述

1)Binder:跟外部消息中间件集成的组件,用来创建Binding,各消息中间件都有自己的Binder实现

例如:Kafka 的实现 KafkaMessageChannelBinder
   RabbitMQ 的实现 RabbitMessageChannelBinder
   RocketMQ 的实现 RocketMQMessageChannelBinder

2)Binding:包括InputBinding和OutputBinding

Binding在消息中间件与应用程序提供的Provider和Consumer之间提供了一个桥梁,实现了开发者只需使用应用程序的 Provider 或 Consumer 生产或消费数据即可,屏蔽了开发者与底层消息中间件的接触。

3)input:应用程序通过input(相当于消费者consumer)与Spring Cloud Stream 中Binder交互,而Binder负责与消息中间件交互,因此,我们只需关注如何与Binder交互即可,而无需关注与具体消息中间件的交互。

4)output:output(相当于生产者producer)与Spring Cloud Stream中Binder交互

组成 说明
Binder Binder是应用与消息中间件之间的封装,目前实现了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现
@Input 该注解标识输入通道,通过该输入通道接收消息进入应用程序
@Output 该注解标识输出通道,发布的消息将通过该通道离开应用程序
@StreamListener 监听队列,用于消费者的队列的消息接收
@EnableBinding 将信道channel和exchange、topic绑定在一起

集成RocketMQ

前提:安装RocketMQ,并能够正常启动

基本用法

1、引入相关的依赖

<!--boot web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--spring-cloud-starter-stream-rocketmq 2.2.7.RELEASE-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>

<!-- actuator 不引入会报错哦-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2、application.yml配置

说明:项目中的pom.xml如果只包含以上列举的这几个starter,并不是一个SpringCloud项目,不能使用bootstrap.yml配置文件。

server:
  port: 7788

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
      #bindings 具体生产消息、消费消息的桥梁
      bindings:
        #Produce Config
        output: # 查看org.springframework.cloud.stream.config.BindingProperties
          destination: test-topic #指定发送的topic
          contentType: text/plain #默认是application/json
          group: test-group
        #Consumer Config
        input:
          destination: test-topic
          contentType: text/plain #默认是application/json
          group: test-group


# 日志级别
logging:
  level:
    com.alibaba.cloud.stream.binder.rocketmq: info

3、发送消息

@Service
public class SenderService {

    /**
     * spring cloud stream里面发消息通过 Source 发送
     */
    @Autowired
    private Source source;

    /**
     * 发送消息的方法
     *
     * @param message
     */
    public void sendMessage(String message) {
        boolean send = source.output().send(MessageBuilder.withPayload(message).build());
        System.out.println(send);
    }
}

4、接收消息

@Service
public class ReceiveService {

    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private Sink sink;

    @StreamListener(value = Sink.INPUT)
    public void getListener(String message) {
        System.out.println("test-group=" + message);
    }
}

上面我们是直接使用字符串类型直接接收消息体。其实我们也可以这样接收:

@StreamListener(value = MySink.INPUT1)
public void listener(Message<byte[]> message) throws UnsupportedEncodingException {
    System.out.println("headers:" + message.getHeaders());
    byte[] payload = message.getPayload();
    System.out.println("接收到的消息:" + new String(payload, "utf-8"));
}

5、启动类

/**
 * 记得添加@EnableBinding注解指定消息信道
 * Sink.class 表示信道 input , Source.class 表示信道 output
 * 这两个分别与application.yml中的spring.cloud.stream.bindings.input和spring.cloud.stream.bindings.output对应
 * 如果信道名称换成是sender和receiver,就得配置spring.cloud.stream.bindings.sender和spring.cloud.stream.bindings.receiver
 */
@EnableBinding(value = {Sink.class, Source.class})
@SpringBootApplication
public class RocketMQApp {
    public static void main(String[] args) {
        SpringApplication.run(RocketMQApp.class, args);
    }
}

6、新建一个controller,测试验证消息的发送与接收

@RequestMapping("/message")
@RestController
public class MessageController {

    @Autowired
    private SenderService senderService;

    @RequestMapping("/senderService")
    public String sendNormalMsg(@RequestParam("message") String message) {
        senderService.sendMessage(message);
        return "发送普通消息成功, 流水号:" + UUID.randomUUID().toString().replaceAll("-", "");
    }
}

启动后,访问 http://localhost:7788/message/senderService?message=hello, 可以在控制台看到消息能够接收到,输出如下:

自定义信道

1)自定义Sink和Source

/**
 * 自定义Source
 */
public interface MySource {

    String OUTPUT1 = "output1";

    @Output(MySource.OUTPUT1)
    MessageChannel output1();

    String OUTPUT2 = "output2";

    @Output(MySource.OUTPUT2)
    MessageChannel output2();
}
/**
 * 自定义sink
 */
public interface MySink {

    String INPUT1 = "input1";

    @Input(MySink.INPUT1)
    SubscribableChannel input1();

    String INPUT2 = "input2";

    @Input(MySink.INPUT2)
    SubscribableChannel input2();
}

2)application.yml配置

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
      #bindings 具体生产消息、消费消息的桥梁
      bindings:
        # producer config
        output1:
          destination: test-topic1
          contentType: text/plain
          group: test-group1
        output2:
          destination: test-topic2
          contentType: text/plain
          group: test-group2
        # consumer config
        input1:
          destination: test-topic1
          contentType: text/plain
          group: test-group1
        input2:
          destination: test-topic2
          contentType: text/plain
          group: test-group2

3)启动类

@EnableBinding(value = {MySink.class, MySource.class})
@SpringBootApplication
public class RocketMQApp {
    public static void main(String[] args) {
        SpringApplication.run(RocketMQApp.class, args);
    }
}

4)消息发送

@Service
public class SenderService {

    /**
     * spring cloud stream里面发消息通过 Source 发送
     */
    @Autowired
    private MySource source;

    /**
     * 发送消息的方法
     *
     * @param message
     */
    public void sendMessage(String message) {
        boolean send1 = source.output1().send(MessageBuilder.withPayload(message).build());
        System.out.println("output1 result : " + send1);
        boolean send2 = source.output2().send(MessageBuilder.withPayload(message).build());
        System.out.println("output2 result : " + send2);
    }
}

5)消息接收

@Service
public class ReceiveService {

    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private MySink sink;

    @StreamListener(value = MySink.INPUT1)
    public void listener1(String message) {
        System.out.println("test-group1=" + message);
    }

    @StreamListener(value = MySink.INPUT2)
    public void listener2(String message) {
        System.out.println("test-group2=" + message);
    }
}

6)前端调用 http://localhost:7788/message/senderService?message=hello ,后台查看消息接受的日志输出

标签(Tag)过滤

1、发送消息

@Service
public class SenderTagService {

    /**
     * spring cloud stream里面发消息通过 Source 发送
     */
    @Autowired
    private MySource source;

    /**
     * 发送消息的方法
     *
     * @param msg
     * @param tag
     * @param <T>
     * @throws Exception
     */
    public <T> void sendObject(T msg, String tag) throws Exception {
        Message message = MessageBuilder.withPayload(msg)
                .setHeader(MessageConst.PROPERTY_TAGS, tag) //添加标签
                .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON) //内容类型
                .build();

        boolean flag = source.output2().send(message);
        System.out.println("sendObject==>" + flag);
    }
}

2、接收消息

@Service
public class ReceiveTagService {

    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private MySink sink;

    /**
     * 只接收myTag标签的消息
     * @param message
     * @param headers
     */
    @StreamListener(value = MySink.INPUT2, condition = "headers['ROCKET_TAGS'] == 'myTag'")
    public void listener2(String message, @Headers Map headers, @Header(name = "ROCKET_TAGS") String name) {
        System.out.println("headers=" + headers);
        System.out.println("name=" + name);
        System.out.println("test-group2=" + message);
    }
}

headers的key和value:

3、配置

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          input2: # 与 spring.cloud.stream.bindings.input2对应上
            consumer:
              subscription: myTag||look # 基于 Tag 订阅,多个 Tag 使用 || 分隔,默认为空
      #bindings 具体生产消息、消费消息的桥梁
      bindings:
        # producer config
        output2:
          destination: test-topic2
          contentType: text/plain #数据格式
          group: test-group2
        # consumer config
        input2:
          destination: test-topic2
          contentType: text/plain
          group: test-group2

4、测试验证

@Autowired
private SenderTagService senderTagService;

@RequestMapping(value = "/sendObject")
public String sendObject() throws Exception {
    JSONObject ss = new JSONObject();
    ss.put("name", "name");
    senderTagService.sendObject(ss, "myTag");
    return "ok";
}

消息监听的异常处理

代码如下:分为局部异常处理和全局异常处理

@Service
public class ReceiveService {

    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private MySink sink;

    /**
     * 接收消息
     *
     * @param message
     */
    @StreamListener(value = MySink.INPUT1)
    public void listener1(String message) {
        System.out.println("test-group1=" + message);
        throw new IllegalArgumentException("抛异常"); //模拟异常
    }

    /**
     * 局部异常处理
     * inputChannel格式:${topic-name}.{group-name}.errors
     * 分别对应我们在配置文件配置的destination和group
     *
     * @param message
     */
    @ServiceActivator(inputChannel = "test-topic1.test-group1.errors")
    public void handleError(ErrorMessage message) {
        Throwable throwable = message.getPayload();
        System.out.println("截获异常" + throwable);

        Message<?> originalMessage = message.getOriginalMessage();
        assert originalMessage != null;

        System.out.println("原始消息体 = " + new String((byte[]) originalMessage.getPayload()));
    }


    /**
     * 全局异常处理
     *
     * @param message
     */
    @StreamListener("errorChannel")
    public void error(Message<?> message) {
        ErrorMessage errorMessage = (ErrorMessage) message;
        System.out.println("Handling ERROR: " + errorMessage);
    }
}

异常捕获到了,但是message.getOriginalMessage()获取为空,如何获取原始消息体?

先说解决方案:

spring:
  cloud:
    stream:
      bindings:
        input1:
          consumer:
            maxAttempts: 1 #默认是3,1表示不重试

也就是说,关闭掉spring cloud stream默认的重试即可。

具体原因暂时不明,可能是Spring Cloud Alibaba Stream实现的一个小坑,重试和错误处理不能同时使用。

ACK(Acknowledgement)确认机制

RocketMQ是以consumer group+queue为单位是管理消费进度的,以一个consumer offset标记这个这个消费组在这条queue上的消费进度。

如果某已存在的消费组出现了新消费实例的时候,依靠这个组的消费进度,就可以判断第一次是从哪里开始拉取的。

每次消息成功后,本地的消费进度会被更新,然后由定时器定时同步到broker,以此持久化消费进度。

但是每次记录消费进度的时候,只会把一批消息中最小的offset值为消费进度值,如下图:

message ack

这种方式和传统的一条message单独ack的方式有本质的区别。性能上提升的同时,会带来一个潜在的重复问题——由于消费进度只是记录了一个下标,就可能出现拉取了100条消息,如 2101-2200的消息,后面99条都消费结束了,只有2101消费一直没有结束的情况。

在这种情况下,RocketMQ为了保证消息肯定被消费成功,消费进度只能维持在2101,直到2101也消费结束了,本地的消费进度才能标记2200消费结束了(注:consumerOffset=2201)。

在这种设计下,就有消费大量重复的风险。如2101在还没有消费完成的时候消费实例突然退出(机器断电,或者被kill)。这条queue的消费进度还是维持在2101,当queue重新分配给新的实例的时候,新的实例从broker上拿到的消费进度还是维持在2101,这时候就会又从2101开始消费,2102-2200这批消息实际上已经被消费过还是会投递一次。

对于这个场景,RocketMQ暂时无能为力,所以业务必须要保证消息消费的幂等性,这也是RocketMQ官方多次强调的态度。

实际上,从源码的角度上看,RocketMQ可能是考虑过这个问题的,截止到3.2.6的版本的源码中,可以看到为了缓解这个问题的影响面,DefaultMQPushConsumer中有个配置consumeConcurrentlyMaxSpan

/**
 * Concurrently max span offset.it has no effect on sequential consumption
 */
private int consumeConcurrentlyMaxSpan = 2000;

这个值默认是2000,当RocketMQ发现本地缓存的消息的最大值-最小值差距大于这个值(2000)的时候,会触发流控——也就是说如果头尾都卡住了部分消息,达到了这个阈值就不再拉取消息。

但作用实际很有限,像刚刚这个例子,2101的消费是死循环,其他消费非常正常的话,是无能为力的。一旦退出,在不人工干预的情况下,2101后所有消息全部重复!

使用Exactly-Once投递语义收发消息

Exactly-Once 是指发送到消息系统的消息只能被消费端处理且仅处理一次,即使生产端重试消息发送导致某消息重复投递,该消息也在消费端也只被消费一次。

Exactly-Once 语义是消息系统和流式计算系统中消息流转的最理想状态,但是在业界并没有太多理想的实现,因为真正意义上的 Exactly-Once 依赖消息系统的服务端、消息系统的客户端和用户消费逻辑这三者状态的协调,例如,当您的消费端完成一条消息的消费处理后出现异常宕机,而消费端重启后由于消费的位点没有同步到消息系统的服务端,该消息又可能被重复消费。

消息队列 RocketMQ 的 Exactly-Once 投递语义适用于“接收消息 -> 处理消息 -> 结果持久化到数据库”的流程,能够保证您的每一条消息消费的最终处理结果写入到您的数据库一次且仅一次,保证消息消费的幂等。

顺序消息

顺序消息包括全局顺序消息和局部顺序消息:

  • 全局顺序消息:全局顺序消息最大的特性就在于严格保证消息是按照生产者投递的顺序来消费的。所以其使用的是单分区来处理消息,用户不可自定义分区数,这种类型消息的性能较低。

  • 局部顺序消息:局部顺序消息相较于普通消息类型,多了一个局部有顺序的特性。即同一个分区下,其消费者在消费消息的时候,严格按照生产者投递到该分区的顺序进行消费。局部顺序消息在保证了一定顺序性的同时,保留了分区机制提升性能。但局部顺序消息不能保证不同分区之间的顺序。

普通发送的技术原理

普通消息发送就是并发式的发送和消费消息,这也是我们日常开发过程中最常用的方式,最常用的场景是系统间的异步解耦和流量的削峰填谷,这些场景下尽量保证消息高性能收发即可。

从普通消息与顺序消息的对比来看,普通消息在发送时选择消息队列的策略不同。普通消息发送选择队列有两种机制:轮询机制和故障规避机制(也称故障延迟机制)。默认使用轮询机制,一个Topic有多个队列,轮询选择其中一个队列。

轮询机制的原理是路由信息TopicPublishInfo中维护了一个计数器sendWhichQueue,每发送一次消息需要查询一次路由,计算器就进行“+1”,通过计数器的值index与队列的数量取模计算来实现轮询算法。

并发消费的技术原理

RocketMQ支持顺序消费和并发消费。并发消费是默认的消费方式,日常开发过程中最常用的方式,除了顺序消费就是并发消费。

并发消费也称为乱序消费,其原理是同一个消息队列提供给Consumer中的多个消费线程拉取消费。Consumer中会维护一个消费线程池,多个消费线程可以并发去同一个消息队列中拉取消息进行消费。如果某个消费线程在监听器中进行业务处理时抛出异常,当前消费线程拉取的消息会进行重试,不影响其他消费线程和消息队列的消费进度,消费成功的线程正常提交消费进度。

并发消费相比顺序消费没有资源争抢上锁的过程,消费消息的速度比顺序消费要快很多。

1、设置同步发送

spring:
  cloud:
    stream:
      rocketmq:
        bindings:
          output1:
            producer:
              sendType: Sync #同步发送

2、发送消息

@Service
public class SenderService {

    /**
     * spring cloud stream里面发消息通过 Source 发送
     */
    @Autowired
    private MySource source;

    /**
     * 发送消息的方法
     *
     * @param message
     */
    public void sendMessage(String message) {
        // 发送 3 条相同 id 的消息
        for (int i = 1; i <= 10; i++) {
            MessageDto messageDto = new MessageDto();
            messageDto.setId(String.valueOf(i));
            messageDto.setMessage(message);

            Message<MessageDto> payload = MessageBuilder.withPayload(messageDto)
                    .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
                    .build();
            boolean send1 = source.output1().send(payload);
            System.out.println("output1 result (第" + i + "条消息): " + send1);
        }

    }
}

3、设置顺序消费

spring:
  cloud:
    stream:
      rocketmq:
        bindings:
          output1:
            producer:
              sendType: Sync
          input1:
            consumer:
              push:
                orderly: true # 默认是并发消费,这里设置为顺序消费

4、消费消息

@Service
public class ReceiveService {

    private Logger log = LoggerFactory.getLogger(ReceiveService.class);

    @Value("${server.port}")
    private String port;

    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private MySink sink;

    /**
     * 接收消息
     *
     * @param message
     */
    @StreamListener(value = MySink.INPUT1)
    public void listener1(@Payload MessageDto message) {
        log.info("receiver port:{}, test-group1 -- [onMessage][线程编号:{} 消息内容:{}]", port, Thread.currentThread().getId(), message.toString());
    }

}

观察控制台(去掉一些无关紧要的):

(1)未设置顺序消费时

cid=1, timestamp=1652021586604}, queueId=0], queueOffset=229]

cid=2, timestamp=1652021587866}, queueId=1], queueOffset=227]

cid=3, timestamp=1652021587868}, queueId=2], queueOffset=212]

cid=4, timestamp=1652021587870}, queueId=3], queueOffset=227]

cid=5, timestamp=1652021587874}, queueId=0], queueOffset=230]

cid=6, timestamp=1652021587877}, queueId=1], queueOffset=228]

cid=7, timestamp=1652021587883}, queueId=2], queueOffset=213]

cid=8, timestamp=1652021587892}, queueId=3], queueOffset=228]

cid=9, timestamp=1652021587901}, queueId=0], queueOffset=231]

cid=10, timestamp=1652021587905},queueId=1], queueOffset=229]
-----------------------------------------------------------------------------
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='1', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='2', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='3', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='4', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='5', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='6', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='7', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='8', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='9', message='hello'}]
: receiver port:7788, test-group1 -- [onMessage][线程编号:146 消息内容:MessageDto{id='10', message='hello'}]

(2)设置顺序消费时

cid=1, timestamp=1652021130780}, queueId=0], queueOffset=226]

cid=2, timestamp=1652021131899}, queueId=1], queueOffset=224]

cid=3, timestamp=1652021131901}, queueId=2], queueOffset=210]

cid=4, timestamp=1652021131903}, queueId=3], queueOffset=225]

cid=5, timestamp=1652021131905}, queueId=0], queueOffset=227]

cid=6, timestamp=1652021131907}, queueId=1], queueOffset=225]

cid=7, timestamp=1652021131909}, queueId=2], queueOffset=211]

cid=8, timestamp=1652021131913}, queueId=3], queueOffset=226]

cid=9, timestamp=1652021131917}, queueId=0], queueOffset=228]

cid=10, timestamp=1652021131921},queueId=1], queueOffset=226]
------------------------------------------------------------
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='1', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='5', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='9', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='2', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='6', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='10', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='3', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='7', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='4', message='hello'}]
receiver port:7788, test-group1 -- [onMessage][线程编号:145 消息内容:MessageDto{id='8', message='hello'}]

从上面的输出我们可以看到:1、5、9的queueId=0;2、6、10的queueId=1;3、7、4的queueId=2;8的queueId=3;

相同的queueId都按照先进先出的规则顺序消费。

(顺序消息这里还是有点懵)

定时消息

消息在发送至服务端后,实际业务并不希望消费端马上收到这条消息,而是推迟到某个时间点被消费,这类消息统称为定时消息。

注意:https://cloud.tencent.com/document/product/1493/61583

开源 Apache RocketMQ 没有提供让用户自由设定延时时间的 API 的,TDMQ RocketMQ 版为了保证向开源 RocketMQ Client 的兼容,设计了一种通过添加 message 的 property 键值对来指定消息发送时间的方法。该方法只要在需要定时发送消息的 property 属性中增加 __STARTDELIVERTIME 属性值,就能在一定范围内(40天)实现该消息在任意时间的定时发送。延时消息则可以先通过计算得到定时发送的时间点,再以定时消息的形式发送。

延时消息

消息在发送至服务端后,实际业务并不希望消费端马上收到这条消息,而是推迟一段时间后再被消费,这类消息统称为延时消息。

实际上,延时消息可以看成是定时消息的一种特殊用法,其实现的最终效果和定时消息是一致的。

1、发送消息

@Service
public class SenderDelayService {


    private Logger log = LoggerFactory.getLogger(SenderDelayService.class);


    /**
     * spring cloud stream里面发消息通过 Source 发送
     */
    @Autowired
    private MySource source;


    /**
     * 延时消息发送
     *
     * @param message     延时消息体
     * @param delayLevel  延时级别 1~18 (1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 【 1=1s 2=5s 3=10s】)
     * @param consumerTag 消费者TAG标识 通过TAG区分消费对象
     * @param msgKey      消息key 可以通过该字段再次区分
     * @return
     */
    public boolean sendDelayMsg(String message, int delayLevel, String consumerTag, String msgKey) {
        // 构建消息
        Message<String> messageBuild = MessageBuilder.withPayload(message)
                .setHeader(MessageConst.PROPERTY_TAGS, consumerTag)
                .setHeader(MessageConst.PROPERTY_KEYS, msgKey)
                .setHeader(MessageConst.PROPERTY_DELAY_TIME_LEVEL, delayLevel)
                .build();
        // 发送消息
        boolean sendResult = source.output2().send(messageBuild);
        if (sendResult) {
            log.info("延时消息发送成功-ConsumerTag:{}-MsgKey:{}", consumerTag, msgKey);
        } else {
            log.error("延时消息发送失败!:tag={}, key={}", consumerTag, msgKey);
        }
        return sendResult;
    }

}

2、接收消息

@Service
public class ReceiveDelayService {

    private Logger log = LoggerFactory.getLogger(ReceiveDelayService.class);

    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private MySink sink;

    // 监听定时/延时消息通道,只允许key = delayMsg 通过
    @StreamListener(value = MySink.INPUT2, condition = "headers['ROCKET_KEYS'] == 'delayMsg'")
    public void receiveFixTimeMsg(String payResult, @Headers Map headers) {
        log.info("headers:" + headers);
        log.info("接收到延迟消息:{}", payResult);
    }
}

3、测试验证

@Autowired
private SenderDelayService senderDelayService;

@RequestMapping("/sendDelayMsg/{msg}")
public String sendDelayMsg(@PathVariable("msg") String msg) {
    senderDelayService.sendDelayMsg(msg, 2, "myTag", "delayMsg");
    return "SUCCESS";
}

事务消息

在分布式消息队列中,目前唯一提供完整的事务消息的,只有 RocketMQ 。

RocketMQ提供类似XA或Open XA的分布式事务功能,通过消息队列RocketMQ版事务消息,能达到分布式事务的最终一致。

事务消息交互流程如下图所示:

process

1、事务消息生产者配置

开启事务消息。

spring:
  cloud:
    stream:
      rocketmq:
        bindings:
          output1:
            producer:
              producerType: Trans  #普通消息、事务消息。RocketMQProducerProperties.ProducerType定义
              group: tranGroup #生产组,重要

2、事务消息监听接口实现

本地事务执行和回查。新版本的RocketMQ需要实现TransactionListener接口,重写executeLocalTransaction和checkLocalTransaction方法。

@Component
public class RocketMQTransactionListener implements TransactionListener {

    private Logger log = LoggerFactory.getLogger(RocketMQTransactionListener.class);

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info("=========本地事务开始执行=============");
        String message = new String(msg.getBody());
        log.info("原始消息体:{}", message);
        // 事务消息id,唯一
        String tid = msg.getTransactionId();
        log.info("事务消息id:{}", tid);

        //模拟执行本地事务begin=======
        //TODO 为了解决重复消息《==》幂等性,可以将tid放入redis,并设置状态,消费端,每次从redis中获取事务id,根据状态并判断是否被消费过
        /**
         * 本地事务执行会有三种可能
         * 1、commit 成功
         * 2、Rollback 失败
         * 3、网络等原因服务宕机收不到返回结果
         */
        log.info("本地事务执行参数,start......----------------------");
        //从消息 Header 中解析到 args 参数,并使用 JSON 反序列化
        Args args = JSON.parseObject(msg.getProperty("args"), Args.class);
        // ... local transaction process, return rollback, commit or unknown
        log.info("[executeLocalTransaction][执行本地事务,消息:{} args:{}]", msg, args);
        log.info("本地事务执行参数,end......------------------------");
        //TODO 一般我们会创建一张本地事务表,保存了包含事务id的一条记录,主要是回查的时候方便,只需要根据事务查询即可。
        //模拟执行本地事务end========
        //TODO 根据本地事务执行结果返回
        //LocalTransactionState.COMMIT_MESSAGE 二次确认消息,然后消费者可以消费
        //LocalTransactionState.ROLLBACK_MESSAGE 回滚消息,Broker端会删除半消息
        //LocalTransactionState.UNKNOW Broker端会进行回查消息
        return LocalTransactionState.UNKNOW;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        log.info("==========回查接口=========");
        // ... check local transaction status and return rollback, commit or unknown
        log.info("[checkLocalTransaction][回查消息:{}]", msg);
        // 事务消息id,唯一
        String tid = msg.getTransactionId();
        log.info("[checkLocalTransaction][事务消息id:{}]", tid);

        //TODO 1、必须根据key先去检查本地事务消息是否完成。
        /**
         * 因为有种情况就是:上面本地事务执行成功了,但是return LocalTransactionState.COMMIT_MESSAGE的时候
         * 服务挂了,那么最终 Brock还未收到消息的二次确定,还是个半消息 ,所以当重新启动的时候还是回调这个回调接口。
         * 如果不先查询上面本地事务的执行情况 直接在执行本地事务,那么就相当于成功执行了两次本地事务了。
         */
        // TODO 2、这里返回要么commit 要么rollback。没有必要再返回 UNKNOW
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

那如何执行该监听器的两个方法呢?

RocketMQ给我们提供了一个transactionListener配置项,用于配置Spring Bean,如下:

spring:
  cloud:
    stream:
      rocketmq:
        bindings:
          output1:
            producer:
              producerType: Trans  #普通消息、事务消息。RocketMQProducerProperties.ProducerType定义
              transactionListener: rocketMQTransactionListener #Spring Bean, 事务监听器,需要实现TransactionListener接口
              group: tranGroup #生产组,重要

3、事务消息消费者

@Service
public class ReceiveTransService {

    private Logger log = LoggerFactory.getLogger(ReceiveTransService.class);


    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private MySink sink;

    /**
     * 接收消息
     *
     * @param message
     */
    @StreamListener(value = MySink.INPUT1)
    public void listener1(Message<byte[]> message) throws UnsupportedEncodingException {
        //TODO 消息消费需要考虑幂等性
        byte[] payload = message.getPayload();
        String msg = new String(payload, "utf-8");
        log.info("消费消息:内容={}, headers={}", msg, message.getHeaders());
    }

}

4、发送事务消息

@Service
public class SendTransService {

    private Logger log = LoggerFactory.getLogger(SendTransService.class);

    /**
     * spring cloud stream里面发消息通过 Source 发送
     */
    @Autowired
    private MySource source;

    /**
     * 发送消息的方法
     *
     * @param message
     */
    public void sendMessage(String message) {
        MessageDto messageDto = new MessageDto();
        messageDto.setId(UUID.randomUUID().toString());
        messageDto.setMessage(message);

        Args args = new Args().setArgs1(1).setArgs2("2");

        Message<MessageDto> payload = MessageBuilder.withPayload(messageDto)
                //header有妙用.......
                //因为 Spring Cloud Stream 在设计时,并没有考虑事务消息,所以我们只好通过 Header 传递参数。
                .setHeader("args", JSON.toJSONString(args))
                .build();
        boolean send1 = source.output1().send(payload);
        log.info("发送事务消息:{}", send1);
    }
}

实体Args.java:

public class Args {

    private Integer args1;

    private String args2;

    public Integer getArgs1() {
        return args1;
    }

    public Args setArgs1(Integer args1) {
        this.args1 = args1;
        return this;
    }

    public String getArgs2() {
        return args2;
    }

    public Args setArgs2(String args2) {
        this.args2 = args2;
        return this;
    }

    @Override
    public String toString() {
        return "Args{" +
                "args1=" + args1 +
                ", args2='" + args2 + '\'' +
                '}';
    }
}

测试发送事务消息:

@Autowired
private SendTransService sendTransService;

@RequestMapping("/sendTransMsg/{msg}")
public String sendTransMsg(@PathVariable("msg") String msg) {
	sendTransService.sendMessage(msg);
	return "SUCCESS";
}

消息重投

生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在 RocketMQ 中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer 负载变化也会导致重复消息。如下方法可以设置消息重试策略:

  • retryTimesWhenSendFailed:同步发送失败重投次数,默认为 2,因此生产者会最多尝试发送 retryTimesWhenSendFailed + 1 次。不会选择上次失败的broker,尝试向其他 broker 发送,最大程度保证消息不丢。超过重投次数,抛出异常,由客户端保证消息不丢。当出现 RemotingException、MQClientException 和部分 MQBrokerException 时会重投。
  • retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他 broker,仅在同一个 broker 上做重试,不保证消息不丢。
  • retryAnotherBroker:消息刷盘(主或备)超时或 slave 不可用(返回状态非 SEND_OK),是否尝试发送到其他 broker,默认 false。十分重要消息可以开启。

注意:只有普通消息具有发送重试机制,顺序消息是没有的。

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
           output1:
             producer:
               #同步发送失败策略,默认重试次数2
               retryTimesWhenSendFailed: 3
               #异步发送失败策略,默认重试次数2
               retryTimesWhenSendAsyncFailed: 3
               #消息刷盘失败策略,默认false
               retryAnotherBroker: true

消息重试

Consumer 消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer 消费消息失败通常可以认为有以下几种情况:

  • 由于消息本身的原因,例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99%也不成功,所以最好提供一种定时重试机制,即过 10 秒后再重试。
  • 由于依赖的下游应用服务不可用,例如 db 连接不可用,外系统网络不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力。

当然,RocketMQ 并不会无限重新投递消息给 Consumer 重新消费,而是在默认情况下,达到 16 次重试次数时,Consumer 还是消费失败时,该消息就会进入到死信队列

死信队列用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。

RocketMQ 将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在 RocketMQ 中,可以通过使用 console 控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。

每条消息的失败重试,是有一定的间隔时间。实际上,消费重试是基于延迟消息来实现,第一次重试消费按照延迟级别为 3 开始。所以,默认为 16 次重试消费,直到延迟级别为18。一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。

不过要注意,只有集群消费模式下,才有消息重试。

1、suspendCurrentQueueTimeMillis

同步消费(顺序消息)消息模式下消费失败后再次消费的时间间隔。 默认值:1000 ms

顺序消息的重试是无休止的,不间断的,直至消费成功,所以,对于顺序消息的消费, 务必要保证应用能够及时监控并处理消费失败的情况,避免消费被永久性阻塞。

顺序消息没有发送失败重试机制,但具有消费失败重试机制。

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          input2: # 与 spring.cloud.stream.bindings.input2对应上
            consumer:
              subscription: myTag||look # 基于 Tag 订阅,多个 Tag 使用 || 分隔,默认为空
              push:
                suspendCurrentQueueTimeMillis: 100

2、maxReconsumeTimes

无序消息(包括普通消息、延时消息、定时消息和事务消息)的最大重试次数可通过自定义参数 maxReconsumeTimes 取值进行配置。默认值为 16 次,该参数取值无最大限制,建议使用默认值。

间隔时间根据重试次数阶梯变化,取值范围:1 秒~2 小时。不支持自定义配置。

若最大重试次数小于等于 16 次,则间隔时间按照无序消息重试间隔时间阶梯变化。若最大重试次数大于 16 次,则超过 16 次的间隔时间均为 2 小时。

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          input2: # 与 spring.cloud.stream.bindings.input2对应上
            consumer:
              subscription: myTag||look # 基于 Tag 订阅,多个 Tag 使用 || 分隔,默认为空
              push:
                maxReconsumeTimes: 5

3、delayLevelWhenNextConsume

异步消费消息模式下消费失败重试策略:

  •  -1:不重复,直接放入死信队列
  • 0:RocketMQ Broker 控制重试策略
  • >0:RocketMQ Consumer 控制重试策略

可能对 Broker 和 Consumer 控制重试策略有点懵,每次消息首次消费失败时,Consumer 会发回给 Broker,并告诉 Broker 按照什么延迟级别开始,不断重新投递给 Consumer 直到消费成功或者到达最大延迟级别。

举个例子,如果这里我们设置了delayLevelWhenNextConsume配置项为 18,则 2 小时后 Broker 会投递该消息给 Consumer 进行重新消费。

一般情况下,我们设置 delayLevelWhenNextConsume配置项为 0 即可,使用 Broker 控制重试策略即可。

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          input2: # 与 spring.cloud.stream.bindings.input2对应上
            consumer:
              subscription: myTag||look # 基于 Tag 订阅,多个 Tag 使用 || 分隔,默认为空
              push:
                delayLevelWhenNextConsume: 0

4、maxAttempts

对于 maxAttempts 配置项,每次拉取到消息到本地时,如果消费重试,本地重试的最大总次数(包括第一次)。这个是 Spring Cloud Stream 提供的通用消费重试功能,是 Consumer 级别的,而 RocketMQ 提供的独有消费重试功能,是 Broker 级别的。

因为 Spring Cloud Stream 提供的重试间隔,是通过 sleep 实现,会占掉当前线程,影响 Consumer 的消费速度,所以这里并不推荐使用,因此设置maxAttempts 配置项为 1,禁用 Spring Cloud Stream 提供的重试功能,使用 RocketMQ 提供的重试功能。

spring:
  cloud:
    stream:
      #bindings 具体生产消息、消费消息的桥梁
      bindings:
        # consumer config
        input2:
          destination: test-topic2
          contentType: text/plain
          group: test-group2
          consumer:
            maxAttempts: 1
建议:如果无法保证消费重试不会带来副作用,也就是说无法保证消费的幂等性,建议关闭消费重试功能,即设置delayLevelWhenNextConsume配置项为 -1,maxAttempts 配置项为 1。

死信队列(DLQ队列)

当一条消息初次消费失败,消息队列会自动进行消费重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。

正常情况下无法被消费的消息称为 死信消息(Dead-Letter Message),存储死信消息的特殊队列称为 死信队列(Dead-Letter Queue)。

对于 无序消息集群消费 下的重试消费,默认允许每条消息最多重试 16 次,如果消息重试 16 次后仍然失败,消息将被投递至 死信队列。

特征:

  • 不会再被消费者正常消费
  • 有效期与正常消息相同,均为 3 天,3 天后会被自动删除

特性:

  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。名称为 %DLQ%consumerGroup@consumerGroup
  • 如果一个 Group ID 未产生死信消息,则不会为其创建相应的死信队列
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个Topic

消费分区

通过引入消费组的概念,我们已经能够在多实例的清况下,保障每个消息只被组内的一个实例消费。但是消费组无法控制消息具体被哪个实例消费。也就是说,对于同一条消息,它多次到达之后可能是由不同的实例进行消费的。但是对于一些业务场景,需要对一些具有相同特征的消息设置每次都被同一个消费实例处理。

消息分区的引入就是为了解决这样的问题:当生产者将消息数据发送给多个消费者实例时,保证拥有共同特征的消息数据始终是由同一个消费者实例接收和处理。

1、消费者分区

spring:
  cloud:
    stream:
      instance-count: 2
      instance-index: 0 #0、1、2......
      bindings:
        input1:
          consumer:
            partitioned: true
  • spring.cloud.stream.bindings.input1.consumer.partitioned = true 开启消费者分区功能。
  • spring.cloud.stream.instance-count = 2 当前消费者的总实例个数,即应用程序部署的实例数量。
  • spring.cloud.stream.instance-index = 0 当前实例的索引号,从 0 开始,最大为 -1 。用于消息生产的时候锁定该实例,不同的实例(应用)索引号不同。(消息生产的时候 "hashCode(key) % partitionCount" 的计算值等于该设置的值,即转发到该实例上)

2、生产者分区

spring:
  cloud:
    stream:
      bindings:
        output1:
          producer:
            partitionCount: 2
            partitionKeyExtractorName: keyStrategy
  • spring.cloud.stream.bindings.output1.producer.partitionCount = 2,消息生产需要广播的消费者数量。即消息分区的数量。
  • spring.cloud.stream.bindings.output1.producer.partitionKeyExtractorName = keyStrategy ,Spring Bean —— 用来消息的特征值计算。
@Component
public class KeyStrategy implements PartitionKeyExtractorStrategy {
    
    @Override
    public Object extractKey(Message<?> message) {
        return message.getPayload();
    }
}

(分区选择计算规则为 "hashCode(key) % partitionCount" , 这里的 key 根据 partitionKeyExpression 或 partitionKeyExtractorName 的配置计算得到)

spring.cloud.stream.bindings.output1.producer.partitionKeyExpression: 分区 key 表达式。该表达式基于 Spring EL,从消息中获得分区 key。例如:

spring:
  cloud:
    stream:
      bindings:
        output1:
          producer:
            partitionCount: 2
            partitionKeyExpression: payload.id #指定消息体中的id作为sharding key

 3、演示

 1)定义一个消息对象

public class MessageDto implements Serializable {
    private String id;
    private String message;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "MessageDto{" +
                "id='" + id + '\'' +
                ", message='" + message + '\'' +
                '}';
    }
}

2)发送消息

@Service
public class SenderService {

    /**
     * spring cloud stream里面发消息通过 Source 发送
     */
    @Autowired
    private MySource source;

    /**
     * 发送消息的方法
     *
     * @param message
     */
    public void sendMessage(String message) {
        for (int i = 1; i <= 10; i++) {
            MessageDto messageDto = new MessageDto();
            messageDto.setId(String.valueOf(i)); //配置文件中指定id作为实例
            messageDto.setMessage(message);

            Message<MessageDto> payload = MessageBuilder.withPayload(messageDto)
                    .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
                    .build();
            boolean send1 = source.output1().send(payload);
            System.out.println("output1 result (第" + i + "条消息): " + send1);
        }

    }
}

 3)接收消息

@Service
public class ReceiveService {

    private Logger log = LoggerFactory.getLogger(ReceiveService.class);

    @Value("${server.port}")
    private String port;

    /**
     * spring cloud stream里面通过 Sink 接收消息
     */
    @Autowired
    private MySink sink;

    /**
     * 接收消息
     *
     * @param message
     */
    @StreamListener(value = MySink.INPUT1)
    public void listener1(@Payload MessageDto message) {
        log.info("receiver port:{}, test-group1 -- [onMessage][线程编号:{} 消息内容:{}]", port, Thread.currentThread().getId(), message.toString());
    }

}

4)用不同的端口启动两个应用实例,然后调用其中一个应用实例的接口来发送消息,观察两个实例的控制台输出。

 

 

三种消息发送类型

消息队列RocketMQ提供三种方式来发送普通消息:同步(Sync)发送、异步(Async)发送和单向(Oneway)发送。

消息发送类型的逻辑控制入口:com.alibaba.cloud.stream.binder.rocketmq.integration.outbound.RocketMQProducerMessageHandler#send

同步发送

同步发送是指消息发送方发出一条消息后,会在收到服务端同步响应之后才发下一条消息的通讯方式。

一般业务场景下,使用同步发送消息较多。

sync

此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等

我们在配置文件添加sendType配置即可:

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          output1:
            producer:
              sendType: Sync # 同步发送消息,默认是异步的。消息发送类型由RocketMQProducerProperties.SendType定义

异步发送

异步发送是指发送方发出一条消息后,不等服务端返回响应,接着发送下一条消息的通讯方式。消息队列RocketMQ版的异步发送,需要您实现异步发送回调接口(SendCallback)。消息发送方在发送了一条消息后,不需要等待服务端响应即可发送第二条消息。发送方通过回调接口接收服务端响应,并处理响应结果。

async

异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景,例如,您视频上传后通知启动转码服务,转码完成后通知推送转码结果等。

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          output1:
            producer:
              sendType: Async # 异步发送消息。消息发送类型由RocketMQProducerProperties.SendType定义

OneWay发送

发送方只负责发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。

oneway

适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          output1:
            producer:
              sendType: OneWay # 单向发送消息。消息发送类型由RocketMQProducerProperties.SendType定义

两种消息消费模式

RocketMQ的消息模式由org.apache.rocketmq.common.protocol.heartbeat.MessageModel定义:BROADCASTING(广播消费)、CLUSTERING(集群消费)。

在com.alibaba.cloud.stream.binder.rocketmq.integration.inbound.RocketMQConsumerFactory中,无论是在initPushConsumer方法或initPullConsumer方法中,我们都能看到:

consumer.setMessageModel(getMessageModel(consumerProperties.getMessageModel()));
private static MessageModel getMessageModel(String messageModel) {
	for (MessageModel model : MessageModel.values()) {
		if (model.getModeCN().equalsIgnoreCase(messageModel)) {
			return model;
		}
	}
	return MessageModel.CLUSTERING;
}

可以看出默认是集群消费。

我们只需要在配置文件中添加messageModel的配置就可以完成消费模式的切换,如下所示:

spring:
  cloud:
    stream:
      #RocketMQ 通用配置
      rocketmq:
        binder:
          #客户端接入点,必填  rocketMQ的连接地址,binder高度抽象
          name-server: localhost:9876
          # 不加, 会报错:Property 'group' is required - producerGroup
          group: rocketmq-group
        bindings:
          input2: # 与 spring.cloud.stream.bindings.input2对应上
            consumer:
              subscription: myTag||look # 基于 Tag 订阅,多个 Tag 使用 || 分隔,默认为空
          input1:
            consumer:
              messageModel: BROADCASTING # 消费模式,由MessageModel定义

演示步骤:

1)server.port设置成随机端口

server:
  port: ${random.int[10000,19999]}  #${random.int[10000,19999]} # 随机端口,方便启动多个消费者

2)idea开发工具,新增一个RocketMQApp2,Main class与RocketMQApp是一样的

3)分别启动,控制台上查看生成的随机端口

4)调用其中一个接口模拟发送消息,观察两边的控制台,可以看到两边的控制台都有消息的输出日志打印:

说明消费模式设置为广播模式生效了。

集群消费

image-20211109230939135

集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊同一个Topic的消息。即每条消息只会被发送到Consumer Group中的某个Consumer。

广播消费

image-20211109230903858

广播消费模式下,相同Consumer Group的每个Consumer实例都接收同一个Topic的全量消息。即每条消息都会被发送到Consumer Group中的每个Consumer。

MessageBuilder等常用API

org.springframework.integration.support.MessageBuilder

在Spring Integration中,一般情况下,不会直接操作这些类来创建Message。通常使用org.springframework.integration.support.MessageBuilder来生成一些message的对象。

我们可以直接使用要传输的数据创建Message对象如下,使用一个字符串“Hello,world!”来创建一个Message:

MessageBuilder提供了一些方法来设置header如:

另外,MessageBuilder也可以直接通过一个已存在的Message对象来创建新的Message对象,设置Header,如:

@StreamListener等常用注解

@StreamListener

1)condition 参数作为条件设定,其支持 SpEL 表达式,通过条件约束即可满足我们的需求。例如:

/*********************************实现多个监听者应用******************************/
@StreamListener(value = MySink.USER_CHANNEL, condition = "headers['flag']=='aa'")
public void userReceiveByHeader1(@Payload User user) {
    LOGGER.info("Received from {} channel : {} with head (flag:aa)", MySink.USER_CHANNEL, user.getUsername());
}
 
@StreamListener(value = MySink.USER_CHANNEL, condition = "headers['flag']=='bb'")
public void userReceiveByHeader2(@Payload User user) {
    LOGGER.info("Received from {} channel : {} with head (flag:bb)", MySink.USER_CHANNEL, user.getUsername());
}

@Payload

内容为消息体

@Headers

获取所有的Header头信息,是一个Map

@Header

获取指定name的头信息。

@SendTo

@Component
public class ReceiveListener {

    @StreamListener("MQRece")
    @SendTo("back-push")
    public byte[] receive(byte[] bytes){
        log.info("接受消息:"+new String(bytes));
        return "ok".getBytes();
    }

}

配置:

spring.cloud.stream.bindings.back-push.destination=back-topic
spring.cloud.stream.bindings.back-push.group=back-group

Stream & RocketMQ常用配置

Spring Cloud Stream 和RocketMQ都提供了很多配置项,接下来我们就来总结下。

特别注意:

下面所描述的配置项或属性仅仅是基于spring-cloud-starter-stream-rocketmq 2.2.7.RELEASE 进行总结,跟实际的开源的rocketmq版本里面的属性还是有出入的,而且不同的版本,可能也会不一样,使用的时候请注意。

spring:
  cloud:
    # Spring Cloud Stream 配置项,对应 BindingServiceProperties 类
    stream:
      # Binding 配置项,对应 BindingProperties Map
      bindings:
        output1: #provider 生产者
          destination: test-topic # 目的地。这里使用 RocketMQ Topic
          content-type: application/json # 内容格式。这里使用 JSON
          group: test-group # 生产者分组
          # producer生产者配置, 对应 ProducerProperties 类
          producer:
            partitionCount: 2 #消息生产需要广播的消费者数量。即消息分区的数量
            partitionKeyExpression: payload.id #分区 key 表达式。该表达式基于 Spring EL,从消息中获得分区 key
        input1: #consumer 消费者
          destination: test-topic # 目的地。这里使用 RocketMQ Topic
          content-type: application/json # 内容格式。这里使用 JSON
          group: test-group # 消费者分组
          # consumer消费者配置, 对应 ConsumerProperties 类
          consumer:
            partitioned: true #开启消费者分区功能

      # Spring Cloud Stream RocketMQ 配置项
      rocketmq:
        # RocketMQ Binder 配置项,对应 RocketMQBinderConfigurationProperties 类
        binder:
          name-server: 101.133.227.13:9876 # RocketMQ Namesrv 地址
          group: rocketmq-group # 分组,必须的,全局唯一,具体用户暂不明
        # RocketMQ 自定义 Binding 配置项,对应 RocketMQExtendedBindingProperties Map
        bindings:
          output1:
            # RocketMQ Producer 配置项,对应 RocketMQProducerProperties 类
            producer:
              sendMsgTimeout: 3000 #发送消息超时时间
              producerType: Trans #消息类型,分为普通消息和事务消息,由 RocketMQProducerProperties.ProducerType 定义
              sendType: Sync #消息发送的类型,默认是同步的,分为同步、异步、单向,由 RocketMQProducerProperties.SendType 定义
          input1:
            # RocketMQ Consumer 配置项,对应 RocketMQConsumerProperties 类
            consumer:
              enabled: true # 是否开启消费,默认为 true
              subscription: myTag||look # 基于 Tag 订阅,多个 Tag 使用 || 分隔,默认为空
              messageModel: CLUSTERING # 消息消费模式,分为集群消费和广播消费,默认是集群消费,由 MessageModel 定义
              push:
                orderly: true # 是否顺序消费,默认为 false 并发消费。
      instance-count: 2 #当前消费者的总实例个数,即应用程序部署的实例数量。
      instance-index: 0 #0、1、2......当前实例的索引号,从 0 开始,最大为 -1 。用于消息生产的时候锁定该实例,不同的实例(应用)索引号不同

RocketMQProducerProperties

1)sendMsgTimeout

发送超时时间,默认是3000,单位毫秒。

对于异步发送,超时后会进入回调的onException,对于同步发送,超时则会得到一个RemotingTimeoutException。

2)compressMsgBodyThreshold

消息body需要压缩的阈值。默认值是1024 * 4,即4K。

3)retryTimesWhenSendFailed

同步发送失败则重试。默认值是2,即重试2次,总共执行3次。

注:可能会导致消息重复投递。

4)retryTimesWhenSendAsyncFailed

异步发送失败则重试。默认值是2,即重试2次,总共执行3次。

5)retryAnotherBroker

发送失败,是否重试其他broker。默认值是false。

6)maxMessageSize

允许发送的最大消息体大小。默认值:1024 * 1024 * 4,4M。

7)producerType

生产者类型,分为普通生产者和事务生产者,由 com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQProducerProperties.ProducerType 类定义。

默认是普通生产者。

8)sendType

消息发送类型,分为同步、异步和单向,由 com.alibaba.cloud.stream.binder.rocketmq.properties.RocketMQProducerProperties.SendType 类定义。

默认是同步。

9)sendCallBack

可以在 com.alibaba.cloud.stream.binder.rocketmq.integration.outbound.RocketMQProducerMessageHandler#getSendCallback 中查看代码。

实际是一个org.apache.rocketmq.client.producer.SendCallback类型的Spring Bean Name。

实际开发中如果要用的话,我们需要:定义一个实现SendCallback接口的类,重写onSuccess和onException方法,并将其纳入Spring 容器的管理范围内(即用@Component注解标记),默认类名首字母小写就是我们要的spring bean name。

10)transactionListener

事务消息监听器的名称,可以在com.alibaba.cloud.stream.binder.rocketmq.integration.outbound.RocketMQProducerMessageHandler#handleMessageInternal中查看代码。

实际上是一个org.apache.rocketmq.client.producer.TransactionListener类型的Spring Bean Name。

实际开发中如果要用的话,我们需要:定义一个实现TransactionListener接口的类,重写executeLocalTransaction和checkLocalTransaction方法,并将其纳入Spring 容器的管理范围内(即用@Component注解标记),默认类名首字母小写就是我们要的spring bean name。

11)messageQueueSelector

根据自己的算法,选择MessageQueue。com.alibaba.cloud.stream.binder.rocketmq.integration.outbound.RocketMQProducerMessageHandler#onInit

未开启消费分区是MessageQueueSelector,开启消费分区就是PartitionMessageQueueSelector。

12)sendMessageHook

SendMessageHook类型的Spring Bean Name。可以对发送消息的前后操作。

com.alibaba.cloud.stream.binder.rocketmq.integration.outbound.RocketMQProduceFactory#initRocketMQProducer

RocketMQConsumerProperties

1)messageModel

消息消费模式。分为集群消费和广播消费,默认是集群消费。由 org.apache.rocketmq.common.protocol.heartbeat.MessageModel 类定义。

2)allocateMessageQueueStrategy

负载均衡策略算法。默认是AllocateMessageQueueAveragely(取模平均分配)。

这个算法可以自行扩展以使用自定义的算法,目前内置的有以下算法可以使用

AllocateMessageQueueAveragely //取模平均
AllocateMessageQueueAveragelyByCircle //环形平均
AllocateMessageQueueByConfig // 按照配置,传入听死的messageQueueList
AllocateMessageQueueByMachineRoom //按机房,从源码上看,必须和阿里的某些broker命名一致才行
AllocateMessageQueueConsistentHash //一致性哈希算法,本人于4.1提交的特性。用于解决“惊群效应”。

需要自行扩展的算法的,需要实现org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueStrategy

3)subscription

订阅关系(topic->sub expression),有tag和sql两种。

tag配置:

spring:
  cloud:
    stream:
      rocketmq:
        bindings:
          input2: 
            consumer:
              subscription: myTag||look # 基于 Tag 订阅,多个 Tag 使用 || 分隔,默认为空

sql表达式配置:

spring:
  cloud:
    stream:
      rocketmq:
        bindings:
          input2:
            consumer:
              subscription: sql:'color'='blue' AND 'price'>100 # SQL92表达式,记得前面是有sql:前缀的哦

在 com.alibaba.cloud.stream.binder.rocketmq.integration.inbound.RocketMQInboundChannelAdapter#doStart 中使用。

4)consumeFromWhere

启动消费点策略。默认是ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET。

  • CONSUME_FROM_LAST_OFFSET //队列尾消费
  • CONSUME_FROM_FIRST_OFFSET //队列头消费
  • CONSUME_FROM_TIMESTAMP //按照日期选择某个位置消费

注:此策略只生效于新在线侧consumer group,如果是老的已存在的consumer group,都将按照已经持久化的consume offset进行消费

5)consumeTimestamp

CONSUME_FROM_LAST_OFFSET的时候使用,从哪个时间点开始消费。

默认是半小时前。

格式为yyyyMMddhhmmss 如 20131223171201。

6)pullThresholdForQueue

Queue 级别的流量控制阈值,每个 queue 默认最多缓存 1000 条消息,考虑到pullBatchSize,瞬时值可能会超过限制。

7)pullThresholdSizeForQueue 

在队列级别限制缓存的消息大小,每个 queue 默认最多缓存 100 MiB 消息,考虑到 pullBatchSize,瞬时值可能会超过限制。

消息的大小仅包括消息的 body ,因此不准确。

8)pullBatchSize

一次最大拉取的批量大小,默认值是10。

9)consumeMaxSpan 

消费的最大偏移跨度。

10)push

push属性是一个Push对象。

① orderly

顺序消费还是并发消费,默认是false,并发消费。

② suspendCurrentQueueTimeMillis

串行消费使用,如果返回ROLLBACK或者SUSPEND_CURRENT_QUEUE_A_MOMENT,再次消费的时间间隔。

默认值:1000,单位毫秒

注:如果消费回调中对ConsumeOrderlyContext中的suspendCurrentQueueTimeMillis进行过设置,则使用用户设置的值作为消费间隔。

③ maxReconsumeTimes

调用sendMessageBack的时候,如果发现重新消费超过这个配置的值,则投递到死信队列。

默认值是16。

④ pullInterval

拉取的间隔。

默认值:0,单位毫秒

由于RocketMQ采取的pull的方式进行消息投递,每此会发起一个异步pull请求,得到请求后会再发起下次请求,这个间隔默认是0,表示立刻再发起。在间隔为0的场景下,消息投递的及时性几乎等同用Push实现的机制。

⑤ consumeMessageBatchMaxSize

批量消费的最大消息条数,默认值:1。

11)pull

pull属性是一个Pull对象。

① pollTimeoutMillis

一次消息拉取默认超时时间。

② pullThreadNums 

每一个消费者拉取消息的线程数,相比于push模式很大的区别点

③ topicMetadataCheckIntervalMillis

topic 路由信息更新频率

④ consumerTimeoutMillisWhenSuspend

消息消费者拉取消息最大的超时时间,必须大于 brokerSuspendMaxTimeMillis

⑤ pullThresholdForAll 

单个队列积压的消息条数触发限流的阔值

RocketMQCommonProperties

1)enabled

是否开启消费。默认是true。

2)group

组名称。

3)pollNameServerInterval

轮询从NameServer获取路由信息的时间间隔。默认30000,单位毫秒。

4)heartbeatBrokerInterval

定期发送注册心跳到broker的间隔。默认30000,单位毫秒。

5)persistConsumerOffsetInterval

作用于Consumer,持久化消费进度的间隔。默认值5000,单位毫秒。

RocketMQ采取的是定期批量ack的机制以持久化消费进度。也就是说每次消费消息结束后,并不会立刻ack,而是定期的集中的更新进度。 由于持久化不是立刻持久化的,所以如果消费实例突然退出(如断电)或者触发了负载均衡分consue queue重排,有可能会有已经消费过的消费进度没有及时更新而导致重新投递。故本配置值越小,重复的概率越低,但同时也会增加网络通信的负担。

 

posted @ 2022-05-06 16:23  残城碎梦  阅读(4892)  评论(1编辑  收藏  举报