原文链接: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. 剖根问底:Java 不能实现真正泛型的原因是什么?

    大家好,我是二哥呀! 今天我来给大家讲一下,Java 不能实现真正泛型的原因是什么? 本文已同步至 GitHub <教妹学 Java>专栏,风趣幽默,通俗易懂,对 Java 初学者亲切友善 ...

  2. Java 给Word添加印章

    一.概述 本文以Java程序代码展示如何给Word文档添加印章,这里添加的印章为.png格式的图片,添加印章即在Word中的指定位置添加印章图片. 基本思路:加载word文档,获取段落,在段落中插入图 ...

  3. Python开发篇——如何在Flask下编写JWT登录

    首先,HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息)--每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求 ...

  4. 关于clear:both;后有固定高度的原因及解决方法

    不知道从什么时候开始,拥有clear:both;元素的父元素偶尔会出现固定的高度,之前给父元素加diaolay:hidden;临时处理,一直没搞清楚原因,今天又出现该问题,花费半天时间找出了原因记录一 ...

  5. 零基础涂鸦智能面板SDK开发记录(一)

    前言 本人基础背景:在学校学了点JS,在blbl上看过几节node.js视频,现在是一名Android开发工程师,因公司需要学习涂鸦面板SDK开发.说真的除了官方的一些文档外,我真的找不到其他的资料. ...

  6. Docker与k8s的恩怨情仇(七)—— “服务发现”大法让你的内外交互原地起飞

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 第一章:Docker与k8s的恩怨情仇(一)-成为PaaS前浪的Cloud Foundry 第二章:Dock ...

  7. CF832D题解

    题目传送门 Description 给定一棵树上的三个点 \(a,b,c\),你要制定两条起点和终点都在这三个点中的路径,使得两条路径重叠的节点最多. Solution 感觉我的方法和大众不同,显然是 ...

  8. RHCSA_DAY04

    软连接与硬连接 Linux中的链接文件类似于windows中的快捷方式 软连接特点:软连接可以跨分区,可以对目录进行链接,源文件删除后,链接文件不可用 软连接命令格式:ln -s 源文件路径  目标路 ...

  9. java环境的配置——实现win10下双击直接运行jar文件

    java环境的配置--实现win10下双击直接运行jar文件 在渗透测试的过程中很多工具的安装和使用需要java环境,下面我来介绍一下java环境配置的超详细步骤(包含怎样实现win10下双击直接运行 ...

  10. 进程信号的未决状态(pending status)

    这两天看了apue有关进程信号的部分,觉得未决状态这个词很是不一般,呵呵.一开始当我看到这个词,我不理解,什么意思呢,读了好几遍.不知道是书里面讲的晦涩难懂,还是脑子越来越不行了,就是没有搞明白.后来 ...