原文链接:Pulsar の 保证消息的顺序性、幂等性和可靠性

一、背景

前面两篇文章,已经介绍了关于Pulsar消费者的详细使用自研的Pulsar组件

接下来,将简单分析如何保证消息的顺序性、幂等性和可靠性;但并不会每个分析都会进行代码实战,进行代码实战的都是比较有意思的点,如消费消息如何保证顺序性和幂等性,而其他的其实都是比较简单的,就不做代码实战了。

二、特性分析

2.1、顺序性

保证消息是按顺序发送,按顺序消费,一个接着一个。

2.1.1、活动图

2.1.2、分析

producer:

发送者保证消息的顺序性其实是比较简单的:

  1. 利用单队列发送

    • 一个业务对应一个队列
    • 一个队列只能由一个消费者监听消费
  2. 利用 Pulsar 的分区Topic
    • producer发送消息时需要指定key属性,Pulsar自动会根据Key值将消息分配到指定的分区中
    • 支持多个消费者消费,多个消费者可以监听同一个分区,但是相同的Key只会分配给同一个消费者

生产者这里就不做什么实战的,都是比较简单的点,没啥好说的。

consumer:

消费者保证消息的顺序性有下面两种方式:

  1. 当前线程执行

    • 单线程执行保证了消费的顺序性
    • 消费效率低
  2. 自定义线程池列表异步并发消费
    • 如果直接使用线程池,那么虽然能提高消费效率,但是并不能保证顺序性
    • 这里我们会自定义线程池列表,列表中的线程池的核心线程数和最大线程数都是1,保证顺序消费
    • Producer发送的消息体中,需指定key,我们会根据key#hashCode定位到对应的线程池,这里参考HashMap的做法。

2.1.3、代码实战

消费者保证消息顺序性的第二点的实现还是比较有意思的:如何自定义线程池列表、如何根据消息的key来定位线程池。

代码如下:

  1. 发送消息:
/**
* 指定key发送消息
* @author winfun
**/
@Slf4j
public class ThirdProducerDemo { public static void main(String[] args) throws PulsarClientException {
PulsarClient client = PulsarClient.builder()
.serviceUrl("pulsar://127.0.0.1:6650")
.build(); ProducerBuilder<String> productBuilder = client.newProducer(Schema.STRING).topic("winfun/study/test-topic3")
.blockIfQueueFull(Boolean.TRUE).batchingMaxMessages(100).enableBatching(Boolean.TRUE).sendTimeout(3, TimeUnit.SECONDS); Producer<String> producer = productBuilder.create();
for (int i = 0; i < 100; i++) {
MsgDTO msgDTO = new MsgDTO();
String content = "hello"+i;
String key;
if (content.contains("1")){
key = "k213e434y1df";
}else if (content.contains("2")){
key = "keasdgashgfy2";
}else {
key = "other";
}
msgDTO.setId(key);
msgDTO.setContent(content);
producer.send(JSONUtil.toJsonStr(msgDTO));
}
producer.close();
}
}
  1. 消费消息
/**
* 顺序性消费-消费者demo
* @author: winfun
**/
@Slf4j
@PulsarListener(topics = {"test-topic3"})
public class SuccessionConsumerListener extends BaseMessageListener { List<ExecutorService> executorServiceList = new ArrayList<>(); /**
* 初始化自定义线程池列表
*/
@PostConstruct
public void initCustomThreadPool(){
for (int i = 0; i < 10; i++) {
/**
* 1、核心线程数和最大线程数都为1,避免多线程消费导致顺序被打乱
* 2、使用有界队列,设定最大长度,避免无限任务数导致OOM
* 3、使用CallerRunsPolicy拒绝策略,让当前线程执行,避免消息丢失,也可以直接让消费者执行当前任务,阻塞住其他任务,也能保证顺序性
*/
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
1,
1,
60,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(100),
new ThreadFactoryBuilder().setNameFormat(String.format("custom-thread-pool-%d",i)).get(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
this.executorServiceList.add(threadPoolExecutor);
}
} /**
* 消费消息
* 自定义监听器实现方法
* 消息如何响应由开发者决定:
* Consumer#acknowledge
* Consumer#reconsumeLater
* Consumer#negativeAcknowledge
*
* @param consumer 消费者
* @param msg 消息
*/
@Override
protected void doReceived(Consumer<String> consumer, Message<String> msg) {
String value = msg.getValue();
MsgDTO msgDTO = JSONUtil.toBean(value, MsgDTO.class);
// 匹配列表中对应的线程池
int index = (this.executorServiceList.size()-1)&this.spreed(msgDTO.getId().hashCode());
log.info("成功获取线程池列表索引,msgId is {}, index is {}",msgDTO.getId(),index);
ExecutorService executorService = this.executorServiceList.get(index);
executorService.execute(()->{
log.info("成功消费消息,threadName is {},msg is {}",Thread.currentThread().getName(),msg);
consumer.acknowledgeAsync(msg);
});
} /**
* hashCode扩展,保证hashCode前后十六位都能完美进行位运算
* @param hashCode
* @return
*/
private int spreed(int hashCode){
return (hashCode ^ (hashCode >>> 16)) & hashCode;
} /***
* 是否开启异步消费,默认开启
* @return {@link Boolean }
**/
@Override
public Boolean enableAsync() {
// 首先关闭线程池异步并发消费
return Boolean.FALSE;
}
}

2.2、幂等性

幂等性的话,我们主要是分析一下消费者的,如何保证消费者只正确消费一次消息还是非常重要的。

2.2.1、活动图

2.2.2、分析

producer:

生产者如何保证幂等性,感觉这个话题没什么好讨论的,如果发生失败就重新发送,否则就正常发送就好了。

consumer:

消费者保证消息幂等性,最主要是利用中间表来保存消费记录:

  1. 本地新增表来保存消息消费记录
  2. 在消息消费前,先判断MessageId判断是否存在消费记录
  3. 如果存在,直接响应
  4. 如果不存在,则开启本地事务,接着进行消息消费
  5. 当成功消费时提交事务,否则回滚

2.2.3、代码实战

如何利用消费记录表和本地事务来完成消息消费的幂等性,看下面代码:

  1. 发送消息
**
*
* @author winfun
**/
@Slf4j
public class FourthProducerDemo { public static void main(String[] args) throws PulsarClientException {
PulsarClient client = PulsarClient.builder()
.serviceUrl("pulsar://127.0.0.1:6650")
.build(); ProducerBuilder<String> productBuilder = client.newProducer(Schema.STRING).topic("winfun/study/test-topic4")
.blockIfQueueFull(Boolean.TRUE).batchingMaxMessages(100).enableBatching(Boolean.TRUE).sendTimeout(3, TimeUnit.SECONDS); Producer<String> producer = productBuilder.create();
for (int i = 0; i < 20; i++) {
MsgDTO msgDTO = new MsgDTO();
String content = "hello"+i;
String key;
if (content.contains("1")){
key = "k213e434y1df";
}else if (content.contains("2")){
key = "keasdgashgfy2";
}else {
key = "other";
}
msgDTO.setId(key);
msgDTO.setContent(content);
producer.send(JSONUtil.toJsonStr(msgDTO));
}
producer.close();
}
}
  1. 消费消息
package com.github.howinfun.consumer.idempotent;

import cn.hutool.json.JSONUtil;
import com.github.howinfun.core.entity.MessageConsumeRecord;
import com.github.howinfun.core.service.MessageConsumeRecordService;
import com.github.howinfun.dto.MsgDTO;
import io.github.howinfun.listener.BaseMessageListener;
import io.github.howinfun.listener.PulsarListener;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import jodd.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.PulsarClientException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional; /**
* 幂等性消费-消费者demo
* @author: winfun
* @date: 2021/9/2 12:49 下午
**/
@Slf4j
@PulsarListener(topics = {"test-topic4"})
public class IdempotentConsumerListener extends BaseMessageListener { List<ExecutorService> executorServiceList = new ArrayList<>(); @Autowired
private MessageConsumeRecordService service; /**
* 初始化自定义线程池列表
*/
@PostConstruct
public void initCustomThreadPool(){
for (int i = 0; i < 10; i++) {
/**
* 1、核心线程数和最大线程数都为1,避免多线程消费导致顺序被打乱
* 2、使用有界队列,设定最大长度,避免无限任务数导致OOM
* 3、使用CallerRunsPolicy拒绝策略,让当前线程执行,避免消息丢失,也可以直接让消费者执行当前任务,阻塞住其他任务,也能保证顺序性
*/
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
1,
1,
60,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(100),
new ThreadFactoryBuilder().setNameFormat(String.format("custom-thread-pool-%d",i)).get(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
this.executorServiceList.add(threadPoolExecutor);
}
} /**
* 消费消息
* 自定义监听器实现方法
* 消息如何响应由开发者决定:
* Consumer#acknowledge
* Consumer#reconsumeLater
* Consumer#negativeAcknowledge
*
* @param consumer 消费者
* @param msg 消息
*/
@Override
protected void doReceived(Consumer<String> consumer, Message<String> msg) {
boolean flag = preReceived(msg);
if (Boolean.FALSE.equals(flag)){
String value = msg.getValue();
MsgDTO msgDTO = JSONUtil.toBean(value, MsgDTO.class);
int index = (this.executorServiceList.size()-1)&this.spreed(msgDTO.getId().hashCode());
log.info("成功获取线程池列表索引,msgId is {}, index is {}",msgDTO.getId(),index);
ExecutorService executorService = this.executorServiceList.get(index);
executorService.execute(()->{
try {
this.doInnerReceived(consumer,msg);
} catch (PulsarClientException e) {
log.error("消息消费失败",e);
}
});
}else {
log.info("此消息的消费记录已存在,直接响应,messageId is {}", msg.getMessageId().toString());
try {
consumer.acknowledge(msg);
} catch (PulsarClientException e) {
log.error("消息提交失败",e);
}
}
} /**
* 消费前判断,messageId是否存在对应的消费记录
* @param msg 消息
* @return 存在结果
*/
private boolean preReceived(Message<String> msg){
MessageConsumeRecord record = this.service.getByMessageId(msg.getMessageId().toString());
if (Objects.isNull(record)){
return false;
}
return true;
} /**
* 消息消费
* @param consumer 消费者
* @param msg 消息
*/
@Transactional(rollbackFor = Exception.class)
public void doInnerReceived(Consumer<String> consumer,Message<String> msg) throws PulsarClientException {
String messageContent = msg.getValue();
String messageId = msg.getMessageId().toString();
log.info("成功消费消息,threadName is {},msg is {}",Thread.currentThread().getName(),messageContent);
this.service.save(new MessageConsumeRecord()
.setMessageId(messageId)
.setMessageContent(messageContent)
.setCreateTime(new Date()));
// 模拟重复消费,如果消息内容包含8,则插入数据库,但是不响应
if (messageContent.contains("8")){
log.info("消息已被消费入库,但不响应,模拟重复消费,messageId is {},messageContent is {}",messageId,messageContent);
}else {
consumer.acknowledge(msg);
}
} /**
* hashCode扩展,保证hashCode前后十六位都能完美计算
* @param hashCode
* @return
*/
private int spreed(int hashCode){
return (hashCode ^ (hashCode >>> 16)) & hashCode;
} /***
* 是否开启异步消费,默认开启
* @return {@link Boolean }
**/
@Override
public Boolean enableAsync() {
// 首先关闭线程池异步并发消费
return Boolean.FALSE;
}
}

2.3、可靠性

2.3.1、活动图

生产者:

消费者:

关于保证消息的可靠性,我们只分析 Producer 和 Consuemr,Pulsar服务器就不分析了。

2.3.2、分析

producer:

生产者主要还是利用中间表来保证消息发送的可靠性:

  1. 发送消息前,先插入一条发送记录表
  2. 接着开启本地事务,开始发送消息
  3. 发送完毕,接到broker返回的响应
  4. 更新发送记录为已发送
  5. 开启定时任务,定时扫描未发送的记录,重新进行发送

consumer:

消费者保证消息的可靠性,只需要利用Pulsar提供的重试策略即可:

  1. 开启重试策略,指定重试次数、重试队列和死信队列
  2. 捕获异常,调用reconsumeLater方法进行重新消费
  3. 监控死信队列,即使进行消息消费异常人工处理

Pulsar の 保证消息的顺序性、幂等性和可靠性的更多相关文章

  1. RabbitMQ保证消息的顺序性

    当我们的系统中引入了MQ之后,不得不考虑的一个问题是如何保证消息的顺序性,这是一个至关重要的事情,如果顺序错乱了,就会导致数据的不一致.       比如:业务场景是这样的:我们需要根据mysql的b ...

  2. Kafka如何保证消息的顺序性

    1. 问题 比如说我们建了一个 topic,有三个 partition.生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到 ...

  3. kafka如何保证消息得顺序性

    1. 问题 比如说我们建了一个 topic,有三个 partition.生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到 ...

  4. 如何保证MQ的顺序性?比如Kafka

    三.如何保证消息的顺序性 1. rabbitmq 拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点:或者就一个queue但是对应一个consumer,然后 ...

  5. MQ如何解决消息的顺序性

    一.消息的顺序性 1.延迟队列:设置一个全局变量index,根据实际情况一次按照index++的逻辑一次给消息队列设置延迟时间段,可以是0.5s,甚至1s; 弊端:如果A,B,C..消息队列消费时间不 ...

  6. 《即时消息技术剖析与实战》学习笔记3——IM系统如何保证消息的实时性

    IM 技术经历过几次迭代升级,如图所示: 从简单.低效的短轮询逐步升级到相对效率可控的长轮询: 全双工的 Websocket 彻底解决了服务端的推送问题: 基于 TCP 长连接衍生的 IM 协议,能够 ...

  7. 高可用保证消息绝对顺序消费的BROKER设计方案

    转自: http://www.infoq.com/cn/articles/high-availability-broker-design?utm_source=tuicool&utm_medi ...

  8. 关于MQ的几件小事(五)如何保证消息按顺序执行

    1.为什么要保证顺序 消息队列中的若干消息如果是对同一个数据进行操作,这些操作具有前后的关系,必须要按前后的顺序执行,否则就会造成数据异常.举例: 比如通过mysql binlog进行两个数据库的数据 ...

  9. kafka分布式的情况下,如何保证消息的顺序?

    作者:可期链接:https://www.zhihu.com/question/266390197/answer/772404605来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...

随机推荐

  1. jvm源码解读--19 Java的join()方法解读 以及 invokestatic 字节码 执行 流程图

  2. HCNA Routing&Switching之交换技术基础

    什么是交换机?顾名思义,交换机就是用来数据包交换的:广泛用于终端接入:它的前身是hub(集线器),hub是一个古老的设备,它的作用也是用于终端接入,但hub有一个最大的缺点是它不能隔离冲突域:所谓冲突 ...

  3. Java8 Lambda表达式(一)

    目录 一.应用场景引入 优化一:使用策略模式 优化二:使用匿名内部类 优化三:使用Lambda表达式 优化四:使用Stream API 二.Lambda运算符和对应语法 语法格式 Lambda表达式需 ...

  4. linux signal信号(SIGHUP、SIGINT、SIGQUIT、SIGILL、SIGTRAP、SIGABRT...........................)

    SIGHUP /* hangup */       ~~~~~~      SIGHUP,hong up ,挂断.本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知 ...

  5. Java基础——JavaDoc生成文档

    JavaDoc生成文档  package Top1; ​ /**  * @author lwt  * @version 1.0  * @since 1.8  *  */ ​ public class ...

  6. Android Jetpack 架构组件最佳实践之“网抑云”APP

    背景 近几年,Android 相关的新技术层出不穷.往往这个技术还没学完,下一个新技术又出来了.很多人都是一脸黑人问号? 不少开发者甚至开始哀嚎:"求求你们别再创造新技术了,我们学不动了!& ...

  7. 1549页Android最新面试题含答案

    在今年年初的疫情中,成了失业人员之一,于是各种准备面试,发现面试题网上很多,但是都是很凌乱的,而且一个地方一点,没有一个系统的面试题库,有题库有的没有答案或者是答案很简洁,没有达到面试的要求.所以一直 ...

  8. Vue实现多文件上传功能(前端 + 后端代码)

    本人业余前端开发,因为公司(很坑)觉得我很牛逼,所以让我前后端一起玩,无奈的我只能磕磕碰碰的研究起了vue. 开发项目的时候,用到文件上传的功能很常见,包括单文件上传和多文件上传,上传各种类型的文件. ...

  9. awk-04-流程控制

    if 格式: if ( 条件 ) 语句 [ else 语句 ] 单分支 正则匹配判断 双分支 多分支 while 格式 while (条件) 语句 awk是按行处理的,每次读取一行,并遍历打印每个字段 ...

  10. webpack 配置文件外置

    转自:https://www.jianshu.com/p/377bfd2d9034 1.问题初衷 解决问题的初衷,源于vue项目中公共路径在打包之后一旦遇到整体的路径更改就需要再次build一下. 如 ...