前景回顾

【mq】从零开始实现 mq-01-生产者、消费者启动

【mq】从零开始实现 mq-02-如何实现生产者调用消费者?

【mq】从零开始实现 mq-03-引入 broker 中间人

【mq】从零开始实现 mq-04-启动检测与实现优化

【mq】从零开始实现 mq-05-实现优雅停机

【mq】从零开始实现 mq-06-消费者心跳检测 heartbeat

【mq】从零开始实现 mq-07-负载均衡 load balance

【mq】从零开始实现 mq-08-配置优化 fluent

【mq】从零开始实现 mq-09-消费者拉取消息 pull message

消息的推与拉

大家好,我是老马。

这一节我们来一起看一下 MQ 消息中的推和拉两种模式。

消息由 broker 直接推送给消费者,实时性比较好。

缺点是如果消费者处理不过来,就会造成大量问题。

消息由消费者定时从 broker 拉取,优点是实现简单,可以根据消费者自己的处理能力来消费。

缺点是实时性相对较差。

实际业务中,需要结合具体的场景,选择合适的策略。

拉取策略实现

push 策略

我们首先看一下 push 策略的简化核心实现:

package com.github.houbb.mq.consumer.core;

/**
* 推送消费策略
*
* @author binbin.hou
* @since 1.0.0
*/
public class MqConsumerPush extends Thread implements IMqConsumer { @Override
public void run() {
// 启动服务端
log.info("MQ 消费者开始启动服务端 groupName: {}, brokerAddress: {}",
groupName, brokerAddress); //1. 参数校验
this.paramCheck(); try {
//0. 配置信息
//1. 初始化
//2. 连接到服务端
//3. 标识为可用
//4. 添加钩子函数 //5. 启动完成以后的事件
this.afterInit(); log.info("MQ 消费者启动完成");
} catch (Exception e) {
log.error("MQ 消费者启动异常", e);
throw new MqException(ConsumerRespCode.RPC_INIT_FAILED);
}
} /**
* 初始化完成以后
*/
protected void afterInit() { } // 其他方法 /**
* 获取消费策略类型
* @return 类型
* @since 0.0.9
*/
protected String getConsumerType() {
return ConsumerTypeConst.PUSH;
} }

我们在 push 中预留了一个 afterInit 方法,便于子类重载。

pull 策略

消费者实现

package com.github.houbb.mq.consumer.core;

/**
* 拉取消费策略
*
* @author binbin.hou
* @since 0.0.9
*/
public class MqConsumerPull extends MqConsumerPush { private static final Log log = LogFactory.getLog(MqConsumerPull.class); /**
* 拉取定时任务
*
* @since 0.0.9
*/
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); /**
* 单次拉取大小
* @since 0.0.9
*/
private int size = 10; /**
* 初始化延迟毫秒数
* @since 0.0.9
*/
private int pullInitDelaySeconds = 5; /**
* 拉取周期
* @since 0.0.9
*/
private int pullPeriodSeconds = 5; /**
* 订阅列表
* @since 0.0.9
*/
private final List<MqTopicTagDto> subscribeList = new ArrayList<>(); // 设置 @Override
protected String getConsumerType() {
return ConsumerTypeConst.PULL;
} @Override
public synchronized void subscribe(String topicName, String tagRegex) {
MqTopicTagDto tagDto = buildMqTopicTagDto(topicName, tagRegex); if(!subscribeList.contains(tagDto)) {
subscribeList.add(tagDto);
}
} @Override
public void unSubscribe(String topicName, String tagRegex) {
MqTopicTagDto tagDto = buildMqTopicTagDto(topicName, tagRegex); subscribeList.remove(tagDto);
} private MqTopicTagDto buildMqTopicTagDto(String topicName, String tagRegex) {
MqTopicTagDto dto = new MqTopicTagDto();
dto.setTagRegex(tagRegex);
dto.setTopicName(topicName);
return dto;
} }

订阅相关

pull 策略可以把订阅/取消订阅放在本地,避免与服务端的交互。

定时拉取

我们重载了 push 策略的 afterInit 方法。

/**
* 初始化拉取消息
* @since 0.0.6
*/
@Override
public void afterInit() {
//5S 发一次心跳
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if(CollectionUtil.isEmpty(subscribeList)) {
log.warn("订阅列表为空,忽略处理。");
return;
}
for(MqTopicTagDto tagDto : subscribeList) {
final String topicName = tagDto.getTopicName();
final String tagRegex = tagDto.getTagRegex();
MqConsumerPullResp resp = consumerBrokerService.pull(topicName, tagRegex, size);
if(MqCommonRespCode.SUCCESS.getCode().equals(resp.getRespCode())) {
List<MqMessage> mqMessageList = resp.getList();
if(CollectionUtil.isNotEmpty(mqMessageList)) {
for(MqMessage mqMessage : mqMessageList) {
IMqConsumerListenerContext context = new MqConsumerListenerContext();
mqListenerService.consumer(mqMessage, context);
}
}
} else {
log.error("拉取消息失败: {}", JSON.toJSON(resp));
}
}
}
}, pullInitDelaySeconds, pullPeriodSeconds, TimeUnit.SECONDS);
}

应用启动时,指定时间定时拉取消息并进行消费处理。

其中 consumerBrokerService.pull(topicName, tagRegex, size); 拉取实现如下:

public MqConsumerPullResp pull(String topicName, String tagRegex, int fetchSize) {
MqConsumerPullReq req = new MqConsumerPullReq();
req.setSize(fetchSize);
req.setGroupName(groupName);
req.setTagRegex(tagRegex);
req.setTopicName(topicName);
final String traceId = IdHelper.uuid32();
req.setTraceId(traceId);
req.setMethodType(MethodType.C_MESSAGE_PULL); Channel channel = getChannel(null);
return this.callServer(channel, req, MqConsumerPullResp.class);
}

Borker 相关

消息分发

// 消费者主动 pull
if(MethodType.C_MESSAGE_PULL.equals(methodType)) {
MqConsumerPullReq req = JSON.parseObject(json, MqConsumerPullReq.class);
return mqBrokerPersist.pull(req, channel);
}

实现

mqBrokerPersist 是一个接口,此处演示基于本地实现的,后续会实现基于数据库的持久化。

原理是类似的,此处仅作为演示。

@Override
public MqConsumerPullResp pull(MqConsumerPullReq pullReq, Channel channel) {
//1. 拉取匹配的信息
//2. 状态更新为代理中
//3. 如何更新对应的消费状态呢?
// 获取状态为 W 的订单
final int fetchSize = pullReq.getSize();
final String topic = pullReq.getTopicName();
final String tagRegex = pullReq.getTagRegex();
List<MqMessage> resultList = new ArrayList<>(fetchSize);
List<MqMessagePersistPut> putList = map.get(topic);
// 性能比较差
if(CollectionUtil.isNotEmpty(putList)) {
for(MqMessagePersistPut put : putList) {
final String status = put.getMessageStatus();
if(!MessageStatusConst.WAIT_CONSUMER.equals(status)) {
continue;
}
final MqMessage mqMessage = put.getMqMessage();
List<String> tagList = mqMessage.getTags();
if(InnerRegexUtils.hasMatch(tagList, tagRegex)) {
// 设置为处理中
// TODO: 消息的最终状态什么时候更新呢?
// 可以给 broker 一个 ACK
put.setMessageStatus(MessageStatusConst.PROCESS_CONSUMER);
resultList.add(mqMessage);
}
if(resultList.size() >= fetchSize) {
break;
}
}
}
MqConsumerPullResp resp = new MqConsumerPullResp();
resp.setRespCode(MqCommonRespCode.SUCCESS.getCode());
resp.setRespMessage(MqCommonRespCode.SUCCESS.getMsg());
resp.setList(resultList);
return resp;
}

我们遍历找到匹配的消息,将其状态更新为中间状态。

不过这里还是缺少了一个关键的步骤,那就是消息的 ACK。

我们将在下一小节进行实现。

小结

消息的推送和拉取各有自己的优缺点,需要我们结合自己的业务,进行选择。

一般而言,IM 更加适合消息的推送;一般的业务,为了削峰填谷,更加适合拉取的模式。

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次重逢。

开源地址

The message queue in java.(java 简易版本 mq 实现) https://github.com/houbb/mq

拓展阅读

rpc-从零开始实现 rpc https://github.com/houbb/rpc

【mq】从零开始实现 mq-09-消费者拉取消息 pull message的更多相关文章

  1. 【mq】从零开始实现 mq-10-消费者拉取消息回执 pull message ack

    前景回顾 [mq]从零开始实现 mq-01-生产者.消费者启动 [mq]从零开始实现 mq-02-如何实现生产者调用消费者? [mq]从零开始实现 mq-03-引入 broker 中间人 [mq]从零 ...

  2. kafka 消费者拉取消息

    本文只跟踪消费者拉取消息的流程.对于 java 客户端, kafka 的生产者和消费者复用同一个网络 io 类 NetworkClient. 入口在 KafkaConsumer#pollOnce 中, ...

  3. RocketMQ入门(3)拉取消息

    转自:http://www.changeself.net/archives/rocketmq入门(3)拉取消息.html RocketMQ入门(3)拉取消息 RocketMQ不止可以直接推送消息,在消 ...

  4. RocketMQ 拉取消息-文件获取

    看完了上一篇的<RocketMQ 拉取消息-通信模块>,请求进入PullMessageProcessor中,接着 PullMessageProcessor.processRequest(f ...

  5. Kafka消费者拉取数据异常Unexpected error code 2 while fetching data

    Kafka消费程序间歇性报同一个错: 上网没查到相关资料,只好自己分析.通过进一步分析日志发现,只有在拉取某一个特定的topic的数据时报错,如果拉取其他topic的数据则不会报错.而从这个异常信息来 ...

  6. 12.Git分支-推送(push)、跟踪分支、拉取(pull)、删除远程分支

    1.推送 本地的分支并不会自动与远程仓库同步,你可以显示的向远程仓库推送你的分支.例如你在本地创建了一个dev分支,你想其他的人和你一样在dev之下进行工作,可以使用 git push <rem ...

  7. Azure DevOps Server: 使用Rest Api获取拉取请求Pull Request中的变更文件清单

    需求: Azure DevOps Server 的拉取请求模块,为开发团队提供了强大而且灵活的代码评审功能.拉取请求中变更文件清单,对质量管理人员,是一个宝贵的材料.质量保障人员可以从代码清单中分析不 ...

  8. RocketMQ 拉取消息-通信模块

    首先看server端:class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer 下面这个实现了 ...

  9. .net MVC 微信公众号 点击菜单拉取消息时的事件推送

    官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141016&token=&lang=zh_CN ...

随机推荐

  1. Centos最小化安装

    1.选择最小安装: 2.选择相应的安装包 老男孩提示: 1.根据经验,选择安装包时应该按最小化原则,即不需要的或者不确定是否需要的就不安装,这样可以最大程度上确保系统安全. 2.如果安装过程落了部分包 ...

  2. vue中ajax请求发送

    示例 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8& ...

  3. 数据库学习之"清理表内所有数据"

    今天在写定时任务的时候表内的数据都出现了问题,所以用了 1 truncate table 表名 来清空表内的数据

  4. nRF Connect SDK(NCS)/Zephyr固件升级详解 – 重点讲述MCUboot和蓝牙空中升级

    如何在nRF Connect SDK(NCS)中实现蓝牙空中升级?MCUboot和B0两个Bootloader有什么区别?MCUboot升级使用的image格式是怎么样的?什么是SMP协议?CBOR编 ...

  5. 13_奈奎斯特稳定性判据_Nyquist Stability Criterion_Part 1

    A曲线内有4个极点两个零点,则B曲线绕(0,0)逆时针两圈 A曲线是nyqyict contour中的曲线,P是A曲线内的()极点个数,Z是()极点个数,N是曲线B逆时针围绕(-1,0)的圈数 没过( ...

  6. .NET Best Practices: Architecture & Design Patterns (5 Days Training)

    .NET Best Practices: Architecture & Design Patterns (5 Days Training) .NET最佳实践:架构及设计模式 5天培训课程 课程 ...

  7. 使用Egret插件压缩代码包体积,减少请求数量的实战教程

    在白鹭引擎发布了5.2.7版本中新增加了命令行,增加自动合图插件TextureMergerPlugin功能.今天,我们以一个EUI案例来展示自动合图插件的具体使用方法和注意事项. 此外,我们在本文还融 ...

  8. 解决HDFS无法启动namenode,报错Premature EOF from inputStream;Failed to load FSImage file, see error(s) above for more info

    一.情况描述 启动hadoop后发现无法打开hdfs web界面,50070打不开,于是jps发现少了一个namenode: 查看日志信息,发现如下报错: 2022-01-03 23:54:10,99 ...

  9. 更改spinner字体大小

    做下拉框的时候,我采用的是spinner和string-array,因为比较简单. 可是整个界面的字体大小与下拉框里面的字体大小不符合,所以我们要更改spinner里面的字体大小. 方法是: 在布局中 ...

  10. mysql各个集群方案的优劣

    集群的好处 高可用性:故障检测及迁移,多节点备份. 可伸缩性:新增数据库节点便利,方便扩容. 负载均衡:切换某服务访问某节点,分摊单个节点的数据库压力. 集群要考虑的风险 网络分裂:群集还可能由于网络 ...