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

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


  • 首页

  • 归档

  • 搜索

年更博主冒个泡,或将开启可视化之旅

发表于 2020-08-27

很久没更新了呢,要是有人曾望穿秋水、盼星星盼月亮地希望我更文,那我只能对此表示歉意,虽然多半并不存在这样的人。

可能大家是带着好奇与疑惑点进文章的,想着怎么时隔14个月突然更新了?是啊,为什么呢。那自然是因为愧疚,想念大家啊…

好吧,并没有,そんな事ないよ。其实是之前实现了几个可视化的图,想着倒是可以记录并分享下,毕竟教练看我实在不是打篮球的料,于是改教我可视化了,我也终于对自己每周都会看的节目进行了可视化。

说起来因为一直对数据可视化感兴趣,对能自己设计/DIY,并用D3.js等实现各种可视化效果而向往与憧憬,因而终于好好学习了下HTML/CSS/JS等前端基础知识以及可视化利器D3.js库。

鉴于谈到数据可视化时,可能每个人所想到的内容不尽相同,这里举个例子来说明下我所指代的可视化究竟是什么样的。

下图是荷兰的著名数据可视化创作者 Nadieh Bremer 对自己小时候接触到并很喜欢的「百变小樱」漫画进行可视化后最终呈现的效果。大概没有人能抵抗的了这么漂亮的可视化吧。而这就是我所憧憬和想要实现的那类可视化作品。项目链接:www.datasketch.es/june/code/n…](www.datasketch.es/june/code/n…)

在写这篇文章前,我重新看了下这个项目的交互和开源代码,以及原作者详细描述的实现流程和遇上的种种问题,由于这不是本文的重点,因而关于这个总计投入86小时完成的项目的更多实现细节/流程将在后续文章中更新,这里仅一笔带过。当然广而告之下,之后也会围绕这类可视化进行更多输出,敬请期待;剧透下 Nadieh Bremer 这个名字也会多次出现,可以搜素了解下。

言归正传,自己时不时会看看各种可视化作品,觉得不错的网站会随手放到浏览器书签里吃灰,也会在 behance 和 pinterest 上收藏好看的可视化图片。一直私心认为可视化的创作就像画画,虽然自己并不会画画,但大概都是需要多看看优秀作品,多开开眼,才能创作出真正内涵丰富,具有美感,令人惊艳的作品吧。下图是 pinterest 上自己收藏的一些图。

有着上述所说的习惯,因而某天看到了 poppy field 这个将过去一百多年里发生过的战争中的死亡人数,以罂粟花这一视觉元素进行可视化的非常棒的项目。项目链接:
poppyfield.org/

下面这段话大概阐释了战争与罂粟花之间的内在联系。

Following the end of the First World War, the poppy became a symbol of commemoration. It was among the first plants to spring to life on Europe’s devastated battlefields. Its colour, reminiscent of bloodshed,
and its resilient yet delicate nature evoke the
human relationship to war.

“第一次世界大战结束后,罂粟花成为纪念的象征。它是在欧洲饱受摧残的战场上重生的第一批植物之一。它的颜色让人联想到流血,其柔韧而细腻的本性唤起了人类与战争的关系。”

罂粟花元素的使用,生长开花的动画效果,都使我很喜欢这个项目,因而在微博分享后的第二天晚上就试着把该项目的n年前老旧源代码翻新跑了下,也借此啃啃代码实现方式,至于数据筛选等交互则是后来才加进来的。

原本也打算开源来着,不过一生之敌的CSS实在是令人畏惧,因而暂待填坑。不过自己确实有想过把很多很酷的可视化项目都统一整理到一起,统一翻新实现下,据我所知,似乎并没有人做过这件事,毕竟没有人比我更喜欢可视化,doge。下图是自己最终复现的效果,因为实在不想用 jQuery,所以大概算是自己给自己增加了难度。

也正是受这个项目的影响,顺理成章地也会想着照猫画虎,对每周都会看的乃木坂46的团综「乃木坂工事中」的b站熟肉进行可视化,最初是想看看字幕组每期更新熟肉有多快,毕竟「肝之大者,为国为民」,各家字幕组爆肝、用爱发电的痕迹也可以借此挖掘并记录下。

确定目标后,爬取数据,融合数据,处理数据,进行可视化等都按部就班地推进着,其实也做了不少工作,回过头看,许多甚至没啥必要。

详情在后续新的文章里再介绍,这里就看看最终实现的可视化效果。一开始先基于现成的播放量和弹幕数,不断优化到自己还算满意的效果,并且首次引入d3-annotation进行数据标注,以使画面看上去内容丰富些。

出图后发现播放量最高的一期居然是EP64 赤脚来进行○○大会,污力十足的游戏对决,以压倒性优势占据第一,猜测是出圈吸引了路人,果然都是lsp。视频指路:https://www.bilibili.com/video/av5495147/

上述可视化的代码已在GitHub和码云开源:https://github.com/DesertsX/nogizaka-under-construction-dataviz。
演示地址:http://desertsx.gitee.io/nogizaka-under-construction-dataviz/。

后续也会详细讲讲代码原理,剖析这样的可视化是如何诞生的。虽然就是这个折线图啦。

接着,按照 出熟肉所需时间 = b站更新时间 -(开播时间 + 当期节目时长)的简单假设,暂不考虑其他因素的影响,对时间数据进行处理,在一番操作后,也以同样的方式进行展示。虽然最初的想法是,可以自己画画「肝」的SVG图形,然后计算出某个数值作为爆肝指数,然后参照战争与罂粟花的形式进行展示,但最终先取巧完成。

自 20150420 开播的「乃木坂工事中」,前身是「乃木坂在哪儿」(20111002-20150413),同样是日本搞笑艺人组合香蕉人「バナナマン」主持,开播至今也近10年。下图为「banana鳗」的本体 。

而自己是2019年大概这个时候才渐渐接触并饭上乃团,开始每周都看「工事中」以来印象里周日晚上23点(北京时间)播出后,基本周一就能看到熟肉,图一也佐证了这一点;而再往前追溯,诸多原因导致和现在不一样的情形,但总体而言字幕组还是很给力的。


再后来想走马灯的展示每期的封面/标题/播放量等数据,于是改成动态可视化版本,配上乃团超棒的曲子「きっかけ/契机」钢琴版作为BGM,回顾至今所有的期,就像弹幕里有人说的真的都是回忆。

录制的视频可在b站观看:「乃木坂工事中」B站熟肉各期(EP01-269)播放量的动态数据可视化|「DataViz EP01」

想讲的大概就是这些,也自己给自己挖了些坑,可以慢慢填。对数据可视化感兴趣的可以关注一下哈。

本文转载自: 掘金

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

PowerJob 在线日志饱受好评的秘诀:小但实用的分布式日

发表于 2020-08-26

本文适合有 Java 基础知识的人群

作者:HelloGitHub-Salieri

HelloGitHub 推出的《讲解开源项目》系列。

项目地址:

github.com/KFCFans/Pow…

PowerJob 的在线日志一直是饱受好评的一个功能,它能在前端界面实时展示开发者在任务处理过程中输出的日志,帮助开发者更好的监控任务的执行情况。其功能展示如下图所示(前端界面略丑,请自动忽略~)。

在线日志这个功能,乍一听很简单,无非 worker 向 server 发日志数据,server 接受后前端展示。但对于 PowerJob 这种任意节点都支持分布式部署且支持分布式计算的系统来说,还是存在着不少难点的,简单来说,有以下几点:

  1. 多对多问题:在 PowerJob 的理想部署模式中,会存在多个 server 和多个 worker,当某个任务开始分布式计算时,其日志散布于各台机器上,要想在前端统一展示,需要有收集器将分散的日志汇集到一起。
  2. 并发问题:当 worker 集群规模较大时,一旦执行分布式计算任务,其产生的日志 QPS 也是一个不小的数目,要想轻松支持百万量级的分布式任务,需要解决并发情况下 QPS 过高的问题。
  3. 排序问题:分布式计算时,日志散布在不同机器,即便收集汇总到同一台机器,由于网络延迟等原因,不能保证日志的有序性,而日志按时间排序是强需求(否则根本没法看啊…),因此,还需要解决大规模日志数据的排序问题。
  4. 数据的存储问题:当日志数据量非常大时,如何高效的存储和读取这一批数据,也是需要解决的问题。

因此,为了完美实现在线日志功能,PowerJob 在内部实现了一个麻雀虽小五脏俱全的分布式日志系统。话不多说,下面正式开始逐一分析~

一、多对多问题

这个问题,其实在 PowerJob 解决多 worker 多 server 的选主问题时顺带着解决了。简单来说,PowerJob 系统中,某一个分组下的所有 worker,在运行时都只会连接到某一台 server。因此,日志数据上报时,选择当前 worker 进行上报即可。由于任务不可能跨分组执行,因此某个任务在运行过程中产生的所有日志数据都会上报给该分组当前连接的 server,这样就做到了日志的收集,即日志会汇总到负责当前分组调度的 powerjob-server,由该 server 统一处理。

二、并发问题

并发问题的解决也不难。

大家一定都听说过消息中间件,也知道消息中间件的一大功能为削峰。引入消息中间件后,把原来同步式的调用转化为异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。消息中间件就像水库一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。

PowerJob 在处理日志的高并发问题时也采用了类似的方式,通过引入本地队列,对需要发送给 server 的消息进行缓存,再定时将消息批量发送给 server,化同步为异步,并引入批量发送的机制,充分利用每一次数据传输的机会发送尽可能多的数据,从而降低对 server 的冲击。

三、排序问题

3.1 日志的存储

将排序问题之前,先来聊一聊 server 怎么处理接收到的日志数据,也就是如何存储日志。

这个抉择其实并不难,用一下简单的排除法就能获取正确答案:

  1. 存内部还是存外部?PowerJob 作为任务调度中间件,最小依赖一直是需要牢牢把控的指导思想。因此,在已知最小依赖仅为数据库的情况下,似乎不太可能使用外部的存储介质,至少不能把收到的日志直接发送到外部存储介质,否则又是一波庞大的 QPS,会对依赖的外部组件有非常高的性能要求,不符合框架设计原则。因此,在线日志的第一级存储介质应该由 server 本身来承担。
  2. 存内存还是磁盘?既然确定了由 server 来存储原始数据,那么就面临内存和磁盘二选一的问题了。但,这还用选吗?成百上千万的文本数据存内存,这不妥妥的 OutOfMemory 吗?显然,存磁盘。

经过一波简单的排除法,日志的一级存储方案确定了:server 的本机磁盘。那么,存磁盘会带来什么问题呢?

且不说文件操作的复杂性和难度,一个最简单的需求就能让这个方案跌入万丈深渊,那就是:排序。

众所周知,日志必须按时间排序,否则根本没法看。而 PowerJob 又是一个纯粹的分布式系统,显然不可能指望所有的日志数据按顺序发到 server,因此对日志的再排序是一件必须要做的事情。但让我们来考虑一下难度。

  • 首先,日志是纯文本数据,要想做排序,首先要将整个日志文件变为一堆日志记录,即分行。
  • 其次,分完行后,由于日志是给人看的,时间肯定已经被转化为 yyyy-MM-dd HH:mm:ss.SSS 这种方便人阅读的格式,那么将它反解析回可排序的时间戳又是一件麻烦事。
  • 最后,也是最终 BOSS,就是排序了。要知道,之所以会选择磁盘存储这个方案,是因为没有足够的内存。这也就意味着,这个排序没办法在内存完成。外部排序的难度和效率,想必不用我多说了吧。同时,我也相信,大部分程序员(包括我在内)应该从来没有接触过外部排序,这趟浑水,我又何必去趟呢?

3.2 H2 数据库简介

那么,有没有什么既能使用磁盘做存储,又有排序能力的框架/软件呢?世上会有这等好事吗?你别说,还真有。而且是远在天边,近在眼前,可以说是和程序员形影不离的一样东西——数据库。

“等等,你刚才不是说,不拿数据库作为一级存储介质吗?怎么滴,出尔反尔?”

“哼,年轻人。此数据库非彼数据库,这个数据库啊,是 powerjob-server 内置的嵌入式数据库 H2”

H2 是一个用 Java 开发的嵌入式数据库,它本身只是一个类库,即只有一个 jar 文件,可以直接嵌入到应用项目中。嵌入式模式下,应用在 JVM 中启动 H2 数据库并通过 JDBC 连接。该模式同时支持数据持久化和内存两种方式。

H2 的使用很简单,在项目中引入依赖后,便会自动随 JVM 启动,应用可以通过 JDBC URL 进行连接,并在 JDBC URL 中指定所使用的模式,比如对于 powerjob-server 来说,需要使用嵌入式磁盘持久化模式,因此使用以下 JDBC URL 进行连接:

1
javascript复制代码jdbc:h2:file:~/powerjob-server/powerjob_server_db

同时,H2 支持相当标准的 SQL 规范,也和 Spring Data Jpa、MyBatis 等 ORM 框架完美兼容,因此使用非常方便。在 powerjob-server 中,我便通过 Spring Data Jpa 来使用 H2,用户体验非常友好(当然,多数据源的配置很不友好!)。

综上,有了内置的 H2 数据库,日志的存储和排序也就不再是难以解决的问题了~

3.3 存储与排序

引入 H2 之后,powerjob-server 处理在线日志的流程如下:

  1. 接收来自 worker 的日志数据,直接写入内嵌数据库 H2 中
  2. 在线调用时,通过 SQL 查询语句的 order by log_time 功能,完成日志的排序和输出

可见,合适的技术选型能让问题的解决简单很多~

四、一些其他的优化

以上介绍了 PowerJob 分布式日志组件的核心原理和实现,当然,在实际使用中,还引入了许多优化,限于篇幅,这里简单提一下,有兴趣的同学可以自己去看源码~

  • 高频率在线访问降压:如果每次用户查看日志,都需要从数据库中查询并输出,这个效率和速度都会非常慢。毕竟当数据量达到一定程度时,光是磁盘 I/O 就得花去不少时间。因此,powerjob-server 会为每次查询生成缓存文件,一定时间范围内的日志查询,会通过文件缓存直接返回,而不是每次都走 DB 查询方案。
  • 日志分页:成百上千万条数据的背后,生成的文件大小也以及远远高于正常网络带宽所能轻松承载的范围了。因此,为了在前端控制台快速显示在线日志,需要引入分页功能,一次显示部分日志数据。这也是一项较为复杂的文件操作。
  • 远程存储:所有日志都存在 server 本地显然不符合高可用的设计目标,毕竟换一台 server 就意味着所有的日志数据都丢了,因此 PowerJob 引入了 mongoDB 作为日志的持久化存储介质。mongodb 支持用户直接使用其底层的分布式文件系统 GridFS,经过我仔细的考量,认为这是一个可接受且较为强大的扩展依赖,因此选择引入。

五、最后

好了,本期的内容就到这里结束了,下一期,我将会大家讲述 PowerJob 作为一个各个节点时刻需要进行通讯的框架,底层序列化框架该如何选择,具体的序列化方案又该如何设计~

那么我们下期再见喽~



关注 HelloGitHub 公众号

本文转载自: 掘金

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

学会反射后,我被录取了(干货)

发表于 2020-08-24

反射的思想及作用

有反必有正,就像世间的阴和阳,计算机的0和1一样。天道有轮回,苍天…(净会在这瞎bibi)

在学习反射之前,先来了解正射是什么。我们平常用的最多的 new 方式实例化对象的方式就是一种正射的体现。假如我需要实例化一个HashMap,代码就会是这样子。

1
2
java复制代码Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1);

某一天发现,该段程序不适合用 HashMap 存储键值对,更倾向于用LinkedHashMap存储。重新编写代码后变成下面这个样子。

1
2
java复制代码Map<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);

假如又有一天,发现数据还是适合用 HashMap来存储,难道又要重新修改源码吗?

发现问题了吗?我们每次改变一种需求,都要去重新修改源码,然后对代码进行编译,打包,再到 JVM 上重启项目。这么些步骤下来,效率非常低。

对于这种需求频繁变更但变更不大的场景,频繁地更改源码肯定是一种不允许的操作,我们可以使用一个开关,判断什么时候使用哪一种数据结构。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public Map<Integer, Integer> getMap(String param) {
Map<Integer, Integer> map = null;
if (param.equals("HashMap")) {
map = new HashMap<>();
} else if (param.equals("LinkedHashMap")) {
map = new LinkedHashMap<>();
} else if (param.equals("WeakHashMap")) {
map = new WeakHashMap<>();
}
return map;
}

通过传入参数param决定使用哪一种数据结构,可以在项目运行时,通过动态传入参数决定使用哪一个数据结构。

如果某一天还想用TreeMap,还是避免不了修改源码,重新编译执行的弊端。这个时候,反射就派上用场了。

在代码运行之前,我们不确定将来会使用哪一种数据结构,只有在程序运行时才决定使用哪一个数据类,而反射可以在程序运行过程中动态获取类信息和调用类方法。通过反射构造类实例,代码会演变成下面这样。

1
2
3
4
5
java复制代码public Map<Integer, Integer> getMap(String className) {
Class clazz = Class.forName(className);
Consructor con = clazz.getConstructor();
return (Map<Integer, Integer>) con.newInstance();
}

无论使用什么 Map,只要实现了Map接口,就可以使用全类名路径传入到方法中,获得对应的 Map 实例。例如java.util.HashMap / java.util.LinkedHashMap····如果要创建其它类例如WeakHashMap,我也不需要修改上面这段源码。

我们来回顾一下如何从 new 一个对象引出使用反射的。

  • 在不使用反射时,构造对象使用 new 方式实现,这种方式在编译期就可以把对象的类型确定下来。
  • 如果需求发生变更,需要构造另一个对象,则需要修改源码,非常不优雅,所以我们通过使用开关,在程序运行时判断需要构造哪一个对象,在运行时可以变更开关来实例化不同的数据结构。
  • 如果还有其它扩展的类有可能被使用,就会创建出非常多的分支,且在编码时不知道有什么其他的类被使用到,假如日后Map接口下多了一个集合类是xxxHashMap,还得创建分支,此时引出了反射:可以在运行时才确定使用哪一个数据类,在切换类时,无需重新修改源码、编译程序。

第一章总结:

  • 反射的思想:在程序运行过程中确定和解析数据类的类型。
  • 反射的作用:对于在编译期无法确定使用哪个数据类的场景,通过反射可以在程序运行时构造出不同的数据类实例。

反射的基本使用

Java 反射的主要组成部分有4个:

  • Class:任何运行在内存中的所有类都是该 Class 类的实例对象,每个 Class 类对象内部都包含了本来的所有信息。记着一句话,通过反射干任何事,先找 Class 准没错!
  • Field:描述一个类的属性,内部包含了该属性的所有信息,例如数据类型,属性名,访问修饰符······
  • Constructor:描述一个类的构造方法,内部包含了构造方法的所有信息,例如参数类型,参数名字,访问修饰符······
  • Method:描述一个类的所有方法(包括抽象方法),内部包含了该方法的所有信息,与Constructor类似,不同之处是 Method 拥有返回值类型信息,因为构造方法是没有返回值的。

我总结了一张脑图,放在了下面,如果用到了反射,离不开这核心的4个类,只有去了解它们内部提供了哪些信息,有什么作用,运用它们的时候才能易如反掌。

我们在学习反射的基本使用时,我会用一个SmallPineapple类作为模板进行说明,首先我们先来熟悉这个类的基本组成:属性,构造函数和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class SmallPineapple {
public String name;
public int age;
private double weight; // 体重只有自己知道

public SmallPineapple() {}

public SmallPineapple(String name, int age) {
this.name = name;
this.age = age;
}
public void getInfo() {
System.out.print("["+ name + " 的年龄是:" + age + "]");
}
}

反射中的用法有非常非常多,常见的功能有以下这几个:

  • 在运行时获取一个类的 Class 对象
  • 在运行时构造一个类的实例化对象
  • 在运行时获取一个类的所有信息:变量、方法、构造器、注解

获取类的 Class 对象

在 Java 中,每一个类都会有专属于自己的 Class 对象,当我们编写完.java文件后,使用javac编译后,就会产生一个字节码文件.class,在字节码文件中包含类的所有信息,如属性,构造方法,方法······当字节码文件被装载进虚拟机执行时,会在内存中生成 Class 对象,它包含了该类内部的所有信息,在程序运行时可以获取这些信息。

获取 Class 对象的方法有3种:

  • 类名.class:这种获取方式只有在编译前已经声明了该类的类型才能获取到 Class 对象
1
java复制代码Class clazz = SmallPineapple.class;
  • 实例.getClass():通过实例化对象获取该实例的 Class 对象
1
2
java复制代码SmallPineapple sp = new SmallPineapple();
Class clazz = sp.getClass();
  • Class.forName(className):通过类的全限定名获取该类的 Class 对象
1
java复制代码Class clazz = Class.forName("com.bean.smallpineapple");

拿到 Class对象就可以对它为所欲为了:剥开它的皮(获取类信息)、指挥它做事(调用它的方法),看透它的一切(获取属性),总之它就没有隐私了。

不过在程序中,每个类的 Class 对象只有一个,也就是说你只有这一个奴隶。我们用上面三种方式测试,通过三种方式打印各个 Class 对象都是相同的。

1
2
3
4
5
6
7
java复制代码Class clazz1 = Class.forName("com.bean.SmallPineapple");
Class clazz2 = SmallPineapple.class;
SmallPineapple instance = new SmallPineapple();
Class clazz3 = instance.getClass();
System.out.println("Class.forName() == SmallPineapple.class:" + (clazz1 == clazz2));
System.out.println("Class.forName() == instance.getClass():" + (clazz1 == clazz3));
System.out.println("instance.getClass() == SmallPineapple.class:" + (clazz2 == clazz3));

内存中只有一个 Class 对象的原因要牵扯到 JVM 类加载机制的双亲委派模型,它保证了程序运行时,加载类时每个类在内存中仅会产生一个Class对象。在这里我不打算详细展开说明,可以简单地理解为 JVM 帮我们保证了一个类在内存中至多存在一个 Class 对象。

构造类的实例化对象

通过反射构造一个类的实例方式有2种:

  • Class 对象调用newInstance()方法
1
2
3
4
java复制代码Class clazz = Class.forName("com.bean.SmallPineapple");
SmallPineapple smallPineapple = (SmallPineapple) clazz.newInstance();
smallPineapple.getInfo();
// [null 的年龄是:0]

即使 SmallPineapple 已经显式定义了构造方法,通过 newInstance() 创建的实例中,所有属性值都是对应类型的初始值,因为 newInstance() 构造实例会调用默认无参构造器。

  • Constructor 构造器调用newInstance()方法
1
2
3
4
5
6
java复制代码Class clazz = Class.forName("com.bean.SmallPineapple");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
SmallPineapple smallPineapple2 = (SmallPineapple) constructor.newInstance("小菠萝", 21);
smallPineapple2.getInfo();
// [小菠萝 的年龄是:21]

通过 getConstructor(Object… paramTypes) 方法指定获取指定参数类型的 Constructor, Constructor 调用 newInstance(Object… paramValues) 时传入构造方法参数的值,同样可以构造一个实例,且内部属性已经被赋值。

通过Class对象调用 newInstance() 会走默认无参构造方法,如果想通过显式构造方法构造实例,需要提前从Class中调用getConstructor()方法获取对应的构造器,通过构造器去实例化对象。

这些 API 是在开发当中最常遇到的,当然还有非常多重载的方法,本文由于篇幅原因,且如果每个方法都一一讲解,我们也记不住,所以用到的时候去类里面查找就已经足够了。

获取一个类的所有信息

Class 对象中包含了该类的所有信息,在编译期我们能看到的信息就是该类的变量、方法、构造器,在运行时最常被获取的也是这些信息。

获取类中的变量(Field)

  • Field[] getFields():获取类中所有被public修饰的所有变量
  • Field getField(String name):根据变量名获取类中的一个变量,该变量必须被public修饰
  • Field[] getDeclaredFields():获取类中所有的变量,但无法获取继承下来的变量
  • Field getDeclaredField(String name):根据姓名获取类中的某个变量,无法获取继承下来的变量

获取类中的方法(Method)

  • Method[] getMethods():获取类中被public修饰的所有方法
  • Method getMethod(String name, Class…<?> paramTypes):根据名字和参数类型获取对应方法,该方法必须被public修饰
  • Method[] getDeclaredMethods():获取所有方法,但无法获取继承下来的方法
  • Method getDeclaredMethod(String name, Class…<?> paramTypes):根据名字和参数类型获取对应方法,无法获取继承下来的方法

获取类的构造器(Constructor)

  • Constuctor[] getConstructors():获取类中所有被public修饰的构造器
  • Constructor getConstructor(Class…<?> paramTypes):根据参数类型获取类中某个构造器,该构造器必须被public修饰
  • Constructor[] getDeclaredConstructors():获取类中所有构造器
  • Constructor getDeclaredConstructor(class…<?> paramTypes):根据参数类型获取对应的构造器

每种功能内部以 Declared 细分为2类:

有Declared修饰的方法:可以获取该类内部包含的所有变量、方法和构造器,但是无法获取继承下来的信息

无Declared修饰的方法:可以获取该类中public修饰的变量、方法和构造器,可获取继承下来的信息

如果想获取类中**所有的(包括继承)**变量、方法和构造器,则需要同时调用getXXXs()和getDeclaredXXXs()两个方法,用Set集合存储它们获得的变量、构造器和方法,以防两个方法获取到相同的东西。

例如:要获取SmallPineapple获取类中所有的变量,代码应该是下面这样写。

1
2
3
4
5
6
7
8
9
java复制代码Class clazz = Class.forName("com.bean.SmallPineapple");
// 获取 public 属性,包括继承
Field[] fields1 = clazz.getFields();
// 获取所有属性,不包括继承
Field[] fields2 = clazz.getDeclaredFields();
// 将所有属性汇总到 set
Set<Field> allFields = new HashSet<>();
allFields.addAll(Arrays.asList(fields1));
allFields.addAll(Arrays.asList(fields2));

不知道你有没有发现一件有趣的事情,如果父类的属性用protected修饰,利用反射是无法获取到的。

protected 修饰符的作用范围:只允许同一个包下或者子类访问,可以继承到子类。

getFields() 只能获取到本类的public属性的变量值;

getDeclaredFields() 只能获取到本类的所有属性,不包括继承的;无论如何都获取不到父类的 protected 属性修饰的变量,但是它的的确确存在于子类中。

获取注解

获取注解单独拧了出来,因为它并不是专属于 Class 对象的一种信息,每个变量,方法和构造器都可以被注解修饰,所以在反射中,Field,Constructor 和 Method 类对象都可以调用下面这些方法获取标注在它们之上的注解。

  • Annotation[] getAnnotations():获取该对象上的所有注解
  • Annotation getAnnotation(Class annotaionClass):传入注解类型,获取该对象上的特定一个注解
  • Annotation[] getDeclaredAnnotations():获取该对象上的显式标注的所有注解,无法获取继承下来的注解
  • Annotation getDeclaredAnnotation(Class annotationClass):根据注解类型,获取该对象上的特定一个注解,无法获取继承下来的注解

只有注解的@Retension标注为RUNTIME时,才能够通过反射获取到该注解,@Retension 有3种保存策略:

  • SOURCE:只在**源文件(.java)**中保存,即该注解只会保留在源文件中,编译时编译器会忽略该注解,例如 @Override 注解
  • CLASS:保存在字节码文件(.class)中,注解会随着编译跟随字节码文件中,但是运行时不会对该注解进行解析
  • RUNTIME:一直保存到运行时,用得最多的一种保存策略,在运行时可以获取到该注解的所有信息

像下面这个例子,SmallPineapple 类继承了抽象类Pineapple,getInfo()方法上标识有 @Override 注解,且在子类中标注了@Transient注解,在运行时获取子类重写方法上的所有注解,只能获取到@Transient的信息。

1
2
3
4
5
6
7
8
9
10
java复制代码public abstract class Pineapple {
public abstract void getInfo();
}
public class SmallPineapple extends Pineapple {
@Transient
@Override
public void getInfo() {
System.out.print("小菠萝的身高和年龄是:" + height + "cm ; " + age + "岁");
}
}

启动类Bootstrap获取 SmallPineapple 类中的 getInfo() 方法上的注解信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class Bootstrap {
/**
* 根据运行时传入的全类名路径判断具体的类对象
* @param path 类的全类名路径
*/
public static void execute(String path) throws Exception {
Class obj = Class.forName(path);
Method method = obj.getMethod("getInfo");
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
public static void main(String[] args) throws Exception {
execute("com.pineapple.SmallPineapple");
}
}
// @java.beans.Transient(value=true)

通过反射调用方法

通过反射获取到某个 Method 类对象后,可以通过调用invoke方法执行。

  • invoke(Oject obj, Object... args):参数``1指定调用该方法的**对象**,参数2`是方法的参数列表值。

如果调用的方法是静态方法,参数1只需要传入null,因为静态方法不与某个对象有关,只与某个类有关。

可以像下面这种做法,通过反射实例化一个对象,然后获取Method方法对象,调用invoke()指定SmallPineapple的getInfo()方法。

1
2
3
4
5
6
7
8
9
java复制代码Class clazz = Class.forName("com.bean.SmallPineapple");
Constructor constructor = clazz.getConstructor(String.class, int.class);
constructor.setAccessible(true);
SmallPineapple sp = (SmallPineapple) constructor.newInstance("小菠萝", 21);
Method method = clazz.getMethod("getInfo");
if (method != null) {
method.invoke(sp, null);
}
// [小菠萝的年龄是:21]

反射的应用场景

反射常见的应用场景这里介绍3个:

  • Spring 实例化对象:当程序启动时,Spring 会读取配置文件applicationContext.xml并解析出里面所有的 标签实例化到IOC容器中。
  • 反射 + 工厂模式:通过反射消除工厂中的多个分支,如果需要生产新的类,无需关注工厂类,工厂类可以应对各种新增的类,反射可以使得程序更加健壮。
  • JDBC连接数据库:使用JDBC连接数据库时,指定连接数据库的驱动类时用到反射加载驱动类

Spring 的 IOC 容器

在 Spring 中,经常会编写一个上下文配置文件applicationContext.xml,里面就是关于bean的配置,程序启动时会读取该 xml 文件,解析出所有的 <bean>标签,并实例化对象放入IOC容器中。

1
2
3
4
5
6
7
8
9
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="smallpineapple" class="com.bean.SmallPineapple">
<constructor-arg type="java.lang.String" value="小菠萝"/>
<constructor-arg type="int" value="21"/>
</bean>
</beans>

在定义好上面的文件后,通过ClassPathXmlApplicationContext加载该配置文件,程序启动时,Spring 会将该配置文件中的所有bean都实例化,放入 IOC 容器中,IOC 容器本质上就是一个工厂,通过该工厂传入 标签的id属性获取到对应的实例。

1
2
3
4
5
6
7
8
java复制代码public class Main {
public static void main(String[] args) {
ApplicationContext ac =
new ClassPathXmlApplicationContext("applicationContext.xml");
SmallPineapple smallPineapple = (SmallPineapple) ac.getBean("smallpineapple");
smallPineapple.getInfo(); // [小菠萝的年龄是:21]
}
}

Spring 在实例化对象的过程经过简化之后,可以理解为反射实例化对象的步骤:

  • 获取Class对象的构造器
  • 通过构造器调用 newInstance() 实例化对象

当然 Spring 在实例化对象时,做了非常多额外的操作,才能够让现在的开发足够的便捷且稳定。

在之后的文章中会专门写一篇文章讲解如何利用反射实现一个简易版的IOC容器,IOC容器原理很简单,只要掌握了反射的思想,了解反射的常用 API 就可以实现,我可以提供一个简单的思路:利用 HashMap 存储所有实例,key 代表 标签的 id,value 存储对应的实例,这对应了 Spring IOC容器管理的对象默认是单例的。

反射 + 抽象工厂模式

传统的工厂模式,如果需要生产新的子类,需要修改工厂类,在工厂类中增加新的分支;

1
2
3
4
5
6
7
8
9
java复制代码public class MapFactory {
public Map<Object, object> produceMap(String name) {
if ("HashMap".equals(name)) {
return new HashMap<>();
} else if ("TreeMap".equals(name)) {
return new TreeMap<>();
} // ···
}
}

利用反射和工厂模式相结合,在产生新的子类时,工厂类不用修改任何东西,可以专注于子类的实现,当子类确定下来时,工厂也就可以生产该子类了。

反射 + 抽象工厂的核心思想是:

  • 在运行时通过参数传入不同子类的全限定名获取到不同的 Class 对象,调用 newInstance() 方法返回不同的子类。细心的读者会发现提到了子类这个概念,所以反射 + 抽象工厂模式,一般会用于有继承或者接口实现关系。

例如,在运行时才确定使用哪一种 Map 结构,我们可以利用反射传入某个具体 Map 的全限定名,实例化一个特定的子类。

1
2
3
4
5
6
7
8
9
10
java复制代码public class MapFactory {
/**
* @param className 类的全限定名
*/
public Map<Object, Object> produceMap(String className) {
Class clazz = Class.forName(className);
Map<Object, Object> map = clazz.newInstance();
return map;
}
}

className 可以指定为 java.util.HashMap,或者 java.util.TreeMap 等等,根据业务场景来定。

JDBC 加载数据库驱动类

在导入第三方库时,JVM不会主动去加载外部导入的类,而是等到真正使用时,才去加载需要的类,正是如此,我们可以在获取数据库连接时传入驱动类的全限定名,交给 JVM 加载该类。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class DBConnectionUtil {
/** 指定数据库的驱动类 */
private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver";

public static Connection getConnection() {
Connection conn = null;
// 加载驱动类
Class.forName(DRIVER_CLASS_NAME);
// 获取数据库连接对象
conn = DriverManager.getConnection("jdbc:mysql://···", "root", "root");
return conn;
}
}

在我们开发 SpringBoot 项目时,会经常遇到这个类,但是可能习惯成自然了,就没多大在乎,我在这里给你们看看常见的application.yml中的数据库配置,我想你应该会恍然大悟吧。

这里的 driver-class-name,和我们一开始加载的类是不是觉得很相似,这是因为MySQL版本不同引起的驱动类不同,这体现使用反射的好处:不需要修改源码,仅加载配置文件就可以完成驱动类的替换。

在之后的文章中会专门写一篇文章详细地介绍反射的应用场景,实现简单的IOC容器以及通过反射实现工厂模式的好处。

在这里,你只需要掌握反射的基本用法和它的思想,了解它的主要使用场景。

反射的优势及缺陷

反射的优点:

  • 增加程序的灵活性:面对需求变更时,可以灵活地实例化不同对象

但是,有得必有失,一项技术不可能只有优点没有缺点,反射也有两个比较隐晦的缺点:

  • 破坏类的封装性:可以强制访问 private 修饰的信息
  • 性能损耗:反射相比直接实例化对象、调用方法、访问变量,中间需要非常多的检查步骤和解析步骤,JVM无法对它们优化。

增加程序的灵活性

这里不再用 SmallPineapple 举例了,我们来看一个更加贴近开发的例子:

  • 利用反射连接数据库,涉及到数据库的数据源。在 SpringBoot 中一切约定大于配置,想要定制配置时,使用application.properties配置文件指定数据源

角色1 - Java的设计者:我们设计好DataSource接口,你们其它数据库厂商想要开发者用你们的数据源监控数据库,就得实现我的这个接口!

角色2 - 数据库厂商:

  • MySQL 数据库厂商:我们提供了 com.mysql.cj.jdbc.MysqlDataSource 数据源,开发者可以使用它连接 MySQL。
  • 阿里巴巴厂商:我们提供了 com.alibaba.druid.pool.DruidDataSource 数据源,我这个数据源更牛逼,具有页面监控,慢SQL日志记录等功能,开发者快来用它监控 MySQL吧!
  • SQLServer 厂商:我们提供了 com.microsoft.sqlserver.jdbc.SQLServerDataSource 数据源,如果你想实用SQL Server 作为数据库,那就使用我们的这个数据源连接吧

角色3 - 开发者:我们可以用配置文件指定使用DruidDataSource数据源

1
properties复制代码spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

需求变更:某一天,老板来跟我们说,Druid 数据源不太符合我们现在的项目了,我们使用 MysqlDataSource 吧,然后程序猿就会修改配置文件,重新加载配置文件,并重启项目,完成数据源的切换。

1
properties复制代码spring.datasource.type=com.mysql.cj.jdbc.MysqlDataSource

在改变连接数据库的数据源时,只需要改变配置文件即可,无需改变任何代码,原因是:

  • Spring Boot 底层封装好了连接数据库的数据源配置,利用反射,适配各个数据源。

下面来简略的进行源码分析。我们用ctrl+左键点击spring.datasource.type进入 DataSourceProperties 类中,发现使用setType() 将全类名转化为 Class 对象注入到type成员变量当中。在连接并监控数据库时,就会使用指定的数据源操作。

1
2
3
4
5
java复制代码private Class<? extends DataSource> type;

public void setType(Class<? extends DataSource> type) {
this.type = type;
}

Class对象指定了泛型上界DataSource,我们去看一下各大数据源的类图结构。

上图展示了一部分数据源,当然不止这些,但是我们可以看到,无论指定使用哪一种数据源,我们都只需要与配置文件打交道,而无需更改源码,这就是反射的灵活性!

破坏类的封装性

很明显的一个特点,反射可以获取类中被private修饰的变量、方法和构造器,这违反了面向对象的封装特性,因为被 private 修饰意味着不想对外暴露,只允许本类访问,而setAccessable(true)可以无视访问修饰符的限制,外界可以强制访问。

还记得单例模式一文吗?里面讲到反射破坏饿汉式和懒汉式单例模式,所以之后用了枚举避免被反射KO。

回到最初的起点,SmallPineapple 里有一个 weight 属性被 private 修饰符修饰,目的在于自己的体重并不想给外界知道。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class SmallPineapple {
public String name;
public int age;
private double weight; // 体重只有自己知道

public SmallPineapple(String name, int age, double weight) {
this.name = name;
this.age = age;
this.weight = weight;
}

}

虽然 weight 属性理论上只有自己知道,但是如果经过反射,这个类就像在裸奔一样,在反射面前变得一览无遗。

1
2
3
4
5
6
java复制代码SmallPineapple sp = new SmallPineapple("小菠萝", 21, "54.5");
Clazz clazz = Class.forName(sp.getClass());
Field weight = clazz.getDeclaredField("weight");
weight.setAccessable(true);
System.out.println("窥觑到小菠萝的体重是:" + weight.get(sp));
// 窥觑到小菠萝的体重是:54.5 kg

性能损耗

在直接 new 对象并调用对象方法和访问属性时,编译器会在编译期提前检查可访问性,如果尝试进行不正确的访问,IDE会提前提示错误,例如参数传递类型不匹配,非法访问 private 属性和方法。

而在利用反射操作对象时,编译器无法提前得知对象的类型,访问是否合法,参数传递类型是否匹配。只有在程序运行时调用反射的代码时才会从头开始检查、调用、返回结果,JVM也无法对反射的代码进行优化。

虽然反射具有性能损耗的特点,但是我们不能一概而论,产生了使用反射就会性能下降的思想,反射的慢,需要同时调用上100W次才可能体现出来,在几次、几十次的调用,并不能体现反射的性能低下。所以不要一味地戴有色眼镜看反射,在单次调用反射的过程中,性能损耗可以忽略不计。如果程序的性能要求很高,那么尽量不要使用反射。

反射基础篇文末总结

  • 反射的思想:反射就像是一面镜子一样,在运行时才看到自己是谁,可获取到自己的信息,甚至实例化对象。
  • 反射的作用:在运行时才确定实例化对象,使程序更加健壮,面对需求变更时,可以最大程度地做到不修改程序源码应对不同的场景,实例化不同类型的对象。
  • 反射的应用场景常见的有3个:Spring的 IOC 容器,反射+工厂模式 使工厂类更稳定,JDBC连接数据库时加载驱动类
  • 反射的3个特点:增加程序的灵活性、破坏类的封装性以及性能损耗

你好,我是 cxuan,我自己手写了四本 PDF,分别是 Java基础总结、HTTP 核心总结、计算机基础知识,操作系统核心总结,我已经整理成为 PDF,可以关注公众号 Java建设者 回复 PDF 领取优质资料。

本文转载自: 掘金

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

Java 14 版本特性【一文了解】 特性总览 一 Swi

发表于 2020-08-23

  • 「MoreThanJava」 宣扬的是 「学习,不止 CODE」,本系列 Java 基础教程是自己在结合各方面的知识之后,对 Java 基础的一个总回顾,旨在 「帮助新朋友快速高质量的学习」。
  • 当然 不论新老朋友 我相信您都可以 从中获益。如果觉得 「不错」 的朋友,欢迎 「关注 + 留言 + 分享」,文末有完整的获取链接,您的支持是我前进的最大的动力!

特性总览

以下是 Java 14 中的引入的部分新特性。关于 Java 14 新特性更详细的介绍可参考这里。

语言及特性更改:

  • Switch 表达式-标准(JEP 361)
  • instanceof 的模式匹配-预览(JEP 305)
  • 有用的 NullPointerExceptions(JEP 358)
  • record-预览(JEP 359)
  • 文本块-预览(JEP 368)

JVM 更改:

  • 针对 G1 NUMA 感知内存分配的优化(JEP 345)
  • 删除并发标记扫描(CMS)垃圾收集器(JEP 363)
  • JFR 事件流(JEP 349)
  • macOS 上的 ZGC-实验性(JEP 364)
  • Windows 上的 ZGC-实验性(JEP 365)
  • 弃用 ParallelScavenge + SerialOld 的 GC 组合(JEP 366)

其他特性:

  • 打包工具(JEP 343)
  • 非易失性映射字节缓冲区(JEP 352)
  • 弃用 Solaris 和 SPARC 端口(JEP 362)
  • 删除 Pack200 工具和 API(JEP 367)
  • 外部存储器访问 API(JEP 370)

一. Switch 表达式-标准(JEP 361)

在上两个版本中保留的预留功能,如今终于在 Java 14 中获得了永久性的地位。

  • Java 12 为表达是引入了 Lambda 语法,从而允许使用多个大小写标签进行模式匹配,并防止出现导致冗长代码的错误。它还强制执行穷尽情况,如果没有涵盖所有输入情况,则会抛出编译错误。
  • Java 13 在第二个预览版本使用了 yield 替代了原有的 break 关键字来返回表达式的返回值。

Java 14 现在终于使这些功能成为了标准:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码String result = switch (day) {
case "M", "W", "F" -> "MWF";
case "T", "TH", "S" -> "TTS";
default -> {
if (day.isEmpty()) {
yield "Please insert a valid day.";
} else {
yield "Looks like a Sunday.";
}
}
};
System.out.println(result);

注意,yield 不是 Java 中的新关键字,它仅用于 Switch 表达式中。

二. instanceof 的模式匹配-预览(JEP 305)

在 Java 14 之前,我们用于 instanceof-and-cast 检查对象的类型并将其转换为变量。

1
2
3
4
5
6
7
8
java复制代码if (obj instanceof String) {        // instanceof
String s = (String) obj; // cast
if("jdk14".equalsIgnoreCase(s)){
//...
}
}else {
System.out.println("not a string");
}

现在,在 Java 14 中,我们可以像这样重构上面的代码:

1
2
3
4
5
6
7
java复制代码if (obj instanceof String s) {      // instanceof, cast and bind variable in one line.
if("jdk4".equalsIgnoreCase(s)){
//...
}
}else {
System.out.println("not a string");
}

如果 obj 是的实例 String,则将其 String 强制转换为绑定变量并分配给该绑定变量 s。

三. 有用的 NullPointerExceptions(JEP 358)

空指针异常是任何开发人员的噩梦。以前,直到 Java 13 为止,调试臭名昭著的 NPE 都很棘手。开发人员不得不依靠其他调试工具,或者手动计算为空的变量/ 方法,因为堆栈跟踪只会显示行号。

在 Java 14 之前:

1
2
3
4
5
java复制代码String name = jd.getBlog().getAuthor()

//Stacktrace
Exception in thread "main" java.lang.NullPointerException
at NullPointerExample.main(NullPointerExample.java:5)

Java 14 引入了新的 JVM 功能(带-XX:+ShowCodeDetailsInExceptionMessages选项),它通过更具描述性的堆栈提供了更好的见解,如下所示:

1
2
bash复制代码Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Blog.getAuthor()" because the return value of "Journaldev.getBlog()" is null
at NullPointerExample.main(NullPointerExample.java:4)

注意:以上功能不是语言功能。这是对运行时环境的增强。

四. record-预览(JEP 359)

record 是存储纯数据的数据类型。引入 record 背后的想法是快速创建没有样板代码的简单简洁类。(这有点类似 Kotlin 中的数据类型,这也是为什么有言论说 Java 逐渐 “Kotlin 化” 的原因之一)

通常,Java 中的类需要您实现 equals()、hashCode()、getters 和 setters 方法。虽然某些 IDE 支持此类的自动生成,但是代码仍然很冗长。使用 record 您只需按照以下方式定义一个类。

1
2
3
java复制代码record Author(){}
//or
record Author (String name, String topic) {}

Java 编译器将自动生成一个带有构造函数、私有 final 字段、访问器和 equals、 hashCode 和 toString 方法的类。上一类的自动生成的 getter 方法是 name() 和 topic()。

编译之后,我们可以查看上面 record Author (String name, String topic){} 语句,编译器为我们自动生成的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码final class Author extends java.lang.Record {
private final java.lang.String name;
private final java.lang.String topic;

public Author(java.lang.String name, java.lang.String topic) { /* compiled code */ }

public java.lang.String toString() { /* compiled code */ }

public final int hashCode() { /* compiled code */ }

public final boolean equals(java.lang.Object o) { /* compiled code */ }

public java.lang.String name() { /* compiled code */ }

public java.lang.String topic() { /* compiled code */ }
}

此外,我们可以通过以下方式向记录添加其他字段,方法和构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码record Author (int id, String name, String topic) {
static int followers;

public static String followerCount() {
return "Followers are "+ followers;
}

public String description(){
return "Author "+ name + " writes on "+ topic;
}

public Author{
if (id < 0) {
throw new IllegalArgumentException( "id must be greater than 0.");
}
}
}

record 内定义的其他构造函数称为 Compact 构造函数。它不包含任何参数,只是规范本身构造函数的参数。

关于 record 需要注意的几件事:

  • record 既不能扩展一个类,也不能被另一个类扩展。这是一个 final class。
  • record 不能是抽象的。
  • record 不能扩展任何其他类,也不能在主体内定义实例字段。实例字段只能在状态描述中定义。
  • 声明的字段是私有字段和 final 字段。
  • record 定义中允许使用静态字段和方法。

record 类的引用字段内的值可以被改变

值得注意的是,对于定义为对象的字段,只有引用是不可变的。底层值可以修改。

下面显示了修改 ArrayList 的一条记录。可以看到,每当 ArrayList 被更改时,该值都会被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码jshell> record Author(String name, List<String> topics){}
| 已创建 记录 Author

jshell> var topicList = new ArrayList<String>(Arrays.asList("Java"));
topicList ==> [Java]

jshell> var author = new Author("Wmyskxz", topicList);
author ==> Author[name=Wmyskxz, topics=[Java]]

jshell> topicList.add("Python");
$6 ==> true

jshell> author.topics();
$7 ==> [Java, Python]

jshell>

record 能实现接口

下面的代码显示了一个实现有记录接口的示例:

1
2
3
4
5
6
7
8
9
java复制代码interface Information {
String getFullName();
}

record Author(String name, String topic) implements Information {
public String getFullName() {
return "Author "+ name + " writes on " + topic;
}
}

record 支持多个构造函数

记录允许声明多个有或没有参数的构造函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
java复制代码record Author(String name, String topic) {
public Author() {

this("NA", "NA");
}

public Author(String name) {

this(name, "NA");
}
}

record 允许修改访问器方法

虽然 record 为状态描述中定义的字段生成公共访问方法,但它们也允许您在主体中重新定义访问方法,如下所示:

1
2
3
4
5
java复制代码record Author(String name, String topic) {
public String name() {
return "This article was written by " + this.name;
}
}

在运行时检查 record 及其组件

record 为我们提供了 isRecord() 和 getRecordComponents() 来检查类是否是一条记录,并查看它的字段和类型。下面展示了它是如何做到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码jshell> record Author(String name, String topic){}
| 已创建 记录 Author

jshell> var author = new Author("Wmyskxz", "MoreThanJava");
author ==> Author[name=Wmyskxz, topic=MoreThanJava]

jshell> author.getClass().isRecord();
$3 ==> true

jshell> author.getClass().getRecordComponents();
$4 ==> RecordComponent[2] { java.lang.String name, java.lang.String topic }

jshell>

Tips:虽然我们在上面的代码示例中向记录添加了额外的字段和方法,但请确保不要做得过火。记录被设计为普通的数据载体,如果您想实现许多其他方法,最好回到常规类。

五. 文本块-预览(JEP 368)

文本块是 Java 13 中的预览功能,其目的是允许轻松创建多行字符串文字。在轻松创建 HTML 和 JSON 或 SQL 查询字符串时很有用。

在 Java 14 中,文本块仍在预览中,并增加了一些新功能。我们现在可以使用:

  • \ 反斜杠用于显示美观的多行字符串块。
  • \s 用于考虑尾随空格,默认情况下编译器会忽略它们。它保留了前面的所有空间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码String text = """
Did you know \
Java 14 \
has the most features among\
all non-LTS versions so far\
""";

String text2 = """
line1
line2 \s
line3
""";

String text3 = "line1\nline2 \nline3\n"

//text2 and text3 are equal.

六. 针对 G1 NUMA 感知内存分配的优化(JEP 345)

新的 NUMA感知内存 分配模式提高了大型计算机上的 G1 性能。添加 +XX:+UseNUMA 选项以启用它。

七. 删除并发标记扫描(CMS)垃圾收集器(JEP 363)

Java 9 – JEP 291 已弃用此并发标记扫描(CMS)垃圾收集器,现在正式将其删除。

1
2
3
bash复制代码/usr/lib/jvm/jdk-14/bin/java -XX:+UseConcMarkSweepGC Test

OpenJDK 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; support was removed in 14.0

八. JFR 事件流(JEP 349)

JDK Flight Recorder(JFR)是用于收集有关正在运行的 Java 应用程序的诊断和性能分析数据的工具。通常,我们开始记录,停止记录,然后将记录的事件转储到磁盘以进行分析,它可以很好地进行概要分析,分析或调试。

该 JEP 改进了现有的 JFR 以支持事件流,这意味着现在我们可以实时传输 JFR 事件,而无需将记录的事件转储到磁盘并手动解析它。

九. macOS 上的 ZGC-实验性(JEP 364)

Java 11 – JEP 333 在 Linux 上引入了 Z 垃圾收集器(ZGC),现在可移植到 macOS。

十. Windows 上的 ZGC-实验性(JEP 365)

Java 11 – JEP 333 在 Linux 上引入了 Z 垃圾收集器(ZGC),现在可移植到 Windows版本 >= 1803 的机器上。

十一. 弃用 ParallelScavenge + SerialOld 的 GC 组合(JEP 366)

由于较少的使用和大量的维护工作,Java 14 不赞成使用并行年轻代和串行老一代 GC 算法的组合。

1
2
3
bash复制代码/usr/lib/jvm/jdk-14/bin/java -XX:-UseParallelOldGC Test

OpenJDK 64-Bit Server VM warning: Option UseParallelOldGC was deprecated in version 14.0 and will likely be removed in a future release.

十二. 其他特性

打包工具(JEP 343)

jpackage 是将 Java 应用程序打包到特定于平台的程序包中的新工具。

  • Linux:deb 和 rpm
  • macOS:pkg 和 dmg
  • Windows:MSI 和 EXE

例如,将 JAR 文件打包到支持 exe 的 Windows 平台上。

非易失性映射字节缓冲区(JEP 352)

改进的 FileChannel API 可创建 MappedByteBuffer 对 非易失性存储器(NVM)的 访问,该存储器即使在关闭电源后也可以检索存储的数据。例如,此功能可确保将可能仍在高速缓存中的所有更改写回到内存中。

ps:仅 Linux / x64 和 Linux / AArch64 OS 支持此功能!

弃用 Solaris 和 SPARC 端口(JEP 362)

不再支持 Solaris / SPARC,Solaris / x64 和 Linux / SPARC 端口,更少的平台支持意味着更快地交付新功能。

删除 Pack200 工具和 API(JEP 367)

Java 11 – JEP 336 不赞成使用 pack200 和 unpack200 工具,以及软件包中的 Pack200 API java.util.jar,现在正式将其删除。

外部存储器访问 API(JEP 370)

孵化器模块,允许 Java API 访问 Java 堆外部的外部内存。

外部存储器访问 API 引入了三个主要抽象:

  • MemorySegment:提供对具有给定范围的连续内存区域的访问。
  • MemoryAddress:提供到 MemorySegment 的偏移量(基本上是一个指针)。
  • MemoryLayout:提供一种描述内存段布局的方法,该方法大大简化了使用 var 句柄访问 MemorySegment 的过程。使用此方法,不必根据内存的使用方式来计算偏移量。例如,一个整数或长整数数组的偏移量将有所不同,但将使用 MemoryLayout 透明地对其进行处理。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码import jdk.incubator.foreign.*;
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;

public class Test {

public static void main(String[] args) {

VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

try (MemorySegment segment = MemorySegment.allocateNative(1024)) {

MemoryAddress base = segment.baseAddress();

System.out.println(base); // print memory address

intHandle.set(base, 999); // set value 999 into the foreign memory

System.out.println(intHandle.get(base)); // get the value from foreign memory
}
}
}

编译并使用孵化器模块运行 jdk.incubator.foreign。

1
2
3
4
5
6
7
8
9
10
bash复制代码$ /usr/lib/jvm/jdk-14/bin/javac --add-modules jdk.incubator.foreign Test.java

warning: using incubating module(s): jdk.incubator.foreign
1 warning

$ /usr/lib/jvm/jdk-14/bin/java --add-modules jdk.incubator.foreign Test

WARNING: Using incubator modules: jdk.incubator.foreign
MemoryAddress{ region: MemorySegment{ id=0x4aac6dca limit: 1024 } offset=0x0 }
999

进一步阅读:官方文档 - download.java.net/java/GA/jdk…

参考资料

  1. OpenJDK 官方说明 - openjdk.java.net/projects/jd…
  2. What is new in Java 14 - mkyong.com/java/what-i…
  3. Java 14 Features | JournalDev - www.journaldev.com/37273/java-…

文章推荐

  1. 这都JDK15了,JDK7还不了解? - www.wmyskxz.com/2020/08/18/…
  2. 全网最通透的 Java 8 版本特性讲解 - www.wmyskxz.com/2020/08/19/…
  3. Java9的这些史诗级更新你都不知道? - www.wmyskxz.com/2020/08/20/…
  4. 你想了解的 JDK 10 版本更新都在这里 - www.wmyskxz.com/2020/08/21/…
  5. 这里有你不得不了解的 Java 11 特性 - www.wmyskxz.com/2020/08/22/…
  6. Java 12 版本特性【一文了解】 - www.wmyskxz.com/2020/08/22/…
  7. Java 13 版本特性【一文了解】 - www.wmyskxz.com/2020/08/22/…
  8. 「MoreThanJava」系列文集 - www.wmyskxz.com/categories/…
  • 本文已收录至我的 Github 程序员成长系列 【More Than Java】,学习,不止 Code,欢迎 star:github.com/wmyskxz/Mor…
  • 个人公众号 :wmyskxz,个人独立域名博客:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长!

非常感谢各位人才能 看到这里,如果觉得本篇文章写得不错,觉得 「我没有三颗心脏」有点东西 的话,求点赞,求关注,求分享,求留言!

创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

本文转载自: 掘金

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

探索MySQL是否走索引(一)——范围查询一定走索引吗?

发表于 2020-08-23

首先,准备4个版本的数据库,5.5/5.6/5.7/8.0

然后,每个库中有一模一样的表,数据量一样,建立的索引一样,InnoDB引擎。

先看一看,各个版本中,索引统计信息(Cardinality代表该列在总数据中有多少个不同的值,该值为估算值)

  1. MySQL5.5


2. MySQL5.6


3. MySQL5.7


4. MySQL8.0

对比可知,统计的数据量方面,基本都差不多。

本文探索>,<,>=,<=走索引的情况,首先回答标题的问题,>,<,>=,<=不一定走索引,因为优化器会计算,符合条件的值的总和,与总数据量的比值,比值<=0.30,才会走索引。

官网原话如下:

Each table index is queried, and the best index is used unless the optimizer believes that it is more efficient to use a table scan. At one time, a scan was used based on whether the best index spanned more than 30% of the table, but a fixed percentage no longer determines the choice between using an index or a scan. The optimizer now is more complex and bases its estimate on additional factors such as table size, number of rows, and I/O block size.

查询每个表索引,并使用最佳索引,除非优化程序认为使用表扫描更有效。
一次使用扫描是基于**最佳索引**是否**跨越了表的30%以上**,但是固定百分比不再决定使用索引还是扫描。现在,优化器更加复杂,其估计基于**其他因素**,**例如表大小,行数和I / O块大小**。
注意:这段话有多个重点

  1. 假如where条件中,涉及到了多个索引,MySQL会选择一个最佳索引。最佳索引就是选择性最高的索引,因为它可以过滤掉很多的无用数据行。
  2. 0.30比例值。以前可能是>0.30就不走索引,但文档最低版本是5.6已无从考证。现在不仅取决于比例值,也取决于其他因素。所以,用 > 或 >= 或 < 或 <=其中测试以下即可。

接下来,准备开干。

  1. MySQL5.5版本中,通过不断测试,找到了8这个关键数字。
1
2
csharp复制代码explain select user_name,user_age from user1 where user_age <= 8;
explain select user_id,user_name,user_age from user1 where user_age < 8;

第二张图中Extra虽然没有明确说明Using index ,但是type显示为range范围查询,扫描行数rows为35042行,但第一张图中type:ALL,rows:210463与文章一开始的全表统计信息中,主键id数据量一致,主键id的数据量=全表数据多少行,可知这是全表扫描。

差距只是在于是否包含8这个值,因此统计一下,<8 和 =8的数据量(特别说明,8这个值是试出来的,不可能一下找到该值)

计算一下比值:user_age < 8 比值为:35042/210463 = 0.1664(走索引)

user_age <= 8比值为:(35042 + 2221)/210463 = 0.1770(不走索引)

以上可知:比例值<=0.1664可以走索引,比例值>=0.1770优化器认为全表扫描更快,所以就不走索引。

测试大于的情况(89也是不断测试出来的)

1
2
csharp复制代码explain select user_name,user_age from user1 where user_age >= 89;
explain select user_id,user_name,user_age from user1 where user_age > 89;

一样,统计一下 >89 和 =89 的数据量

user_age > 89的比例值:35894 / 210463 = 0.1705(走索引)

user_age >= 89的比例值:(35894 + 2088)/210463 = 0.1804(不走索引)

结合user_age < 8的比例值,和user_age > 89的比例值,可知,对于5.5版本,user_age这个索引列,比例值在 0.1705~0.1770之间。

特殊情况1,可以使用FORCE INDEX强制走索引,不管比例值是多少

1
2
csharp复制代码explain select user_name,user_age from user1 force index(idx_user_age) where user_age < 50;
explain select user_name,user_age from user1 force index(idx_user_age) where user_age > 20;

特殊情况2:只返回索引列时,不管比例值是多少,都会走索引

1
csharp复制代码explain select user_age from user1 where user_age < 80;


2. MySQL5.6只测试一下<的情况(通过测试关键值也是8)

1
2
csharp复制代码explain select user_name,user_age from user1 where user_age <= 8;
explain select user_id,user_name,user_age from user1 where user_age < 8;

同样计算一下比例值:

user_age < 8 :35042 / 208534 = 0.1680(走索引)

user_age <= 8:(35042+2221)/208534 = 0.1786(不走索引)

由此可大致推断:5.6版本下,该比例值在 0.1680 ~ 0.1786之间。

特别注意:第二张图片中,出现了using index condition,这是MySQL5.6做出的优化,ICP(index condition pushdown)可对比5.5版本,Extra中显示的是Using where,代表数据最终是在MySQL服务层过滤数据的,5.6版本显示的是Using index condition,代表数据在存储引擎层就过滤了无用的数据,ICP可以减少存储引擎必须访问基表的次数以及MySQL服务器必须访问存储引擎的次数。

官网地址如下:dev.mysql.com/doc/refman/…

同样,如果使用FORCE INDEX,则肯定会走索引,只返回索引列,不管比例值,照样走索引,此处不再贴图
3. MySQL5.7只测试一下<的情况(测试到20是个关键值)

1
2
csharp复制代码explain select user_name,user_age from user1 where user_age <= 20;
explain select user_id,user_name,user_age from user1 where user_age < 20;

计算比例值如下:

user_age < 20:87966 / 208303 = 0.422 (走索引)

user_age <= 20:(87966 + 2093)/208303 = 0.4323(不走索引)

同样,如果使用FORCE INDEX,则肯定会走索引,只返回索引列,不管比例值,照样走索引,此处不再贴图

特别注意:此处出现MRR,本处不讨论,后边会有详细讨论。
4. MySQL8.0只测试一下<的情况(测试到关键值为12)

1
2
csharp复制代码explain select user_name,user_age from user1 where user_age <= 12;
explain select user_id,user_name,user_age from user1 where user_age < 12;

计算一下比例值:

user_age < 12:54798 / 208611 = 0.2626(走索引)

user_age <= 12:(54798 + 2051)/208611 = 0.2725(不走索引)

由此可推测,比例值在 0.2626 ~ 0.2725之间

同样,如果使用FORCE INDEX,则肯定会走索引,只返回索引列,不管比例值,照样走索引,此处不再贴图

总结:

  1. 并不是给一个列建立了索引,对这个列进行范围查询的时候,就会走索引,他是有一个比例值的。比例值会随着版本、服务器、IO、数据量、数据重复情况而不同。也就是说,同一个版本,同一个库表,此时和下一时刻,比例值就可能不一样。测试中途遇到过该问题。
  2. MySQL5.6版本的时候,进行了优化,ICP 和 MRR,极大提升性能。后边也会有例子。
  3. FORCE INDEX 的作用,特殊情况下,可以只返回索引列。

本文转载自: 掘金

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

面试官:一千万数据,怎么快速查询?

发表于 2020-08-22

前言

  • 面试官: 来说说,一千万的数据,你是怎么查询的?
  • B哥:直接分页查询,使用limit分页。
  • 面试官:有实操过吗?
  • B哥:肯定有呀

此刻献上一首《凉凉》

也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。

今天就来带大家实操一下,这次是基于MySQL 5.7.26做测试

准备数据

没有一千万的数据怎么办?

创建呗

代码创建一千万?那是不可能的,太慢了,可能真的要跑一天。可以采用数据库脚本执行速度快很多。

创建表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码CREATE TABLE `user_operation_log`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `ip` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `op_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr4` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr6` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr7` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr8` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr9` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr10` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr11` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `attr12` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
创建数据脚本

采用批量插入,效率会快很多,而且每1000条数就commit,数据量太大,也会导致批量插入效率慢

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
复制代码DELIMITER ;;
CREATE PROCEDURE batch_insert_log()
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE userId INT DEFAULT 10000000;
 set @execSql = 'INSERT INTO `test`.`user_operation_log`(`user_id`, `ip`, `op_data`, `attr1`, `attr2`, `attr3`, `attr4`, `attr5`, `attr6`, `attr7`, `attr8`, `attr9`, `attr10`, `attr11`, `attr12`) VALUES';
 set @execData = '';
  WHILE i<=10000000 DO
   set @attr = "'测试很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的属性'";
  set @execData = concat(@execData, "(", userId + i, ", '10.0.69.175', '用户登录操作'", ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ",", @attr, ")");
  if i % 1000 = 0
  then
     set @stmtSql = concat(@execSql, @execData,";");
    prepare stmt from @stmtSql;
    execute stmt;
    DEALLOCATE prepare stmt;
    commit;
    set @execData = "";
   else
     set @execData = concat(@execData, ",");
   end if;
  SET i=i+1;
  END WHILE;

END;;
DELIMITER ;

开始测试

哥的电脑配置比较低:win10 标压渣渣i5 读写约500MB的SSD

由于配置低,本次测试只准备了3148000条数据,占用了磁盘5G(还没建索引的情况下),跑了38min,电脑配置好的同学,可以插入多点数据测试

1
复制代码SELECT count(1) FROM `user_operation_log`

返回结果:3148000

三次查询时间分别为:

  • 14060 ms
  • 13755 ms
  • 13447 ms

普通分页查询

MySQL 支持 LIMIT 语句来选取指定的条数数据, Oracle 可以使用 ROWNUM 来选取。

MySQL分页查询语法如下:

1
复制代码SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
  • 第一个参数指定第一个返回记录行的偏移量
  • 第二个参数指定返回记录行的最大数目

下面我们开始测试查询结果:

1
复制代码SELECT * FROM `user_operation_log` LIMIT 10000, 10

查询3次时间分别为:

  • 59 ms
  • 49 ms
  • 50 ms

这样看起来速度还行,不过是本地数据库,速度自然快点。

换个角度来测试

相同偏移量,不同数据量
1
2
3
4
5
6
复制代码SELECT * FROM `user_operation_log` LIMIT 10000, 10
SELECT * FROM `user_operation_log` LIMIT 10000, 100
SELECT * FROM `user_operation_log` LIMIT 10000, 1000
SELECT * FROM `user_operation_log` LIMIT 10000, 10000
SELECT * FROM `user_operation_log` LIMIT 10000, 100000
SELECT * FROM `user_operation_log` LIMIT 10000, 1000000

查询时间如下:

数量 第一次 第二次 第三次
10条 53ms 52ms 47ms
100条 50ms 60ms 55ms
1000条 61ms 74ms 60ms
10000条 164ms 180ms 217ms
100000条 1609ms 1741ms 1764ms
1000000条 16219ms 16889ms 17081ms

从上面结果可以得出结束:数据量越大,花费时间越长

相同数据量,不同偏移量
1
2
3
4
5
复制代码SELECT * FROM `user_operation_log` LIMIT 100, 100
SELECT * FROM `user_operation_log` LIMIT 1000, 100
SELECT * FROM `user_operation_log` LIMIT 10000, 100
SELECT * FROM `user_operation_log` LIMIT 100000, 100
SELECT * FROM `user_operation_log` LIMIT 1000000, 100
偏移量 第一次 第二次 第三次
100 36ms 40ms 36ms
1000 31ms 38ms 32ms
10000 53ms 48ms 51ms
100000 622ms 576ms 627ms
1000000 4891ms 5076ms 4856ms

从上面结果可以得出结束:偏移量越大,花费时间越长

1
2
复制代码SELECT * FROM `user_operation_log` LIMIT 100, 100
SELECT id, attr FROM `user_operation_log` LIMIT 100, 100

如何优化

既然我们经过上面一番的折腾,也得出了结论,针对上面两个问题:偏移大、数据量大,我们分别着手优化

优化偏移量大问题

采用子查询方式

我们可以先定位偏移位置的 id,然后再查询数据

1
2
3
4
5
复制代码SELECT * FROM `user_operation_log` LIMIT 1000000, 10

SELECT id FROM `user_operation_log` LIMIT 1000000, 1

SELECT * FROM `user_operation_log` WHERE id >= (SELECT id FROM `user_operation_log` LIMIT 1000000, 1) LIMIT 10

查询结果如下:

sql 花费时间
第一条 4818ms
第二条(无索引情况下) 4329ms
第二条(有索引情况下) 199ms
第三条(无索引情况下) 4319ms
第三条(有索引情况下) 201ms

从上面结果得出结论:

  • 第一条花费的时间最大,第三条比第一条稍微好点
  • 子查询使用索引速度更快

缺点:只适用于id递增的情况

id非递增的情况可以使用以下写法,但这种缺点是分页查询只能放在子查询里面

注意:某些 mysql 版本不支持在 in 子句中使用 limit,所以采用了多个嵌套select

1
复制代码SELECT * FROM `user_operation_log` WHERE id IN (SELECT t.id FROM (SELECT id FROM `user_operation_log` LIMIT 1000000, 10) AS t)
采用 id 限定方式

这种方法要求更高些,id必须是连续递增,而且还得计算id的范围,然后使用 between,sql如下

1
2
3
复制代码SELECT * FROM `user_operation_log` WHERE id between 1000000 AND 1000100 LIMIT 100

SELECT * FROM `user_operation_log` WHERE id >= 1000000 LIMIT 100

查询结果如下:

sql 花费时间
第一条 22ms
第二条 21ms

从结果可以看出这种方式非常快

注意:这里的 LIMIT 是限制了条数,没有采用偏移量

优化数据量大问题

返回结果的数据量也会直接影响速度

1
2
3
4
5
复制代码SELECT * FROM `user_operation_log` LIMIT 1, 1000000

SELECT id FROM `user_operation_log` LIMIT 1, 1000000

SELECT id, user_id, ip, op_data, attr1, attr2, attr3, attr4, attr5, attr6, attr7, attr8, attr9, attr10, attr11, attr12 FROM `user_operation_log` LIMIT 1, 1000000

查询结果如下:

sql 花费时间
第一条 15676ms
第二条 7298ms
第三条 15960ms

从结果可以看出减少不需要的列,查询效率也可以得到明显提升

第一条和第三条查询速度差不多,这时候你肯定会吐槽,那我还写那么多字段干啥呢,直接 * 不就完事了

注意本人的 MySQL 服务器和客户端是在同一台机器上,所以查询数据相差不多,有条件的同学可以测测客户端与MySQL分开

SELECT * 它不香吗?

在这里顺便补充一下为什么要禁止 SELECT *。难道简单无脑,它不香吗?

主要两点:

  1. 用 “SELECT * “ 数据库需要解析更多的对象、字段、权限、属性等相关内容,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
  2. 增大网络开销,* 有时会误带上如log、IconMD5之类的无用且大文本字段,数据传输size会几何增涨。特别是MySQL和应用程序不在同一台机器,这种开销非常明显。

结束

最后还是希望大家自己去实操一下,肯定还可以收获更多,欢迎留言!!

创建脚本我给你正好了,你还在等什么!!!

再奉上我之前 MySQL 如何优化

本文转载自: 掘金

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

Go微服务架构系列--gin框架(上) 🏆 技术专题第二

发表于 2020-08-21

hi,大家好,小弟飞狐。这次带来的是Golang微服务系列。Deno从零到架构级系列文章里就提到过微服务。最近一次项目重构中,采用了go-micro微服务架构。又恰逢deno1.0正式版推出,于是乎node业务层也用deno重写。把Java的业务模块也全部用go重构了。

Go-micro重构Java业务

重构业务的时候,我们用go-micro来做微服务,全面的替代了Java栈。比如:

  • 服务注册发现用到了etcd
  • 通信用到了grpc
  • 框架集成了gin

订单、支付等等都作为单独的服务。而deno之上都归前端来处理业务层,这样职责明确,更利于前后端协作。另外,我们这套将会采用最新的go-micro V3来搭建架构。

gin框架初体验

话不多说,即刻开始。这套微服务系列不是入门教程,需要有go项目经验。从框架选型开始,到go-micro构建微服务架构。go的框架选型不用纠结。在go的web框架中,飞狐推荐两个框架:

  • echo
  • gin

介绍这两框架的文章太多了,优势与区别我就不多说了。这两个框架大家可以任选其一,可以任凭喜好,那飞狐选择gin框架,并将gin框架集成到go-micro中。我们先从gin基础架构搭建开始。先来个简单的例子,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main
// 获取gin
import "github.com/gin-gonic/gin"

// 主函数
func main() {
// 取r是router的缩写
r := gin.Default()
// 这里非常简单,很像deno、node的路由吧
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
// 监听端口8080
r.Run(":8080")
}

这个例子非常简单,直接copy的gin官方代码。加了中文注释,运行即可,相信有点基础的童鞋都能看懂。这里的路由,一般会单独写文件来维护。不过,我在deno架构系列中提到过,拿到项目直接就是干路由,不要去维护一个单独的路由文件。deno系列我们用的是注解路由。虽然go也可以通过反射实现注解路由,但go不是一门面向对象的语言。根据go的语法特性,飞狐推荐把路由放到控制层中维护。

路由改造

路由改造之前我们新建controller层,然后操作如下:

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
go复制代码// 新建userController.go
package controller
import (
"github.com/gin-gonic/gin"
)
type UserController struct {
*gin.Engine
}

// 这里是构造函数
func NewUserController(e *gin.Engine) *UserController {
return &UserController{e}
}

// 这里是业务方法
func (this *UserController) GetUser() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"data": "hello world",
})
}
}

// 这里是处理路由的地儿
func (this *UserController) Router () {
this.Handle("GET", "/", this.GetUser())
}

这样路由就维护到每个控制器中了,那如何映射呢?我们改造主文件如下:

1
2
3
4
5
go复制代码func main () {
r := gin.Default()
NewUserController(r).Router()
r.Run(":8080")
}

关键代码就是将构造器的Router方法在主函数中执行。这样就达到目的,不用去维护单独的路由文件了。不过,大家发现没?这样也带来了一些弊端。比如:

  • 规范性很差
  • 代码耦合性高
  • 灵活性不够、维护起来就很麻烦

搭建脚手架

为了解决上述弊端,基于gin我们搭建一个脚手架。就如同我们基于oak搭建deno的脚手架一样。同样换做echo框架也同样适用。新建server目录,在此目录下新建server.go文件,代码如下:

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
go复制代码package server

import (
"github.com/gin-gonic/gin"
)
// 这里是定义一个接口,解决上述弊端的规范性
type IController interface {
// 这个传参就是脚手架主程
Router(server *Server)
}

// 定义一个脚手架
type Server struct {
*gin.Engine
// 路由分组一会儿会用到
g *gin.RouterGroup
}

// 初始化函数
func Init() *Server {
// 作为Server的构造器
s := &Server{Engine: gin.New()}
// 返回作为链式调用
return s
}

// 监听函数,更好的做法是这里的端口应该放到配置文件
func (this *Server) Listen() {
this.Run(":8080")
}

// 这里是路由的关键代码,这里会挂载路由
func (this *Server) Route(controllers ...IController) *Server {
// 遍历所有的控制层,这里使用接口,就是为了将Router实例化
for _, c := range controllers {
c.Router(this)
}
return this
}

这一步完成了,主函数就减负了,主函数改造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go复制代码// main.go
package main

import (
. "feihu/controller"
"feihu/server"
)
// 这里其实之前飞狐讲的deno入口文件改造几乎一样
func main () {
// 这里就是脚手架提供的服务
server.
// 初始化
Init().
// 路由
Route(
NewUserController(),
).
// 监听端口
Listen()
}

那控制层的代码也会相应简化,之前的控制层代码改造如下:

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
go复制代码package controller
import (
"github.com/gin-gonic/gin"
"feihu/server"
)

// 这里的gin引擎直接移到脚手架server里
type UserController struct {
}

// 这里是构造函数
func NewUserController() *UserController {
return &UserController{}
}

// 这里是业务方法
func (this *UserController) GetUser() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"data": "hello world",
})
}
}

// 这里依然是处理路由的地儿,而由于我们定义了接口规范,就必须实现Router方法
func (this *UserController) Router (server *server.Server) {
server.Handle("GET", "/", this.GetUser())
}

这样就比较完善了。不过众所周知,gin支持路由分组。如何实现呢?我们继续往下。

路由分组

路由分组只需要在server.go里加一个方法就OK了,代码如下:

1
2
3
4
5
6
7
kotlin复制代码func (this *Server) GroupRouter(group string, controllers ...IController) *Server {
this.g = this.Group(group)
for _, c := range controllers {
c.Router(this)
}
return this
}

使用路由分组时,主函数main.go的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码package main

import (
. "feihu/controller"
"feihu/server"
)

func main () {
server.
Init().
Route(
NewUserController(),
).
// 这里就是路由分组啦
GroupRouter("v1",
NewOrderController(),
).
Listen()
}

好啦,这篇内容就结束了。下面是彩蛋部分,还有激情的小伙伴,鼓励继续学。

彩蛋:Go设计模式之单例模式

今天的内容其实很轻松,加餐部分我们来个Go的设计模式好了。几年前《听飞狐聊JavaScript设计模式》中有讲到单利模式。JS、Java实现单利模式都特别简单,但Go不太一样,我们就拿单利模式来玩玩儿。从最简单的例子开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package main

import "fmt"
// 定义结构
type Singleton struct {
MobileUrl string
}
// 变量
var instance *Singleton
// 这里是单例,返回的是单例结构
func GetSingleton() *Singleton {
// 先判断变量是否存在,如果不存在才创建
if instance == nil {
instance = &Singleton{MobileUrl: "https://www.aizmen.com"}
}
return instance
}

func main () {
x := GetSingleton() // 单独打印x,可以得到:&{https://www.aizmen.com}

x1 := GetSingleton() // 单独打印x1,也得到:&{https://www.aizmen.com}
fmt.Println(x == x1)
}

打印结果为:true,说明是同一块内存。这样就实现了最简单的单利模式了。

sync.Once单例模式

Go其实提供了一个更简洁的sync.Once,实现如下:

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
go复制代码package main

import (
"fmt"
"sync"
)

type Singleton struct {
MobileUrl string
}

var (
once sync.Once
instance *Singleton
)
func GetSingleton() *Singleton {
once.Do(func() {
instance = &Singleton{MobileUrl: "https://www.aizmen.com"}
})
return instance
}

func main () {
x := GetSingleton()
x1 := GetSingleton()
fmt.Println(x == x1)
}

众所周知,Go语言的协程很强大,在使用协程时,可以使用sync.Once来控制。

单例模式之加锁机制

Go还提供了一个基础对象sync.Mutex,用以实现协程之间的同步逻辑,代码实现如下:

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
go复制代码package main
import (
"fmt"
"sync"
)

type Singleton struct {
MobileUrl string
}
var (
once sync.Once
instance *Singleton
mutex sync.Mutex
)
func GetSingleton() *Singleton {
mutex.Lock()
defer mutex.Unlock()
if instance == nil {
instance = &Singleton{MobileUrl: "https://www.aizmen.com"}
}
return instance
}

func main () {
x := GetSingleton()
x1 := GetSingleton()
fmt.Println(x == x1)
}

好啦,这篇的内容就全部结束啦,后续内容会讲中间件、错误处理等等。

🏆 技术专题第二期 | 我与 Go 的那些事……

本文转载自: 掘金

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

花三个小时入门了GO,还写了个爬虫? 🏆 技术专题第二期征

发表于 2020-08-20

纸上得来终觉浅,绝知此事要躬行。

这两天看到了掘金的GO征文活动,看着奖品有小册兑换码,我不禁心动了起来,不过转念一想,自己根本没接触过GO,怎么写得好征文呢?

想到这,我摇了摇头,男人怎么能说不行,既然我从不了解GO,我现学不就行了,把学完的感受与总结写下来,不是更贴合此次征文的主题?

可以参考的技术主题 : golang入门系列

说干就干,我直接打开百度搜索go浏览了第一页之后,找到两个入门级的教程和官网,就开始了我的Golang入门之旅。

  1. 安装

Golang官网小白学习第一步,装环境。

我首先打开官网用我初中的英语水平准确找到了windows环境安装包的下载链接,像现在的所有语言一样,双击 -> 安装 -> Next Step -> Finish一气呵成,安装就像喝水一样简单。

因为有着Java的安装经验,所以我熟练的打开DOS窗口,分别输入:go version 和 go env命令进行测试。

go安装

看到这两个命令都输出了预想中的结果,我欣慰的笑了笑,安装,不过如此。

不过我注意到网上的教程都让我安装之后先设置一个叫GO-PATH的东西,是熟悉的感觉,我仿佛又回到了四年前初学Java的下午,安装完JDK的第一步也是要设置一下JAVA-PATH,现在的JDK已经不需要手动设置了,安装的时候就已经设置好了。

GO语言作为一门新兴语言总不能还有这种历史包袱吧,我怀着悲痛的心情点开了电脑的环境变量:

go环境变量

果然,系统已经帮我设置好了,不用我去手动设置了。

不过坑爹的是这个目录下我原先并没有go文件夹,它居然也没有帮我创建,而且根据GO的规定,你每个项目都要在这里指定一次的环境变量,也就是说如果电脑上有10个项目,你就要在GO-PATH后面写10个项目的文件夹地址,一点也不自由了

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237


好了不说这个,环境变量既然好了,那我们就可以编写我们的Hello-World了,Hello-World入门必敲。


2. Hello-World
--------------


[Golang官网](https://golang.org/)上面有一段代码,教你如何用GO输出Hello-World,我COPY下来就准备贴了。


但是转念一想,根据GO的规定要在go项目的文件夹下必须有`bin`,`pkg`,`src`目录,我直接用默认的go文件夹作为项目的根目录,所以我就在这下面又手动建了这三个文件夹。


建好了之后,我打开万能编辑器-VSCODE,下载一个go语言支持,就开写了。


准备写的时候呢,这个VSCODE又提醒我要安装一堆插件:


![go插件](https://gitee.com/songjianzaina/juejin_p7/raw/master/img/39509ce03cc9c970f278a515bc6ad41899e5244537056623ae729801ec98fc35)


从这堆东西的名字看来,应该是go相关的工具包,不过因为是连GitHub下载我几次下载都失败了,最好在网上找了方法,可以用go-proxy来设置国内代理,不过我设置好了之后依旧是从GitHub下载,最后重启了一下VSCODE才慢吞吞的下载好(依旧是从GITHUB下载)。


这些准备工作做好之后,我在src包下编写了我的第一个GO程序-Hello-World,新建一个以go.go文件,然后把官网的代码copy进去如下:



```
go复制代码package main

import "fmt"

func main() {
fmt.Println("Hello, 世界")
}

```

这样我们的程序就编写好了,紧接着进行手动编译运行,go提供了`go build`和`go run`命令帮助我们编译运行。


![go-hello-world](https://gitee.com/songjianzaina/juejin_p7/raw/master/img/910ee26bcbfdaff2f3bfe7dc529be5907c1177a26c36cf4bbd5df45279da4cf1)


`go build`命令是编译,`go run`是编译后运行,不过他俩还有其他区别,暂时可以只记这么多。




---


我可以再简要说说这段Hello-World代码,`package main`声明这是主函数的入口类,`import "fmt"`则是我们用到了go内置的`fmt`包,最后是定义了一个名为`main`的函数,使用`fmt`包下的`Println`进行字符串打印。


如果你有其他语言的基础,这点东西太小kiss了。


3. 巧遇拦路虎
--------


接下来我准备仗着自己有点Java基础,直接写一个爬虫来练练手,我看大家都是爬豆瓣电影TOP250练手,我就爬[豆瓣读书TOP250](https://book.douban.com/top250)吧。


在这之前,我没有看过任何关于GO的东西,仅仅看了如何定义变量,的确有点反常识,因为Go语言的变量定义是把类型放在后面,我给大家演示一下:



```
go复制代码var str string

```

第一次看到的时候的确一点也不喜欢,因为太有违我的习惯了,不过真正写起来之后发现还真不是这么回事,Go有点像JS,它的类型是可以直接推断的,加之GO定义变量有个简单写法:



```
go复制代码str := "和耳朵真棒!"

```

我们平常可以用这种简单写法来写,这代表变量赋值二合一。


在后面我写爬虫的时候基本都是用这种方式来定义变量的,根本轮不到自己去指定类型。


语法什么的都不是问题,我遇到的第一个问题是导包,因为要写爬虫,我起码要有一个DOM树解析的工具,不然自己正则写起来要累死了。


在最开始的时候我还在嘲笑GO居然非要把项目放在`GOPATH`下面,后来在导包这个问题上面我查了查资料发现自从GO引入了mod模块之后已经可以把项目放在任何一个文件夹了。


mod模块用我的理解就是一个包管理器,把平常项目中需要用到的包都下载起来,然后项目用到的时候直接导,没有的话就去远程下载,类似Java中的Maven和Gradle概念。


既然牵扯到包了,那远程的肯定慢啊,必须要加入国内镜像才能快起来:



```
go复制代码go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

```

加入国内镜像之后,就可以愉快的下载我们所需要的包了:



```
go复制代码go get github.com/PuerkitoBio/goquery

```


> goquery类似jquery,它是jquery的go版本实现,使用它,可以很方便的对HTML进行处理。


说白了就是一个方便操作DOM树的一个工具而已。


下载好依赖之后,我们就可以创建自己的项目了~


随便找一个文件夹,打开dos输入`go mod init xxx`,即可初始化自己的GO项目,这个XX指的是本项目的名字,然后这个文件夹下面就会出现go.mod文件,这个文件记录了项目中引入了那些依赖,慢慢还会出现go.sum文件,都是自动生成,不用理会,专注编码即可。


4. 爬取豆瓣读书TOP250
---------------


我在创建了一个`hello-go`文件夹,初始化之后,建立了一个go.go文件,编写好main函数之后,我们的程序主体就可以开始编写了。



```
go复制代码package main

import (
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"

"github.com/PuerkitoBio/goquery"
)

func main() {

// 创建我们要导出的TXT文件
file, err := os.Create("豆瓣读书TOP250.txt")
if err != nil {
fmt.Println(err)
}
defer file.Close()

// 创建一个客户端对象,用于发送请求
var client = http.Client{}

for i := 0; i < 250; i += 25 {
// 发送一条新请求
req, _ := http.NewRequest("GET", "https://book.douban.com/top250?start="+strconv.Itoa(i), nil)
// 设置User-Agent 必须
req.Header.Set("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)")

// 发送请求
resp, err := client.Do(req)
// 处理异常
if err != nil {
fmt.Println("http get error", err)
return
}
// 关闭流
defer resp.Body.Close()

// 通过goquery将流中的内容构建成一棵DOM树
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Fatal(err)
}

// 拿到所有标记的节点集合
// 并进行each循环,i = 序号,s = 节点本身
doc.Find("div.indent>table>tbody>tr.item").Each(func(i int, s *goquery.Selection) {
// 拿到节点集合中的Items
item := s.Find("td[valign=top]")

// 通过Item节点获取到我们所需要的数据
// 对数据进行去空格 去换行操作
bookName := strings.Replace(strings.Replace(item.Find("div.pl2>a").Text(), "\n", "", -1), " ", "", -1)

author := strings.Split(s.Find("p.pl").Text(), "/")[0]

quote := strings.Replace(strings.Replace(s.Find("p.quote").Text(), "\n", "", -1), " ", "", -1)

// 拿到我们已经处理好的数据之后 接下来就是往TXT里面填充了

//fmt.Print("TOP" + fmt.Sprint(i) + "-" + bookName + "-" + author + "-" + quote)

// 处理字符直接的空格长度,尽力对齐
bookName = bookName + strings.Repeat(" ", (120-len(bookName)))
author = author + strings.Repeat(" ", (50-len(author)))

content := "TOP" + strconv.Itoa(i) + "\t" + bookName + author + quote + "\n"

file.WriteString(content)
})
}

fmt.Print("程序执行完毕,请查看结果。")

}

```

小伙伴们可以直接粘贴到自己编辑器中,包什么的会自动导入的,然后运行此文件即可。


这里面的代码基本都是我到处搜索:


* `go 如何发送请求?`
* `go 如何去除字符串空格?`
* `go 如何拿到字符串长度?`
* `go 如何读写文件?`


我都是遇山开山,遇水搭桥一步步的找齐我自己需要用到的知识点,然后把他们凑到一块

给大家看看效果:

GO爬虫效果展示

导出Excel的话就更完美了,对了,run大家应该会把: go run go.go即可。

好了,今天的征文就到这里,总体看下来GO还是用起来挺方便的,就是时间太短,没有好好学习GO的并发的相关知识~

下期见。

🏆 技术专题第二期 | 我与 Go 的那些事……

本文转载自: 掘金

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

Nginx进阶使用-负载均衡原理及配置实例

发表于 2020-08-20

介绍

跨多个应用程序实例的负载平衡是一种用于优化资源利用率,最大化吞吐量,减少延迟和确保容错配置的常用技术。可以将Nginx用作非常有效的HTTP负载平衡器,以将流量分配到多个应用程序服务器,并使用Nginx改善Web应用程序的性能,可伸缩性和可靠性。

负载均衡

什么是负载均衡,单从字面理解可以解释为N台服务器平均分担负载,不会因为某台服务器负载高宕机而出现某台服务器闲置的情况。那么负载均衡的前提就是要有多台服务器才能实现,目的是达到整个系统的高性能和高可用性。

Nginx负载均衡介绍

严格地说,Nginx仅仅是作为Nginx Proxy反向代理使用的,因为这个反向代理功能表现的效果是负载均衡集群的效果,所以我们称之为Nginx负载均衡。

Nginx负载均衡组件

实现Nginx负载均衡的组件主要有两个:

  • ngx_http_upstream_module

负载均衡模块,可以实现网站的负载均衡功能及节点的健康检查

  • ngx_http_proxy_module

proxy代理模块,用于把请求转发给服务器节点或upstream服务器池

http_upstream模块

upstream模块介绍

upstream模块允许Nginx定义一组或多组节点服务器组,使用时可以通过proxy_pass代理方式把网站的请求发送到事先定义好的对应upstream组的名字上,具体写法为:

1
conf复制代码proxy_pass http://server_pool_name

其中server_pool_name就是一个upstream节点服务器组名字。

upstream典配
1
2
3
4
5
conf复制代码upstream server_pool_name {
server 192.168.0.2; #这行标签和下行是等价的
server 192.168.0.3:80 weight=1 max_fails=1 fail_timeout=10s; #这行标签和上一行是等价的,此行多余的部分就是默认配置,不写也可以。
server 192.168.0.4:80 weight=1 max_fails=2 fail_timeout=20s backup; #server最后面可以加很多参数,具体参数作用看下文的表格
}
upstream模块参数
  • server

负载后面的RS配置,可以是ip或者域名。

  • weight

请求服务器的权重。默认值为1,越大表示接受的请求比例越大。

  • max_fails

nginx 尝试连接后端主机失败的次数。
这个数值需配合proxy_net_upstream,fastcgi_next_upstream和memcached_next_upstream这三个参数来使用的。
当nginx接收后端服务器返回这三个参数定义的状态码时,会将这个请求转发给正常工作的后端服务器,例如404,502,503

  • fail_timeout

在max_fails定义的失败次数后,距离下次检查的时间间隔,默认10s

  • backup

热备配置,标志这台服务器作为备份服务器,若主服务器全部宕机了,就会向它转发请求

  • down

表示这个服务器永不可用,可配合ip_hash使用

  • 举例
1
2
3
4
5
conf复制代码upstream web_pools {
server 192.168.0.3:80 weight=5;
server 192.168.0.4:80 max_fail=5 fail_timeout=10s;// 当5次连续检查失败后,间隔10s后重新检测。
server 192.168.0.5:80 backup; // 指定备份服务器。作用:等上面服务器全部不可访问时就向它转发请求。
}

http_proxy模块

proxy_pass指令介绍

proxy_pass指令属于ngx_http_proxy_module模块,此模块可以将请求转发到另一台服务器。在实际的反向代理工作中,会通过location功能匹配指定的URI,然后把接收到的符合匹配URI的请求通过proxy_pass抛给定义好的upstream节点池,就location指令具体如何配置和使用,会在后续文章中专题介绍,敬请期待。

1
2
3
4
5
6
conf复制代码        location / {
proxy_pass http://upstream_name;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
http proxy模块参数
  • proxy_set_header

设置http请求header项传给后端服务器节点。例如,可以实现让代理后端的服务器节点获取访问客户端用户真实的IP地址

  • client_body_buffer_size

用于指定客户端请求主题缓冲区大小

  • proxy_connect_timeout

表示反向代理与后端节点服务器连接的超时时间,即发起握手等候响应的超时时间

  • proxy_send_timeout

表示代理后端服务器的数据回传时间,即在规定时间之内,后端服务器必须传完所有的数据,否则,nginx将断开这个连接

  • proxy_read_timeout

设置nginx从代理的后端服务器获取信息的时间,表示连接建立成功后,nginx等待后端服务器的响应时间,其实是nginx在后端排队等候处理的时间

  • proxy_buffer_size

设置缓冲区大小,默认该缓冲区大小等于指令proxy_buffers设置的大小

  • proxy_buffers

设置缓冲区的数量和大小,nginx从代理的后端服务器获取的响应信息,会放置在缓冲区

  • proxy_busy_buffers_size

用于设置系统很忙时可以使用的proxy_buffers大小,官方推荐的大小为proxy_bufer*2

  • proxy_temp_file_write_size

指定proxy缓存临时文件的大小

Nginx负载均衡调度算法

Nginx负载均衡是通过upstream模块来实现的,内置实现了三种负载策略,配置还是比较简单的。官网负载均衡配置说明:nginx.org/en/docs/htt…

round-robin(轮询)

默认调度算法,按照客户端请求逐一分配到不同的后端服务器,宕机的服务器会自动从节点服务器池中剔除。

1
2
3
4
5
conf复制代码upstream myapp1 {
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}

注意:对于服务器性能不同的集群,该算法容易引发资源分配不合理等问题。

least-connected(最少连接)

最少连接允许在某些请求需要较长时间才能完成的情况下更公平地控制应用程序实例上的负载。使用最少连接的负载平衡,nginx将尝试不使繁忙的应用程序服务器因过多的请求而过载,而是将新的请求分配给不太繁忙的服务器。可以将nginx用作非常有效的HTTP负载平衡器,以将流量分配到多个应用程序服务器,并使用nginx改善Web应用程序的性能,可伸缩性和可靠性

1
2
3
4
5
6
conf复制代码upstream myapp1 {
least_conn;
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}

ip-hash

每个请求按访问 IP 的hash结果分配,每个访客固定访问一个后端服务器,可解决session不共享的问题。

1
2
3
4
5
6
conf复制代码upstream myapp1 {
ip_hash;
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}

参考

  • nginx.org/en/docs/htt…
  • juejin.cn/post/684490…

结语

欢迎关注微信公众号『码仔zonE』,专注于分享Java、云计算相关内容,包括SpringBoot、SpringCloud、微服务、Docker、Kubernetes、Python等领域相关技术干货,期待与您相遇!

本文转载自: 掘金

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

SpringBoot(四)如何使用 tk-mybatis-g

发表于 2020-08-19

使用 Mybatis 进行开发时,为了减少程序编写时间,提交开发效率,通常都会使用逆向生成工具来生成部分代码,比如Mapper文件、Mapper.xml文件和实体类等。

本文使用的是 tk-mybatis-generator。废话不多说,直接开干!

  1. 创建一张测试表
  2. 新建一个SpringBoot项目
  3. 使用逆向生成工具,生成Mapper接口、Mapper.xml文件、以及实体对象。

项目结构:

1
2
3
4
5
6
7
xml复制代码--src
----main
--------com.common
------------my.mapper MyMApper.interface
------------mybatis.utils GeneratorDisplay.class
----generatorConfig.xml
----pom.xml
  • MyMapper 接口的作用是我们写的写的接口都会继承这个结构,使用Mapper中的方法
  • GeneratorDisplay 是一个读取配置文件(generatorConfig.xml)的工具类
  • generatorConfig.xml 是逆向生成文件配置文件
  • 具体代码见文章末尾链接。

创建表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(11) NULL DEFAULT NULL COMMENT '年龄',
`gender` tinyint(3) NULL DEFAULT NULL COMMENT '性别(0-女 1-男)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

创建完表结构后,只需要按照要求修改 generatorConfig.xml。
点击运行GeneratorDisplay 的main函数,会在配置的文件夹下生成对应的文件。


示例代码-GitHub

示例代码-Gitee

个人博客-ifknow

本文转载自: 掘金

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

1…785786787…956

开发者博客

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