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

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


  • 首页

  • 归档

  • 搜索

【Spring Boot】 Spring Boot jar远

发表于 2021-11-07

一、前期准备

说明:

  1. 相关工具的安装均为 64 位,版本不同可能将导致无法正常启动 IDE 或无法完成某些配置,请务必注意版本信息,具体如下表所示:
工具 版本 备注
Windows/Mac 10/10.13 64位
IntelliJ IDEA ULTIMATE 2017.3 自行破解
Maven 3.3.9
JDK 1.8 64位

二、环境搭建

2.1 添加远程调试项

)​

)​

2.2 配置远程信息

)​

设置 Host 为远程 IP,Port 为远程开放的端口(此处默认使用 5005)选择 Module,此处未指定,请自行选择。

2.3 添加pom.xml远程调试插件

)​

2.4 启动远程Jar包

远程启动 jar 包,命令如下:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar <包名>

e.g.

1
ini复制代码java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar test.jar

2.5 本地启动Debug调试

2.6 远程调试启动成功结果

)​

接下去只需要按正常调试方式调试即可远程调试。

本文转载自: 掘金

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

【Spring Boot】 Spring Boot + re

发表于 2021-11-07

一、准备工作

项 版本 更新或下载URL
Windows 10 64位 略
IntelliJ IDEA 2017.1.5 IntelliJ IDEA: The Capable & Ergonomic Java IDE by JetBrains
JDK8 64位 [Java Downloads
redis 3.2.100 github.com/MSOpenTech/…

Spring-data-redis Jar包

1
2
3
4
5
xml复制代码<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.0.1.RELEASE</version>
</dependency>

jedis jar包

1
2
3
4
5
xml复制代码<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

二、搭建步骤

  1. 安装redis

1.1 下载Windows redis

下载地址见准备工作步骤。

1.2 解压redis至自定义目录

)​

1.3 设置redis需要访问密码

1) 找到redis所在目录的redis.windows.conf文件的requirepass字段,去掉注释,修改你需要的密码

2) 设置永久登录需要密码连接
)​

1.4 启动redis

)​

  1. 建立Spring boot项目

2.1 新建Spring boot项目

)​

2.2 Maven依赖、JDK、Package设置

)​

2.3 项目依赖项选择(pom.xml文件内均可查看)

)​

2.4 新建项目成功,依据pom.xml文件自动更新jar

本地仓库默认路径:${user.home}/.m2/repository,也可以自行设定

)​

2.5 运行Spring boot 应用是否能启动

)​
未添加相关的redis支持项时无法正常启动。

添加相关配置,步骤如下:

  1. application.properties
    )​
  2. pom.xml
    )​
  3. CodeCaptionApplication.java
    )​

2.6 重新启动Spring boot应用,并访问session测试路径

访问路径:

http://localhost:8080/sessionRedis/test

访问结果:

)​

)​

此步骤需要添加的controller源码见附录。

至此已能正常访问集成session+redis的Spring boot应用。源码下载地址见准备工作。

三、 FAQ

  1. Spring-boot-redis与jedis版本不一致

  1. 应用无法正常启动;
  2. Jedis对应jar包版本大于2.4.2,详细情况请在Maven官网进行查询。
  1. Redis服务端设置临时密码方式

  1. 打开cmd命令行
  2. 进入redis所在目录
  3. 使用startup.bat脚本启动redis服务
  4. 输入命令redis-cli –h 127.0.0.1 –p 6379 登录至客服端
  5. 设置临时密码:config set requirepass <你的密码>
  6. 查看密码设置情况:config get requirepass
  7. 结果如下:(密码设置后需要重新登录)
    )​

四、 附录

  1. Maven本地仓库设置文件settings.xml下载路径

已添加阿里云仓库更新地址(位于测试工程源码下载地址的xml文件夹下)。

  1. Maven仓库

查询地址:
mvnrepository.com/

  1. Redis startup.bat脚本

内容

1
vbscript复制代码redis-server.exe redis.windows.conf
  1. com.example.controller.TestController.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
27
28
29
30
31
32
33
34
35
kotlin复制代码package com.example.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@SuppressWarnings("unchecked")
@RestController
public class TestController
{
private Logger logger = LoggerFactory.getLogger(this.getClass());

@RequestMapping("/sessionRedis/test")
public ResponseEntity<?> httpSessionRedisTest(HttpSession httpSession)
{
if (httpSession.isNew()) {
logger.info("Successfully creates a session ,the id of session :" + httpSession.getId());
httpSession.setAttribute("key", "hello");
} else {
logger.info("session already exists in the server, the id of session :" + httpSession.getId());
logger.info(httpSession.getAttribute("key").toString());
}


ResponseEntity<?> entity = new ResponseEntity<Object>("Hello world, session id:" + httpSession.getId(), HttpStatus.OK);


return entity;
}
}

效果图:

)​

五、示例代码

Spring boot session + redis

GitHub - Cavan2477/SpringBoot-Session-Redis: Spring boot + redis 实现session 共享管理

本文转载自: 掘金

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

Java最常见基础知识总结 一.基本数据类型 二.数组: 三

发表于 2021-11-07

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

一.基本数据类型

整数型 byte 1个字节 short 2个字节 int 4个字节(默认的) long 8个字节

文本型 char 2个字节 (声明变量时要加上单引号)

布尔型 Boolean 其值只有2个,true fase 默认初始值是fase

浮点数类型 float double float声明时 后面要加l 默认是double float占4个字符 double占8个字符。

二.数组:

概念:数组就是一组有顺序,具有相同数据类型的数据集合,也可以说是,用来存储一组相同数据类型的数据结构。(引用类型)

就是一个小整体,在整个程序中被看作是一个对象,而数组中每一个元素都可以看作是数组的成员变量。(自己理解的)

1.声明数组

  1. 数组元素名 数组名 [];2.数组元素名 []数组名;

创建数组:

  1. 利用 new关键字创建。格式:数组名=new 数组元素的数据类型[整数元素个数]
  2. 声明数组和创建数组同时完成。格式:int [] x = new int [100].

2.数组的初始化

  1. 先定义在初始化(动态初始化):int [] ia = new int[4]; ia[0]=1; ia[1]=2;ia[a]=3
  2. 定义数组同时为数组元素分配空间并赋值(静态初始化):int [] ia = {1,2,3};

或int [] ia = new int[]{1,2,3};

3.数组的引用

引用方式:arrayName[index]。Index是数组的下标,描述了元素从数组中相对位置,(元素下标从0开始)

4.一维数组

选择排序:获取当前没有排好序中的最大元素和数组最右端的元素交换,循环这个过程即可实现对整个数组排序。

冒泡排序:依次两两比较排序元素,将带排序元素从左至右比较一遍称为一趟“冒泡”。

每趟冒泡都将带排序列中的最大关键字交换到最后位置。

5.栈内存与堆内存的关系

数组的引用保存在占内存中,在堆内存中开辟一片空间(连续的),然后栈内存上的引用指向这片空间的首地址。

三.基本语法

标识符:可以自己起名字的都叫标识符,都遵循标识符的命名规则。

标识符命名规则:

标识符由字母、数字、下划线“_”和美元符“$”开头。

标识符对大小写敏感,长度无限制。

取名应见名知意,且不能与关键字重名。

Java约定:

类名的首字母要大写。

变量名和方法名的首字母要小写,关键字要小写。

常量名全部字母大写。

运用驼峰标志。

字符变量:

‘\n‘是换行符;

‘\t‘是制表符,相当于table键;

‘\b‘是退格键,相当于Back Space;

‘\‘‘是单引号,而‘\“‘是双引号;

‘\‘是一个斜杠“\”。

四.控制结构****

If:if··else 只有一句需要执行的语句时可以省略{} 等号两边不用空格

For:形式:for(表达式1;表达式2;表达式3;){语句;}执行时判断是否结束的是表达式2,如果表达式2是false输出结果。

While/do··while:while:先判断在执行,do···while:先执行在判断。(无论是否满足表达式,都先执行一遍)

Switch:条件补充语句:格式case xx; case xx; default ;在需要输出case值时用break语句,多个case可以合并到一起。

Break/continue:break语句终止某个语句块的执行,在循环语句中可以强行退出循环 continue 终止某一次循环,跳过这次循环,开始下一次循环过程。

If 语句:

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
csharp复制代码public class test {

public static void main (String[] args) {

int i = 50;

if ( i < 20 ) {

System.out.print("i < 20" );

}

else if ( i < 40 ){

System.out.print("i < 40 ");

}

else if ( i < 60 ){

System.out.print("i < 60 ");



}

else if ( i >60 ){

System.out.print("i >=60 ");

}

}

}//最终输出i<60

For语句

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
ini复制代码public class adf {

public sta ic void main(String[] args) {

long result = 0;

long f = 1 ;



for (int i = 1; i <= 10; i++)  {

f = f * i;

result +=f;

}

System.out.print("result =" +result );

}



}//最终输出result= 4037913

Switch和break

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
csharp复制代码public class testswitch {

public static void main (String[] args) {

int i = 5;

switch (i){

case  5 :

System.out.print("e");

 break;

case 1 :

case 4 :

System.out.print(14);

default :

System.out.print("wang");

}

}

}//最终输出e

五.Java虚拟机的概念,JDK的概念

1. J ava虚拟机

Java虚拟机是抽象的计算机,是java语言的运行环境。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域、还有相应的指令系统。

特点:

Java虚拟机是实现java跨平台的关键。

JDK概念:java开发工具。

JDK包含的基本组件:

javac – 编译器,将源程序转成字节码

jar – 打包工具,将相关的类文件打包成一个文件

javadoc – 文档生成器,从源码注释中提取文档

jdb – debugger,查错工具

java – 运行编译后的java程序(.class后缀的)

appletviewer:小程序浏览器,一种执行HTML文件上的Java小程序的Java浏览器。

Javah:产生可以调用Java过程的C过程,或建立能被Java程序调用的C过程的头文件。

Javap:Java反汇编器,显示编译类文件中的可访问功能和数据,同时显示字节代码含义。

Jconsole: Java进行系统调试和监控的工具

Java程序的命令行编译、运行

六.类的封装性

通常将类声明为private(私有类)然后在用public(公共方法) 对这个变量进行访问。

对一个变量的操作通常是赋值(setXX)和读取(getXXX)

类的封装有2层意思:

将数据和对数据的操作组合起来构成类,类是一个不可分割的独立单位。

类中既要有与外部联系的方法,同时也要尽可能的隐藏类的实现细节。

七.方法的调用,静态方法和实例方法

1.方法的调用

方法与方法重载

形式参数:在方法被调用的时候用于接受外部传入的数据的变量。

实际参数:在方法被调用的时候实际传给XX的值

参数类型:形式参数的数据类型。

返回值:方法在执行完毕之后还调用它的程序的数据,如没有必须给出返回值类型void。

返回值类型:函数要返回的结果的数据类型。

Java语言中使用下属形式调用调用方法:对象名,方法名,(实参列表)

实参的数目、数据类型和次序必须和所调用方法声明的形参列表匹配

Return语句终止方法的运行并指出要返回的数据。

只有涉及到方法的时候才用到形参和实参,

调用方式:

1.如果方法没有返回值或调用方法不关心方法的返回值,可以下面的格式调用定义的函数;

方法名(实参1,实参2,·····)

2.如果调用程序需要返回结果,可以下面的格式调用定义的函数;

变量=方法名(实参1,实参2,·····)

3.对于有返回值的方法调用,也可以在程序中直接使用返回的结果。

4.实参的数目、数据类型和次序必须与所调用方法声明的形参列表匹配。

方法的重载:

Return语句:终止方法的运行并指定要返回的值,然后把控制权交给调用它的语句。

格式:return【返回值】; retrun;

一个类中可以同时存在名字相同,参数不同(个数或类型)不同的多个方法。

调用时,编译器会根据不同的参数列表选择对应的方法。

为了程序的可读性,最好重载相同含义的方法、

不能以不同的返回值来区别重载的方法。

2.静态方法和实例方法

静态方法是使用公共内存空间的,所有对象都可以直接引用,不需要创建对在使用JAVA中使用静态方法

编程时我们心里一定要清楚静态方法和类的非静态方法方法的区别:

最根本区别从编译角度来说吧:

  1. 静态(static)方法是编译时直接加载加载到内存中(离cpu最近的一块内存区域也称为堆栈),比如程序的public static main(args []){}方法。静态方法不能被实例化,也不允许被实例化!

因此你可以通过“类名”+“.”+“静态方法的名()”来调用

2)非静态方法(类的非静态方法)通过关键字 “new” 字来实例化一个对象(object),这个对象放在内存的另一块区域堆(heap)中。

也就是说编译时,非静态方法必须先实例化类的一个对象,通过“对象名”+“非静态方法名()”来调用,

或者是“对象引用(句柄)”+“.”+“静态方法的名()”;

静态变量:

1.类的静态变量可以

直接引用,类的静态变量相当于某些程序语言的全局变量。

2.静态方法只能使用静态变量,不能使用实例变量。

3.类的静态变量只有一个版本,所有实例对象引用的都是同一个版本。

3、实例方法

实例方法也叫做对象方法。(非静态方法)

类方法是属于整个类的,而实例方法是属于类的某个对象的。

由于类方法是属于整个类的,并不属于类的哪个对象,所以类方法的方法体中不能有与类的对象有关的内容。即类方法体有如下限制:

(1) 类方法中不能引用对象变量;

(2) 类方法中不能调用类的对象方法;

(3) 在类方法中不能使用super、this关键字。

(4)类方法不能被覆盖。

如果违反这些限制,就会导致程序编译错误。

与类方法相比,对象方法几乎没有什么限制:

(1) 对象方法中可以引用对象变量,也可以引用类变量;

(2) 对象方法中可以调用类方法;

(3) 对象方法中可以使用super、this关键字。

在程序设计中,一些通用的、公用型的方法与类的对象无关,因此常常被作为类方法实现。如Java类库中Math类,其中多数的数学运算操作都被定义成静态方法。因此,可以使用类方法把一些通用的、公用型的方法放在合适的类中,从而很好地将它们组织起来。应用程序中的主方法main就是类方法。

八.面对对象编程的三个表现

1.子类继承父类

Java继承特点:只能单继承不支持多继承,具有层次结构,子类继承了父类的方法和属性。

构造方法:

子类继承父类所有的成员方法,但不继承父类的构造方法。子类可在自己的构造方法中调用父类的的构造方法。 Super()或 super(参数列表)

2.方法的重写:

在子类中可以根据需要对从基类继承来的方法进行重写。

重写方法必须和被重写方法具有相同方法名称、参数列表和返回类型。

重写方法不能使用比被重写方法更严格的访问权限。

如:父类中的方法是public的,那么子类中的方法就不能是private的。

子类和父类的方法具有相同的名称和类型,用子类调用子类中的重写方法,super.成员用于调用父类中的成员。

Final 声明的方法,属性,类不能被子类引用————最终方法,属性,类。

标记的常量只能赋一次值,不能被改变。

用public static final 共同标记常量时,这个常量就成了全局常量,只能在定义时赋值。

Public static final pe = 3.1415;

Object类:类中的最高层,是所有类的父类。

2.1Object 中的toString方法

定义形式:public String toString()

在进行String与其它类型数据的连接操作时(如:System.out.println(“info ” +obj)),将自动调用该对象类的toString()方法。

通常情况下,重写toString()方法,返回对象的有用信息。

2.2 Object 的 equals 方法

定义形式:public boolean equals()

此方法提供了比较对象是否相等的逻辑

Object的equals方法定义为:x.equals(y)当x和y是同一个对象的引用时返回true,否则返回false。

通常情况下,想要比较对象内容是否相等,需要重写equals方法。

对象的类型转换: 一个基类的引用类型变量可以“指向”其子类的对象。

一个基类的引用不可以访问其子类对象新增加的成员(属性和方法)。

可以使用引用变量 instanceof 类名 来判断该引用类型变量所“指向”的对象是否属于该类或该类的子类。

子类的对象可以当作基类的对象来使用称作向上转型(自动转换),反之称为向下转型(强制转换)。

3.多态****

多态性:一种方法,可以多种实现版本,即一种定义,多种实现。类的多态性提供方法设计的灵活性和执行的多样性。

存在的三个条件:有继承,有方法重写,父类引用指向子类对象

​

本文转载自: 掘金

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

一起用代码吸猫吧-Python爬取猫咪图片 一、网站分析 二

发表于 2021-11-07

一起用代码吸猫!本文正在参与【喵星人征文活动】。

一、网站分析

爬取图片的网站地址:www.ivsky.com/

1、首先,打开网站,在搜索框输入关键词,我们可以发现,图片数据是分页的,我们可以动态爬取多页数据。

image.png

2、查看浏览器搜索框网址,我们可以发现我们将网址中的“猫”替换成其他任意,就可以获取我们想要的任意主题的图片,当然前提是该网址的服务器存在这些主题的图片。

image.png

3、之后单击快捷键Ctrl+Shift+I,打开开发者工具。

4、然后鼠标右键单击任意一张图片,点击检查,我们可以发现下图中的img标签中的href属性包含每一张图片真正的下载地址,这就是我们最终要匹配的内容。

image.png

二、代码实践

1、根据搜索框主题“猫”,获取该主题相关的网页。

(1)设置请求头

1
2
3
python复制代码headers = { 
"Connection": "close", 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36'
}

(2)设置关键词

1
2
3
4
python复制代码kw = {
"q": picDes,
"page": 1
}

(3)发送请求

1
2
python复制代码url = "https://www.ivsky.com/search.php" 
response = requests.get(url, headers=headers, params=kw)

2、通过上一步获取的网页内容,结合正则表达式,匹配每一页的每张图片的下载地址。

(1)匹配每张图片的下载地址的正则表达式

1
python复制代码picUrls = re.findall('<div class="il_img pic_box"><a href=.*?><img src="(.*?)" alt=.*?></a></div>', html)

(2)下一页索引的正则表达式

1
python复制代码nextUrl = re.findall('<a class="page-next" href="(.*?)">下一页</a>', html) nextPage = nextUrl[0].split("=")[-1]

3、通过上一步获取的图片的下载地址的列表,下载图片到本地。

1
2
3
python复制代码with open(filePath + "/" + str(i + 1) + ".jpg", "wb") as f: 
print(filePath + "/" + str(i + 1) + ".jpg")
f.write(res.content)

三、数据爬取结果

结果如下图:

image.png

本文转载自: 掘金

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

搞透Kafka的存储架构,看这篇就够了 一、kafka 存储

发表于 2021-11-06

阅读本文大约需要30分钟。这篇文章干货很多,希望你可以耐心读完。

你好, 我是华仔,在这个 1024 程序员特殊的节日里,又和大家见面了。

从这篇文章开始,我将对 Kafka 专项知识进行深度剖析, 今天我就来聊聊 kafka 的存储系统架构设计, 说到存储系统,大家可能对 MySQL 比较熟悉,也知道 MySQL 是基于 B+ tree 来作为它的索引数据结构。

Kafka 又是基于什么机制来存储?为什么要设计成这样?它解决了什么问题?又是如何解决的?里面又用到了哪些高大上的技术?

带着这些疑问,我们就来和你聊一聊 Kafka 存储架构设计背后的深度思考和实现原理。

认真读完这篇文章,我相信你会对 Kafka 存储架构,有更加深刻的理解。也能有思路来触类旁通其他存储系统的架构。

image.png

一、kafka 存储场景剖析

在讲解 Kafka 的存储方案之前,我们先来看看 Kafka 官网给的定义:

Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.

翻译成中文如下:

Apache kafka 是一个开源的分布式事件流处理平台,由成千上万的公司用于高性能的数据管道流分析、数据集成和关键任务的应用程序。

了解 Kafka 的老司机都知道它是从 Linkedin 内部孵化的项目,从一开始,Kafka 就是为了解决大数据的实时日志流而生的, 每天要处理的日志量级在千亿规模。对于日志流的特点主要包括 1)、数据实时产生 2)、海量数据存储与处理, 所以它必然要面临分布式系统遇到的高并发、高可用、高性能等三高挑战。

通过上面的背景可以得出:一切脱离业务场景谈架构设计都是耍流氓

综上我们看对于 Kafka 的存储需求来说,要保证以下几点:

  1. 存储的主要是消息流(可以是简单的文本格式也可以是其他格式,对于 Broker 存储来说,它并不关心数据本身)
  2. 要支持海量数据的高效存储、高持久化(保证重启后数据不丢失)
  3. 要支持海量数据的高效检索(消费的时候可以通过offset或者时间戳高效查询并处理)
  4. 要保证数据的安全性和稳定性、故障转移容错性

二、kafka 存储选型

有了上面的场景需求分析后, 我们接下来分析看看 Kafka 到底基于什么机制来存储的,能否直接用现有我们了解到的关系型数据库来实现呢?我们接着继续深度分析。

2.1、存储基本知识

我们先来了解下存储的基本知识或者常识, 在我们的认知中,对于各个存储介质的速度大体同下图所示的,层级越高代表速度越快。很显然,磁盘处于一个比较尴尬的位置,然而,事实上磁盘可以比我们预想的要快,也可能比我们预想的要慢,这完全取决于我们如何使用它。

image.png

关于磁盘和内存的 IO 速度,我们可以从下图性能测试的结果看出普通机械磁盘的顺序I/O性能指标是53.2M values/s,而内存的随机I/O性能指标是36.7M values/s。由此似乎可以得出结论:磁盘的顺序I/O性能要强于内存的随机I/O性能。

image.png

另外从整个数据读写性能方面,有不同的实现方式,要么提高读速度,要么提高写速度。

  1. 提高读速度:利用索引,来提高查询速度,但是有了索引,大量写操作都会维护索引,那么会降低写入效率。常见的如关系型数据库:mysql等
  2. 提高写速度:这种一般是采用日志存储, 通过顺序追加写的方式来提高写入速度,因为没有索引,无法快速查询,最严重的只能一行行遍历读取。常见的如大数据相关领域的基本都基于此方式来实现。

2.2、Kafka 存储方案剖析

上面从存储基础知识,以及存储介质 IO 速度、读写性能方面剖析了存储类系统的实现方式,那么我们来看看 Kafka 的存储到底该采用哪种方式来实现呢?

对于 Kafka 来说, 它主要用来处理海量数据流,这个场景的特点主要包括:

  1. 写操作:写并发要求非常高,基本得达到百万级 TPS,顺序追加写日志即可,无需考虑更新操作
  2. 读操作:相对写操作来说,比较简单,只要能按照一定规则高效查询即可(offset或者时间戳)

根据上面两点分析,对于写操作来说,直接采用顺序追加写日志的方式就可以满足 Kafka 对于百万TPS写入效率要求。但是如何解决高效查询这些日志呢? 直接采用 MySQL 的 B+ tree 数据结构存储是否可以?我们来逐一分析下:

如果采用 B+ tree 索引结构来进行存储,那么每次写都要维护索引,还需要有额外空间来存储索引、更会出现关系型数据库中经常出现的“数据页分裂”等操作, 对于 Kafka 这种高并发的系统来说,这些设计都太重了,所以并不适合用。

但是在数据库索引中,似乎有一种索引看起来非常适合此场景,即:哈希索引【底层基于Hash Table 实现】 ,为了提高读速度, 我们只需要在内存中维护一个映射关系即可,每次根据 Offset 查询消息的时候,从哈希表中得到偏移量,再去读文件就可以快速定位到要读的数据位置。但是哈希索引通常是需要常驻内存的,对于Kafka 每秒写入几百万消息数据来说,是非常不现实的,很容易将内存撑爆, 造成 oom。

这时候我们可以设想把消息的 Offset 设计成一个有序的字段,这样消息在日志文件中也就有序存放了,也不需要额外引入哈希表结构, 可以直接将消息划分成若干个块,对于每个块,我们只需要索引当前块的第一条消息的 Offset ,这个是不是有点二分查找算法的意思。 即先根据 Offset 大小找到对应的块, 然后再从块中顺序查找。如下图所示:

存储2.png

这样就可以快速定位到要查找的消息的位置了,在 Kafka 中,我们将这种索引结构叫做 “稀疏索引”。

三、kafka 存储架构设计

上面从 Kafka 诞生背景、 存储场景分析、存储介质 IO 对比、以及 Kafka 存储方案选型等几个方面进行深度剖析, 得出了 Kafka 最终的存储实现方案, 即**基于顺序追加写日志 + 稀疏哈希索引。

接下来我们来看看 Kafka 日志存储结构:

图5:kafka日志存储结构 .jpg

从上图可以看出来,Kafka 是基于「主题 + 分区 + 副本 + 分段 + 索引」的结构:

  1. kafka 中消息是以主题 Topic 为基本单位进行归类的,这里的 Topic 是逻辑上的概念,实际上在磁盘存储是根据分区 Partition 存储的, 即每个 Topic 被分成多个 Partition,分区 Partition 的数量可以在主题 Topic 创建的时候进行指定。

2.Partition 分区主要是为了解决 Kafka 存储的水平扩展问题而设计的, 如果一个 Topic 的所有消息都只存储到一个 Kafka Broker上的话, 对于 Kafka 每秒写入几百万消息的高并发系统来说,这个 Broker 肯定会出现瓶颈, 故障时候不好进行恢复,所以 Kafka 将 Topic 的消息划分成多个 Partition, 然后均衡的分布到整个 Kafka Broker 集群中。

  1. Partition 分区内每条消息都会被分配一个唯一的消息 id,即我们通常所说的 偏移量 Offset, 因此 kafka 只能保证每个分区内部有序性,并不能保证全局有序性。
  1. 然后每个 Partition 分区又被划分成了多个 LogSegment,这是为了防止 Log 日志过大,Kafka 又引入了日志分段(LogSegment)的概念,将 Log 切分为多个 LogSegement,相当于一个巨型文件被平均分割为一些相对较小的文件,这样也便于消息的查找、维护和清理。这样在做历史数据清理的时候,直接删除旧的 LogSegement 文件就可以了。
  1. Log 日志在物理上只是以文件夹的形式存储,而每个 LogSegement 对应磁盘上的一个日志文件和两个索引文件,以及可能的其他文件(比如以”.snapshot”为后缀的快照索引文件等)

也可以直接看之前写的 Kafka 基础入门篇 中的存储机制部分,也有详细的说明。

四、kafka 日志系统架构设计

了解了 Kafka 存储选型和存储架构设计后, 我们接下来再深度剖析下 Kafka 日志系统的架构设计。

根据上面的存储架构剖析,我们知道 kafka 消息是按主题 Topic 为基础单位归类的,各个 Topic 在逻辑上是独立的,每个 Topic 又可以分为一个或者多个 Partition,每条消息在发送的时候会根据分区规则被追加到指定的分区中,如下图所示:

图6:4个分区的主题逻辑结构图.jpg

4.1、日志目录布局

那么 Kafka 消息写入到磁盘的日志目录布局是怎样的?接触过 Kafka 的老司机一般都知道 Log 对应了一个命名为 topic-partition 的文件夹。举个例子,假设现在有一个名为“topic-order”的 Topic,该 Topic 中有4个 Partition,那么在实际物理存储上表现为“topic-order-0”、“topic-order-1”、“topic-order-2”、“topic-order-3” 这4个文件夹。

看上图我们知道首先向 Log 中写入消息是顺序写入的。但是只有最后一个 LogSegement 才能执行写入操作,之前的所有 LogSegement 都不能执行写入操作。为了更好理解这个概念,我们将最后一个 LogSegement 称为 “activeSegement”,即表示当前活跃的日志分段。随着消息的不断写入,当 activeSegement 满足一定的条件时,就需要创建新的 activeSegement,之后再追加的消息会写入新的 activeSegement。

图7:activeSegment示意图.jpg

为了更高效的进行消息检索,每个 LogSegment 中的日志文件(以“.log”为文件后缀)都有对应的几个索引文件:偏移量索引文件(以“.index”为文件后缀)、时间戳索引文件(以“.timeindex”为文件后缀)、快照索引文件 (以“.snapshot”为文件后缀) 。其中每个 LogSegment 都有一个 Offset 来作为基准偏移量(baseOffset),用来表示当前 LogSegment 中第一条消息的 Offset。偏移量是一个64位的 Long 长整型数,日志文件和这几个索引文件都是根据基准偏移量(baseOffset)命名的,名称固定为20位数字,没有达到的位数前面用0填充。比如第一个 LogSegment 的基准偏移量为0,对应的日志文件为00000000000000000000.log。

我们来举例说明,向主题topic-order中写入一定量的消息,某一时刻topic-order-0目录中的布局如下所示:

存储文件.png

上面例子中 LogSegment 对应的基准位移是12768089,也说明了当前 LogSegment 中的第一条消息的偏移量为12768089,同时可以说明当前 LogSegment 中共有12768089条消息(偏移量从0至12768089的消息)。

注意每个 LogSegment 中不只包含“.log”、“.index”、“.timeindex”这几种文件,还可能包含“.snapshot”、“.txnindex”、“leader-epoch-checkpoint”等文件, 以及 “.deleted”、“.cleaned”、“.swap”等临时文件。

另外 消费者消费的时候,会将提交的位移保存在 Kafka 内部的主题__consumer_offsets中,对它不了解的可以直接查看之前写的 聊聊 Kafka Consumer 那点事 中的位移提交部分,下面我们来看一个整体的日志目录结构图:

图9:log 整体目录布局示意图.jpg

4.2、日志格式演变

对于一个成熟的消息中间件来说,日志格式不仅影响功能的扩展,还关乎性能维度的优化。所以随着 Kafka 的迅猛发展,其日志格式也在不断升级改进中,Kafka 的日志格式总共经历了3个大版本:V0,V1和V2版本。

我们知道在 Kafka Partition 分区内部都是由每一条消息进行组成,如果日志格式设计得不够精巧,那么其功能和性能都会大打折扣。

4.2.1、V0 版本

在 Kafka 0.10.0 之前的版本都是采用这个版本的日志格式的。在这个版本中,每条消息对应一个 Offset 和 message size。Offset 用来表示它在 Partition分区中的偏移量。message size 表示消息的大小。两者合起来总共12B,被称为日志头部。日志头部跟 Record 整体被看作为一条消息。如下图所示:

图10:V0 版本日志格式示意图.jpg

  1. crc32(4B):crc32校验值。校验范围为magic至value之间。
  1. magic(1B):日志格式版本号,此版本的magic值为0。
  1. attributes(1B):消息的属性。总共占1个字节,低3位表示压缩类型:0 表示NONE、1表示GZIP、2表示SNAPPY、3表示LZ4(LZ4自Kafka 0.9.x 版本引入),其余位保留。
  1. key length(4B):表示消息的key的长度。如果为-1,则没有设置key。
  1. key:可选,如果没有key则无此字段。
  1. value length(4B):实际消息体的长度。如果为-1,则消息为空。
  1. value:消息体。

从上图可以看出,V0 版本的消息最小为 14 字节,小于 14 字节的消息会被 Kafka 认为是非法消息。

下面我来举个例子来计算一条消息的具体大小,消息的各个字段值依次如下:

  • CRC:对消息进行 CRC 计算后的值;
  • magic:0;
  • attribute:0x00(未使用压缩);
  • key 长度:5;
  • key:hello;
  • value 长度:5;
  • value:world。

那么该条消息长度为:4 + 1 + 1 + 4 + 5 + 4 + 5 = 24 字节。

4.2.2、V1 版本

随着 Kafka 版本的不断迭代发展, 用户发现 V0 版本的日志格式由于没有保存时间信息导致 Kafka 无法根据消息的具体时间进行判断,在进行清理日志的时候只能使用日志文件的修改时间导致可能会被误删。

从 V0.10.0 开始到 V0.11.0 版本之间所使用的日志格式版本为 V1,比 V0 版本多了一个 timestamp 字段,表示消息的时间戳。如下图所示:

图11:V1 版本日志格式示意图.jpg

V1 版本比 V0 版本多一个 8B 的 timestamp 字段;
那么 timestamp 字段作用:
对内:会影响日志保存、切分策略;
对外:影响消息审计、端到端延迟等功能扩展

从上图可以看出,V1 版本的消息最小为 22 字节,小于 22 字节的消息会被 Kafka 认为是非法消息。

总的来说比 V0 版本的消息大了 8 字节,如果还是按照 V0 版本示例那条消息计算,则在 V1 版本中它的总字节数为:24 + 8 = 32 字节。

4.2.3、V0、V1 版本的设计缺陷

通过上面我们分析画出的 V0、V1 版本日志格式,我们会发现它们在设计上的一定的缺陷,比如:

  1. 空间使用率低:无论 key 或 value 是否存在,都需要一个固定大小 4 字节去保存它们的长度信息,当消息足够多时,会浪费非常多的存储空间。
  2. 消息长度没有保存:需要实时计算得出每条消息的总大小,效率低下。
  3. 只保存最新消息位移。
  4. 冗余的 CRC 校验:即使是批次发送消息,每条消息也需要单独保存 CRC。

4.2.4、V2 版本

针对 上面我们分析的 关于 V0、V1 版本日志格式的缺陷,Kafka 在 0.11.0.0 版本对日志格式进行了大幅度重构,使用可变长度类型解决了空间使用率低的问题,增加了消息总长度字段,使用增量的形式保存时间戳和位移,并且把一些字段统一抽取到 RecordBatch 中。

图12:V2 版本日志格式示意图.jpg

从以上图可以看出,V2 版本的消息批次(RecordBatch),相比 V0、V1 版本主要有以下变动:

  1. 将 CRC 值从消息中移除,被抽取到消息批次中。
  1. 增加了 procuder id、producer epoch、序列号等信息主要是为了支持幂等性以及事务消息的。

3.使用增量形式来保存时间戳和位移。

  1. 消息批次最小为 61 字节,比 V0、V1 版本要大很多,但是在批量消息发送场景下,会提供发送效率,降低使用空间。

综上可以看出 V2 版本日志格式主要是通过可变长度提高了消息格式的空间使用率,并将某些字段抽取到消息批次(RecordBatch)中,同时消息批次可以存放多条消息,从而在批量发送消息时,可以大幅度地节省了磁盘空间。

4.2.5、日志清理机制

Kafka 将消息存储到磁盘中,随着写入数据不断增加,磁盘占用空间越来越大,为了控制占用空间就需要对消息做一定的清理操作。从上面 Kafka 存储日志结构分析中每一个分区副本(Replica)都对应一个 Log,而 Log 又可以分为多个日志分段(LogSegment),这样就便于 Kafka 对日志的清理操作。

Kafka提供了两种日志清理策略:

  1. 日志删除(Log Retention):按照一定的保留策略直接删除不符合条件的日志分段(LogSegment)。

2.日志压缩(Log Compaction):针对每个消息的key进行整合,对于有相同key的不同value值,只保留最后一个版本。

这里我们可以通过 Kafka Broker 端参数 log.cleanup.policy 来设置日志清理策略,默认值为 “delete”,即采用日志删除的清理策略。如果要采用日志压缩的清理策略,就需要将 log.cleanup.policy 设置为 “compact” ,这样还不够,必须还要将log.cleaner. enable(默认值为 true)设为 true。

如果想要同时支持两种清理策略, 可以直接将 log.cleanup.policy 参数设置为“delete,compact”。

4.3.1 日志删除

基于时间策略

Kafka 的日志管理器(LogManager)中有一个专门的日志清理任务通过周期性检测和删除不符合条件的日志分段文件(LogSegment),这里我们可以通过 Kafka Broker 端的参数 log.retention.check.interval.ms 来配置,默认值为300000,即5分钟。

在 Kafka 中一共有3种保留策略:

日志删除任务会周期检查当前日志文件中是否有保留时间超过设定的阈值 (retentionMs) 来寻找可删除的日志段文件集合 ( deletableSegments) 。

其中retentionMs可以通过 Kafka Broker 端的这几个参数的大小判断的

log.retention.ms > log.retention.minutes > log.retention.hours优先级来设置,默认情况只会配置 log.retention.hours 参数,值为168即为7天。

这里需要注意:删除过期的日志段文件,并不是简单的根据该日志段文件的修改时间计算的,而是要根据该日志段中最大的时间戳 largestTimeStamp 来计算的,首先要查询该日志分段所对应的时间戳索引文件,查找该时间戳索引文件的最后一条索引数据,如果时间戳值大于0,则取值,否则才会使用最近修改时间(lastModifiedTime)。

【删除步骤】:

  1. 首先从 Log 对象所维护的日志段的跳跃表中移除要删除的日志段,用来确保已经没有线程来读取这些日志段。
  2. 将日志段所对应的所有文件,包括索引文件都添加上“.deleted”的后缀。
  3. 最后交给一个以“delete-file”命名的延迟任务来删除这些以“ .deleted ”为后缀的文件。默认1分钟执行一次, 可以通过 file.delete.delay.ms 来配置。

图13:基于时间保留策略示意图.jpg

基于日志大小策略

日志删除任务会周期检查当前日志大小是否超过设定的阈值 (retentionSize) 来寻找可删除的日志段文件集合 (deletableSegments) 。

其中 retentionSize 这里我们可以通过 Kafka Broker 端的参数log.retention.bytes来设置, 默认值为-1,即无穷大。

这里需要注意的是 log.retention.bytes 设置的是Log中所有日志文件的大小,而不是单个日志段的大小。单个日志段可以通过参数 log.segment.bytes 来设置,默认大小为1G。

【删除步骤】:

  1. 首先计算日志文件的总大小Size和retentionSize的差值,即需要删除的日志总大小。
  2. 然后从日志文件中的第一个日志段开始进行查找可删除的日志段的文件集合 (deletableSegments)
  3. 找到后就可以进行删除操作了。

图14:基于日志大小保留策略示意图.jpg

该策略判断依据是日志段的下一个日志段的起始偏移量 baseOffset 是否小于等于 logStartOffset,如果是,则可以删除此日志分段。

【如下图所示 删除步骤】:

  1. 首先从头开始遍历每个日志段,日志段 1 的下一个日志分段的起始偏移量为20,小于logStartOffset的大小,将日志段1加入deletableSegments。
  2. 日志段2的下一个日志偏移量的起始偏移量为35,也小于logStartOffset的大小,将日志分段2页加入deletableSegments。
  3. 日志段3的下一个日志偏移量的起始偏移量为50,也小于logStartOffset的大小,将日志分段3页加入deletableSegments。
  4. 日志段4的下一个日志偏移量通过对比后,在logStartOffset的右侧,那么从日志段4开始的所有日志段都不会加入deletableSegments。
  5. 待收集完所有的可删除的日志集合后就可以直接删除了。

4.3.2 日志压缩

日志压缩 Log Compaction 对于有相同key的不同value值,只保留最后一个版本。 如果应用只关心 key 对应的最新 value 值,则可以开启 Kafka 相应的日志清理功能,Kafka会定期将相同 key 的消息进行合并,只保留最新的 value 值。

Log Compaction 可以类比 Redis 中的 RDB 的持久化模式。我们可以想象下,如果每次消息变更都存 Kafka,在某一时刻, Kafka 异常崩溃后,如果想快速恢复,可以直接使用日志压缩策略, 这样在恢复的时候只需要恢复最新的数据即可,这样可以加快恢复速度。

图16:日志压缩策略示意图.jpg

4.3、磁盘数据存储

我们知道 Kafka 是依赖文件系统来存储和缓存消息,以及典型的顺序追加写日志操作,另外它使用操作系统的 PageCache 来减少对磁盘 I/O 操作,即将磁盘的数据缓存到内存中,把对磁盘的访问转变为对内存的访问。

在 Kafka 中,大量使用了 PageCache, 这也是 Kafka 能实现高吞吐的重要因素之一, 当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据页是否在 PageCache 中,如果命中则直接返回数据,从而避免了对磁盘的 I/O 操作;如果没有命中,操作系统则会向磁盘发起读取请求并将读取的数据页存入 PageCache 中,之后再将数据返回给进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检查数据页是否在页缓存中,如果不存在,则 PageCache 中添加相应的数据页,最后将数据写入对应的数据页。被修改过后的数据页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。

除了消息顺序追加写日志、PageCache以外, kafka 还使用了零拷贝(Zero-Copy)技术来进一步提升系统性能, 如下图所示:

图17:kafka 零拷贝示意图.jpg

这里也可以查看之前写的 Kafka 三高架构设计剖析 中高性能部分。

消息从生产到写入磁盘的整体过程如下图所示:

图18:日志消息写入磁盘过程示意图.jpg

五、总结

本文从 Kafka 存储的场景剖析出发、kafka 存储选型分析对比、再到 Kafka 存储架构设计剖析、以及 Kafka 日志系统架构设计细节深度剖析,一步步带你揭开了 Kafka 存储架构的神秘面纱。

如果我的文章对你有所帮助,还请关注、点赞、在看、转发一下,非常感谢!

下面为自己做个宣传,关注我的公众号: 华仔聊技术 坚持总结, 持续输出高质量文章

image.png

原链接:搞透Kafka的存储架构,看这篇就够了

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

本文转载自: 掘金

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

MySQL之定位慢查询 慢查询日志 explain prof

发表于 2021-11-06

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

慢查询日志

  1. 修改配置文件(一般为/etc/my.cnf),开启慢查询日志
1
2
3
4
5
ini复制代码[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow-query.log # 若没有指定,默认名字为hostname_slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
  1. 配合mysqldumpslow使用

如何开启MySQL慢查询日志 - 阿里云云栖号的文章 - 知乎

explain

原理

MySQL会在查询上设置一个标记。当执行查询时,这个标记会使其返回关于在执行计划中每一步的信息,而不是执行它。它会返回一行或多行信息,显示出执行计划中的每一部分和执行的次序。

explain会执行吗

如果查询在FROM子句中包括子查询,那么MySQL实际上会执行子查询,将其结果放在一个临时表中,然后完成外层查询优化。它必须在可以完成外层查询优化前处理所有类似的子查询,这对于explain来说是必须要做的(这个限制在MySQL5.6中将被取消)。

限制

  • 不会告知触发器、存储过程、UDF会如何影响查询
  • 不会告知查询执行中所作的特定优化

详见高性能MySQL附录D

5.6中的改进

能对类似UPDATE、INSERT等的查询进行解释
允许匿名表尽可能晚的具体化,而不总是在优化和执行使用到此临时表的部分查询时创建并填充它们。这将允许MySQL可以直接解释带子查询的查询语句,而不是要先实际地执行子查询。

profile

具体使用

MySQL查询优化(4)-show profile - 很six的文章 - 知乎
zhuanlan.zhihu.com/p/58387072

优化器跟踪

  1. 查看优化器状态
* show variables like 'optimizer\_trace';
  1. 会话级别临时开启
* set session optimizer\_trace="enabled=on",end\_markers\_in\_json=on;
  1. 设置优化器追踪的内存大小
* set OPTIMIZER\_TRACE\_MAX\_MEM\_SIZE=1000000;
  1. 执行自己的SQL
* select host,user,plugin from user;
  1. information_schema.optimizer_trace表
* SELECT trace FROM information\_schema.OPTIMIZER\_TRACE;
  1. 导入到一个命名为xx.trace的文件,然后用JSON阅读器来查看(如果没有控制台权限,或直接交由运维,让他把该 trace 文件,输出给你就行了。 )。
* `SELECT TRACE INTO DUMPFILE "E:\\test.trace" FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;`

注意:不设置优化器最大容量的话,可能会导致优化器返回的结果不全。

本文转载自: 掘金

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

Golang 入门篇

发表于 2021-11-06

Go语言是Google公司研发的开源编程语言,出生于2016年。由于Go原生的并发机制,使其在多核和网络开发方面有独特的优势。这也是其现在在国内爆火的主要原因。

开发环境搭建

官方地址: golang.org/dl/

国内地址:golang.google.cn/dl/

图片

下面以Mac版本为例演示

下载可执行文件 go1.17.3.darwin-amd64.pkg,双击安装

图片

按照提示一步一步就完成了Go语言的安装。

默认会将go安装到/usr/local/go目录。最好将这个目录添加到PATH环境变量中。

在终端(terminal)中,输入命令 go env 或 go version 如果安装没有错误,会输出以下内容:

图片

开发软件 (IDE)

这里主推visual studio code (vscode) 和 goland 这两款IDE。其中vscode免费,插件多,适合有极客精神的coder。

图片

个人使用感受上,还是更倾向于goland,毕竟人民币玩家和非人民币玩家之间的差距(你懂得 😏)

图片

Hello World

让我们一起看看编程语言通用的第一个例子:

1
2
3
4
5
6
7
8
9
go复制代码// main.go文件
// 使用 go run main.go 在终端中运行
package main

import "fmt"

func main() {
fmt.Println("Hello World!!!!!")
}

欢迎关注我的公众号 “道渔”,原创技术文章第一时间推送。

图片

本文转载自: 掘金

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

🏆【JVM深层系列】「云原生时代的Java虚拟机」针对于Gr

发表于 2021-11-06

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

GraalVM 背景

新、旧编程语言的兴起躁动,说明必然有其需求动力所在,譬如互联网之于JavaScript、人工智能之于Python,微服务风潮之于Golang等等。大家都清楚不太可能有哪门语言能在每一个领域都尽占优势,Java已是距离这个目标最接近的选项,但若“天下第一”还要百尺竿头更进一步的话,似乎就只能忘掉Java语言本身,踏入无招胜有招的境界。

  • 更进一步提升JVM上运行的程序的性能
  • 通过预编译(ahead-of-time)编译Java程序为原生可执行程序
  • 多种编程语言混编在一个程序中(polyglot)
  • 类似于LLVM,GraalVM也提供了方便的机制方便开发新的编程语言

官方网站在: www.graalvm.org/

当前痛点

在云原生时代,Java程序是有很大的劣势的,为什么这么说呢?一般的Java应用程序都要几十兆的内存,启动也不不快。

最流行的SpringBoot/SpringCloud微服务框架为例,启动一个已经优化好,很多bean需要lazy load的application至少需要3-4秒时间,内存需要几百兆,业务逻辑稍微复杂一点点,没有1G以上的内存是很难满足业务的需要呢?

那么在云原生时代,一个充满黑科技的JVM介绍给大家,它能帮助我们让Java程序的启动速度加快100倍,内存只需要原来的五分之一,甚至更少。

Graalvm的介绍

  • GraalVM是2018年Oracle开发的下一代JVM实现,被官方称为“Universal VM”和“Polyglot VM”,这是一个在HotSpot虚拟机基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言”的运行平台使用。
  • 这里“任何语言”包括了Java、Scala、Groovy、Kotlin等基于Java虚拟机之上的语言,还包括了C、C++、Rust等基于LLVM的语言,同时支持其他像JavaScript、Ruby、Python和R语言等等。

GraalVM可以无额外开销地混合使用这些编程语言,支持不同语言中混用对方的接口和对
象,也能够支持这些语言使用已经编写好的本地库文件。

它的口号“Run Programs Faster Anywhere”就能感觉到一颗蓬勃的野心

Graalvm性能的对比

GraalVM的性能真的不错。以JDK 8为例

  • OpenJDK
  • Oracle JDK

其中OpenJDK是通过“GPL v2 with CE”协议开源的,可以免费商用的。

之前在用Apache Spark测试性能时, 对比一下两者性能, 稍微数据量大点的查询,会发现Oracle JDK一般都会比OpenJDK快30%以上。

  • 而GraalVM分为社区版和商业版,其中GraalVM的社区版也是采用了和OpenJDK一样的“GPL v2 with CE”协议开源的。
  • 对于GraalVM的社区版,非常惊喜的发现其比Oracle JDK也会快10%以上。
  • 没有试过GraalVM的商业版, 官方报道,其商业版比社区版提升的性能更多

Graalvm主要特性

  • 高性能的现代Java
  • 占用资源少,启动速度快
  • JavaScript,Java, Ruby以及R混合编程
  • 在JVM上运行原生语言
  • 跨语言工具
  • JVM应用扩展
  • 原生应用扩展
  • 本地Java库
  • 数据库支持多语言
  • 创建自己的语言

Graalvm工作原理

GraalVM的基本工作原理是将这些语言的源代码(例如,JavaScript)或源代码编译后的中间格式(例如,LLVM字节码、Class字节码)通过解释器转换为能被GraalVM接受的中间表示(Intermediate Representation,IR),譬如设计一个解释器专门对LLVM输出的字节码进行转换来支持C和C++语言,这个过程称为“程序特化”(Specialized,也常称为Partial Evaluation)。

GraalVM提供了Truffle工具集来快速构建面向一种新语言的解释器,并用它构建了一个称为Sulong的高性能LLVM字节码解释器。

从某个角度来看,GraalVM才是真正意义上与物理计算机相对应的高级语言虚拟机,因为它与物理硬件的指令集一样,做到了只与机器特性相关而不与某种高级语言特性相关。

Graalvm的高等优化能力

Oracle Labs的研究总监Thomas Wuerthinger在接受采访时谈到:“随着GraalVM1.0的发布,已经证明了拥有高性能的多语言虚拟机是可能的,并且实现这个目标的最佳方式不是通过类似Java虚拟机和微软CLR那样带有语言特性的字节码”。

本来就不以速度见长的语言运行环境,由于GraalVM本身能够对输入的中间表示进行自动优化,在运行时还能进行即时编译优化,往往使用GraalVM实现能够获得比原生编译器更优秀的执行效率,譬如Graal.js要优于Node.js、Graal.Python要优于CPtyhon,TruffleRuby要优于Ruby MRI,FastR要优于R语言等等。

Graalvm与Hotspot的对比

GraalVM本来就是在HotSpot基础上诞生的,天生就可作为一套完整的符合Java SE8标准Java虚拟机来使用。

它和标准的HotSpot差异主要在即时编译器上,其执行效率、编译质量目前与标准版的HotSpot相比也是互有胜负。

Oracle Labs和美国大学里面的研究院所做的最新即时编译技术的研究全部都迁移至基于GraalVM之上进行了,其发展潜力令人期待。

如果Java语言或者HotSpot虚拟机真的有被取代的一天,那从现在看来GraalVM是希望最大的一个候选项,这场革命很可能会在Java使用者没有明显感觉的情况下悄然而来,Java世界所有的软件生态都没有发生丝毫变化,但天下第一的位置已经悄然更迭。

Graalvm即时编译器

自JDK 10起,HotSpot中又加入了一个全新的即时编译器:Graal编译器,看名字就可以联想到它是来自于Graal VM。

C1/C2即时编译器

对需要长时间运行的应用来说,由于经过充分预热,热点代码会被HotSpot的探测机制准确定位捕获,并将其编译为物理硬件可直接执行的机器码,在这类应用中Java的运行效率很大程度上是取决于即时编译器所输出的代码质量。

HotSpot虚拟机中包含有两个即时编译器:

  • 编译时间较短但输出代码优化程度较低的客户端编译器(简称为C1)
  • 编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2)

通常它们会在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统的。

C2即时编译器

Graal编译器是作为C2编译器替代者的角色登场的。C2的历史已经非常长了,可以追溯到Cliff Click大神读博士期间的作品,这个由C++写成的编译器尽管目前依然效果拔群,但已经复杂到连Cliff Click本人都不愿意继续维护的程度。

Graal编译器

而Graal编译器本身就是由Java语言写成,实现时又刻意与C2采用了同一种名为“Sea-of-Nodes”的高级中间表示(High IR)形式,使其能够更容易借鉴C2的优点。

Graal编译器比C2编译器晚了足足二十年面世,有着极其充沛的后发优势,在保持能输出相近质量的编译代码的同时,开发效率和扩展性上都要显著优于C2编译器,这决定了C2编译器中优秀的代码优化技术可以轻易地移植到Graal编译器上,但是反过来Graal编译器中行之有效的优化在C2编译器里实现起来则异常艰难。

这种情况下,Graal的编译效果短短几年间迅速追平了C2,甚至某些测试项中开始逐渐反超C2编译器。

Graal能够做比C2更加复杂的优化:

  • “部分逃逸分析”(Partial Escape Analysis)
  • 比C2更容易使用“激进预测性优化”(Aggressive Speculative Optimization)的策略
  • 支持自定义的预测性假设
未来可期

Graal编译器尚且年幼,还未经过足够多的实践验证,所以仍然带着“实验状态”的标签,需要用开关参数去激活,这让笔者不禁联想起JDK 1.3时代,HotSpot虚拟机刚刚横空出世时的场景,同样也是需要用开关激活,也是作为Classic虚拟机的替代品的一段历史。

Graal编译器未来的前途可期,作为Java虚拟机执行代码的最新引擎,它的持续改进,会同时为HotSpot与Graal VM注入更快更强的驱动力。

编译为原生执行程序

编译为原生程序有一定的假设条件,比如:

  • 尽量少的JNI调用
  • 尽量少的使用反射
  • 尽量少的class loader隔离等

当没有用这些复杂功能的时候,很容易可以使用GraalVM提供的 native image 编译Jar为可执行程序。

当然即使当程序使用了 JNI、反射时,也没关系,我们可以使用一些配置文件告诉GraalVM单独处理这些信息,比如:

  • 通过参数 -H:JNIConfigurationFiles 告诉JNI相关配置JSON文件
  • 通过参数 -H:ReflectionConfigurationFiles 告诉反射相关配置JSON文件

稍微会复杂一些,但是只要有足够的耐心,理论上也是可以编译成功的!

不过我们可以使用一些原生支持GraalVM native image的框架, 比如:Quarkus。

GraalVM的原生编译非常适合微服务和 Serverless

当可以把Java程序也编译为原生的可执行程序后 (目前GraalVM已经支持编译为Windows, MacOS, Linux上的原生程序),最主要的两个变化:

  • 启动时间变短了,之前启动一个有“依赖注入”的Java程序,可能启动时间要2秒以上。如果Java程序是要长期运行的,那启动时间稍慢一点是没问题的,但是对于 Serverless 应用,这就变为冷启动(cold start)了,影响比较大。
  • 程序运行的内存需求变小了,之前启动一个Java程序,控制的好的话(heap设置的比较小),也要100M以上的内存,但是编译为原生程序后,只需要4M内存就可以了。 这样同样的一台机器就可以启动非常多的进程,适合简单的微服务。

参考资料

  • it.deepinmind.com/jvm/2019/08…
  • www.zhihu.com/question/27…
  • www.jianshu.com/p/9e578398b…

本文转载自: 掘金

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

GO进阶训练营JK

发表于 2021-11-06

download:GO进阶训练营

一.创立列表1.创立一个普通列表123>>> tabulation1 = [‘大圣’,’天蓬’,’卷帘’]>>> tabulation1[‘大圣’, ‘天蓬’, ‘卷帘’]123>>> tabulation2 = [72,36,18]>>> tabulation2[72, 36, 18]2.创立一个混合列表12345>>> mix tabulation = [‘大圣’,72,’天蓬’,36]SyntaxError: invalid syntax>>> mixtabulation = [‘大圣’,72,’天蓬’,36]>>> mixtabulation[‘大圣’, 72, ‘天蓬’, 36]3.创立一个空列表123>>> empty = []>>> empty[]三种方式就引见给大家了,接下来,假如想向列表中添加元素,该怎样办呢?二.向列表中添加元素1.append>>> tabulation1.append(‘紫霞’)>>> tabulation1[‘大圣’, ‘天蓬’, ‘卷帘’, ‘紫霞’]2.extend>>> tabulation1.extend([‘紫霞’,’青霞’])>>> tabulation1[‘大圣’, ‘天蓬’, ‘卷帘’, ‘紫霞’, ‘紫霞’, ‘青霞’]有关于用extend拓展列表的办法,大家需求留意的是,此办法是用列表去拓展列表,而不是直接添加元素,所以“()”中要加上“[]”。

本文转载自: 掘金

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

Spring IOC容器初始化原理分析 (第一节)

发表于 2021-11-06

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

1.IOC容器简介

IoC(控制反转)也称为依赖注入(DI)。这是一个过程,对象仅通过构造函数参数、工厂方法的参数或在对象实例构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖关系(即它们使用的其他对象)。然后,容器在创建bean时注入这些依赖项。这个过程基本上是bean本身的逆过程(因此称为控制反转),通过使用类的直接构造或服务定位器模式等机制来控制其依赖项的实例化或位置。

The org.springframework.beans and org.springframework.context packages are the basis for Spring Framework’s IoC container. The BeanFactory interface provides an advanced configuration mechanism capable of managing any type of object. ApplicationContext is a sub-interface of BeanFactory

这是从官网拷贝的,翻译过来就是:
org.springframework.bean和org.springframework.context包是Spring框架的IoC容器的基础。BeanFactory接口提供了一种高级配置机制,能够管理任何类型的对象。ApplicationContext是BeanFactory的子接口。

即 IOC容器主要有两种实现方式,一种是继承了BeanFactory,另一种是继承了ApplicationContext,其中BeanFactory提供了配置框架和基本功能,而ApplicationContext提供了更多更丰富的功能。

2.如何工作

2.1 原理图

图片.png

从官网的结构图,我们可以很容易看到,容器的本质就是管理业务类,读取配置文件,给我们准备一个生成充分配置的系统

  • 这里pojo可以理解成我们的业务类
  • metadata 就是我们的配置文件,如xml,配置类等

2.2 ApplicationContext代码分析

我们以前刚学 spring的时候 会经常看到这种代码。

1
2
ini复制代码ApplicationContext appContext = new ClassPathXmlApplicationContext("/**/beans.xml");
Person p = (Person)appContext.getBean("person");

点进ClassPathXmlApplicationContext类,然后打开类的继承关系

图片.png

我们发现它继承了实现了ApplicationContext接口的类。点进ApplicationContext类发现它又继承了很多接口

1
2
3
4
5
6
kotlin复制代码public interface ApplicationContext extends EnvironmentCapable, 
ListableBeanFactory,
HierarchicalBeanFactory,
MessageSource,
ApplicationEventPublisher,
ResourcePatternResolver
  • EnvironmentCapable :所有Spring应用程序上下文都支持环境,并且主要使用接口用于在接受BeanFactory的框架方法中执行类型检查
  • ListableBeanFactory :实现了BeanFactory接口, Listable意思是能出来的,ListableBeanFactory可以枚举它们的所有bean信息,而不用一个个通过bean的名称或类型一个个查找。
  • HierarchicalBeanFactory:实现了HierarchicalBeanFactory接口,返回值不考虑层级的信息,只读取当前容器定义的信息
  • MessageSource:用于支持信息的国际化和包含参数的信息的替换
  • ApplicationEventPublisher:发布事件,即某个事件的发布信息告诉所有监听这个事件的监听器。
  • ResourcePatternResolver:解析资源文件的策略的接口,继承ResourceLoader,用于获取Resource

2.3 ClassPathXmlApplicationContext实例化过程

1.根据上面的代码直接点进去 看它的构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
//这里发现它调用了 另一个构造器,接着点进去
this(new String[] {configLocation}, true, null);
}

public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {

//调用父类构造器
super(parent);
//设置此应用程序上下文的配置位置。如果未设置,则实现可能会根据需要使用默认值。
setConfigLocations(configLocations);
if (refresh) {
//核心代码
refresh()方法
}
}

2.查看refresh()方法,这个是容器初始化的核心方法!

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
scss复制代码public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
//2.1 给这个容器的刷新做好准备。
prepareRefresh();

//2.2 这步比较重要(解析),告诉子类刷新内部bean工厂,这步完成后配置文件就解析成一个个bean定义,
//注册到BeanFactory(但是未被初始化,仅将信息写到了beanDefination的map中)
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 2.3 准备bean工厂,以便在此上下文中使用。配置工厂的标准上下文特征,例如上下文的类加载器和后处理器。
prepareBeanFactory(beanFactory);

try {
// 2.4 允许在上下文子类中对bean工厂进行后处理
postProcessBeanFactory(beanFactory);

// 2.5 注册BeanFactory的后置处理器 实例化并调用所有已注册的BeanFactory后置处理器实现类的方法,必须在单例实例化之前调用
invokeBeanFactoryPostProcessors(beanFactory);

//2.6 注册bean后置处理器,并不会执行后置处理器,在后面实例化的时候执行
// 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization
// 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化
registerBeanPostProcessors(beanFactory);

//2.7 国际化
initMessageSource();

//2.8 初始化事件多路广播器
initApplicationEventMulticaster();

//2.9 初始化上下文子类中的其他特殊bean
onRefresh();

//2.11 检查监听器并注册监听器
registerListeners();

//2.12 初始化所有非懒加载的单实例bean
finishBeanFactoryInitialization(beanFactory);

//2.13 广播事件,ApplicationContext初始化完成,至此容器创建完成
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// 销毁已经建立的单实例bean,避免浪费资源
destroyBeans();

// 设置激活状态 false
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
2.1 : prepareRefresh()方法解析
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
kotlin复制代码protected void prepareRefresh() {
// 设置启动时间
this.startupDate = System.currentTimeMillis();
//设置状态
this.closed.set(false);
//设置激活状态
this.active.set(true);
//日志输出
if (logger.isDebugEnabled()) {
if (logger.isTraceEnabled()) {
logger.trace("Refreshing " + this);
}
else {
logger.debug("Refreshing " + getDisplayName());
}
}

// 初始化上下文环境中的任何占位符属性源
initPropertySources();

// 验证所有必须的属性是否都可以解析
// 这里面必须的属性是通过ConfigurablePropertyResolver的setRequiredProperties方法设置的
getEnvironment().validateRequiredProperties();

// 刷新监听器的状态为预刷新
if (this.earlyApplicationListeners == null) {
this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
}
else {
// Reset local application listeners to pre-refresh state.
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}

// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<>();
}
2.2:obtainFreshBeanFactory()
1
2
3
4
5
6
scss复制代码protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
//刷新BeanFactory
refreshBeanFactory();
//返回BeanFactory
return getBeanFactory();
}
  1. AbstractRefreshableApplicationContext的refreshBeanFactory()方法解析(以它为例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码@Override
protected final void refreshBeanFactory() throws BeansException {
// 如果原先有BeanFactory,先销毁bean实例,再关闭BeanFactory
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
// 创建一个新的 beanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory();
//设置 serializationId
beanFactory.setSerializationId(getId());
//自定义beanFactory 设置是否允许Bean定义重写,是否允许循环引用
customizeBeanFactory(beanFactory);
//该方法会对DOM文档(如xml文件)对象进行解析,生成BeanDefinition对象
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

2.getBeanFactory() 默认返回的是DefaultListableBeanFactory类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码@Nullable
private DefaultListableBeanFactory beanFactory;

@Override
public final ConfigurableListableBeanFactory getBeanFactory() {
synchronized (this.beanFactoryMonitor) {
if (this.beanFactory == null) {
throw new IllegalStateException("BeanFactory not initialized or already closed - " +
"call 'refresh' before accessing beans via the ApplicationContext");
}
return this.beanFactory;
}
}
2.3 prepareBeanFactory(beanFactory)
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
scss复制代码protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 设置类加载器 ,如果有则直接设置进去,没有的话 新建一个默认的
beanFactory.setBeanClassLoader(getClassLoader());

//为bean定义值中的表达式指定解析策略。默认情况下,BeanFactory中没有活动的表达式支持。
//ApplicationContext通常会设置标准的表达式策略这里,以统一的EL兼容样式支持“#{…}”表达式。
//设置EL表达式解析器(Bean初始化完成后填充属性时会用到)
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));

//spring内部的属性编辑器
//添加PropertyEditor属性编辑器(可以将我们的property动态设置为bean里面对应的属性类型)
//比如:property赋值的是路径名(classpath/spring.xml),而对应bean属性设置的是Resource,则有spring的ResourceEditor完成转换
// springframework-bean下的propertyEditors包下有很多spring自带的属性编辑器
// 其中刚才提到的ResourceEditor在springframework-core下的io包里面
// 可以自定义属性编辑器,通过实现PropertyEditorSupport接口,spring中自带的属性编辑器也是这么做的
// 使用ApplicationContext,只需要在配置文件中通过CustomEditorConfigurer注册即可。
// CustomEditorConfigurer实现了BeanFactoryPostProcessor接口,因而是一个Bean工厂后置处理器
// 在Spring容器中加载配置文件并生成BeanDefinition后会被执行。
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

// 将当前的ApplicationContext对象交给ApplicationContextAwareProcessor类来处理,
//从而在Aware接口实现类中的注入applicationContext
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));

//以下6个属性可忽略,自动装配
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

// 注册几个可以解析,自动装配相关的类和实例
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// 添加一个BeanPostProcessor,它有两个方法postProcessBeforeInitialization 在bean初始化前执行
//postProcessAfterInitialization在bean初始化后处理。ApplicationListenerDetector重写了postProcessAfterInitialization方法
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// 判断是否存在名字为loadTimeWeaver的bean
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
//如果有,则注册一个LoadTimeWeaverAwareProcessor到容器中
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// 创建一个临时的classLoader来让其处理真正的bean
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}

//以下三部分主要是 注册默认的环境bean
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
}

肝不动了,今天就先到这,剩下的2.4–2.13方法,我会在后续的文章中继续分析。

本文转载自: 掘金

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

1…406407408…956

开发者博客

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