读 RocketMQ 源码,学习并发编程三大神器

news2024/11/30 20:45:29

笔者是 RocketMQ 的忠实粉丝,在阅读源码的过程中,学习到了很多编程技巧。

这篇文章,笔者结合 RocketMQ 源码,分享并发编程三大神器的相关知识点。

1 CountDownLatch 实现网络同步请求

CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步,它能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。

下图是 CountDownLatch 的核心方法:

我们可以认为它内置一个计数器,构造函数初始化计数值。每当线程执行 countDown 方法,计数器的值就会减一,当计数器的值为 0 时,表示所有的任务都执行完成,然后在 CountDownLatch 上等待的线程就可以恢复执行接下来的任务。

举例,数据库有100万条数据需要处理,单线程执行比较慢,我们可以将任务分为5个批次,线程池按照每个批次执行,当5个批次整体执行完成后,打印出任务执行的时间 。

 long start = System.currentTimeMillis();
 ExecutorService executorService = Executors.newFixedThreadPool(10);
 int batchSize = 5;
 CountDownLatch countDownLatch = new CountDownLatch(batchSize);
 for (int i = 0; i < batchSize; i++) {
   final int batchNumber = i;
   executorService.execute(new Runnable() {
      @Override
      public void run() {
        try {
           doSomething(batchNumber);
        } catch (Exception e) {
           e.printStackTrace();
        } finally {
           countDownLatch.countDown();
        }
      }
   });
}
countDownLatch.await();
System.out.println("任务执行耗时:" + (System.currentTimeMillis() - start) + "毫秒");
复制代码

温习完 CountDownLatch 的知识点,回到 RocketMQ 源码。

笔者在没有接触网络编程之前,一直很疑惑,网络同步请求是如何实现的?

同步请求指:客户端线程发起调用后,需要在指定的超时时间内,等到响应结果,才能完成本次调用如果超时时间内没有得到结果,那么会抛出超时异常。

RocketMQ 的同步发送消息接口见下图:

追踪源码,真正发送请求的方法是通讯模块的同步请求方法 invokeSyncImpl

整体流程:

  1. 发送消息线程 Netty channel 对象调用 writeAndFlush 方法后 ,它的本质是通过 Netty 的读写线程将数据包发送到内核 , 这个过程本身就是异步的;
  2. ResponseFuture 类中内置一个 CountDownLatch 对象 ,responseFuture 对象调用 waitRepsone 方法,发送消息线程会阻塞 ;

  1. 客户端收到响应命令后, 执行 processResponseCommand 方法,核心逻辑是执行 ResponseFuture 的 putResponse 方法。

该方法的本质就是填充响应对象,并调用 countDownLatch 的 countDown 方法 , 这样发送消息线程就不再阻塞。

CountDownLatch 实现网络同步请求是非常实用的技巧,在很多开源中间件里,比如 Metaq ,Xmemcached 都有类似的实现。

2 ReadWriteLock 名字服务路由管理

读写锁是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,而写锁则是互斥锁。

它的规则是:读读不互斥,读写互斥,写写互斥,适用于读多写少的业务场景。

我们一般都使用 ReentrantReadWriteLock ,该类实现了 ReadWriteLock 。ReadWriteLock 接口也很简单,其内部主要提供了两个方法,分别返回读锁和写锁 。

 public interface ReadWriteLock {
    //获取读锁
    Lock readLock();
    //获取写锁
    Lock writeLock();
}
复制代码

读写锁的使用方式如下所示:

  1. 创建 ReentrantReadWriteLock 对象 , 当使用 ReadWriteLock 的时候,并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用 lock / unlock 方法 ;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
复制代码
  1. 读取共享数据 ;
Lock readLock = readWriteLock.readLock();
readLock.lock();
try {
   // TODO 查询共享数据
} finally {
   readLock.unlock();
}
复制代码
  1. 写入共享数据;
Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
try {
   // TODO 修改共享数据
} finally {
   writeLock.unlock();
}
复制代码

RocketMQ架构上主要分为四部分,如下图所示 :

  1. Producer :消息发布的角色,Producer 通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
  2. Consumer :消息消费的角色,支持以 push 推,pull 拉两种模式对消息进行消费。
  3. BrokerServer :Broker主要负责消息的存储、投递和查询以及服务高可用保证。
  4. NameServer :名字服务是一个非常简单的 Topic 路由注册中心,其角色类似 Dubbo 中的zookeeper,支持Broker的动态注册与发现。

NameServer 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。Broker 启动之后会向所有 NameServer 定期(每 30s)发送心跳包(路由信息),NameServer 会定期扫描 Broker 存活列表,如果超过 120s 没有心跳则移除此 Broker 相关信息,代表下线。

那么 NameServer 如何保存路由信息呢?

路由信息通过几个 HashMap 来保存,当 Broker 向 Nameserver 发送心跳包(路由信息),Nameserver 需要对 HashMap 进行数据更新,但我们都知道 HashMap 并不是线程安全的,高并发场景下,容易出现 CPU 100% 问题,所以更新 HashMap 时需要加锁,RocketMQ 使用了 JDK 的读写锁 ReentrantReadWriteLock 。

  1. 更新路由信息,操作写锁

  1. 查询主题信息,操作读锁

读写锁适用于读多写少的场景,比如名字服务,配置服务等。

3 CompletableFuture 异步消息处理

RocketMQ 主从架构中,主节点与从节点之间数据同步/复制的方式有同步双写异步复制两种模式。

异步复制是指消息在主节点落盘成功后就告诉客户端消息发送成功,无需等待消息从主节点复制到从节点,消息的复制由其他线程完成。

同步双写是指主节点将消息成功落盘后,需要等待从节点复制成功,再告诉客户端消息发送成功。

同步双写模式是阻塞的,笔者按照 RocketMQ 4.6.1 源码,整理出主节点处理一个发送消息的请求的时序图。

整体流程:

  1. 生产者将消息发送到 Broker , Broker 接收到消息后,发送消息处理器 SendMessageProcessor 的执行线程池 SendMessageExecutor 线程池来处理发送消息命令;
  2. 执行 ComitLog 的 putMessage 方法;
  3. ComitLog 内部先执行 appendMessage 方法;
  4. 然后提交一个 GroupCommitRequest 到同步复制服务 HAService ,等待 HAService 通知 GroupCommitRequest 完成;
  5. 返回写入结果并响应客户端 。

我们可以看到:发送消息的执行线程需要等待消息复制从节点 , 并将消息返回给生产者才能开始处理下一个消息

RocketMQ 4.6.1 源码中,执行线程池的线程数量是 1 ,假如线程处理主从同步速度慢了,系统在这一瞬间无法处理新的发送消息请求,造成 CPU 资源无法被充分利用 , 同时系统的吞吐量也会降低。

那么优化同步双写呢 ?

从 RocketMQ 4.7 开始,RocketMQ 引入了 CompletableFuture 实现了异步消息处理

  1. 发送消息的执行线程不再等待消息复制到从节点后再处理新的请求,而是提前生成 CompletableFuture 并返回 ;
  2. HAService 中的线程在复制成功后,调用 CompletableFuture 的 complete 方法,通知 remoting 模块响应客户端(线程池:PutMessageExecutor ) 。

我们分析下 RocketMQ 4.9.4 核心代码:

  1. Broker 接收到消息后,发送消息处理器 SendMessageProcessor 的执行线程池 SendMessageExecutor 线程池来处理发送消息命令;
  2. 调用 SendMessageProcessor 的 asyncProcessRequest 方法;

  1. 调用 Commitlog 的 aysncPutMessage 方法写入消息 ;

    这段代码中,当 commitLog 执行完 appendMessage 后, 需要执行刷盘任务同步复制两个任务。

    但这两个任务并不是同步执行,而是异步的方式。

  2. 复制线程复制消息后,唤醒 future ;

  1. 组装响应命令 ,并将响应命令返回给客户端。

为了便于理解这一段消息发送处理过程的线程模型,笔者在 RocketMQ 源码中做了几处埋点,修改 Logback 的日志配置,发送一条普通的消息,观察服务端日志。

从日志中,我们可以观察到:

  1. 发送消息的执行线程(图中红色)在执行完创建刷盘 Future 和同步复制 future 之后,并没有等待这两个任务执行完成,而是在结束 asyncProcessRequest 方法后就可以处理发送消息请求了 ;
  2. 刷盘线程和复制线程执行完各自的任务后,唤醒 future,然后通过刷盘线程组装存储结果,最后通过 PutMessageExecutor 线程池(图中黄色)将响应命令返回给客户端。

笔者一直认为:异步是更细粒度的使用系统资源的一种方式,在异步消息处理的过程中,通过 CompletableFuture 这个神器,各个线程各司其职,优雅且高效的提升了 RocketMQ 的性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/41072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高性能MySQL-创建高性能索引

什么是索引 MySQL并没有统一的索引标准&#xff0c;不同存储引擎的索引的工作方式并不一样&#xff0c;也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引&#xff0c;其底层的实现也可能不同。 索引是存储引擎用于快速找到记录的一种数据结构&…

LeetCode 图解 | 206.反转链表(附有知识点回顾)

206.反转链表题目描述思路分析递归方式代码实现迭代方式&#xff08;非递归&#xff09;代码实现知识点回顾题目描述 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 /*** Definition for singly-linked list.* public class ListNode {*…

AtCoder Beginner Contest 279 G. At Most 2 Colors(计数/组合数学/dp递推)

题目 n(2<n<1e6)个格子&#xff0c;从左到右一字排开&#xff0c; 现在需要给格子涂色&#xff0c;有c(1<c<1e9)种颜色&#xff0c; 要求连续的k(2<k<n)个格子的颜色数最多只有2种&#xff0c; 求方案数&#xff0c;答案对998244353取模 思路来源 TOYO…

3. HTML的语法规范

3. HTML的语法规范 3.1.2 注释的作用和写法 ➢ 注释的作用&#xff1a; ​ •为代码添加的具有解释性、描述性的信息&#xff0c;主要用来帮助开发人员理解代码 ​ •浏览器执行代码时会忽略所有的注释 ➢ 注释的快捷键&#xff1a; ​ • 在VS Code中&#xff1a;ctrl / 3.1.…

中国的LPR改革及其意义

中国的LPR改革及其意义 – 潘登同学的宏观经济学笔记 文章目录中国的LPR改革及其意义 -- 潘登同学的宏观经济学笔记LPR的两次改革为什么需要LPR改革LPR改革的意义LPR的两次改革 LPR&#xff1a;商业银行对其最优质的客户执行的贷款利率 LPR在我国经历了两次改革&#xff0c;一…

PyTorch中torch.gather()函数

一. torch.gather()函数 官方文档&#xff1a;torch.gather函数&#xff0c;定义&#xff1a;从原tensor中获取指定dim和指定index的数据。 看到这个核心定义&#xff0c;我们很容易想到gather()的基本想法其实就类似从完整数据中按索引取值般简单&#xff0c;比如下面从列表中…

Kafka - 07 Zookeeper中存储的 Kafka 信息

我们在前面的文章中搭建了 Kafka 集群&#xff0c;分别是伪集群和真实的集群&#xff1a; Kafka - 03 Kafka安装 | 单机环境搭建 | 伪集群环境搭建 (一台虚拟机) Kafka - 06 Kafka 集群环境搭建&#xff08;三台虚拟机&#xff09; 两种方式中&#xff0c;Zookeeper 存储的 …

微机-------8086/8088寻址方式

目录 8086/8088寻址方式8086/8088寻址方式 1、立即寻址 立即数只能作为源操作数 MOV AL,80H MOV AX,306AH2、寄存器寻址 8位操作数的寄存器可以是:AH、AL、BH、BL、CH、CL、DH、DL 16位操作数的寄存器可以是:

如何录制视频?有了这款视频录制软件,粉丝多了,转发量也起来了

一款好用的视频录制软件&#xff0c;可以为视频录制工作带来便捷&#xff0c;高效的解决如何录制视频的难题。视频经济时代&#xff0c;很多人都用视频录制软件来进行视频录制&#xff0c;并分享到社交平台。但是很多时候&#xff0c;往往是两极分化&#xff0c;有的人粉丝几百…

[操作系统笔记]内存管理1

内容系听课复习所做笔记&#xff0c;图例多来自课程截图 覆盖与交换 下图为覆盖技术图示&#xff1a; 交换技术和中级调度密切相关&#xff1a; 中级调度&#xff08;内存调度&#xff09;&#xff1a;就是要决定将哪个处于挂起状态的进程重新调入内存 在哪保存被换出的进程 具…

m基于遗传优化算法的公式参数拟合matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 遗传算法的原理 遗传算法GA把问题的解表示成“染色体”&#xff0c;在算法中也即是以二进制编码的串。并且&#xff0c;在执行遗传算法之前&#xff0c;给出一群“染色体”&#xff0c;也即是假…

MySQL窗口函数

窗口函数在统计类的需求中很常见&#xff0c;稍微复杂一点的查询需求就有可能用到它&#xff0c;使用窗口函数可以极大的简化我们的 SQL 语句。像 Oracle、SQL Server 这些数据库在较早的版本就支持窗口函数了&#xff0c;MySQL 直到 8.0 版本后才支持它。 一般来说涉及复杂的分…

pytorch深度学习实战lesson29

第二十九课 深度学习硬件 这节课讲一下深度学习的硬件。具体来讲一下所谓的 CPU 和 GPU有什么区别&#xff0c;为什么 GPU 会快&#xff1f; 目录 CPU GPU 首先大家如果学习深度学习的话基本上都有一个GPU的电脑&#xff0c;如果你自己装一台机器的话&#xff0c;很有可能是…

【JavaScript高级】04-JavaScript函数增强

JavaScript函数增强函数属性和arguments函数对象的属性函数中的argumentsarguments转Array函数的剩余参数纯函数的理解和应用JavaScript纯函数定义纯函数案例柯里化的理解和应用柯里化定义组合函数理解和应用函数属性和arguments 函数对象的属性 在JavaScript中函数也是一个对…

维格云自定义按钮入门教程

目录 维格云自定义按钮功能简介 维格云自定义按钮适用场景 维格云自定义按钮设置步骤 开启插件 在拓展设置中创建按钮 在数据表中添加自定义按钮 效果展示 维格云自定义按钮注意事项 维格云自定义按钮功能简介 给不同情境下的数据详情页面,添加按钮,点击后可往另一个…

【博弈论】极小极大搜索(Minimax Algorithm)与α-β剪枝(Alpha-Beta Pruning)

文章目录一、极大极小搜索&#xff08;Minimax Algorithm&#xff09;二、α-β剪枝&#xff08;Alpha-Beta Pruning&#xff09;三、解题技巧一、极大极小搜索&#xff08;Minimax Algorithm&#xff09; 在零和博弈&#xff08;有完整信息的&#xff0c;确定的、轮流行动的&…

steam搬砖项目月入过万靠谱吗

大家好&#xff0c;我是阿阳 什么是国外steam游戏装备汇率差项目 一、项目介绍 其实&#xff0c;Steam就是一个美国的游戏平台&#xff0c;搬砖主要是搬的一款火遍全球的游戏CSGO的装备和饰品。CS听说过吧&#xff0c;这款游戏就是CS的一个系列。&#xff08;通俗易懂的理解就…

【C++数据结构】性能测量

性能测量 4.1 引言 容易计算内存的需求大小&#xff0c;只要知道编译后的代码和数据空间的大小就可以了&#xff1b; 数据空间的大小取决于用户所要解决的问题实例的大小。 要确定程序运行时间&#xff0c;需要通过实验来测量。 程序性能不仅依赖操作类型和数量&#xff0c…

Excel逆向查询的多种方法,赶快学起来

关于匹配查询&#xff0c;我们平时用的做多的就是VLOOKUP函数了&#xff0c;但VLOOKUP函数只能正向查找&#xff0c;不能逆向查询。 像是下面这种情况就不可以&#xff1a; &#xff08;现在我们想用编码去匹配书名&#xff09; 如果在不改变原表格结构的基础上查找出书目编码…

当MySQL和java通过媒婆navicate谈上了恋爱 ------ MySQL的安装和Java的连接

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 **收录于专栏 数据库 ⭐当MySQL和java通过媒婆navicate谈上了恋爱⭐ 文章目录⭐当MySQL和java通过媒婆navicate谈上了恋爱…