Pulsar の 保证消息的顺序性、幂等性和可靠性
原文链接:Pulsar の 保证消息的顺序性、幂等性和可靠性
一、背景
前面两篇文章,已经介绍了关于Pulsar消费者的详细使用和自研的Pulsar组件。
接下来,将简单分析如何保证消息的顺序性、幂等性和可靠性;但并不会每个分析都会进行代码实战,进行代码实战的都是比较有意思的点,如消费消息如何保证顺序性和幂等性,而其他的其实都是比较简单的,就不做代码实战了。
二、特性分析
2.1、顺序性
保证消息是按顺序发送,按顺序消费,一个接着一个。
2.1.1、活动图
2.1.2、分析
producer:
发送者保证消息的顺序性其实是比较简单的:
- 利用单队列发送
- 一个业务对应一个队列
- 一个队列只能由一个消费者监听消费
- 利用 Pulsar 的分区Topic
- producer发送消息时需要指定key属性,Pulsar自动会根据Key值将消息分配到指定的分区中
- 支持多个消费者消费,多个消费者可以监听同一个分区,但是相同的Key只会分配给同一个消费者
生产者这里就不做什么实战的,都是比较简单的点,没啥好说的。
consumer:
消费者保证消息的顺序性有下面两种方式:
- 当前线程执行
- 单线程执行保证了消费的顺序性
- 消费效率低
- 自定义线程池列表异步并发消费
- 如果直接使用线程池,那么虽然能提高消费效率,但是并不能保证顺序性
- 这里我们会自定义线程池列表,列表中的线程池的核心线程数和最大线程数都是1,保证顺序消费
- Producer发送的消息体中,需指定key,我们会根据key#hashCode定位到对应的线程池,这里参考HashMap的做法。
2.1.3、代码实战
消费者保证消息顺序性的第二点的实现还是比较有意思的:如何自定义线程池列表、如何根据消息的key来定位线程池。
代码如下:
- 发送消息:
/**
* 指定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();
}
}
- 消费消息
/**
* 顺序性消费-消费者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:
消费者保证消息幂等性,最主要是利用中间表来保存消费记录:
- 本地新增表来保存消息消费记录
- 在消息消费前,先判断MessageId判断是否存在消费记录
- 如果存在,直接响应
- 如果不存在,则开启本地事务,接着进行消息消费
- 当成功消费时提交事务,否则回滚
2.2.3、代码实战
如何利用消费记录表和本地事务来完成消息消费的幂等性,看下面代码:
- 发送消息
**
*
* @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();
}
}
- 消费消息
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:
生产者主要还是利用中间表来保证消息发送的可靠性:
- 发送消息前,先插入一条发送记录表
- 接着开启本地事务,开始发送消息
- 发送完毕,接到broker返回的响应
- 更新发送记录为已发送
- 开启定时任务,定时扫描未发送的记录,重新进行发送
consumer:
消费者保证消息的可靠性,只需要利用Pulsar提供的重试策略即可:
- 开启重试策略,指定重试次数、重试队列和死信队列
- 捕获异常,调用reconsumeLater方法进行重新消费
- 监控死信队列,即使进行消息消费异常人工处理
Pulsar の 保证消息的顺序性、幂等性和可靠性的更多相关文章
- RabbitMQ保证消息的顺序性
当我们的系统中引入了MQ之后,不得不考虑的一个问题是如何保证消息的顺序性,这是一个至关重要的事情,如果顺序错乱了,就会导致数据的不一致. 比如:业务场景是这样的:我们需要根据mysql的b ...
- Kafka如何保证消息的顺序性
1. 问题 比如说我们建了一个 topic,有三个 partition.生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到 ...
- kafka如何保证消息得顺序性
1. 问题 比如说我们建了一个 topic,有三个 partition.生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到 ...
- 如何保证MQ的顺序性?比如Kafka
三.如何保证消息的顺序性 1. rabbitmq 拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点:或者就一个queue但是对应一个consumer,然后 ...
- MQ如何解决消息的顺序性
一.消息的顺序性 1.延迟队列:设置一个全局变量index,根据实际情况一次按照index++的逻辑一次给消息队列设置延迟时间段,可以是0.5s,甚至1s; 弊端:如果A,B,C..消息队列消费时间不 ...
- 《即时消息技术剖析与实战》学习笔记3——IM系统如何保证消息的实时性
IM 技术经历过几次迭代升级,如图所示: 从简单.低效的短轮询逐步升级到相对效率可控的长轮询: 全双工的 Websocket 彻底解决了服务端的推送问题: 基于 TCP 长连接衍生的 IM 协议,能够 ...
- 高可用保证消息绝对顺序消费的BROKER设计方案
转自: http://www.infoq.com/cn/articles/high-availability-broker-design?utm_source=tuicool&utm_medi ...
- 关于MQ的几件小事(五)如何保证消息按顺序执行
1.为什么要保证顺序 消息队列中的若干消息如果是对同一个数据进行操作,这些操作具有前后的关系,必须要按前后的顺序执行,否则就会造成数据异常.举例: 比如通过mysql binlog进行两个数据库的数据 ...
- kafka分布式的情况下,如何保证消息的顺序?
作者:可期链接:https://www.zhihu.com/question/266390197/answer/772404605来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...
随机推荐
- tomcat与springmvc 结合 之---第19篇 springmvc 加载.xml文件的bean 过程
writedby 张艳涛,看springmvc 的源码太难了,怎么办, 这篇文章主要分析了看透springmvc的第9章结尾的 如何解析xml 命名空间标签 <?xml version=&quo ...
- 如何临时发布部署Cocos小游戏到Linux服务器,让别人能在微信打开
两个星期前,我们发布了第一个小游戏教程: 教程:制作一个小游戏送给喜欢的TA(不会编程也能学会哦) 上周有好几位小伙伴在b站催更,呃,作为小透明,收到催更信息后还是很激动的!竟然有同学在看我们的小教程 ...
- js 倒计时退出
<p>您已提交成功!<span id='submit_second'></span></p> $("#submit_second") ...
- Android 帧动画使用
帧动画 使用一系列不同的图片,然后像一卷胶卷一样按顺序播放,这是一种传统的动画,也可称为帧动画.也可以比喻为像一卷胶卷一样按顺序播放. 播放起来,有点像在看gif图. 本文介绍使用AnimationD ...
- SQL 查询总是先执行SELECT语句吗?你们都错了!
SELECT语句中子句的顺序.SELECT语句中使用时必须遵循的次序. 经过一段时间的学习,我们知道了SELECT语句超简版的语法如下: SELECT 字段名 FROM 表名 后来,我们又陆续学习了W ...
- Spring Cloud Gateway自定义异常处理Exception Handler
版本: Spring Cloud 2020.0.3 常见的方法有 实现自己的 DefaultErrorWebExceptionHandler 或 仅实现ErrorAttributes. 方法1: Er ...
- GitHub标星8k,字节跳动高工熬夜半月整理的“组件化实战学习手册”,全是精髓!
前言 什么是组件化? 最初的目的是代码重用,功能相对单一或者独立.在整个系统的代码层次上位于最底层,被其他代码所依赖,所以说组件化是纵向分层. 为什么要使用组件化? 当我们的项目越做越大的时候,有时间 ...
- [TensorFlow2.0]-手写神经网络实现鸢尾花分类
本人人工智能初学者,现在在学习TensorFlow2.0,对一些学习内容做一下笔记.笔记中,有些内容理解可能较为肤浅.有偏差等,各位在阅读时如有发现问题,请评论或者邮箱(右侧边栏有邮箱地址)提醒. 若 ...
- CircuitBreaker断路器Fallback如何获取异常
在Spring Cloud 2020新版里, 可以使用新版的 CircuitBreaker 断路器, 可以配置Fallback, 可以是内部的, 也可以是外部的Fallback. 内部 Fallbac ...
- SSM集成FastJson
FastJson Json数据格式回顾 什么是json JSON:(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式.它基于 ECMAScript( ...