开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

你真的了解MySQL的order by吗

发表于 2021-11-09

排序这个词,我的第一感觉是几乎所有App都有排序的地方,淘宝商品有按照购买时间的排序、B站的评论有按照热度排序的…,当然我们今天说的并不是大数据下该如何优雅的排序,如何提升排序性能的问题,我们说一说MySQL中的排序。

对于MySQL,一说到排序,你第一时间想到的是什么?关键字order by?order by的字段最好有索引?叶子结点已经是顺序的?还是说尽量不要在MySQL内部排序?

事情的起因

现在假设有一张用户的朋友表:

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `user` (
`id` int(10) AUTO_INCREMENT,
`user_id` int(10),
`friend_addr` varchar(1000),
`friend_name` varchar(100),
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB;

表中目前有两个点需要关注下:

  1. 用户的 user_id ,朋友的姓名 friend_name*、朋友的地址 *friend_addr
  2. user_id 是有索引的

有一天,有个初级开发工程师小猿,收到了来自初级产品经理小汪的需求:

小汪:小猿同志,现在需要在后台加个功能,这个功能要支持根据用户 id 能查到他所有的朋友姓名和地址,并且要求朋友的姓名是按照字典排序的。

小猿:好的,这个功能简单,我马上就上线。

于是小猿书写了这样的sql:

1
sql复制代码select friend_name,friend_addr from user where user_id=? order by name

在电光石火的瞬间,小猿趾高气昂的上线了,这一切都很顺利,直到有一天有个运营同学导致了这样的查询:

1
sql复制代码select friend_name,friend_addr from user where user_id=10086 order by name

然而,这个查询竟然比平时慢很多,数据库报了慢查询,小猿此时慌的一b:这是怎么回事?user_id 明明有索引啊,而且机智地我还只用了 select friend_name,friend_addr,并没有用 select *呀。小猿此时不停地安慰自己,要淡定要淡定,然后突然想到有个explain命令,用explain来查看下那条sql的执行计划吧,当小猿用了explain之后,发现extra字段里面有个看起来很危险的字眼:using filesort。

“这个查询竟然用到了传说中的文件排序,但是如果一个人朋友不是很多,就算了用了文件排序,应该也很快吧”,除非这个user_id=10086的朋友很多,后来小猿去查了下,这个用户的朋友竟然有10w多个~。

陷入了沉思的小猿心想:这个锅看来是背定了,10w数据是有点大了,还有这个 using filesort 到底是怎么个排序原理?

解剖文件排序

有人可能说上面的问题是10w数据太大了,就算不排序也慢,这个其实是有道理的,10w数据一次性查出来,无论是MySQL内存缓冲区的占用,还是网络带宽的消耗都是非常大的,那如果我加了limit 1000呢?网络带宽的问题肯定是解决了,因为数据包整体变小了,但是 using filesort 的问题其实还是没有解决,看到这里你可能会有疑问,using filesort 难道是在文件中排序的?在文件中到底是怎么排序的?或者我这样问:如果给你来设计排序你会怎么处理?带着这些疑问和思考我们来看看 using filesort 会涉及到哪些技术难点以及是如何解决的?

  1. 首先我们的 user_id 是有索引的,所以会先在 user_id 索引树上检索我们的目标数据,即 user_id=10086 的数据,但是我们要查询的是 friend_name 和 friend_addr 字段,很不幸,光靠 user_id 索引是找不到这两个字段值的
  2. 于是需要回表,通过 user_id 对应的主键去主键索引树上去查找,ok,我们找到了第一条 user_id=10086 的 friend_name 和 friend_addr 字段
  3. 这时该怎么办?直接返回回去肯定不对,因为我需要对 friend_name 排序,如何排?数据都还没找全,那么就得把查到的数据先放在一个地方,这个地方就是 sort_buffer,看到名字我想你应该猜出来,没错,sort_buffer 就是用于这种情况下排序用的缓冲区,这里需要注意的是每个线程都会有一个单独的 sort_buffer,这么做的目的主要是为了避免多个线程对同一块内存进行操作带来锁竞争的问题。
  4. 当第一条数据的 friend_name 和 friend_addr 已经放入 sort_buffer 中,这当然没完,会一直重复同步的步骤,直至把所有 user_id=10086 的 friend_name 和 friend_addr 都放入到 sort_buffer 中才结束
  5. sort_buffer 中的数据已经放入完毕,接下来就该排序了,这里 MySQL 会对 friend_name 进行快排,通过快排后,sort_buffer 中 friend_name 就是有序的了
  6. 最后返回 sort_buffer 中的前1000条,结束。

一切看起来很丝滑,但是 sort_buffer 占用的是内存空间,这就尴尬了,内存本身就不是无限大的,它肯定是有上限的,当然 sort_buffer 也不能太小,太小的话,意义不大。在 InnoDB 存储引擎中,这个值是默认是256K。

1
2
3
4
5
6
sql复制代码mysql> show variables  like 'sort_buffer_size';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| sort_buffer_size | 262144 |
+------------------+--------+

也就是说,如果要放进 sort_buffer 中的数据是大于256K的话,那么采用在 sort_buffer 中快排的方式肯定是行不通的,这时候,你可能会问:MySQL难道不能根据数据大小自动扩充吗?额,MySQL是多线程模型,如果每个线程都扩充,那么分给其他功能buffer就小了(比如change buffer等),就会影响其他功能的质量。

这时就得换种方式来排序了,没错,此时就是真正的文件排序了,也就是磁盘的临时文件,MySQL会采用归并排序的思想,把要排序的数据分成若干份,每一份数据在内存中排序后会放入临时文件中,最终对这些已经排序好的临时文件的数据再做一次合并排序就ok了,典型的分而治之原理,它的具体步骤如下:

  1. 先将要排序的数据分割,分割成每块数据都可以放到 sort_buffer 中
  2. 对每块数据在 sort_buffer 中进行排序,排序好后,写入某个临时文件中
  3. 当所有的数据都写入临时文件后,这时对于每个临时文件而言,内部都是有序的,但是它们并不是一个整体,整体还不是有序的,所以接下来就得合并数据了
  4. 假设现在存在 tmpX 和 tmpY 两个临时文件,这时会从 tmpX 读取一部分数据进入内存,然后从 tmpY 中读取一部分数据进入内存,这里你可能会好奇为什么是一部分而不是整个或者单个?因为首先磁盘是缓慢的,所以尽量每次多读点数据进入内存,但是不能读太多,因为还有 buffer 空间的限制。
  5. 对于 tmpX 假设读进来了的是 tmpX[0-5] ,对于 tmpY 假设读进来了的是 tmpY[0-5],于是只需要这样比较:
    如果 tmpX[0] < tmpY[0],那么 tmpX[0] 肯定是最小的,然后 tmpX[1] 和 tmpY[0] 比较,如果 tmpX[1] > tmpY[0],那么 tmpY[0] 肯定是第二小的…,就这样两两比较最终就可以把 tmpX 和 tmpY 合并成一个有序的文件tmpZ,多个这样的tmpZ再次合并…,最终就可以把所有的数据合并成一个有序的大文件。

文件排序很慢,还有其他办法吗

通过上面的排序流程我们知道,如果要排序的数据很大,超过 sort_buffer 的大小,那么就需要文件排序,文件排序涉及到分批排序与合并,很耗时,造成这个问题的根本原因是 sort_buffer 不够用,不知道你发现没有我们的 friend_name 需要排序,但是却把 friend_addr 也塞进了 sort_buffer 中,这样单行数据的大小就等于 friend_name 的长度 + friend_addr 的长度,能否让 sort_buffer 中只存 friend_name 字段,这样的话,整体的利用空间就大了,不一定用得到到临时文件。没错,这就是接下来要说的另一种排序优化rowid排序。

rowid 排序的思想就是把不需要的数据不要放到 sort_buffer 中,让 sort_buffer 中只保留必要的数据,那么你认为什么是必要的数据呢?只放 friend_name?这肯定不行,排序完了之后,friend_addr 怎么办?因此还要把主键id放进去,这样排完之后,通过 id 再回次表,拿到 friend_addr 即可,因此它的大致流程如下:

  1. 根据 user_id 索引,查到目标数据,然后回表,只把 id 和 friend_name 放进 sort_buffer 中
  2. 重复1步骤,直至全部的目标数据都在 sort_buffer 中
  3. 对 sort_buffer 中的数据按照 friend_name 字段进行排序
  4. 排序后根据 id 再次回表查到 friend_addr 返回,直至返回1000条数据,结束。

这里面其实有几点需要注意的:

  1. 这种方式需要两次回表的
  2. sort_buffer 虽然小了,但是如果数据量本身还是很大,应该还是要临时文件排序的

那么问题来了,两种方式,MySQL 该如何选择?得根据某个条件来判断走哪种方式吧,这个条件就是进 sort_buffer 单行的长度,如果长度太大(friend_name + friend_addr的长度),就会采用 rowid 这种方式,否则第一种,长度的标准是根据 max_length_for_sort_data 来的,这个值默认是1024字节:

1
2
3
4
5
6
sql复制代码mysql> show variables like 'max_length_for_sort_data';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| max_length_for_sort_data | 1024 |
+--------------------------+-------+

不想回表,不想再次排序

其实不管是上面哪种方法,他们都需要回表+排序,回表是因为二级索引上没有目标字段,排序是因为数据不是有序的,那如果二级索引上有目标字段并且已经是排序好的了,那不就两全其美了嘛。

没错,就是联合索引,我们只需要建立一个 (user_id,friend_name,friend_addr)的联合索引即可,这样我就可以通过这个索引拿到目标数据,并且friend_name已经是排序好的,同时还有friend_addr字段,一招搞定,不需要回表,不需要再次排序。因此对于上述的sql,它的大致流程如下:

  1. 通过联合索引找到user_id=10086的数据,然后读取对应的 friend_name 和 friend_addr 字段直接返回,因为 friend_name 已经是排序好的了,不需要额外处理
  2. 重复第一步骤,顺着叶子节点接着向后找,直至找到第一个不是10086的数据,结束。

联合索引虽然可以解决这种问题,但是在实际应用中切不可盲目建立,要根据实际的业务逻辑来判断是否需要建立,如果不是经常有类似的查询,可以不用建立,因为联合索引会占用更多的存储空间和维护开销。

总结

  1. 对于 order by 没有用到索引的时候,这时 explain 中 Extra 字段大概是会出现 using filesort 字眼
  2. 出现 using filesort 的时候也不用太慌张,如果本身数据量不大,比如也就几十条数据,那么在 sort buffer 中使用快排也是很快的
  3. 如果数据量很大,超过了 sort buffer 的大小,那么是要进行临时文件排序的,也就是归并排序,这部分是由 MySQL 优化器决定的
  4. 如果查询的字段很多,想要尽量避免使用临时文件排序,可以尝试设置下 max_length_for_sort_data 字段的大小,让其小于所有查询字段长度的总和,这样放入或许可以避免,但是会多一次回表操作
  5. 实际业务中,我们也可以给经常要查询的字段组合建立个联合索引,这样既不用回表也不需要单独排序,但是联合索引会占用更多的存储和开销
  6. 大量数据查询的时候,尽量分批次,提前 explain 来观察 sql 的执行计划是个不错的选择。

最后

微信搜一搜【假装懂编程】,与作者共同学习,共同进步。
创作不易,各位的三连就是对作者最大的支持,也是作者最大的创作动力,我们下期见。

往期精彩:
  • 内存管理:程序装载那些事
  • 简单!代码原来是这样被CPU跑起来的
  • 20张图!常见分布式理论与解决方案

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java web-正确处理时间和时区问题 时区的概念 正确的

发表于 2021-11-09

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

日期和时间在程序中应用广泛,每种程序开发语言都自带处理日期和时间的相关函数,很多开发者把日期和时间存入数据库中,但是,一旦涉及到跨时区的日期和时间的处理时,大多数开发者根本就不明白如何正确地处理日期和时间。

首先,我们来看大部分的程序都是这么创建当前时间并存入数据库的:

1
2
java复制代码Date date = new Date();
store2db(date);

这么做的问题在于,数据库的DateTime类型没有时区(time zone)信息,因此,存入的是本地时间,并且丢掉了时区信息。如果你把数据库服务器的时区改了,或者把应用服务器的时区改了,读出来的日期和时间就是错误的。如果以Timestamp类型存储,各数据库的实现也不相同,有的进行了内部时区自动转换,而且,存储的时间不超过2037年。

如果应用服务器的时区和数据库服务器的时区不一致,你无法确定数据库驱动程序会不会自动帮你转换。

大多数开发者遇到这个问题不是去探索正确的解决方法,而是自作聪明地在存入数据库之前先来个“调整”,比如把当前时间减掉8小时,在显示的时候遇到不正确的时间时,又来个“调整”,以“负负得正”的方式来掩盖错误。在遇到夏令时的时区时,还需要写更复杂的代码来调整小时。

正确的做法是先理解时间和时区的概念。

时区的概念

之所以有时区的概念是因为住在地球上不同地方的人看到太阳升起的时间是不一样的。我们假设北京人民在早上8:00看到了太阳刚刚升起,而此刻欧洲人民还在夜里,他们还需要再过7个小时才能看到太阳升起,所以,此刻欧洲人民的手表上显示的是凌晨1:00。如果你强迫他们用北京时间那他们每天看到日出的时间就是下午3点。

也就是说,东8区的北京人民的手表显示的8:00和东1区欧洲人民手表显示的1:00是相同的时刻:

1
sql复制代码"2014-10-14 08:00 +8:00" = "2014-10-14 01:00 +1:00"

这就是本地时间的概念。

但是,在计算机中,如果用本地时间来存储日期和时间,在遇到时区转换的问题上,即便你非常清楚地知道如何转换,也非常麻烦,尤其是矫情的美国人还在采用夏令时。

所以我们需要引入“绝对时间”的概念。绝对时间不需要年月日,而是以秒来计时。当前时间是指从一个基准时间(1970-1-1 00:00:00 +0:00),到现在的秒数,用一个整数表示。

当我们用绝对时间表示日期和时间时,无论服务器在哪个时区,任意时刻,他们生成的时间值都是相等的。所有编程语言都提供了方法来生成这个时间戳,Java和JavaScript输出以毫秒计算的Long型整数,Python等输出标准的Unix时间戳,以秒计算的Float型浮点数,这两者转换只存在1000倍的关系。

实际上,操作系统内部的计时器也是这个标准的时间戳,只有在显示给用户的时候,才转换为字符串格式的本地时间。

正确的存储方式

基于“数据的存储和显示相分离”的设计原则,我们只要把表示绝对时间的时间戳(无论是Long型还是Float)存入数据库,在显示的时候根据用户设置的时区格式化为正确的字符串。

数据的存储和显示相分离是非常基本的设计原则,却常常被大多数开发人员忽略。举个例子,在Excel中编写一个表格,表格的数据可视为数据的存储格式,你可以把表格的数据以柱状图或饼图表示出来,这些不同的图表是数据的不同显示格式,存储数据的时候,我们应该存储表格数据,绝不应该存储柱状图等图片信息。

HTML和CSS也是数据的存储和显示相分离的设计思想。

所以,数据库存储时间和日期时,只需要把Long或者Float表示的时间戳存到BIGINT或REAL类型的列中,完全不用管数据库自己提供的DATETIME或TIMESTAMP,也不用担心应用服务器和数据库服务器的时区设置问题,遇到Oracle数据库你不必去理会with timezone和with local timezone到底有啥区别。

读取时间时,读到的是一个Long或Float,只需要按照用户的时区格式化为字符串就能正确地显示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// Java:
long t = System.currentTimeMillis();
System.out.println("long = " + t);

// current time zone:
SimpleDateFormat sdf_default = new SimpleDateFormat("yyyy-MM-dd HH:mm");
System.out.println(sdf_default.format(t));

// +8:00 time zone:
SimpleDateFormat sdf_8 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
sdf_8.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
System.out.println("GMT+8:00 = " + sdf_8.format(t));

// +7:00 time zone:
SimpleDateFormat sdf_7 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
sdf_7.setTimeZone(TimeZone.getTimeZone("GMT+7:00"));
System.out.println("GMT+7:00 = " + sdf_7.format(t));

// -9:00 time zone:
SimpleDateFormat sdf_la = new SimpleDateFormat("yyyy-MM-dd HH:mm");
sdf_la.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println("America/Los_Angeles = " + sdf_la.format(t));

输出:

1
2
3
4
5
sql复制代码long = 1413230086802
2014-10-14 03:54
GMT+8:00 = 2014-10-14 03:54
GMT+7:00 = 2014-10-14 02:54
America/Los_Angeles = 2014-10-13 12:54

总结:

==基于绝对时间戳的时间存储,从根本上就没有时区的问题。时区只是一个显示问题==。额外获得的好处还包括:

  1. 两个时间的比较就是数值的比较,根本不涉及时区问题,极其简单;
  2. 时间的筛选也是两个数值之间筛选,写出SQL就是between(?, ?);
  3. 显示时间时,把Long或Float传到页面,无论用服务端脚本还是用JavaScript都能简单而正确地显示时间。

你唯一需要编写的两个辅助函数就是String->Long和Long->String。String->Long的作用是把用户输入的时间字符串按照用户指定时区转换成Long存进数据库。

==唯一的缺点是数据库查询你看到的不是时间字符串,而是类似1413266801750之类的数字==。

个人感悟:时区只是一个显示问题(人为造成的,不过也是为了方便显示),但是使用绝对时间的时间戳就不存在时区问题,毕竟无论你在世界的哪个地方你的1s和我的1s的时长是一模一样的,这个是永远不会变的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

稳了!我准备了1个晚上的CMS垃圾收集器

发表于 2021-11-09

面试官:今天还是来聊聊CMS垃圾收集器呗?

候选者:嗯啊…

候选者:如果用Seria和Parallel系列的垃圾收集器:在垃圾回收的时,用户线程都会完全停止,直至垃圾回收结束!

候选者:CMS的全称:Concurrent Mark Sweep,翻译过来是「并发标记清除」

候选者:用CMS对比上面的垃圾收集器(Seria和Parallel和parNew):它最大的不同点就是「并发」:在GC线程工作的时候,用户线程「不会完全停止」,用户线程在「部分场景下」与GC线程一起并发执行。

候选者:但是,要理解的是,无论是什么垃圾收集器,Stop The World是一定无法避免的!

候选者:CMS只是在「部分」的GC场景下可以让GC线程与用户线程并发执行

候选者:CMS的设计目标是为了避免「老年代 GC」出现「长时间」的卡顿(Stop The World)

面试官:那你清楚CMS的工作流程吗?

候选者:只了解一点点,不能多了。

候选者:CMS可以简单分为5个步骤:初始标记、并发标记、并发预清理、重新标记以及并发清除

候选者:从步骤就不难看出,CMS主要是实现了「标记清除」垃圾回收算法

面试官:嗯…是的

候选者:我就从「初始标记」来开始吧

候选者:「初始标记」会标记GCRoots「直接关联」的对象以及「年轻代」指向「老年代」的对象

候选者:「初始标记」这个过程是会发生Stop The World的。但这个阶段的速度算是很快的,因为没有「向下追溯」(只标记一层)

候选者:在「初始标记」完了之后,就进入了「并发标记」阶段啦

候选者:「并发标记」这个过程是不会停止用户线程的(不会发生 Stop The World)。这一阶段主要是从GC Roots向下「追溯」,标记所有可达的对象。

候选者:「并发标记」在GC的角度而言,是比较耗费时间的(需要追溯)

候选者:「并发标记」这个阶段完成之后,就到了「并发预处理」阶段啦

候选者:「并发预处理」这个阶段主要想干的事情:希望能减少下一个阶段「重新标记」所消耗的时间

候选者:因为下一个阶段「重新标记」是需要Stop The World的

面试官:嗯…

候选者:「并发标记」这个阶段由于用户线程是没有被挂起的,所以对象是有可能发生变化的

候选者: 可能有些对象,从新生代晋升到了老年代。可能有些对象,直接分配到了老年代(大对象)。可能老年代或者新生代的对象引用发生了变化…

面试官:那这个问题,怎么解决呢?

候选者:针对老年代的对象,其实还是可以借助类card table的存储(将老年代对象发生变化所对应的卡页标记为dirty)

候选者:所以「并发预处理」这个阶段会扫描可能由于「并发标记」时导致老年代发生变化的对象,会再扫描一遍标记为dirty的卡页

面试官:嗯…

候选者:对于新生代的对象,我们还是得遍历新生代来看看在「并发标记」过程中有没有对象引用了老年代..

候选者:不过JVM里给我们提供了很多「参数」,有可能在这个过程中会触发一次 minor GC(触发了minor GC 是意味着就可以更少地遍历新生代的对象)

候选者:「并发预处理」这个阶段阶段结束后,就到了「重新标记」阶段

候选者:「重新标记」阶段会Stop The World,这个过程的停顿时间其实很大程度上取决于上面「并发预处理」阶段(可以发现,这是一个追赶的过程:一边在标记存活对象,一边用户线程在执行产生垃圾)

候选者:最后就是「并发清除」阶段,不会Stop The World

候选者:一边用户线程在执行,一边GC线程在回收不可达的对象

候选者:这个过程,还是有可能用户线程在不断产生垃圾,但只能留到下一次GC 进行处理了,产生的这些垃圾被叫做“浮动垃圾”

候选者:完了以后会重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备

面试官:嗯,CMS的回收过程,我了解了

面试官:听下来,其实就是把垃圾回收的过程给”细分”了,然后在某些阶段可以不停止用户线程,一边回收垃圾,一边处理请求,来减少每次垃圾回收时 Stop The World的时间

面试官:当然啦,中间也做了很多的优化(dirty card标记、可能中途触发minor gc等等,在我理解下,这些应该都提供了CMS的相关参数配置)

面试官:不过,我看现在很多企业都在用G1了,那你觉得CMS有什么缺点呢?

候选者:1.空间需要预留:CMS垃圾收集器可以一边回收垃圾,一边处理用户线程,那需要在这个过程中保证有充足的内存空间供用户使用。

候选者:如果CMS运行过程中预留的空间不够用了,会报错(Concurrent Mode Failure),这时会启动 Serial Old垃圾收集器进行老年代的垃圾回收,会导致停顿的时间很长。

候选者:显然啦,空间预留多少,肯定是有参数配置的

候选者:2. 内存碎片问题:CMS本质上是实现了「标记清除算法」的收集器(从过程就可以看得出),这会意味着会产生内存碎片

候选者:由于碎片太多,又可能会导致内存空间不足所触发full GC,CMS一般会在触发full GC这个过程对碎片进行整理

候选者:整理涉及到「移动」/「标记」,那这个过程肯定会Stop The World的,如果内存足够大(意味着可能装载的对象足够多),那这个过程卡顿也是需要一定的时间的。

面试官:嗯…

候选者:使用CMS的弊端好像就是一个死循环:

候选者:1. 内存碎片过多,导致空间利用率减低。

候选者:2. 空间本身就需要预留给用户线程使用,现在碎片内存又加剧了空间的问题,导致有可能垃圾收集器降级为Serial Old,卡顿时间更长。

候选者:3. 要处理内存碎片的问题(整理),同样会卡顿

候选者:不过,技术实现就是一种trade-off(权衡),不可能你把所有的事情都做得很完美

候选者:了解这个过程,是非常有趣的

面试官:那G1垃圾收集器你了解吗

候选者:只了解一点点,不能多了

候选者:不过,留到下次吧,先让你消化下,不然怕你顶不住了。

本文总结:

  • CMS垃圾回收器设计目的:为了避免「老年代 GC」出现「长时间」的卡顿(Stop The World)
  • CMS垃圾回收器回收过程:初始标记、并发标记、并发预处理、重新标记和并发清除。初始标记以及重新标记这两个阶段会Stop The World
  • CMS垃圾回收器的弊端:会产生内存碎片&&需要空间预留:停顿时间是不可预知的

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud Gateway实战之二:更多路由配

发表于 2021-11-09

欢迎访问我的GitHub

github.com/zq2599/blog…

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概览

  • 本文是《Spring Cloud Gateway实战》系列的第二篇,通过前文咱们了解到Spring Cloud Gateway的核心是路由配置,然后在本地application.yml中配置了一条路由,但这种修改本地配置文件的方式缺乏灵活性,未必能满足灵活多变的业务需求,因此,本篇的目的就是找出本地配置之外的其他配置方式来,满足各种实际需求;
  • 总的来说以下三种方式都是常用的:
  1. 目标地址支持用服务名(取代之前的IP+端口);
  2. 支持在nacos上配置;
  3. 支持写代码的方式配置;
  • 另外还有一种更加灵活的配置方式:动态代理,因为涉及到不少的代码所以会单独出一篇文章详细介绍

源码下载

  • 本篇实战中的完整源码可在GitHub下载到,地址和链接信息如下表所示(github.com/zq2599/blog…%EF%BC%9A)
名称 链接 备注
项目主页 github.com/zq2599/blog… 该项目在GitHub上的主页
git仓库地址(https) github.com/zq2599/blog… 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在spring-cloud-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

准备工作

  • 正式开始前需要再做一点准备工作,整个《Spring Cloud Gateway实战》系列中,所有请求最后都会被路由到provider-hello这个web上去,该服务目前只有一个web接口/hello/str,现在咱们再给它增加一个,后面的实战会用到
  • 新增加的web接口来自LBTest.java,可见非常简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码package com.bolingcavalry.provider.controller;

import com.bolingcavalry.common.Constants;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;

@RestController
@RequestMapping("/lbtest")
public class LBTest {

private String dateStr(){
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
}

/**
* 返回字符串类型
* @return
*/
@GetMapping("/str")
public String helloStr() {
return Constants.LB_PREFIX + ", " + dateStr();
}
}
  • 上述代码中的Constants.LB_PREFIX来自子工程common:
1
2
3
4
5
6
java复制代码package com.bolingcavalry.common;

public interface Constants {
String HELLO_PREFIX = "Hello World";
String LB_PREFIX = "Load balance";
}
  • 写完代码后,先确保nacos已经启动
  • 在启动provider-hello工程,启动成功后去看nacos,确认已经注册:

在这里插入图片描述

  • 准备完毕,可以开始实战了

目标地址支持用服务名(取代之前的IP+端口)

  • 咱们从最简单的开始,先看前文的路由配置,如下图红框,目标地址是IP+端口:

在这里插入图片描述

  • 玩过Spring Cloud的您自然看出了问题所在:没有注册发现,确实,这样将地址和端口写死在配置文件中是不合适的,咱们先来解决这个问题;
  • 新增名为gateway-by-loadbalance的子工程,其pom.xml中的依赖情况如下,可见重点是spring-cloud-starter-loadbalancer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
xml复制代码<dependencies>
<dependency>
<groupId>com.bolingcavalry</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 路由策略使用lb的方式是,这个依赖一定要有 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--nacos:注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
  • 启动类的代码省去了(和前文的一样)
  • 配置信息如下,重点是uri的值lb://provider-hello,用了前缀lb:,后面的provider-hello就是在nacos注册的服务名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yml复制代码server:
#服务端口
port: 8085
spring:
application:
name: gateway-by-loadbalance
cloud:
nacos:
# 注册中心的配置
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: path_route_lb
uri: lb://provider-hello
predicates:
- Path=/lbtest/**
  • 单元测试类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码package com.bolingcavalry.gateway;

import com.bolingcavalry.common.Constants;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
@ExtendWith(SpringExtension.class)
@AutoConfigureWebTestClient
public class HelloTest {

@Autowired
private WebTestClient webClient;

@Test
void testLoadBalance() {
webClient.get()
.uri("/lbtest/str")
.accept(MediaType.APPLICATION_JSON)
.exchange()
// 验证状态
.expectStatus().isOk()
// 验证结果,注意结果是字符串格式
.expectBody(String.class).consumeWith(result -> assertTrue(result.getResponseBody().contains(Constants.LB_PREFIX)));
}
}
  • 运行单元测试,通过,可见上述配置可以通过前缀lb:准确找到服务:

在这里插入图片描述

支持在nacos上配置

  • 将所有配置信息写在application.yml中有个问题:不能远程配置,这在应用数量较多的场景就不方便了,好在nacos提供了远程配置的能力,应用启动后可以从nacos取得自己的配置信息,咱们来试试
  • 新增名为gateway-nacos-config的子工程,其pom.xml中的依赖情况如下,请注意里面的中文注释,每指明了每一个依赖的作用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
xml复制代码<dependencies>
<dependency>
<groupId>com.bolingcavalry</groupId>
<artifactId>common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 使用bootstrap.yml的时候,这个依赖一定要有 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 路由策略使用lb的方式是,这个依赖一定要有 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--nacos:配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos:注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
  • 本地的配置文件bootstrap.yml,非常简单,就是nacos的地址和远程配置信息:
1
2
3
4
5
6
7
8
9
yml复制代码spring:
application:
name: gateway-nacos-config
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yml
group: DEFAULT_GROUP
  • 接下来再nacos增加一个配置文件,操作如下图红框:

在这里插入图片描述

  • 增加一个配置,要注意的地方如下(配置信息的文本稍后给出,便于复制):

在这里插入图片描述

  • 上图中完整的配置信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
yml复制代码server:
port: 8083
spring:
cloud:
gateway:
routes:
- id: path_route_addr
uri: http://127.0.0.1:8082
predicates:
- Path=/hello/**
- id: path_route_lb
uri: lb://provider-hello
predicates:
- Path=/lbtest/**
  • 测试类中的两个测试方法如下所示,和前面没有任何区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Test
void testHelloPredicates() {
webClient.get()
.uri("/hello/str")
.accept(MediaType.APPLICATION_JSON)
.exchange()
// 验证状态
.expectStatus().isOk()
// 验证结果,注意结果是字符串格式
.expectBody(String.class).consumeWith(result -> assertTrue(result.getResponseBody().contains(Constants.HELLO_PREFIX)));
}

@Test
void testLoadBalance() {
webClient.get()
.uri("/lbtest/str")
.accept(MediaType.APPLICATION_JSON)
.exchange()
// 验证状态
.expectStatus().isOk()
// 验证结果,注意结果是字符串格式
.expectBody(String.class).consumeWith(result -> assertTrue(result.getResponseBody().contains(Constants.LB_PREFIX)));
}
  • 运行单元测试类,测试通过,证明从nacos获取配置文件成功:

在这里插入图片描述

写代码的方式配置

  • 前面的几个例子,路由信息都是写在配置文件中的,其实还有一种方式:写代码配置路由,能自己写代码来配置,这灵活性就更强了
  • 新增名为gateway-by-code的子工程,其pom.xml文件参照前面工程的即可
  • 接下来的本例的重点,在配置类中增加一个RouteLocator类型的bean,通过以下代码即可增加一个路由:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码package com.bolingcavalry.gateway.cofig;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RouteConfig {

@Bean
public RouteLocator customizeRoute(RouteLocatorBuilder builder) {
return builder
.routes()
.route(
// 第一个参数是路由的唯一身份
"path_route_lb",
// 第二个参数是个lambda实现,
// 设置了配套条件是按照请求路径匹配,以及转发地址,
// 注意lb://表示这是个服务名,要从
r -> r.path("/lbtest/**").uri("lb://provider-hello")
)
.build();
}
}
  • 上述代码只配置了一个路由,还有一个在配置文件中,这样就能验证代码和配置文件能不能同时生效了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yml复制代码server:
#服务端口
port: 8084
spring:
application:
name: gateway-by-code
cloud:
nacos:
discovery:
# nacos服务地址
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: path_route_addr
uri: http://127.0.0.1:8082
predicates:
- Path=/hello/**
  • 测试类和之前工程的一模一样,就不占用篇幅了,依旧是两个测试方法testHelloPredicates和testLoadBalance
  • 执行单元测试可以顺利通过,证明代码配置路由没有问题:

在这里插入图片描述

  • 至此,负载均衡、nacos配置、代码配置的实例咱们都尝试过了,它们合起来会给实际生存环境的配置带来很大的方便,希望能够给您一些参考

缺陷和解决之道

  • 上述配置方式虽多,但有一个共同的问题:每当配置变动后,Gateway应用需要重启才能生效,这在请求不间断的生产环境是难以接受的
  • 为了让最新的路由配置能在Gateway应用不重启的前提下生效,接下来的文章咱们一起去探索动态路由是如何实现的

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界…
github.com/zq2599/blog…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在IntelliJ IDEA中,开发一个摸鱼看书插件

发表于 2021-11-09

作者:小傅哥

博客:bugstack.cn

原文:mp.weixin.qq.com/s/R8qvoSNye…

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、说明

方向不对,努力白费!

总有人拿到产品的需求,就着急开干,反正也懒的想开发中会发生啥,上线后多少人使用,管它三七二十一先堆起来代码看一看,反正能跑就行,无论代码还是你!

其实很多时候在编写代码前,所需要做的技术调研、架构设计、模块分层、数据结构、详细分析、方案评审等,与三七二十一那家伙对比起来,好像都会显得有点慢。但这个看上去慢的过程,却能解决以后很多常见和麻烦的问题,比如产品需求迭代、业务流程变更、代码逻辑更改、线上异常排查。虽然看着慢,但这个积基树本的过程就像打地基一样,总得有一个稳定的根基,才能盖好整栋大楼。万丈高楼平地起,勿在浮沙筑高台

二、需求目的

如果你需要开发一个自定义功能的插件,无论是处理代码、辅助ORM生成、日志信息记录等,都会需要进行一个插件的功能配置进行初始化操作以及把对应功能展示到整个 IDEA 窗体中的右边栏或者下边栏中,这样才能满足一个插件的基本需求。

那么这样就需要在 IDEA 窗体 File -> Settings 中扩展自己的配置窗体,以及开发自己需要的 ToolWindow 嵌入到 IDEA 中(左侧、右侧、下侧),这里窗体的开发需要用到 Swing 但目前在 IDEA 中开发这样的功能只需要拖拽窗体就可以,还是蛮容易的。

那么接下来我们以一个在 IDEA 中摸鱼看书的场景为案例,学习配置窗体和阅读窗体的功能实现。

三、案例开发

1. 工程结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码guide-idea-plugin-tool-window
├── .gradle
└── src
├── main
│ └── java
│ └── cn.bugstack.guide.idea.plugin
│ └── factory
│ │ ├── ReadFactory.java
│ │ └── SettingFactory.java
│ └── ui
│ │ ├── ReadUI.java
│ │ ├── ReadUI.form
│ │ ├── SettingUI.java
│ │ └── SettingUI.form
│ └── Config
├── resources
│ └── META-INF
│ └── plugin.xml
├── build.gradle
└── gradle.properties
  • 源码获取:#公众号:bugstack虫洞栈 回复:idea 即可下载全部 IDEA 插件开发源码

此工程主要涉及两部分,在factory中一个是配置窗体、一个是阅读窗体,与之对应的两组UI的实现。最后 factory 类的实现都会配置到 plugin.xml 中进行使用,同时也是在 plugin.xml 中控制窗体位置和图标。

2. 创建 UI 窗体

2.1 创建方式

New -> Swing UI Designer -> GUI Form

  • 在 Java 中创建窗体的方式主要有 AWT、Swing、JavaFx,由于 IDEA 使用 Swing 开发,所以这里创建 Swing 窗体的兼容性会更好。
  • 那么这里 Swing 窗体的创建可以是自己手写窗体结构,也可以使用可视化拖拽的 GUI Form 如果你的窗体不复杂,其实拖拽的方式就可以满足使用。

2.2 配置页窗体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public class SettingUI {

private JPanel mainPanel;
private JPanel settingPanel;
private JLabel urlLabel;
private JTextField urlTextField;
private JButton urlBtn;

public SettingUI() {
// 给按钮添加一个选择文件的事件
urlBtn.addActionListener(e -> {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.showOpenDialog(settingPanel);
File file = fileChooser.getSelectedFile();
urlTextField.setText(file.getPath());
});
}

public JComponent getComponent() {
return mainPanel;
}

public JTextField getUrlTextField() {
return urlTextField;
}
}

  • 配置页窗体主要提供文章路径的选择,这里需要用到的标签包括:JLabel、JTextField、JButton
  • 在使用 GUI Form 创建完窗体后,就会出现这样一个可视化的页面,右侧可以把各类标签拖到中间的面板中,左侧进行设置展示名称和属性名称。
  • 最终这里的代码标签代码会展示到 SettingUI.java 中,而渲染内容会被隐藏,这样的方式也比较方便控制一些自定义内容的添加,例如事件和新窗体等
  • 另外在 SettingUI.java 中,还需要在构造函数添加一个按钮事件,用于打开文件选择器,把我们需要打开的文件,设置到 urlTextField 中。

2.3 阅读页窗体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class ReadUI {

private JPanel mainPanel;
private JTextPane textContent;

public JComponent getComponent() {
return mainPanel;
}

public JTextPane getTextContent() {
return textContent;
}

}

  • 在窗体创建和配置页窗体是一样的,也是通过拖拽到面板中,用于展示路径文件内容。
  • 你可以适当的添加一些其他按钮进去,比如翻页阅读、滚动条、字数展示等。

3. ToolWindow 工具框

为了把我们自己实现的阅读窗体放到整个 IDEA 右侧侧边栏中,我们需要创建一个实现了 ToolWindowFactory 的接口,并把实现类配置到 plugin.xml 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class ReadFactory implements ToolWindowFactory {

private ReadUI readUI = new ReadUI();

@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
// 获取内容工厂的实例
ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
// 获取 ToolWindow 显示的内容
Content content = contentFactory.createContent(readUI.getComponent(), "", false);
// 设置 ToolWindow 显示的内容
toolWindow.getContentManager().addContent(content);
// 全局使用
Config.readUI = readUI;
}

}
  • 接口方法 ToolWindowFactory#createToolWindowContent 是需要自己工具框类实现的方法,在这个 createToolWindowContent 方法中把自己的窗体 ReadUI 实例化后填充进去即可。
  • 添加窗体的补助主要依赖于 ContentFactory.SERVICE.getInstance() 创建出 ContentFactory 并最终使用 toolWindow 添加窗体显示 UI 即可。
  • 这里我们额外的还添加了一个全局属性 Config.readUI 这是为了后续可以在配置窗体中使用这个 UI 进行设置文件内容。

4. Configurable 配置框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码public class SettingFactory implements SearchableConfigurable {

private SettingUI settingUI = new SettingUI();

@Override
public @NotNull String getId() {
return "test.id";
}

@Override
public @Nls(capitalization = Nls.Capitalization.Title) String getDisplayName() {
return "test-config";
}

@Override
public @Nullable JComponent createComponent() {
return settingUI.getComponent();
}

@Override
public boolean isModified() {
return true;
}

@Override
public void apply() throws ConfigurationException {
String url = settingUI.getUrlTextField().getText();
// 设置文本信息
try {
File file = new File(url);
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
randomAccessFile.seek(0);

byte[] bytes = new byte[1024 * 1024];
int readSize = randomAccessFile.read(bytes);

byte[] copy = new byte[readSize];
System.arraycopy(bytes, 0, copy, 0, readSize);

String str = new String(copy, StandardCharsets.UTF_8);

// 设置内容
Config.readUI.getTextContent().setText(str);

} catch (Exception ignore) {
}
}

}
  • 实现自 SearchableConfigurable 接口的方法比较多,包括:getId、getDisplayName、createComponent、isModified、apply 这些里面用于写逻辑实现的主要是 createComponent 和 apply
  • createComponent 方法主要是把我们自己创建的 UI 面板提供给 JComponent
  • apply 是一个事件,当我们点击完成配置的 OK、完成,时候就会触发到这个方法。在这个方法中我们拿到文件的 URL 地址使用 RandomAccessFile 进行读取解析文件,并最终把文件内容展示到阅读窗体中 Config.readUI.getTextContent().setText(str);

5. 配置 plugin.xml

1
2
3
4
5
6
7
8
9
10
java复制代码<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<!-- 配置 File -> Settings -> Tools -->
<projectConfigurable groupId="tools" displayName="My Test Config" id="test.id"
instance="cn.bugstack.guide.idea.plugin.factory.SettingFactory"/>

<!-- 窗体 (IDEA 界面右侧) -->
<toolWindow id="Read-Book" secondary="false" anchor="right" icon="/icons/logo.png"
factoryClass="cn.bugstack.guide.idea.plugin.factory.ReadFactory"/>
</extensions>
  • 本次在 plugin.xml 中的主要配置内容就是 projectConfigurable 和 toolWindow,另外在 toolWindow 中还添加了一个 icon 的 logo,配置完成后就可以在 IDEA 页面展示出我们的自己添加的窗体了。

四、插件测试

  • 通过 Plugin 启动插件,这个时候会打开一个新的 IDEA 窗体,在这个新窗体中就可以看到我们添加的功能了。

配置文件路径

  • 点击选择按钮,选择你的文件位置,选择后点击 OK

查看展示文件

  • 确认好文件路径后,就可以再右侧栏看到自己的文件展示内容了。是不是在扩展些,就适合你摸鱼了!?

五、总结

  • 学习自定义开发UI,把UI填充到需要放置的 IDEA 窗体位置,并在窗体中添加功能的流程步骤,其实主要包括三方面:Swing UI、Factory 实现类、plugin 配置。
  • 在 plugin 配置中,主要包括如窗体ID、位置、icon图标、对应的实现类,如果不添加这些是不能正常展示窗体信息的。
  • 另外可以以这个案例为基础,添加自己想完成的功能,比如让这个摸鱼看书的功能更加完善,可以支持不同类型的文件,甚至可以是 PDF 的阅读,以及你想看的书籍。

六、系列推荐

  • IDEA 插件怎么开发?
  • 如何发布Jar包到Maven中心仓库
  • 12种 vo2dto 方法,就 BeanUtils.copyProperties 压测最拉胯!
  • 炸!1024,小傅哥的博客升级啦,文章开源、支持PR,冲哇!
  • 面经手册 · 开篇《面试官都问我啥》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker 教程(三):Docker 命令

发表于 2021-11-09

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」

你好,我是看山。

本文源自并发编程网的翻译邀请,翻译的是 Jakob Jenkov 的 《Docker 教程》 中的第三篇。

我们通过 Docker 命令与操作 Docker 服务,可以构建 Docker 镜像、运行 Docker 容器、将 Docker 镜像推送到远程等。本文主要介绍一些常用的 Docker 命令。

Docker 有很多命令,本文不会介绍所有的命令,如果需要完整的命令教程,可以参考 Docker command line reference。

请注意,根据在 Linux 系统上安装 Docker 的方式,可能需要在所有命令前面加上sudo,使用 root 权限运行他们。比如:

1
erlang复制代码sudo docker build .

… 而不仅仅是:

1
erlang复制代码docker build .

Docker 命令行工具

在 Linux 系统中安装 Docker 时,会安装一个名为docker的命令行工具,可以在 Linux 命令行执行。

docker有很多的参数,不同的参数作用不同,可以指挥 Docker 做出不同的行为,可以认为是给 Docker 的命令。以下是docker命令示例:

1
erlang复制代码docker build .

这个示例中包含三部分:docker命令、参数build、参数.。

参数build是一个 Docker 命令,换句话说,是一个给 docker 可执行命令行的命令。通常,docker 命令行的第一个参数都是 Docker 命令。

参数.是build命令的参数。

docker build

docker build命令是调用 Docker 从 Dockerfile 文件构建 docker 镜像,要使用docker build命令,必须告诉它从哪个 Dockerfile 文件生成镜像。关于 Dockerfile 的内容,可以查看 这里。以下是docker build命令示例:

1
erlang复制代码docker build .

参数.表示从当前目录找到 Dockerfile 文件。

docker images

docker images命令是列出本机的 Docker 镜像,以下是docker images命令示例:

1
复制代码docker images

运行上述命令会输出类似下面的内容:

1
2
复制代码REPOSITORY       TAG        IMAGE ID        CREATED          SIZE
hello-world latest fce289e99eb9 9 months ago 1.84kB

docker run

docker run命令用来基于给定的 Docker 镜像运行 Docker 容器,docker run的参数,可以是 Docker 镜像的名称或 ID,以下是运行 Docker 容器的示例:

1
arduino复制代码docker run hello-world

这个例子会基于hello-world镜像运行 Docker 容器。

我们还可以通过 Docker 镜像 ID 运行 Docker 容器,命令如下:

1
arduino复制代码docker run fce289e99eb9

docker ps

docker ps命令用于显示当前系统中正在运行的 Docker 容器,示例如下:

1
复制代码docker ps

注意,一些 Docker 容器会在完成任务后立即关闭,在docker ps的结果列表中,这种 Docker 容器很有可能会很长时间都不可见。

【译者注:本文就 4 个命令,还有很多常用的命令没有提到,比如docker exec之类的。其实,对于 Docker 命令行来说,我们可以先浏览一遍,知道个大概,等有需要的时候,重点看一下。个人感觉,命令的需要关键是要经常用。如果不用,转眼就忘,可以类比 Linux 命令。】

推荐阅读

  • Docker 教程(一):Docker 是什么
  • Docker 教程(二):Dockerfile
  • Docker 教程(三):Docker 命令

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务中服务注册和发现的可行性方案 服务注册表 服务发现 服

发表于 2021-11-09

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」

你好,我是看山。

在 微服务的基建工作 中提到过,在云原生、微服务时代,如果还是手动修改服务地址,是几乎不可完成的工作,需要一种机制完成自动上报和获取服务地址的支撑组件,可以保障服务的快速上线和下线,这就是服务注册/发现组件。

为了表述方便,从系统规模定义几个阶段:

  • 巨型应用架构时期:很多应用都是一个巨型服务,一个应用包含所有功能,部署在小型机和大型机上,或者直接部署在物理服务器上。
  • 单体架构时期:应用体量缩小,服务增多,而且出现虚拟化技术,物理服务器被连接成虚拟化平台,应用部署在虚拟机中。
  • SOA架构时期:应用通用功能逐渐沉淀,业务应用借助沉淀的通用组件逐渐解耦,微服务的很多组件也是从这个时期开始成型。
  • 微服务架构时期:这个时期承接模块化时期,甚至有一种说法是微服务只是SOA的一种特殊形式。系统进一步解耦,根据业务角色不同,应用以业务为分界,缩小为业务单元。
  • 函数架构时期:应用进一步分割为函数,实现serverless架构,不需要具体的服务器概念,只需要执行函数的服务即可。目前来看,这个时期是比较理想的时期,因为不同人相互协作定义的函数,可能重复或者冲突,不利于架构的演进。

随着大家对在微服务或者函数架构中趟坑,很多人开始提出回归单体应用架构,这应该也是架构螺旋进步的一种方式。

在微服务中,还有一种角色是根据调用关系定义的:

  • 客户端服务(简称客户端):调用其他服务的实例
  • 服务端服务(简称服务端):被其他服务实例调用的实例

微服务中客户端和服务端只对一个调用定义的,客户端在其他调用关系中,角色可能会转变为服务端。

服务注册表

说到服务发现时,必须要说一个重要组件:服务注册表,它是服务发现的核心,是一个包含了所有服务实例的网络位置和监控状态的数据库,通过服务注册组件将信息写入服务注册表,通过服务发现组件获取有效的服务实例的网络位置信息。目前常用的服务注册表有:Eureka、etcd、Consul、Zookeeper,Kibernetes等镜像调度服务没有明确的服务注册表组件,是通过内置的服务注册功能实现。对于比如F5和Nginx这种代理器,其中的upstream配置也属于服务注册表。

服务发现

在微服务架构中,服务之间通过轻量级协议互相调用,一般是HTTP请求,为了完成一次请求,服务需要知道目标服务实例的网络位置(IP和端口)。

在巨型应用架构时期,配置一个符合要求的服务器环境需要花费大量的时间,也就意味着服务地址发生变动的概率和频率都非常低,而且很多应用部署在一台小型机或者大型机上。到了单体架构时期,应用体量大数量少,发生地址变动所需要修改的地方就比较少,所以对于服务发现也就没有那么强的需求。换句话说,在单体架构之前,服务实例的相对位置固定,变动频率低,可以通过硬编码到代码中。

但是到了云时代,服务器环境配置变得简单,数量逐渐增多,扩展和迁移逐渐频繁。而且,随着虚拟化和容器的应用,服务器地址都是根据规则动态分配,由于服务升级、扩展、失败回滚等情况增多,服务的网络位置甚至不可预知。这个时候必须使用服务发现机制保证客户端服务能够自动获取服务端服务的地址。

通常,服务发现有两种模式:客户端发现模式、服务端发现模式。

客户端发现模式

客户端发现模式通过客户端组件根据负载均衡算法决定相应服务实例的网络位置,也就是说,客户端组件保存有服务端所有实例的服务注册表,调用发生时,根据负载均衡算法,从服务注册表中选择一个网络位置,向服务端发起请求,完成调用。由于网络的不可靠性,有的客户端组件还会实现访问失败重试、访问超时时间设定等功能。

这种模式的架构如图:

图片

具体的过程为:

  1. 服务实例向服务注册器上报网络位置,即注册
  2. 客户端服务发现组件定时拉取服务注册器中服务实例的网络位置信息及健康状态,保存在服务注册表中
  3. 客户端服务调用服务端服务时,通过客户端服务发现组件,根据负载均衡算法,选取可用一个服务实例,发起调用

在Spring Cloud(或者说是Netflix开源组件)中,组件Eureka Server组件相当于服务注册器,Eureka Client组件实现了服务注册表,Ribbon实现了负载均衡算法和重试策略。

客户端发现模式优缺点兼备。优点是对已有服务友好,除了客户端组件外,其他部分无需改动。而且,客户端存有所有服务实例信息,可以有针对性的定义负载均衡算法。缺点是客户端与服务注册器绑定,需要针对每种语言实现不同的客户端组件。

服务端发现模式

服务端发现模式是有一个单独的服务发现组件,这个实例持有服务注册表,同时也起到负载均衡器的作用,客户端调用服务端时,直接调用服务发现实例,通过服务实例代理到后端服务实例中,所以服务端发现模式也被称为代理模式。

这种模式的架构如图:

图片

具体的过程为:

  1. 服务实例向服务注册器上报网络位置,即注册
  2. 服务发现实例定时通过某种机制获取服务注册器中服务实例的网络位置信息及健康状态,保存在服务注册表中
  3. 客户端服务调用服务端服务时,直接调用服务发现实例,服务发现实例根据内部实现,查询服务注册表,将请求代理到后端服务实例

服务端发现模式中的服务发现组件有两种实现方式:

  • 第一种,集中式代理,服务发现组件是单独的服务实例,这个实例是高可用高吞吐的系统组件,代理后端服务实例,代表性的是F5和Nginx。
  • 第二种,主机进程代理,服务发现组件由系统环境提供,集成在主机上或者集成在操作系统中,代表性的是Istio ServiceMesh。

两种实现方式的优点是语言无关,客户端不需要关心任何服务发现的细节,只需要将原有的调用实例的请求修改为向服务发现实例发送请求。集中式代理的缺点是,存在单点问题,需要单独部署一个高可用、高吞吐的系统服务,由原来的一次调用增加为两次调用,有性能开销。主机进程代理的缺点是运维复杂,需要能力强的运维团队做支持。

服务注册

服务注册是将服务实例的网络信息和健康状态写入服务注册表中,有两种方式:自注册模式、第三方注册模式。

自注册模式

这种模式是服务实例主动向服务注册表上报网络位置和健康状态,有的实现中,服务实例还会通过心跳保障注册信息不会过期。

图片

Eureka就是采用的这种方式,服务实例通过Eureka Client组件主动上报自己的网络位置信息和健康状态。

这种模式实现相对简单,但是把服务实例和服务注册表耦合,优缺点明显。

第三方注册模式

第三方注册模式是服务实例不需要直接向服务注册表注册信息,而是借助被称为注册器的组件进行注册。服务注册器是通过扫描部署环境或者订阅事件的方式,跟踪服务实例的变更。当监测到服务实例有变化,会向服务注册表上报变化信息。

图片

这种方式可以将服务实例与服务注册表解耦,同时也引入另外的问题。即注册器需要内置在部署环境中,增加了运维复杂性。或者注册器需要部署一个集中式的管理组件,成为系统约束点。

未完待续

在微服务中,服务实例的运行环境会动态变化,实例网络位置也是如此,因此,客户端为了访问服务必须要使用服务发现机制。

服务发现有客户端发现模式和服务端发现模式,服务注册有自注册模式和第三方注册模式。服务发现和服务注册通过服务注册表链接在一起。

后面有时间,会再补充目前比较常用的服务发现、服务注册的相关组件。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go实战 在 Go 项目中基于本地内存缓存的实现及应用

发表于 2021-11-09

大家好,我是Go学堂的渔夫子。今天给大家介绍一下在Go项目中在数据量小、读取频繁的场景中如何实现基于本地内存缓存的方法以提高系统性能。

对于缓存,大家都不陌生。百度百科的定义是这样的:

缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。

由此可知,缓存是用来提高数据交换速度的。我们今天要讲的缓存不是CPU中的缓存,而是在应用程序中对数据库的缓存。应用程序先于数据库,从缓存中读取数据,以降低数据库的压力,提高应用程序的读取性能。

在实际项目中,相信大家也都遇到过类似的情景:数据量小,但访问又较频繁(例如国家标准行政区域数据),想将其完全存放于本地内存中。这样就可以避免直接访问mysql或redis,减少网络传输,提高访问速度。那具体应该怎么实现呢?

本文就介绍一种Go项目中经常使用到的方法:将数据从数据库中加载到本地文件,然后再将文件中的数据加载到内存中,内存中的数据直接供应用程序使用。如下图所示:

本文会忽略数据库到本地文件的过程,因为这个环节就是一个文件上传和下载到本地的过程。所以我们会重点讲解如何从本地文件加载数据到内存中这个环节。

01 目标

在Go语言的项目中,将本地文件的数据加载到应用程序的内存中,以供应用程序直接使用。

我们再将目标拆解成两个目标:

1、程序启动时,将本地文件的数据初始化到内存中,即冷启动

2、程序运行期间,本地文件有更新时,将数据更新到内存中。

02 代码实现

本文主要是目的就是给大家讲解目标的实现,所以不会带大家一步步分析,而是通过讲解已实现的代码来给大家提供一种参考实现。

所以,我们先给出我们设计的类图:

从类图中可知,有两个主要的结构体:FileDoubleBuffer和LocalFileLoader。下面我们一一讲解这两个结构体的属性和方法实现。

2.1 场景假设

我们以城市的天气状况为示例,将每个城市的实时温度和风力以json格式存储在文件中,当城市的温度或风力有变化时,再更新该文件。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
json复制代码{
"beijing": {
"temperature": 23,
"wind": 3
},
"tianjin": {
"temperature": 20,
"wind": 2
},
"shanghai": {
"temperature": 20,
"wind": 20
},
"chongqing": {
"temperature": 30,
"wind": 10
}
}

2.2 main的调用

这里,先给出main函数的调用示例,根据main函数中的实现,我们一步步看图中两个主要结构体的实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
golang复制代码//第一步,定义装载文件中数据的结构体
type WeatherContainer struct {
Weathers map[string]*Weather //每个城市对应的实况天气
}
//文件数据中每个城市的天气状况
type Weather struct {
Temperature int //当前气温 `json:"temperature"`
Wind int //当前风力 `json:"wind"`
}
func main() {
pwd, _ := os.Getwd()
//加载的文件路径
filename := pwd + "/cache/cache.json"
//初始化本地文件加载器
localFileLoader := NewLocalFileLoader(filename)
//初始化文件缓冲实例,将localFileLoader作为底层的文件缓冲
fileDoubleBuffer := NewFileDoubleBuffer(localFileLoader)

// 开始将文件中的内容加载到缓冲变量中,本质上就是通过load和reload加载文件数据
fileDoubleBuffer.StartFileBuffer()

//获取数据
weathersConfig := fileDoubleBuffer.Data().(*WeatherContainer)
fmt.Println("weathers:", weathersConfig.Weathers["beijing"])

blockCh := make(chan int)
//该通道用于阻塞进程不结束,这样reload的协程就可以执行了
<-blockCh
}

2.3 FileDoubleBuffer结构体及实现

该结构体的作用主要是面向应用程序(我们这里是main函数),供应用程序直接从内存即bufferData中获取数据的。该结构体的定义如下:

1
2
3
4
5
6
7
golang复制代码// main应用主要面向该结构体获取数据
type FileDoubleBuffer struct {
Loader *LocalFileLoader
bufferData []interface{}
curIndex int32
mutex sync.Mutex
}

首先看该结构体的属性:

**Loader:**是一个LocalFileLoader类型(后面会定义该结构体),用于从具体的文件中加载数据到bufferData中。

**bufferData切片:**接收文件中数据的变量。一方面会将文件中的数据加载到该变量中。另一方面,应用程序直接从该变量中获取想要的数据信息,而非文件或数据库。该变量的数据类型是interface{},说明可以加载任何类型的数据结构。另外,我们注意该变量是一个切片,该切片只有2个元素,两个元素具有相同的数据结构,结合curIndex属性使用。

**curIndex:**该属性是指定当前bufferData正在使用哪个索引中的数据,该属性的值在0和1之间循环,用于新老数据的切换。例如,当前对外使用的是curIndex=1这个索引元素的数据,当文件中有新数据时,先将文件的数据加载到索引0这个元素中,当将文件的数据完全加载完后,再将curIndex的值指向0。这样,当文件中有新数据进行刷新内存中的数据时,不会影响应用程序对老数据的使用。

再来看FileDoubleBuffer中的函数:

Data()函数

应用程序通过该函数来获取FileDoubleBuffer中的dataBuffer数据。具体实现如下:

1
2
3
4
5
golang复制代码func (buffer *FileDoubleBuffer) Data() interface{} {
// bufferData实际上存储了两个相同结构的元素,用于切换新老数据
index := atomic.LoadInt32(&buffer.curIndex)
return buffer.bufferData[index]
}

load函数

该函数是用于加载文件中的数据到bufferData中。代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
golang复制代码func (buffer *FileDoubleBuffer) load() {
buffer.mutex.Lock()
defer buffer.mutex.Unlock()
//判断当前使用的是bufferData数组哪个元素
// 因bufferData中只有两个元素,所以要么是0,要么是1
curIndex := 1 - atomic.LoadInt32(&buffer.curIndex)

err := buffer.Loader.Load(buffer.bufferData[curIndex])
if err == nil {
atomic.StoreInt32(&buffer.curIndex, curIndex)
}
}

reload函数

用于从文件中加载新的数据到bufferData中。实际上是一个for循环,每隔一定的时间执行一次load函数,代码如下:

1
2
3
4
5
6
7
golang复制代码func (buffer *FileDoubleBuffer) reload() {
for {
time.Sleep(time.Duration(5) * time.Second)
fmt.Println("开始加载...")
buffer.load()
}
}

StartFileBuffer函数

该函数的作用是启动数据的加载和更新,代码如下:

1
2
3
4
golang复制代码func (buffer *FileDoubleBuffer) StartFileBuffer() {
buffer.load()
go buffer.reload()
}

**NewFileDoubleBuffer(loader LocalFileLoader) FileDoubleBuffer 函数

该函数的作用是初始化FileDoubleBuffer实例,代码如下:

1
2
3
4
5
6
7
8
9
10
golang复制代码func NewFileDoubleBuffer(loader *LocalFileLoader) *FileDoubleBuffer {
buffer := &FileDoubleBuffer{
Loader: loader,
curIndex: 0,
}

//这里分配内存空间,以便将文件中的值加载到该变量中,供应用程序使用
buffer.bufferData = append(buffer.bufferData, loader.Alloc(), loader.Alloc())
return buffer
}

2.4 LocalFileLoader结构体及实现
由于我们是将数据先从数据库加载到本地文件上,然后再将文件的数据加载到内存缓冲区中,故有了LocalFileLoader结构体。该结构体的作用是执行具体的文件数据加载和检测文件更新的任务。LocalFileLoader的定义如下:

1
2
3
4
golang复制代码type LocalFileLoader struct {
filename string //需要加载的文件,完整路径
lastModifyTime int64 //文件最近一次的修改时间
}

首先来看该结构体的属性:

**filename:**指定具体的文件名,说明从该文件中加载数据

**modifyTime:**最后一次加载文件的时间。如果文件的更新时间大于该时间,则说明文件有更新

再来看LocalFileLoader中的函数:

Load(filename string, i interface)函数

该函数用于将filename文件中的数据加载到变量i中。该变量i实际上是从FileDoubleBuffer中传进来的bufferData中的元素,代码如下:

1
2
3
4
5
6
7
8
9
10
11
golang复制代码// 这里i变量实际上是从FileDoubleBuffer结构的load方法中传入的dataBuffer中的一个元素
func (loader *LocalFileLoader) Load(i interface{}) error {
// WeatherContainer结构体是依据文件中具体存储的数据定义的,后面会讲到
weatherContainer := i.(*WeatherContainer)
fileHandler, _ := os.Open(loader.filename)
defer fileHandler.Close()
body, _ := ioutil.ReadAll(fileHandler)
_ := json.Unmarshal(body, &weatherContainer.Weathers)
// 这里我们省略了那些err的判断
return nil
}

DetectNewFile()函数

该函数用于检测filename文件是否有更新,如果文件的修改时间大于modifyTime,则FileDoubleBuffer会将新的数据加载到dataBuffer中。代码如下:

1
2
3
4
5
6
7
8
9
10
golang复制代码// 该函数检查文件是否有更新,如果有更新 则返回true,否则返回false
func (loader *LocalFileLoader) DetectNewFile() bool {
fileInfo, _ := os.Stat(loader.filename)
//文件的修改时间比上次修改时间大,说明文件有更新
if fileInfo.ModTime().Unix() > loader.lastModifyTime {
loader.lastModifyTime = fileInfo.ModTime().Unix()
return true
}
return false
}

**Alloc() interface{} **

用于分配具体的变量,以供装载文件中的数据。这里分配的变量最终会存储到FileDoubleBuffer中的dataBuffer数据中。代码如下:

1
2
3
4
5
6
golang复制代码// 分配具体的变量,来承载文件中的具体内容,变量结构体需要和文件中的结构体保持一致
func (loader *LocalFileLoader) Alloc() interface{} {
return &WeatherContainer{
Weathers: make(map[string]*Weather),
}
}

同样需要一个初始化LocalFileLoader实例的函数:

1
2
3
4
5
6
golang复制代码//指定需要加载的文件路径path
func NewLocalFileLoader(path string) *LocalFileLoader {
return &LocalFileLoader{
filename: path,
}
}

总结

这种方式一般适用于数据量较小、频繁读的场景。在文章开始的图中我们可以看到,因为是服务器往往是集群,所以每台机器上的文件内容可能会有短暂的差异,所以该实现也不适用于对数据具有强一致要求的场景中。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Netty4源码初学习-服务端启动

发表于 2021-11-09

本文基于netty-all:4.1.6.Final版本进行简略分析,如有不对,感谢指出。
初次接触netty,网上发现一张比较好的线程模型,如图1

image.png

图1

从图1中可以看出,抛开细节,整个netty的运转是类似于java线程池,不断地执行任务队列中的任务,后面源码中会分析,线程池的继承关系。

下面就按照图1进行一步步分析,我们服务端常见的启动代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
scss复制代码 public void start(InetSocketAddress address){
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup(5);
try {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(address)
// .handler(null)
.handler(new TestHander())
.childHandler(new ServerPojoChannelInitializer())
// .childHandler(new ServerChannelInitializer())
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口,开始接收进来的连接
ChannelFuture future = bootstrap.bind(address).sync();
System.out.println("Server start listen at " + address.getPort());
future.channel().closeFuture().sync();
System.out.println("******");
} catch (Exception e) {
e.printStackTrace();
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

1. BossGroup初始化工作

NioEventLoopGroup的继承关系如下

image.png
可以看出group其实最顶层服务类Exector,那么其必然是通过线程对各种任务进行管理。

在其父类MultithreadEventLoopGroup构造函数中,如果不传值,就采用默认2cpu数量

1
2
3
java复制代码protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

在向上MultithreadEventExecutorGroup类中,其构造函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
ini复制代码protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}

if (executor == null) {
//通过工厂类DefaultThreadFactory生成FastThreadLocalThread线程类
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}

children = new EventExecutor[nThreads];

for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
//每一个channel都会有有一个线程池与之对应,这个nThereads就是初始化时传入的参数
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
if (!success) {
for (int j = 0; j < i; j ++) {
children[j].shutdownGracefully();
}

for (int j = 0; j < i; j ++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
}

//线程选择策略,有两种,PowerOfTowEventExecutorChooser和GenericEventExecutorChooser
chooser = chooserFactory.newChooser(children);

final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
};

for (EventExecutor e: children) {
e.terminationFuture().addListener(terminationListener);
}

Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}

NioEventLoop的参数就可以看到,图1中的Selector和TaskQueue

image.png

到这里基本就看到taskQueue和selector如何传入。

2. 服务端channel初始化和注入到eventloop

由图1可知道,channel和某一个eventloop是一一对应的关系,服务端初始化好了,那下面就看看channel是如何绑定到eventloop的,直接从bind接口入手,找到AbstractBootstrap的initAndRegister方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
scss复制代码final ChannelFuture initAndRegister() {
Channel channel = null;
try {
//工厂方法获取的是启动代码中赋值的NioServerSocketChannel
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
if (channel != null) {
// channel can be null if newChannel crashed (eg SocketException("too many open files"))
channel.unsafe().closeForcibly();
}
// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
}

ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}

// If we are here and the promise is not failed, it's one of the following cases:
// 1) If we attempted registration from the event loop, the registration has been completed at this point.
// i.e. It's safe to attempt bind() or connect() now because the channel has been registered.
// 2) If we attempted registration from the other thread, the registration request has been successfully
// added to the event loop's task queue for later execution.
// i.e. It's safe to attempt bind() or connect() now:
// because bind() or connect() will be executed *after* the scheduled registration task is executed
// because register(), bind(), and connect() are all bound to the same thread.

return regFuture;
}

2.1 init方法

看下初始化方法做了啥,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
scss复制代码@Override
void init(Channel channel) throws Exception {
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
channel.config().setOptions(options);
}

final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
//每个channel都会绑定一个pipeline
ChannelPipeline p = channel.pipeline();

//客户端eventloop,对应图1的workgroup
final EventLoopGroup currentChildGroup = childGroup;
//workgroup对应的handler,上行和下行,也即启动代码中的ServerPojoChannelInitializer
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}

//添加一个channelHandler,待后面启动
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}

// We add this handler via the EventLoop as the user may have used a ChannelInitializer as handler.
// In this case the initChannel(...) method will only be called after this method returns. Because
// of this we need to ensure we add our handler in a delayed fashion so all the users handler are
// placed in front of the ServerBootstrapAcceptor.
//前面说到,此处是netty自己实现的execute,如果执行线程和eventloop不是同一个线程那么就启动eventloop线程,否则就将任务添加到任务队列,也即taskQueue
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
//这个很关键,任务启动后,就会不断监听事件,然后再对对应的处理,也就是图1中event group下部的圆形循环
pipeline.addLast(new ServerBootstrapAcceptor(
currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

从上面可以,init方法,就是将读写的event loop加载进来,待触发任务回调ServerBootstrapAcceptor的read方法,不断轮询监听scoket事件

2.1 register方法

init方法并没有触发监听任务,由此可以大体猜测到
将服务端channel注册到event loop即可实现监听。由bootstrap初始化可知,其eventloop为NioEventLoop,AbstractChannel的register方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
arduino复制代码@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
if (eventLoop == null) {
throw new NullPointerException("eventLoop");
}
if (isRegistered()) {
promise.setFailure(new IllegalStateException("registered to an event loop already"));
return;
}
if (!isCompatible(eventLoop)) {
promise.setFailure(
new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
return;
}

AbstractChannel.this.eventLoop = eventLoop;
//启动时,是main方法执行此函数
if (eventLoop.inEventLoop()) {
register0(promise);
} else {
//启动时会执行此处,
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
logger.warn(
"Force-closing a channel whose registration task was not accepted by an event loop: {}",
AbstractChannel.this, t);
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}
}

首次启动,会执行eventLoop的execute方法,看下实现SingleThreadEventExecutor.execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码@Override
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}

boolean inEventLoop = inEventLoop();
if (inEventLoop) {
addTask(task);
} else {
//对于服务端启动来说,最开始是main函数执行
//启动后,会将event的线程置为执行他的线程,也就是boss group初始化new出来的线程
startThread();
addTask(task);
if (isShutdown() && removeTask(task)) {
reject();
}
}

if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}

startThread是采用cas来保证并发安全的

1
2
3
4
5
6
7
csharp复制代码private void startThread() {
if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
doStartThread();
}
}
}

然后能看到nioeventloop的run方法是个死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
scss复制代码@Override
protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
select(wakenUp.getAndSet(false));

// 'wakenUp.compareAndSet(false, true)' is always evaluated
// before calling 'selector.wakeup()' to reduce the wake-up
// overhead. (Selector.wakeup() is an expensive operation.)
//
// However, there is a race condition in this approach.
// The race condition is triggered when 'wakenUp' is set to
// true too early.
//
// 'wakenUp' is set to true too early if:
// 1) Selector is waken up between 'wakenUp.set(false)' and
// 'selector.select(...)'. (BAD)
// 2) Selector is waken up between 'selector.select(...)' and
// 'if (wakenUp.get()) { ... }'. (OK)
//
// In the first case, 'wakenUp' is set to true and the
// following 'selector.select(...)' will wake up immediately.
// Until 'wakenUp' is set to false again in the next round,
// 'wakenUp.compareAndSet(false, true)' will fail, and therefore
// any attempt to wake up the Selector will fail, too, causing
// the following 'selector.select(...)' call to block
// unnecessarily.
//
// To fix this problem, we wake up the selector again if wakenUp
// is true immediately after selector.select(...).
// It is inefficient in that it wakes up the selector for both
// the first case (BAD - wake-up required) and the second case
// (OK - no wake-up required).

if (wakenUp.get()) {
selector.wakeup();
}
default:
// fallthrough
}

cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
//会执行init方法中的ch.eventLoop().execute
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}

此时可以看到nioeventloop 就是处理socket时间以及处理nioeventloop添加的任务,
看下runtask里面做了啥,前面看下register里面的eventLoop.execute添加了一个任务,即register0。我们可以看下register0干了啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
scss复制代码private void register0(ChannelPromise promise) {
try {
// check if the channel is still open as it could be closed in the mean time when the register
// call was outside of the eventLoop
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean firstRegistration = neverRegistered;
doRegister();
neverRegistered = false;
registered = true;

// Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
// user may already fire events through the pipeline in the ChannelFutureListener.
//此处是处理管道添加的所有hanler,在register方法中,服务端管道添加了一个new ChannelInitializer<Channel>,这里面追执行启动代码的handler即TestHander
pipeline.invokeHandlerAddedIfNeeded();

safeSetSuccess(promise);
//调用下一个handler
pipeline.fireChannelRegistered();
// Only fire a channelActive if the channel has never been registered. This prevents firing
// multiple channel actives if the channel is deregistered and re-registered.
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
// This channel was registered before and autoRead() is set. This means we need to begin read
// again so that we process inbound data.
//
// See https://github.com/netty/netty/issues/4805
beginRead();
}
}
} catch (Throwable t) {
// Close the channel directly to avoid FD leak.
closeForcibly();
closeFuture.setClosed();
safeSetFailure(promise, t);
}
}

TestHander如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class TestHander implements ChannelHandler {
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
System.out.println();

}

@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println();
}
}

最后服务端通道只有一个channelHandle没执行,就是ServerBootstrapAcceptor

这个在有socket事件的时候才会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
scss复制代码private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
// If the channel implementation throws an exception because there is no event loop, we ignore this
// because we are only trying to determine if ch is registered to this event loop and thus has authority
// to close ch.
return;
}
// Only close ch if ch is still registerd to this EventLoop. ch could have deregistered from the event loop
// and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
// still healthy and should not be closed.
// See https://github.com/netty/netty/issues/5125
if (eventLoop != this || eventLoop == null) {
return;
}
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
}

try {
int readyOps = k.readyOps();
// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
// the NIO JDK channel implementation may throw a NotYetConnectedException.
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);

unsafe.finishConnect();
}

// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}

// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
//监听事件,当时连接时间的时候,会执行ServerBootstrapAcceptor的read方法
unsafe.read();
if (!ch.isOpen()) {
// Connection already closed - no need to handle write.
return;
}
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}

3. 服务端channel绑定address

上面分析完后,最后服务端绑定sserversocket即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码private static void doBind0(
final ChannelFuture regFuture, final Channel channel,
final SocketAddress localAddress, final ChannelPromise promise) {

// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
// the pipeline in its channelRegistered() implementation.
//这个回调也是eventloop的线程池执行的,线程池就一个线程fastThread
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
if (regFuture.isSuccess()) {
channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
promise.setFailure(regFuture.cause());
}
}
});
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中的Integer缓存池

发表于 2021-11-08

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

写在前面

要说Java中有哪些比较有意思的基础知识点,那估计就可以说一下我们平时使用最多的整型数据类型中的缓存池了。

而今天我们要说的就是Integer中的缓存池,从这个缓存池就能大概能了解其他整型数据类型的缓存池了,大概实现原理都差不多的。

同时也告诉大家,基础知识非常重要,这些是我们的根基所在。

Integer中的缓存池

Integer缓存池第一次出现是在JDK5中,在目前适用最多的JDK8中也多了一些不一样的处理方式。

说起Integer缓存池的话,就必须先来做一道题,来看一下:

1
2
3
4
5
6
java复制代码Integer c1 = 1;
Integer c2 = 1;
Integer b1 = 130;
Integer b2 = 130;
System.out.println(c1 == c2);
System.out.println(b1 == b2);

这样的一道题,执行出来的结果是什么呢?

公布答案:

1
2
java复制代码true
false

这是为什么呢?明明是同样的方式去声明对象,返回的值却是不同的,这就需要缓存池的概念啦,先来看Integer类中的一个内部类,IntegerCache,源码如下图所示:

image.png

我虽然没有贴全源码,但是大家可以自行去源码中查看,养成看源码的意识,其实并不是什么太可怕的事情。

IntegerCache类中声明了一个cache[]数组,这个数组就是用来存储一些已经声明过的值。

但是又是什么样的值才能存入这个缓存数组中呢,那么就要更加仔细的看源码了。

不过我在这可以先和大家说一下答案,那就是在最新的JDK8中IntegerCache类的存储范围在-128到127之间,这也是在源码中通读一遍就能理解的。

就算因为对基础不够扎实,对源码的理解不到位,那么我们也可以从注释上看出来,如下图,这样也能知道其的范围。

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…391392393…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%