更新时间:2022-08-14 19:55:14
大家好,我是冰河~~
今天为大家带来一篇RocketMQ性能优化的文章,没想到RocketMQ性能优化还能这么玩儿,厉害了!!
从官方这边获悉,RocketMQ在4.9.1版本中对消息发送进行了大量的优化,性能提升十分显著,接下来请跟着我一起来欣赏大神们的杰作。
根据RocketMQ4.9.1的更新日志,我们从中提取到关于消息发送性能优化的【Issues:2883】,详细链接如下:具体优化点如截图所示:
首先先尝试对上述优化点做一个简单的介绍:
通过阅读上述的变更,总结出优化手段主要包括如下三点:
接下来结合源码,从中挑选具有代表性功能进行详细剖析,一起领悟Java高并发编程的魅力。
本次性能优化,主要针对的是RocketMQ同步复制场景。
我们首先先来简单介绍一下RocketMQ主从同步在编程方面的技巧。
RocketMQ主节点将消息写入内存后, 如果采用同步复制,需要等待从节点成功写入后才能向消息发送客户端返回成功,在代码编写方面也极具技巧性,时许图如下图所示:
温馨提示:在RocketMQ4.7版本开始对消息发送进行了优化,同步消息发送模型引入了jdk的CompletableFuture实现消息的异步发送。
核心步骤解读:
GroupTransferService代码如下图所示:
为了更加方便大家理解接下来的优化点,首先再总结提炼一下GroupTransferService的设计理念:
新版本的优化点主要包括:
正入下图所示,GroupTransferService向外提供接口putRequest,用来接受外部的同步任务,需要对ArrayList加锁进行保护,往ArrayList中添加数据属于一个内存操作,操作耗时小。
故这里没必要采取synchronized这种synchronized,而是可以自旋锁,自旋锁的实现非常轻量级,其实现如下图所示:
整个锁的实现就只需引入一个AtomicBoolean,加锁、释放锁都是基于CAS操作,非常的轻量,并且自旋锁不会发生线程切换。
“锁”的滥用是一个非常普遍的现象,多线程环境编程是一个非常复杂的交互过程,在编写代码过程中我们可能觉得自己无法预知这段代码是否会被多个线程并发执行,为了谨慎起见,就直接简单粗暴的对其进行加锁,带来的自然是性能的损耗,这里将该锁去除,我们就要结合该类的调用链条,判断是否需要加锁。
整个GroupTransferService中在多线程环境中运行需要被保护的主要是requestRead与requestWrite集合,引入的锁的目的也是确保这两个集合在多线程环境下安全访问,故我们首先应该梳理一下GroupTransferService的核心方法的运作流程:
doWaitTransfer方法操作的主要对象是requestRead链表,而且该方法只会被GroupTransferService线程调用,并且requestRead中方法会在swapRequest中被修改,但这两个方法是串行执行,而且在同一个线程中,故无需引入锁,该锁可以移除。
但由于该锁被移除,在swapRequests中进行加锁,因为requestWrite这个队列会被多个线程访问,优化后的代码如下:
从这个角度来看,其实主要是将锁的类型由synchronized替换为更加轻量的自旋锁。
被锁包裹的代码块是串行执行,即无法并发,在无法避免锁的情况下,降低锁的代码块,能有效提高并发度,图解如下:
如果多个线程区访问lock1,lock2,在lock1中domSomeThing1、domSomeThing2这两个方法都必须串行执行,而多个线程同时访问lock2方法,doSomeThing1能被多个线程同时执行,只有doSomething2时才需要串行执行,其整体并发效果肯定是lock2,基于这样理论:得出一个锁使用的***实践:被锁包裹的代码块越少越好。
在老版本中,消息写入加锁的代码块比较大,一些可以并发执行的动作也被锁包裹,例如生成offsetMsgId。
新版本采用函数式编程的思路,只是定义来获取msgId的方法,在进行消息写入时并不会执行,降低锁的粒度,使得offsetMsgId的生成并行化,其编程手段之巧妙,值得我们学习。