太强了!这么设计中间件完美解决了百万并发的问题!

news2024/11/15 21:39:55
V-xin:ruyuan0330 获得600+页原创精品文章汇总PDF

目录

  • 一、大部分人对Java并发仍停留在理论阶段
  • 二、中间件系统的内核机制:双缓冲机制
  • 三、百万并发的技术挑战
  • 四、内存数据写入的锁机制以及串行化问题
  • 五、片机制 + 分段加锁机制 六、缓冲区写满时的双缓冲交换
  • 七、且慢!刷写磁盘不是会导致锁持有时间过长吗?
  • 八、内存 + 磁盘并行写机制
  • 九、为什么必须要用双缓冲机制?
  • 十、总结

这篇文章,给大家聊聊一个百万级并发的中间件系统的内核代码里的锁性能优化。

很多同学都对Java并发编程很感兴趣,学习了很多相关的技术和知识。比如volatile、Atomic、synchronized底层、读写锁、AQS、并发包下的集合类、线程池,等等。


一、对Java并发仍停留在理论阶段

很多同学对Java并发编程的知识,可能看了很多的书,也通过不少视频课程进行了学习。

但是,大部分人可能还是停留在理论的底层,主要是了解理论,基本对并发相关的技术很少实践和使用,更很少做过复杂的中间件系统。

实际上,真正把这些技术落地到中间件系统开发中去实践的时候,是会遇到大量的问题,需要对并发相关技术的底层有深入的理解和掌握。

然后,结合自己实际的业务场景来进行对应的技术优化、机制优化,才能实现最好的效果。

因此,本文将从笔者曾经带过的一个高并发中间件项目的内核机制出发,来看看一个实际的场景中遇到的并发相关的问题。

同时,我们也将一步步通过对应的伪代码演进,来分析其背后涉及到的并发的性能优化思想和实践,最后来看看优化之后的效果。


二、中间件系统的内核机制:双缓冲机制

这个中间件项目整体就不做阐述了,因为涉及核心项目问题。我们仅仅拿其中涉及到的一个内核机制以及对应的场景来给大家做一下说明。

其实这个例子是大量的开源中间件系统、大数据系统中都有涉及到的一个场景,就是:核心数据写磁盘文件。

比如,大数据领域里的hadoop、hbase、elasitcsearch,Java中间件领域里的redis、mq,这些都会涉及到核心数据写磁盘文件的问题。

而很多大型互联网公司自研的中年间系统,同样也会有这个场景。只不过不同的中间件系统,他的作用和目标是不一样的,所以在核心数据写磁盘文件的机制设计上,是有一些区别的。


那么我们公司自研的中间件项目,简单来说,需要实现的一个效果是:开辟两块内存空间,也就是经典的内存双缓冲机制

然后核心数据进来全部写第一块缓冲区,写满了之后,由一个线程进行那块缓冲区的数据批量刷到磁盘文件的工作,其他线程同时可以继续写另外一块缓冲区。

我们想要实现的就是这样的一个效果。这样的话,一块缓冲区刷磁盘的同时,另外一块缓冲区可以接受其他线程的写入,两不耽误。核心数据写入是不会断的,可以持续不断的写入这个中间件系统中。

我们来看看下面的那张图,也来了解一下这个场景。

在这里插入图片描述

如上图,首先是很多线程需要写缓冲区1,然后是缓冲区1写满之后,就会由写满的那个线程把缓冲区1的数据刷入磁盘文件,其他线程继续写缓冲区2。

这样,数据批量刷磁盘和持续写内存缓冲,两个事儿就不会耽误了,这是中间件系统设计中极为常用的一个机制,大家看下面的图。

在这里插入图片描述

三、百万并发的技术挑战

先给大家说一下这个中间件系统的背景:这是一个服务某个特殊场景下的中间件系统,整体是集群部署。

然后每个实例部署的都是高配置机器,定位是单机承载并发达到万级甚至十万级整体集群足以支撑百万级并发,因此对单机的写入性能和吞吐要求极为高。

在超高并发的要求之下,上图中的那个内核机制的设计就显得尤为重要了。弄的不好,就容易导致写入并发性能过差,达不到上述的要求。


此外在这里多提一句,类似的这种机制在很多其他的系统里都有涉及。比如之前一篇文章:《作为一个程序员你懂降级吗?小心系统被高并发请求给击垮。。。》,那里面讲的一个系统也有类似机制。

只不过不同的是,那篇文章是用这个机制来做MQ集群整体故障时的容灾降级机制,跟本文的高并发中间件系统还有点不太一样,所以在设计上考虑的一些细节也是不同的。

而且,之前那篇文章的主题是讲这种内存双缓冲机制的一个线上问题:瞬时超高并发下的系统卡死问题


四、内存数据写入的锁机制以及串行化问题

首先我们先考虑第一个问题,你多个线程会并发写同一块内存缓冲,这个肯定有问题啊!

因为内存共享数据并发写入的时候,必须是要加锁的,否则必然会有并发安全问题,导致内存数据错乱。

所以在这里,我们写了下面的伪代码,先考虑一下线程如何写入内存缓冲。

在这里插入图片描述

好了,这行代码弄好之后,对应着下面的这幅图,大家看一下。

在这里插入图片描述

看到这里,就遇到了Java并发的第一个性能问题了,你要知道高并发场景下,大量线程会并发写内存的,你要是直接这样加一个锁,必然会导致所有线程都是串行化。

即一个线程加锁,写数据,然后释放锁。接着下一个线程干同样的事情。这种串行化必然导致系统整体的并发性能和吞吐量会大幅度降低的。


五、内存缓冲分片机制+分段枷锁机制

因此在这里必须要对内存双缓冲机制引入分段加锁机制,也就是将内存缓冲切分为多个分片,每个内存缓冲分片就对应一个锁。

这样的话,你完全可以根据自己的系统压测结果,调整内存分片数量,提升锁的数量,进而允许大量线程高并发写入内存。

我们看下面的伪代码,对这块就实现了内存缓冲分片机制:

在这里插入图片描述

好!我们再来看看,目前为止的图是什么样子的:

在这里插入图片描述

这里因为每个线程仅仅就是加锁,写内存,然后释放锁。

所以,每个线程持有锁的时间是很短很短的,单个内存分片的并发写入经过压测,达到每秒几百甚至上千是没问题的,因此线上系统我们是单机开辟几十个到上百个内存缓冲分片的。

经过压测,这足以支撑每秒数万的并发写入,如果将机器资源使用的极限,每秒十万并发也是可以支持的。


六、缓冲区写满时的双缓冲交换

那么当一块缓冲区写满的时候,是不是就必须要交换两块缓冲区?接着需要有一个线程来将写满的缓冲区数据刷写到磁盘文件中?

此时的伪代码,大家考虑一下,是不是如下所示:

在这里插入图片描述

同样,我们通过下面的图来看看这个机制的实现:

在这里插入图片描述

七、且慢!刷写磁盘不是会导致锁持有时间过长吗?

且慢,各位同学,如果按照上面的伪代码思路,一定会有一个问题:要是一个线程,他获取了锁,开始写内存数据。

然后,发现内存满了,接着直接在持有锁的过程中,还去执行数据刷磁盘的操作,这样是有问题的。

要知道,数据刷磁盘是很慢的,根据数据的多少,搞不好要几十毫秒,甚至几百毫秒。

这样的话,岂不是一个线程会持有锁长达几十毫秒,甚至几百毫秒?

这当然不行了,后面的线程此时都在等待获取锁然后写缓冲区2,你怎么能一直占有锁呢?

一旦你按照这个思路来写代码,必然导致高并发场景下,一个线程持有锁上百毫秒。刷数据到磁盘的时候,后续上百个工作线程全部卡在等待锁的那个环节,啥都干不了,严重的情况下,甚至又会导致系统整体呈现卡死的状态。


八、内存 + 磁盘并行写机制

所以此时正确的并发优化代码,应该是发现内存缓冲区1满了,然后就交换两个缓冲区。

接着直接就释放锁,释放锁了之后再由这个线程将数据刷入磁盘中,刷磁盘的过程是不会占用锁的,然后后续的线程都可以继续获取锁,快速写入内存,接着释放锁。

大家先看看下面的伪代码的优化:

在这里插入图片描述

按照上面的伪代码的优化,此时磁盘的刷写和内存的写入,完全可以并行同时进行。

因为这里核心的要点就在于大幅度降低了锁占用的时间,这是java并发锁优化的一个非常核心的思路。

大家看下面的图,一起来感受一下:

在这里插入图片描述

九、为什么必须要用双缓冲机制?

其实看到这里,大家可能或多或少都体会到了一些双缓冲机制的设计思想了,如果只用单块内存缓冲的话,那么从里面读数据刷入磁盘的过程,也需要占用锁,而此时想要获取锁写入内存缓冲的线程是获取不到锁的。

所以假只用单块缓冲,必然导致读内存数据,刷入磁盘的过程,长时间占用锁。进而导致大量线程卡在锁的获取上,无法获取到锁,然后无法将数据写入内存。这就是必须要在这里使用双缓冲机制的核心原因。


十、总结

最后做一下总结,本文从笔者团队自研的百万并发量级中间件系统的内核机制出发,给大家展示了Java并发中加锁的时候:

  • 如何利用双缓冲机制
  • 内存缓冲分片机制
  • 分段加锁机制
  • 磁盘 + 内存并行写入机制
  • 高并发场景下大幅度优化多线程对锁的串行化争用问题
  • 长时间占用锁的问题

其实在很多开源的优秀中间件系统中,都有很多类似的Java并发优化的机制,主要就是应对高并发的场景下大幅度的提升系统的并发性能以及吞吐量。大家如果感兴趣,也可以去了解阅读一下相关的底层源码。

V-xin:ruyuan0330 获得600+页原创精品文章汇总PDF

另外推荐儒猿课堂的1元系列课程给您,欢迎加入一起学习~

互联网Java工程师面试突击课(1元专享)

SpringCloudAlibaba零基础入门到项目实战(1元专享)

亿级流量下的电商详情页系统实战项目(1元专享)

Kafka消息中间件内核源码精讲(1元专享)

12个实战案例带你玩转Java并发编程(1元专享)

Elasticsearch零基础入门到精通(1元专享)

基于Java手写分布式中间件系统实战(1元专享)

基于ShardingSphere的分库分表实战课(1元专享)

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

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

相关文章

c++ - 第19节 - c++11

1.C11简介 c11简介:在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1),使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。不过由于C03(TC1)主要是对C98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯…

Pycharm打开Project(工程)时停留在preparing workspace时间过长,导致打开很慢的解决方法...

Pycharm打开Project(工程)时停留在preparing workspace时间过长,导致打开很慢的解决方法… 昊虹君用Pycharm进行Python的开发,一直用得好好的,也没作过什么异常操作,但是从上星期开始,打开Project(工程)时停留在preparing worksp…

【王道操作系统】2.1.1 进程的定义、特征、组成、组织

进程的定义、特征、组成、组织 文章目录进程的定义、特征、组成、组织1.进程的定义2.进程的特征3.进程的组成4.进程的组织1.进程的定义 程序的概念: 进程的概念: 进程和程序的区别和联系: 区别: 进程是动态的,程序是静…

Linux系统下crond任务调度指令的常见用法

Linux系统下crond任务调度指令的常见用法 任务调度 任务调度是指系统在某个时间执行的特定的命令或程序。任务调度分类:1)系统工作:有些重要的工作必须周而复始地执行。如病毒扫描等;2)个别用户工作:个别用户可能希望执行某些程序,比如对mysql数据库的备份。 基本语…

【攻防世界】网鼎杯2018 Web fakebook

学web总是能让我学到新的知识,很开心,很有趣,很好玩 打开题目 一个登录按钮,一个注册按钮,其余没有什么发现,对于web来说,常规先测试一下robots.txt文件 果然又发现,有一个bak文件…

opencv-python常用函数解析及参数介绍(二)——图像填充与图像融合

1.图像填充 函数及参数介绍 在opencv中使用cv2.copyMakeBorder可以进行图像的边界填充,需要的参数为(img, top_size, bottom_size, left_size, right_size, borderType),即(图片,上侧填充值,下侧填充值&a…

【数据结构与算法】KMP算法

文章目录前言一 .KMP的来历二.KMP解决的问题1.引入2.定义的引入1.字符串前缀2.字符串后缀3.最长相等前后缀3.核心思想三.next/prefix1.next的含义定义规律2.next的求取1.准备工作2.思路和图解4.应用前言 在C语言的strcmp的实现过程中,所涉及的算法较为简单&#xff…

Golang - 字符串操作汇总

Golang 字符串操作汇总1 string初始化2 遍历string3 byte & Rune3.1 初始化3.2 byte和rune区别1 string初始化 func newString() {//1. 字符串初始化// 方式一:使用简写声明,带有字符串的变量,支持特殊字符str : "one hello \n world"fmt…

三个月后,快手To B怎么样了?

未来,如何独立作战和走出快手的TOC“客户资源圈”,或将成为快手TOB新的十字路口。 作者|斗斗 编辑|皮爷 出品|产业家 人口红利终结,流量红利终结,超常规的高速增长终结。TOC模式的路越来越难走了。 快手与抖音作为短视频…

光耦特性以及计算

光耦特性 光耦器件电路图 这是我们常用的。光耦器件以及它的连接方式。 左侧R1是我们主要考虑的。 电流最小值是要让LED能够保持发光状态,最大值的话。1.不能让LED烧坏了。2. LED的负极端是单片机的。 这里主要是看引脚灌入电流的最大值。 电气规格 我们看看说…

手机数据包抓包详解

今天继续给大家介绍渗透测试相关知识,本文主要内容是手机数据包抓包详解。 免责声明: 本文所介绍的内容仅做学习交流使用,严禁利用文中技术进行非法行为,否则造成一切严重后果自负! 再次强调:严禁对未授权设…

并发编程——6.共享模型之不可变

目录6.共享模型之不可变6.1.日期转换的问题6.1.1.问题提出6.1.2.解决思路——同步锁6.1.3.解决思路——不可变6.2.不可变设计6.3.享元模式6.3.1.简介6.3.2.体现6.3.3.DIY6.4.final 原理6.5.无状态本文笔记整理来自黑马视频https://www.bilibili.com/video/BV16J411h7Rd/?p197&…

Unity脚本(二)

视频教程:https://www.bilibili.com/video/BV12s411g7gU/?share_sourcecopy_web Transform 对象的位置、旋转和缩放 场景中的每个对象都有一个Transform,用于存储和操作对象的位置、旋转和缩放。 每个Transform都可以有一个父级,能够分…

C语言强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示&am…

gitlab-ci.yml关键字(五)tags 、only 、when

tags 使用Tags用于选择Runner的标签列表 我们在创建Runner 时可以给该Runner打上特定的标签,那后续流水线中的job如果需要使用特定标签的Runner执行时,就需要使用tags来标记 比如这里有两个标签的Runner 也可以对当前的runner进行一些配置上的设置 …

2022总结:我是怎样从一个混子到如今小有所成

前言 🍀作者简介:被吉师散养、喜欢前端、学过后端、练过CTF、玩过DOS、不喜欢java的不知名学生。 🍁个人主页:红中 🍂抽根烟,吹个牛b(不是 入门 如果硬要问我是在什么时候入门的,那就要说到高一…

MySQL添加用户及用户权限管理

目录 1、用户 <1> 用户信息 <2> 创建用户 <3> 删除用户 <4> 修改用户密码 2、用户权限管理 <1> 查看用户权限 <2> 给用户授权 <3> 回收权限 1、用户 <1> 用户信息 MySQL中的用户&#xff0c;都存储在系统数据库mysq…

机器翻译与数据集

机器翻译指的是将文本序列从一种语言自动翻译成另一种语言。 使用单词级词元化时的词表大小&#xff0c;将明显大于使用字符级词元化时的词表大小。为了缓解这一问题&#xff0c;我们可以将低频词元视为相同的未知词元。 通过截断和填充文本序列&#xff0c;可以保证所有的文…

【LeetCode每日一题】——154.寻找旋转排序数组中的最小值 II

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【题目进阶】九【时间频度】十【代码实现】十一【提交结果】一【题目类别】 二分查找 二【题目难度】 困难 三【题目编号】 154.寻找旋转排序数组中的最小…

windows下OpenCV安装教程以及vs2019配置opencv教程

文章目录一. OpenCV下载二. OpenCV安装及配置三. VS2019项目配置OpenCV一. OpenCV下载 官网地址&#xff1a;Home - OpenCV 下载地址&#xff1a;OpenCV download | SourceForge.net 二. OpenCV安装及配置 双击下载好的安装包进行安装 安装过程实际上是一个解压过程 选择…