原文链接: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. tomcat与springmvc 结合 之---第19篇 springmvc 加载.xml文件的bean 过程

    writedby 张艳涛,看springmvc 的源码太难了,怎么办, 这篇文章主要分析了看透springmvc的第9章结尾的 如何解析xml 命名空间标签 <?xml version=&quo ...

  2. 如何临时发布部署Cocos小游戏到Linux服务器,让别人能在微信打开

    两个星期前,我们发布了第一个小游戏教程: 教程:制作一个小游戏送给喜欢的TA(不会编程也能学会哦) 上周有好几位小伙伴在b站催更,呃,作为小透明,收到催更信息后还是很激动的!竟然有同学在看我们的小教程 ...

  3. js 倒计时退出

    <p>您已提交成功!<span id='submit_second'></span></p> $("#submit_second") ...

  4. Android 帧动画使用

    帧动画 使用一系列不同的图片,然后像一卷胶卷一样按顺序播放,这是一种传统的动画,也可称为帧动画.也可以比喻为像一卷胶卷一样按顺序播放. 播放起来,有点像在看gif图. 本文介绍使用AnimationD ...

  5. SQL 查询总是先执行SELECT语句吗?你们都错了!

    SELECT语句中子句的顺序.SELECT语句中使用时必须遵循的次序. 经过一段时间的学习,我们知道了SELECT语句超简版的语法如下: SELECT 字段名 FROM 表名 后来,我们又陆续学习了W ...

  6. Spring Cloud Gateway自定义异常处理Exception Handler

    版本: Spring Cloud 2020.0.3 常见的方法有 实现自己的 DefaultErrorWebExceptionHandler 或 仅实现ErrorAttributes. 方法1: Er ...

  7. GitHub标星8k,字节跳动高工熬夜半月整理的“组件化实战学习手册”,全是精髓!

    前言 什么是组件化? 最初的目的是代码重用,功能相对单一或者独立.在整个系统的代码层次上位于最底层,被其他代码所依赖,所以说组件化是纵向分层. 为什么要使用组件化? 当我们的项目越做越大的时候,有时间 ...

  8. [TensorFlow2.0]-手写神经网络实现鸢尾花分类

    本人人工智能初学者,现在在学习TensorFlow2.0,对一些学习内容做一下笔记.笔记中,有些内容理解可能较为肤浅.有偏差等,各位在阅读时如有发现问题,请评论或者邮箱(右侧边栏有邮箱地址)提醒. 若 ...

  9. CircuitBreaker断路器Fallback如何获取异常

    在Spring Cloud 2020新版里, 可以使用新版的 CircuitBreaker 断路器, 可以配置Fallback, 可以是内部的, 也可以是外部的Fallback. 内部 Fallbac ...

  10. SSM集成FastJson

    FastJson Json数据格式回顾 什么是json JSON:(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式.它基于 ECMAScript( ...