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

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


  • 首页

  • 归档

  • 搜索

提问!JVM类加载你真的【了解】了吗?掌握这几点,不再难!

发表于 2021-10-20

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

二、类加载子系统

缩略图

2.1、类加载器与类的加载过程

2.1.1、类加载器子系统

类加载子系统

类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识,ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。


**加载的类信息存放于一块称为方法区的内存空间**。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

2.1.2、类加载器ClasLoader

类加载器

class file存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个 文件实例化出n个一模一样的实例。


class file加载到JVM中,被称为DNA元数据模板,放在方法区。


.class文件->JVM->最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。

2.1.3、加载阶段

加载阶段

在加载阶段,主要经过了加载、链接(验证、准备、解析)、初始化三个步骤。

2.1.3.1、加载

加载步骤分为三部曲:
  1. 通过一个类的全限定名获取定义此类的二进制字节流,将硬盘中的class文件加载进JVM内存。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(JDK8以前叫永久代,JKD8及以后叫元空间)。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
我们加载class文件的方式主要有以下的七种:
  1. 从本地系统中直接加载
  2. 通过网络获取,典型场景:Web Applet
  3. 从zip压缩包中读取,成为日后jar、war格式的基础
  4. 运行时计算生成,使用最多的是:动态代理技术
  5. 由其他文件生成,典型场景:JSP应用
  6. 从专有数据库中提取.class文件,比较少见
  7. 从加密文件中获取,典型的防Class文件被反编译的保护措施

2.1.3.2、链接阶段

2.1.3.2.1、验证(Verify)
验证的目的在子确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。


他主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
2.1.3.2.2、准备(Prepare)
准备阶段是为类变量分配内存并且设置该类变量的默认初始值,即零值。**这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化。**


他也不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。在准备阶段,只有类没有对象。
2.1.3.2.3、解析(Resolve)
解析阶段是将常量池内的**符号引用转换为直接引用的过程**(用对象的内存地址,而不是对象本身)。事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。


符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。


解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT\_Class\_info,CONSTANT\_Fieldref\_info、CONSTANT\_Methodref\_info等。

2.1.3.3、初始化阶段

初始化阶段就是执行类构造器方法`<clinit>()`的过程。此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。


构造器方法中指令按语句在源文件中出现的顺序执行。`<clinit>()`不同于类的构造器。(关联:构造器是虚拟机视角下的`<init>()`)


若该类具有父类,JVM会保证子类的`<clinit>()`执行前,父类的`<clinit>()`已经执行完毕,且虚拟机必须保证一个类的`<clinit>()`方法在多线程下被同步加锁,也就是说一个类不会被初始化两次。

2.1.4、类加载的过程

1
2
3
4
5
6
7
8
java复制代码/**
*示例代码
*/
public class HelloLoader {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

类加载的过程

2.2、 类加载器分类

JVM支持两种类型的类加载器 。分别为:
  1. 引导类加载器(Bootstrap ClassLoader)。
  2. 自定义类加载器(User-Defined ClassLoader)。(拓展类加载器、系统类加载器、自定义类加载器)
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器,无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个。

image-20210807225824687

2.2.1、虚拟机自带的加载器

2.2.1.1、启动类加载器

启动类加载器也叫引导类加载器(Bootstrap ClassLoader),他是这个类加载使用 C/C++语言实现的,嵌套在 JVM 内部,他并不继承自 `java.lang.ClassLoader`,没有父加载器。我们无法通过Java代码进行获取。


它用来加载 Java 的核心库(JAVA\_HOME/jre/lib/rt.jar、resources.jar 或 sun.boot.class.path 路径下的内容),**用于提供 JVM 自身需要的类**。他同时还加载扩展类和应用程序类加载器,并指定为他们的父类加载器。


出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类。

2.2.1.2、扩展类加载器

扩展类加载器(Extension ClassLoader),他是由Java 语言编写,由 `sun.misc.Launcher$ExtClassLoader` 实现,派生于 ClassLoader 类。


他的父类加载器为启动类加载器,从 `java.ext.dirs` 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/1ib/ext 子目录(扩展目录)下加载类库。**如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载。**

2.2.1.3、应用程序类加载器

应用程序类加载器也叫做系统类加载器(AppClassLoader),他是由java语言编写,由`sun.misc.LaunchersAppClassLoader`实现,派生于ClassLoader类,父类加载器为扩展类加载器。


它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库,该类加载是程序中默认的类加载器,一般来说,J**ava应用的类都是由它来完成加载**,通过`ClassLoader#getSystemclassLoader()` 方法可以获取到该类加载器。

2.2.2、用户自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?我们自定义类加载器的主要有四个原因:
  1. 隔离加载类。
  2. 修改类的加载方式。
  3. 拓展加载源。
  4. 防止源码泄露。
用户自定义类加载器实现步骤:
  1. 开发人员可以通过继承抽象类ava.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
  2. 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass() 方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass() 方法,而是建议把自定义的类加载逻辑写在findClass()方法中。
  3. 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

2.3、ClassLoader的使用说明

ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)。
方法名称 描述
getParent() 返回该类加载器的超类加载器
loadClass(String name) 加载一个名为name的类,返回一个java.lang.Class的实例
findClass(String name) 查找一个名为name的类,返回一个java.lang.Class的实例
findloadedClass(String name) 查找名称为name的已经被加载过的类,返回一个java.lang.Class的实例
defineClass(String name , byte[] b , int off , int len) 把字节数组b中的内容转化为一个Java类,返回一个java.lang.Class的实例
resloveClass(Class<?> c) 指定连接一个Java类
`sun.misc.Launcher` 它是一个java虚拟机的入口应用。

类加载器继承关系

我们如果想获取一个ClassLoader,大致有四种途径。

方式一:获取当前的ClassLoader

1
java复制代码clazz.getClassLoader()

方式二:获取当前线程上下文的ClassLoader

1
java复制代码	Thread.currentThread().getContextClassLoader()

方式三:获取系统的ClassLoader

1
java复制代码ClassLoader.getSystemClassLoader()

方式四:获取调用者的ClassLoader

1
java复制代码DriverManager.getCallerClassLoader()

2.4、双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,**即把请求交由父类处理,它是一种任务委派模式。**


他的工作原理大致是这样:

双亲委派机制

  1. 当一个类加载器收到了加载类的请求时,他并不会先去自己加载,而是把这个请求委托给弗雷德加载器去进行执行。
  2. 如果父类加载器还存在其他的父类加载器,就会进一步向上委托,一次递归,直到请求最终到达顶层的启动类加载器。
  3. 如果父类加载器可以完成这个类的加载任务,那么就会成功返回。假如父类加载器无法完成加载这个类的任务,子类加载器才会尝试去自己加载。

2.4.1、大白话解释双亲委派机制

假如有一个小孩,他有一个又酸又硬的苹果,这个时候家里有奶奶和妈妈。

2.4.1.1、父类成功加载

双亲委派机制大白话-父类成功加载

2.4.1.2、父类加载失败

双亲委派机制白话-父类加载失败

2.4.2、证明双亲委派的存在

我们在自己的项目中新建一个包名为`java.lang`的`String`类,一定要是JDK8,不能是JDK11,否则无法验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package java.lang;

/**
* @Description 这是我们自己定义的Stirng,和java官方的String同包同名
* @Author XiaoLin
* @Date 2021/8/28 16:14
*/
public class String {
// 在对象初始化之前就会执行
static {
System.out.println("我是XiaoLin定义的String");
}

}

我们再新建一个测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package cn.linstudy;

/**
* @Description
* @Author XiaoLin
* @Date 2021/8/28 16:15
*/
public class TestString {

public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("我是XiaoLin");
}
}

image-20210828162242243

我们执行完以后,发现并没有打印我们静态代码块里面的代码,说明执行的并不是我们定义的`Stirng`类,而是Java官方的`Stirng`类。


我们在`Stirng`类中写一个main方法,执行一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package java.lang;

/**
* @Description 这是我们自己定义的Stirng,和java官方的String同包同名
* @Author XiaoLin
* @Date 2021/8/28 16:14
*/
public class String {
static {
System.out.println("我是XiaoLin定义的String");
}

public static void main(String[] args) {
System.out.println("Hello XiaoLin");
}
}

image-20210828162450009

发现报错了,他说找不到`main`方法,再次说明了说明执行的并不是我们定义的`Stirng`类,而是Java官方的`Stirng`类。

2.4.3、双亲委派机制的优势

既然JDK搞了一个双亲委派机制,那么肯定是有他独特的优势,他的优势有两点:
  1. 避免类的重复加载,只要有一个类加载器加载了这个类,那么这个类就不会被其他类加载器加载,防止类被重复加载。
  2. 保护程序安全,防止核心的API被篡改。我们举个例子,自定一个类,在Java.lang包下。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码package java.lang;

/**
* @Description
* @Author XiaoLin
* @Date 2021/8/28 16:34
*/
public class SweetCode {

public static void main(String[] args) {
System.out.println("Hello SweetCode");
}
}

image-20210828163736527

2.4.4、沙箱安全机制

我们自己自定义了一个String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

本文转载自: 掘金

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

漫画:什么是 “建造者模式” ?

发表于 2021-10-20

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

————— 第二天 —————

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

————————————

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

首先,我们来定义一个Product类:

1
2
3
4
5
6
7
8
9
10
11
typescript复制代码public class Product {
ArrayList<String> parts = new ArrayList<String>();

public void add(String part) {
parts.add(part);
}

public void show() {
System.out.println(parts);
}
}

接下来,我们定义抽象的Builder类:

public abstract class Builder {

public abstract void buildPartA();

public abstract void buildPartB();

public abstract Product getResult() ;

}

然后,是具体的Builder实现类:

public class ConcreteBuilder extends Builder {

private Product product = new Product();

public Product getResult() {

return product;

}

@Override

public void buildPartA() {

product.add(“构建产品的上半部分”);

}

@Override

public void buildPartB() {

product.add(“构建产品的下半部分”);

}

}

在Builder类之外,则是Director类来控制Builder的生产过程:

public class Director {

private Builder builder;

public Director(Builder builder) {

this.builder = builder;

}

public void construct() {

builder.buildPartA();

builder.buildPartB();

}

}

最后,是客户端的测试代码:

Builder builder = new ConcreteBuilder();

Director director = new Director(builder);

director.construct();

Product product = builder.getResult();

product.show();

我们来看一下运行的结果:

[构建产品的上半部分, 构建产品的下半部分]

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

// 默认采用Builder进行建造

public OkHttpClient() {

this(new Builder());

}

// 由builder配置分发器、代理、协议以及自定义拦截器等

OkHttpClient(Builder builder) {

this.dispatcher = builder.dispatcher;

this.proxy = builder.proxy;

this.protocols = builder.protocols;

/** 省略大段代码 */

boolean isTLS = false;

for (ConnectionSpec spec : connectionSpecs) {

isTLS = isTLS || spec.isTls();

}

/** 省略大段代码. */

if (interceptors.contains(null)) {

throw new IllegalStateException(“Null interceptor: “ + interceptors);

}

if (

networkInterceptors.contains(null)) {

throw new IllegalStateException(“Null network interceptor: “ + networkInterceptors);

}

}

public static final class Builder {

public Builder() {

// 分发器、协议、代理的默认参数

dispatcher = new Dispatcher();

protocols = DEFAULT_PROTOCOLS;

proxySelector = ProxySelector.getDefault();

if (proxySelector == null) {

proxySelector = new NullProxySelector();

}

}

Builder(OkHttpClient okHttpClient) {

// 反向配置分发器、代理、协议

this.dispatcher = okHttpClient.dispatcher;

this.proxy = okHttpClient.proxy;

this.protocols = okHttpClient.protocols;

// 新增所有自定义拦截器和自定义网络拦截器

this.interceptors.addAll(okHttpClient.interceptors);

this

.networkInterceptors.addAll(okHttpClient.networkInterceptors);

}

// 配置代理

public Builder proxy(@Nullable Proxy proxy) {

this.proxy = proxy;

return this;

}

// 向拦截器链中增加自定义拦截器

public Builder addInterceptor(Interceptor interceptor) {

if (interceptor == null) throw new IllegalArgumentException(“interceptor == null”);

interceptors.add(interceptor);

return this;

}

// 最后是build()方法,生成OkHttpClient对象

public OkHttpClient build() {

return new OkHttpClient(this);

}

}

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

/**将指定的字符串追加到此字符序列*/

@Override

public StringBuilder append(CharSequence s) {

super.append(s);// 实现过程略

return this;

}

/**将此字符序列用其反转形式取代*/

@Override

public StringBuilder reverse() {

super.reverse();// 实现过程略

return this;

}

下面让我们来编写一下测试代码:

StringBuilder sb = new StringBuilder(“若为自由故”);

sb.append(“只要主义真”);

sb.reverse();

System.out.println(sb);

StringBuilder sb1 = new StringBuilder(“若为自由故”);

sb1.reverse();

sb1.append(“只要主义真”);

System.out.println(sb1);

测试结果如下:

System.out: 真义主要只故由自为若

System.out: 故由自为若只要主义真

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

建造者模式与简单工程模式的区别,在于建造者模式多出一个Builder类,使得创建对象的灵活性大大增加,适用于如下场景:

(1)创建一个对象,多个同样的方法的调用顺序不同,产生的结果不同

(2)创建一个对象,特别复杂,参数多,而且很多参数都有默认值

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

System.out.println(“Hello World”);

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

短短一行代码,背后有什么样的机制呢?

Java的编译原理,是将Hello.java编译成能被VM理解的Hello.class,然后再转化为能被不同硬件设备理解的bytecode进而执行的。

著名的字节码增强框架ASM,就是在Hello.java编译成Hello.class时可以读取并分析类信息、改变类行为、增强类功能甚至生成新的类的bytecode分析和操作框架。

我们来看一下相关的代码,代码当中的mv,来自ASM框架的MethodVisitor接口。

// 访问System类的类型为PrintSystem类型的静态变量out

mv.visitFieldInsn(Opcodes.GETSTATIC, “java/lang/System”, “out”, “Ljava/io/PrintStream;”);

// 访问常量池中的数据”Hello World”

mv.visitLdcInsn(“Hello World”);

// 调用PrintStream类的println()方法并把刚才获取到的对象当做String类型的参数传入

mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, “java/io/PrintStream”, “println”, “(Ljava/lang/String;)V”, false);

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

漫画:什么是“建造者模式”?

本文转载自: 掘金

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

MySQL深潜|剖析Performance Schema内存

发表于 2021-10-20

简介: 本文主要是通过对PFS引擎的内存管理源码的阅读,解读PFS内存分配及释放原理,深入剖析其中存在的一些问题,以及一些改进思路。

一 引言

MySQL Performance schema(PFS)是MySQL提供的强大的性能监控诊断工具,提供了一种能够在运行时检查server内部执行情况的特方法。PFS通过监视server内部已注册的事件来收集信息,一个事件理论上可以是server内部任何一个执行行为或资源占用,比如一个函数调用、一个系统调用wait、SQL查询中的解析或排序状态,或者是内存资源占用等。

PFS将采集到的性能数据存储在performance_schema存储引擎中,performance_schema存储引擎是一个内存表引擎,也就是所有收集的诊断信息都会保存在内存中。诊断信息的收集和存储都会带来一定的额外开销,为了尽可能小的影响业务,PFS的性能和内存管理也显得非常重要了。本文主要是通过对PFS引擎的内存管理的源码的阅读,解读PFS内存分配及释放原理,深入剖析其中存在的一些问题,以及一些改进思路。本文源代码分析基于MySQL-8.0.24版本。

二 内存管理模型

PFS内存管理有几个关键特点:

  • 内存分配以Page为单位,一个Page内可以存储多条record
  • 系统启动时预先分配部分pages,运行期间根据需要动态增长,但page是只增不回收的模式
  • record的申请和释放都是无锁的

1 核心数据结构

PFS_buffer_scalable_container是PFS内存管理的核心数据结构,整体结构如下图:

Container中包含多个page,每个page都有固定个数的records,每个record对应一个事件对象,比如PFS_thread。每个page中的records数量是固定不变的,但page个数会随着负载增加而增长。

2 Allocate时Page选择策略

PFS_buffer_scalable_container是PFS内存管理的核心数据结构

涉及内存分配的关键数据结构如下:

1
2
3
4
5
6
7
8
9
10
c复制代码PFS_PAGE_SIZE  // 每个page的大小, global_thread_container中默认为256
PFS_PAGE_COUNT // page的最大个数,global_thread_container中默认为256

class PFS_buffer_scalable_container {
PFS_cacheline_atomic_size_t m_monotonic; // 单调递增的原子变量,用于无锁选择page
PFS_cacheline_atomic_size_t m_max_page_index; // 当前已分配的最大page index
size_t m_max_page_count; // 最大page个数,超过后将不再分配新page
std::atomic<array_type *> m_pages[PFS_PAGE_COUNT]; // page数组
native_mutex_t m_critical_section; // 创建新page时需要的一把锁
}

首先m_pages是一个数组,每个page都可能有free的records,也有可能整个page都是busy的,Mysql采用了比较简单的策略,轮训挨个尝试每个page是否有空闲,直到分配成功。如果轮训所有pages依然没有分配成功,这个时候就会创建新的page来扩充,直到达到page数的上限。

轮训并不是每次都是从第1个page开始寻找,而是使用原子变量m_monotonic记录的位置开始查找,m_monotonic在每次在page中分配失败是加1。

核心简化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ini复制代码value_type *allocate(pfs_dirty_state *dirty_state) {
current_page_count = m_max_page_index.m_size_t.load();

monotonic = m_monotonic.m_size_t.load();
monotonic_max = monotonic + current_page_count;
while (monotonic < monotonic_max) {
index = monotonic % current_page_count;
array = m_pages[index].load();
pfs = array->allocate(dirty_state);
if (pfs) {
// 分配成功返回
return pfs;
} else {
// 分配失败,尝试下一个page,
// 因为m_monotonic是并发累加的,这里有可能本地monotonic变量并不是线性递增的,有可能是从1 直接变为 3或更大,
// 所以当前while循环并不是严格轮训所有page,很大可能是跳着尝试,换者说这里并发访问下大家一起轮训所有的page。
// 这个算法其实是有些问题的,会导致某些page被跳过忽略,从而加剧扩容新page的几率,后面会详细分析。
monotonic = m_monotonic.m_size_t++;
}
}

// 轮训所有Page后没有分配成功,如果没有达到上限的话,开始扩容page
while (current_page_count < m_max_page_count) {
// 因为是并发访问,为了避免同时去创建新page,这里有一个把同步锁,也是整个PFS内存分配唯一的锁
native_mutex_lock(&m_critical_section);
// 拿锁成功,如果array已经不为null,说明已经被其它线程创建成功
array = m_pages[current_page_count].load();
if (array == nullptr) {
// 抢到了创建page的责任
m_allocator->alloc_array(array);
m_pages[current_page_count].store(array);
++m_max_page_index.m_size_t;
}
native_mutex_unlock(&m_critical_section);

// 在新的page中再次尝试分配
pfs = array->allocate(dirty_state);
if (pfs) {
// 分配成功并返回
return pfs;
}
// 分配失败,继续尝试创建新的page直到上限
}
}

我们再详细分析下轮训page策略的问题,因为m_momotonic原子变量的累加是并发的,会导致一些page被跳过轮训它,从而加剧了扩容新page的几率。

举一个极端一些的例子,比较容易说明问题,假设当前一共有4个page,第1、4个page已满无可用record,第2、3个page有可用record。

当同时来了4个线程并发Allocate请求,同时拿到了的m_monotonic=0.

monotonic = m_monotonic.m_size_t.load();

这个时候所有线程尝试从第1个page分配record都会失败(因为第1个page是无可用record),然后累加去尝试下一个page

monotonic = m_monotonic.m_size_t++;

这个时候问题就来了,因为原子变量++是返回最新的值,4个线程++成功是有先后顺序的,第1个++的线程后monotonic值为2,第2个++的线程为3,以次类推。这样就看到第3、4个线程跳过了page2和page3,导致3、4线程会轮训结束失败进入到创建新page的流程里,但这个时候page2和page3里是有空闲record可以使用的。

虽然上述例子比较极端,但在Mysql并发访问中,同时申请PFS内存导致跳过一部分page的情况应该还是非常容易出现的。

3 Page内Record选择策略

PFS_buffer_default_array是每个Page维护一组records的管理类。

关键数据结构如下:

1
2
3
4
5
arduino复制代码class PFS_buffer_default_array {
PFS_cacheline_atomic_size_t m_monotonic; // 单调递增原子变量,用来选择free的record
size_t m_max; // record的最大个数
T *m_ptr; // record对应的PFS对象,比如PFS_thread
}

每个Page其实就是一个定长的数组,每个record对象有3个状态FREE,DIRTY, ALLOCATED,FREE表示空闲record可以使用,ALLOCATED是已分配成功的,DIRTY是一个中间状态,表示已被占用但还没分配成功。

Record的选择本质就是轮训查找并抢占状态为free的record的过程。

核心简化代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码value_type *allocate(pfs_dirty_state *dirty_state) {
// 从m_monotonic记录的位置开始尝试轮序查找
monotonic = m_monotonic.m_size_t++;
monotonic_max = monotonic + m_max;

while (monotonic < monotonic_max) {
index = monotonic % m_max;
pfs = m_ptr + index;

// m_lock是pfs_lock结构,free/dirty/allocated三状态是由这个数据结构来维护的
// 后面会详细介绍它如何实现原子状态迁移的
if (pfs->m_lock.free_to_dirty(dirty_state)) {
return pfs;
}
// 当前record不为free,原子变量++尝试下一个
monotonic = m_monotonic.m_size_t++;
}
}

选择record的主体主体流程和选择page基本相似,不同的是page内record数量是固定不变的,所以没有扩容的逻辑。

当然选择策略相同,也会有同样的问题,这里的m_monotonic原子变量++是多线程并发的,同样如果并发大的场景下会有record被跳过选择了,这样导致page内部即便有free的record也可能没有被选中。

所以也就是page选择即便是没有被跳过,page内的record也有几率被跳过而选不中,雪上加霜,更加加剧了内存的增长。

4 pfs_lock

每个record都有一个pfs_lock,来维护它在page中的分配状态(free/dirty/allocated),以及version信息。

关键数据结构:

struct pfs_lock {

std::atomic m_version_state;

}

pfs_lock使用1个32位无符号整型来保存version+state信息,格式如下:

state

低2位字节表示分配状态。

state PFS_LOCK_FREE = 0x00

state PFS_LOCK_DIRTY = 0x01

state PFS_LOCK_ALLOCATED = 0x11

version

初始version为0,每分配成功一次加1,version就能表示该record被分配成功的次数主要看一下状态迁移代码:

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
objectivec复制代码// 下面3个宏主要就是用来位操作的,方便操作state或version
#define VERSION_MASK 0xFFFFFFFC
#define STATE_MASK 0x00000003
#define VERSION_INC 4

bool free_to_dirty(pfs_dirty_state *copy_ptr) {
uint32 old_val = m_version_state.load();

// 判断当前state是否为FREE,如果不是,直接返回失败
if ((old_val & STATE_MASK) != PFS_LOCK_FREE) {
return false;
}

uint32 new_val = (old_val & VERSION_MASK) + PFS_LOCK_DIRTY;

// 当前state为free,尝试将state修改为dirty,atomic_compare_exchange_strong属于乐观锁,多个线程可能同时
// 修改该原子变量,但只有1个修改成功。
bool pass =
atomic_compare_exchange_strong(&m_version_state, &old_val, new_val);

if (pass) {
// free to dirty 成功
copy_ptr->m_version_state = new_val;
}

return pass;
}

void dirty_to_allocated(const pfs_dirty_state *copy) {
/* Make sure the record was DIRTY. */
assert((copy->m_version_state & STATE_MASK) == PFS_LOCK_DIRTY);
/* Increment the version, set the ALLOCATED state */
uint32 new_val = (copy->m_version_state & VERSION_MASK) + VERSION_INC +
PFS_LOCK_ALLOCATED;

m_version_state.store(new_val);
}

状态迁移过程还是比较好理解的, 由dirty_to_allocated和allocated_to_free的逻辑是更简单的,因为只有record状态是free时,它的状态迁移是存在并发多写问题的,一旦state变为dirty,当前record相当于已经被某一个线程占有,其它线程不会再尝试操作该record了。

version的增长是在state变为PFS_LOCK_ALLOCATED时

5 PFS内存释放

PFS内存释放就比较简单了,因为每个record都记录了自己所在的container和page,调用deallocate接口,最终将状态置为free就完成了。

最底层都会进入到pfs_lock来更新状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码struct pfs_lock {
void allocated_to_free(void) {
/*
If this record is not in the ALLOCATED state and the caller is trying
to free it, this is a bug: the caller is confused,
and potentially damaging data owned by another thread or object.
*/
uint32 copy = copy_version_state();
/* Make sure the record was ALLOCATED. */
assert(((copy & STATE_MASK) == PFS_LOCK_ALLOCATED));
/* Keep the same version, set the FREE state */
uint32 new_val = (copy & VERSION_MASK) + PFS_LOCK_FREE;

m_version_state.store(new_val);
}
}

三 内存分配的优化

前面我们分析到无论是page还是record都有几率出现跳过轮训的问题,即便是缓存中有free的成员也会出现分配不成功,导致创建更多的page,占用更多的内存。最主要的问题是这些内存一旦分配就不会被释放。

为了提升PFS内存命中率,尽量避免上述问题,有一些思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码while (monotonic < monotonic_max) {
index = monotonic % current_page_count;
array = m_pages[index].load();
pfs = array->allocate(dirty_state);
if (pfs) {
// 记录分配成功的index
m_monotonic.m_size_t.store(index);
return pfs;
} else {
// 局部变量递增,避免掉并发累加而跳过某些pages
monotonic++;
}
}

另外一点,每次查找都是从最近一次分配成功的位置开始,这样必然导致并发访问的冲突,因为大家都从同一个位置开始找,起始查找位置应该加入一定的随机性,这样可以避免大量的冲突重试。

总结如下:

每次Allocate是从最近一次分配成功的index开始查找,或者随机位置开始查找

每个Allocate严格轮训所有pages或records

四 内存释放的优化

PFS内存释放的最大的问题就是一旦创建出的内存就得不到释放,直到shutdown。如果遇到热点业务,在业务高峰阶段分配了很多page的内存,在业务低峰阶段依然得不到释放。

要实现定期检测回收内存,又不影响内存分配的效率,实现一套无锁的回收机制还是比较复杂的。

主要有如下几点需要考虑:

释放肯定是要以page为单位的,也就是释放的page内的所有records都必须保证都为free,而且要保证待free的page不会再被分配到

内存分配是随机的,整体上内存是可以回收的,但可能每个page都有一些busy的,如何更优的协调这种情况

释放的阈值怎么定,也要避免频繁分配+释放的问题

针对PFS内存释放的优化,PolarDB已经开发并提供了定期回收PFS内存的特性,鉴于本篇幅的限制,留在后续再介绍了。

五 关于我们

PolarDB 是阿里巴巴自主研发的云原生分布式关系型数据库,于2020年进入Gartner全球数据库Leader象限,并获得了2020年中国电子学会颁发的科技进步一等奖。PolarDB 基于云原生分布式数据库架构,提供大规模在线事务处理能力,兼具对复杂查询的并行处理能力,在云原生分布式数据库领域整体达到了国际领先水平,并且得到了广泛的市场认可。在阿里巴巴集团内部的最佳实践中,PolarDB还全面支撑了2020年天猫双十一,并刷新了数据库处理峰值记录,高达1.4亿TPS。欢迎有志之士加入我们,简历请投递到zetao.wzt@alibaba-inc.com,期待与您共同打造世界一流的下一代云原生分布式关系型数据库。

参考:

[1] MySQL Performance Schema

dev.mysql.com/doc/refman/…

[2] MySQL · 最佳实践 · 今天你并行了吗?—洞察PolarDB 8.0之并行查询

mysql.taobao.org/monthly/201…

[3] Source code mysql / mysql-server 8.0.24

github.com/mysql/mysql…

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

学会这几个 Redis 技巧,让你的程序快如闪电!

发表于 2021-10-20

一、Redis封装架构讲解

实际上NewLife.Redis是一个完整的Redis协议功能的实现,但是Redis的核心功能并没有在这里面,而是在NewLife.Core里面。

这里可以打开看一下,NewLife.Core里面有一个NewLife.Caching的命名空间,里面有一个Redis类,里面实现了Redis的基本功能;另一个类是RedisClient是Redis的客户端。

Redis的核心功能就是有这两个类实现,RedisClient代表着Redis客户端对服务器的一个连接。Redis真正使用的时候有一个Redis连接池,里面存放着很多个RedisClient对象。

)

所以我们Redis的封装有两层,一层是NewLife.Core里面的Redis以及RedisClient;另一层就是NewLife.Redis。这里面的FullRedis是对Redis的实现了Redis的所有的高级功能。

这里你也可以认为NewLife.Redis是Redis的一个扩展。

二、Test实例讲解Redis的基本使用

1、实例

打开Program.cs看下代码:

)

这里XTrace.UseConsole();是向控制台输出日志,方便调试使用查看结果。

接下来看第一个例子Test1,具体的我都在代码中进行了注释,大家可以看下:

)

Set的时候,如果是字符串或者字符数据的话,Redis会直接保存起来(字符串内部机制也是保存二进制),如果是其他类型,会默认进行json序列化然后再保存起来。

Get的时候,如果是字符串或者字符数据会直接获取,如果是其他类型会进行json反序列化。

Set第三个参数过期时间单位是秒。

vs调试小技巧,按F5或者直接工具栏“启动”会编译整个解决方案会很慢(VS默认),可以选中项目然后右键菜单选择调试->启动新实例,会只编译将会用到的项目,这样对调试来说会快很多。

大家运行调试后可以看到控制台输出的内容:向右的箭头=》是ic.Log=XTrace.Log输出的日志。

)

字典的使用:对象的话,需要把json全部取出来,然后转换成对象,而字典的话,就可以直接取某个字段。

队列是List结构实现的,上游数据太多,下游处理不过来的时候,就可以使用这个队列。上游的数据发到队列,然后下游慢慢的消费。另一个应用,跨语言的协同工作,比方说其他语言实现的程序往队列里面塞数据,然后另一种语言来进行消费处理。这种方式类似MQ的概念,虽然有点low,但是也很好用。

集合,用的比较多的是用在一个需要精确判断的去重功能。像我们每天有三千万订单,这三千万订单可以有重复。这时候我想统计下一共有订单,这时候直接数据库group by是不大可能的,因为数据库中分了十几张表,这里分享个实战经验:

比方说揽收,商家发货了,网点要把件收回来,但是收回来之前网点不知道自己有多少货,这时候我们做了一个功能,也就是订单会发送到我们公司来。我们会建一个time_site的key的集合,而且集合本身有去重的功能,而且我们可以很方便的通过set.Count功能来统计数量,当件被揽收以后,我们后台把这个件从集合中Remove掉。然后这个Set中存在的就是网点还没有揽收的件,这时候通过Count就会知道这个网点今天还有多少件没有揽收。实际使用中这个数量比较大,因为有几万个网点。

Redis中布隆过滤器,去重的,面试的时候问的比较多。

小经验分享:

数据库中不合法的时间处理:判断时间中的年份是否大于2000年,如果小于2000就认为不合法;习惯大于小于号不习惯用等于号,这样可以处理很多意外的数据;

Set的时候最好指定过期时间,防止有些需要删除的数据我们忘记删了;

Redis异步尽量不用,因为Redis延迟本身很小,大概在100us-200us,再一个就是Redis本身是单线程的,异步任务切换的耗时比网络耗时还要大;

List用法:物联网中数据上传,量比较大时,我们可以把这些数据先放在Redis的List中,比如说一秒钟1万条,然后再批量取出来然后批量插入数据库中。这时候要设置好key,可以前缀+时间,对已处理的List可以进行remove移除。

2、压力测试

接下来看第四个例子,我们直接做压力测试,代码如下:

)

运行的结果如下图所示:

)

测试就是进行get,set remove,累加等的操作。大家可以看到在我本机上轻轻松松的到了六十万,多线程的时候甚至到了一百多万。

为什么会达到这么高的Ops呢?下面给大家说一下:

Bench会分根据线程数分多组进行添删改压力测试;

rand参数,是否随机产生key/value;

batch批大小,分批执行读写操作,借助GetAll/SetAll进行优化。

3、Redis中NB的函数来提升性能

上面的操作如果大家都掌握了就基本算Redis入门了,接下来进行进阶。如果能全然吃透,差不多就会比别人更胜一筹了。

GetAll()与SetAll()

GetAll:比方说我要取十个key,这个时候可以用getall。这时候Redis就执行了一次命令。比方说我要取10个key那么用get的话要取10次,如果用getall的话要用1次。1次getall时间大概是get的一点几倍,但是10次get的话就是10倍的时间,这个账你应该会算吧?强烈推荐大家用getall。

setall跟getall相似,批量设置K-V。

setall与getall性能很恐怖,官方公布的Ops也就10万左右,为什么我们的测试轻轻松松到五十万甚至上百万?因为我们就用了setall,getall。如果get,set两次以上,建议用getall,setall。

Redis管道Pipeline

比如执行10次命令会打包成一个包集体发过去执行,这里实现的方式是StartPipeline()开始,StopPipeline()结束中间的代码就会以管道的形式执行。

这里推荐使用更强的武器,AutoPipeline自动管道属性。管道操作到一定数量时,自动提交,默认0。使用了AutoPipeline,就不需要StartPipeline,StopPipeline指定管道的开始结束了。

Add与Replace

Add:Redis中没有这个Key就添加,有了就不要添加,返回false;

Replace:有则替换,还会返回原来的值,没有则不进行操作。

Add跟Replace就是实现Redis分布式锁的关键。

三、Redis使用技巧,经验分享

在项目的Readme中,这里摘录下:

1、特性

在ZTO大数据实时计算广泛应用,200多个Redis实例稳定工作一年多,每天处理近1亿包裹数据,日均调用量80亿次;

低延迟,Get/Set操作平均耗时200~600us(含往返网络通信);

大吞吐,自带连接池,最大支持1000并发;

高性能,支持二进制序列化(默认用的json,json很低效,转成二进制性能会提升很多)。

2、Redis经验分享

在Linux上多实例部署,实例个数等于处理器个数,各实例最大内存直接为本机物理内存,避免单个实例内存撑爆(比方说8核心处理器,那么就部署8个实例)。

把海量数据(10亿+)根据key哈希(Crc16/Crc32)存放在多个实例上,读写性能成倍增长。

采用二进制序列化,而非常见的Json序列化。

合理设计每一对Key的Value大小,包括但不限于使用批量获取,原则是让每次网络包控制在1.4k字节附近,减少通信次数(实际经验几十k,几百k也是没问题的)。

Redis客户端的Get/Set操作平均耗时200~600us(含往返网络通信),以此为参考评估网络环境和Redis客户端组件(达不到就看一下网络,序列化方式等等)。

使用管道Pipeline合并一批命令。

Redis的主要性能瓶颈是序列化、网络带宽和内存大小,滥用时处理器也会达到瓶颈。

其它可查优化技巧。

以上经验,源自于300多个实例4T以上空间一年多稳定工作的经验,并按照重要程度排了先后顺序,可根据场景需要酌情采用。

3、缓存Redis的兄弟姐妹

Redis实现ICache接口,它的孪生兄弟MemoryCache,内存缓存,千万级吞吐率。

各应用强烈建议使用ICache接口编码设计,小数据时使用MemoryCache实现;数据增大(10万)以后,改用Redis实现,不需要修改业务代码。

四、关于一些疑问的回复

这一Part我们会来聊聊大数据中Redis使用的经验:

Q1:一条数据多个key怎么设置比较合理?

A1:如果对性能要求不是很高直接用json序列化实体就好,没必要使用字典进行存储。

Q2:队列跟List有什么区别?左进右出的话用List还是用队列比较好?

A2:队列其实就是用List实现的,也是基于List封装的。左进右出的话直接队列就好。Redis的List结构比较有意思,既可以左进右出,也能右进左出。所以它既可以实现列表结构,也能队列,还能实现栈。

Q3:存放多个字段的类性能一样吗?

A3:大部分场景都不会有偏差,可能对于大公司数据量比较大的场景会有些偏差。

Q4:大数据写入到数据库之后,比如数据到亿以上的时候,统计分析、查询这块,能不能分享些经验。

A4:分表分库,拆分到一千万以内。

Q5:CPU为何暴涨?

A5:程序员终极理念——CPU达到百分百,然后性能达到最优,尽量不要浪费。最痛恨的是——如果CPU不到百分百,性能没法提升了,说明代码有问题。

)

虽然Redis大家会用,但是我们可能平时不会有像这样的大数据使用场景。希望本文能够给大家一些值得借鉴的经验。

本文转载自: 掘金

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

【Terraform】云基础设施变更-Azure 虚拟网络

发表于 2021-10-20

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

在上一篇文章《云基础设施创建-Azure 资源组》中,我们使用TerraForm创建第一个云基础设施Resouce Group(资源组)。在这篇文章,我们将通过定义引用资源组并且向资源组添加标记的其他资源来修改已经部署的基础设施。

一、代码编码虚拟网络

在上文的main.tf文件中添加新的资源块,资源为虚拟网络。

1
2
3
4
5
6
7
ini复制代码# 创建虚拟网络
resource "azurerm_virtual_network" "vnet" {
name = "myTFVnet"
address_space = ["10.0.0.0/16"]
location = "westus2"
resource_group_name = azurerm_resource_group.rg.name
}

为了创建一个新的Azure VNet,Azure要求必须指定要包含VNET的资源组的名称,即上一篇文章我们创建的资源。通过引用资源组,我们可以在资源之间建立依赖项。TerraForm可确保通过为配置构造依赖图来以适当的顺序创建资源。

二、新部署改变项

更改配置后,我们将运行Terraform再次部署,在正式部署之前,我们预览以下改变项。

1
2
bash复制代码#预览部署计划,查看改变项
$terraform plan

输出结果如下:

图片.png
图上显示,有一个资源添加,没有改变项,没有销毁。接下来执行部署计划。

1
2
bash复制代码#应用部署计划
$terraform apply

输入yes,输出结果如下:

图片.png

显示虚拟网络资源Vnet资源创建成功。

提示:需要注意的是,在plan和apply时,等待的时间会随着资源的数量增加而显著增加,需要耐心的等待。

检查Azure门户,如下图所示,显示资源创建成功。

图片.png

然后检查资源之间关联的关系,可以用到terraform show命令。

1
bash复制代码$terraform show

输出结果如下:

图片.png

本文转载自: 掘金

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

Golang通脉之指针

发表于 2021-10-20

指针的概念

指针是存储另一个变量的内存地址的变量。

变量是一种使用方便的占位符,用于引用计算机内存地址。

一个指针变量可以指向任何一个值的内存地址。

在上面的图中,变量b的值为156,存储在内存地址0x1040a124。变量a持有b的地址,现在a被认为指向b。

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。

Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

声明指针

声明指针,*T是指针变量的类型,它指向T类型的值。

1
go复制代码var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。

1
2
go复制代码var ip *int        /* 指向整型*/
var fp *float32 /* 指向浮点型 */

示例代码:

1
2
3
4
5
6
7
8
9
10
11
go复制代码func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */

ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量的存储地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}

运行结果:

1
2
3
go复制代码a 变量的地址是: 20818a220
ip 变量的存储地址: 20818a220
*ip 变量的值: 20

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码type name int8
type first struct {
a int
b bool
name
}

func main() {
a := new(first)
a.a = 1
a.name = 11
fmt.Println(a.b, a.a, a.name)
}

运行结果:

1
go复制代码false 1 11

未初始化的变量自动赋上初始值

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码type name int8
type first struct {
a int
b bool
name
}

func main() {
var a = first{1, false, 2}
var b *first = &a
fmt.Println(a.b, a.a, a.name, &a, b.a, &b, (*b).a)
}

运行结果:

1
go复制代码false 1 2 &{1 false 2} 1 0xc042068018 1

获取指针地址在指针变量前加&的方式

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。

取变量指针的语法如下:

1
go复制代码ptr := &v    // v的类型为T       取v的地址   其实就是把v的地址引用给了ptr,此时v和ptr指向了同一块内存地址,任一变量值的修改都会影响另一个变量的值

其中:

  • v:代表被取地址的变量,类型为T
  • ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
1
2
3
4
5
6
7
go复制代码func main() {
var a int = 10
fmt.Printf("变量a的地址: %x\n", &a )
b := &a
fmt.Printf("变量b: %v\n", b )
fmt.Printf("变量b的地址: %v\n", &b )
}

运行结果:

1
2
3
less复制代码变量a的地址: 0x20818a220
变量b: 0x20818a220
变量b的地址: 0x263e94530

b := &a的图示:

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

1
2
3
4
5
6
7
8
9
go复制代码func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}

输出如下:

1
2
3
go复制代码type of b:*int
type of c:int
value of c:10

总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

使用指针传递函数的参数

1
2
3
4
5
6
7
8
9
10
go复制代码func change(val *int) {  
*val = 55
}
func main() {
a := 58
fmt.Println("value of a before function call is",a)
b := &a
change(b)
fmt.Println("value of a after function call is", a)
}

运行结果

1
2
go复制代码value of a before function call is 58  
value of a after function call is 55

不要将一个指向数组的指针传递给函数。使用切片。

假设想对函数内的数组进行一些修改,并且对调用者可以看到函数内的数组所做的更改。一种方法是将一个指向数组的指针传递给函数。

1
2
3
4
5
6
7
8
9
go复制代码func modify(arr *[3]int) {  
(*arr)[0] = 90
}

func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}

运行结果

1
csharp复制代码[90 90 91]

示例代码:

1
2
3
4
5
6
7
8
9
go复制代码func modify(arr *[3]int) {  
arr[0] = 90
}

func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}

运行结果

1
csharp复制代码[90 90 91]

虽然将指针传递给一个数组作为函数的参数并对其进行修改,但这并不是实现这一目标的惯用方法。切片是首选:

1
2
3
4
5
6
7
8
9
go复制代码func modify(sls []int) {  
sls[0] = 90
}

func main() {
a := [3]int{89, 90, 91}
modify(a[:])
fmt.Println(a)
}

运行结果:

1
csharp复制代码[90 90 91]

Go不支持指针算法。

1
2
3
4
5
go复制代码func main() {
b := [...]int{109, 110, 111} p := &b p++
}

nvalid operation: p++ (non-numeric type *[3]int)

指针数组

有一种情况,我们可能需要保存数组,这样就需要使用到指针。

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

func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;

for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}

for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
结果
a[0] = 10
a[1] = 100
a[2] = 200

指针的指针

如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。

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

var a int
var ptr *int
var pptr **int

a = 3000

/* 指针 ptr 地址 */
ptr = &a

/* 指向指针 ptr 地址 */
pptr = &ptr

/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a )
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
结果
变量 a = 3000
指针变量 *ptr = 3000
指向指针的指针变量 **pptr = 3000

指针作为函数参数

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

import "fmt"

func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200

fmt.Printf("交换前 a 的值 : %d\n", a )
fmt.Printf("交换前 b 的值 : %d\n", b )

/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b);

fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}

func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
}
结果
交换前 a 的值 : 100
交换前 b 的值 : 200
交换后 a 的值 : 200
交换后 b 的值 : 100

空指针

Go 空指针 当一个指针被定义后没有分配到任何变量时,它的值为 nil。 nil 指针也称为空指针。 nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。 一个指针变量通常缩写为 ptr。

空指针判断:

1
2
go复制代码if(ptr != nil)     /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */
1
2
3
4
5
go复制代码func main() {
var sp *string
*sp = "张三"
fmt.Println(*sp)
}

运行这些代码,会看到如下错误信息:

1
go复制代码panic: runtime error: invalid memory address or nil pointer dereference

这是因为指针类型的变量如果没有分配内存,就默认是零值 nil,它没有指向的内存,所以无法使用,强行使用就会得到以上 nil 指针错误。

指针使用

  1. 指针可以修改指向数据的值;
  2. 在变量赋值,参数传值的时候可以节省内存。

注意事项

  1. 不要对 map、slice、channel 这类引用类型使用指针;
  2. 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  3. 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  4. 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  5. 像 int、bool 这样的小数据类型没必要使用指针;
  6. 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  7. 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使代码变得异常复杂。

new 和 make

我们知道对于值类型来说,即使只声明一个变量,没有对其初始化,该变量也会有分配好的内存。

1
2
3
4
go复制代码func main() {
var s string
fmt.Printf("%p\n",&s)
}

结构体也是值类型,比如 var wg sync.WaitGroup 声明的变量 wg ,不进行初始化也可以直接使用,Go 语言自动分配了内存,所以可以直接使用,不会报 nil 异常。

于是可以得到结论:如果要对一个变量赋值,这个变量必须有对应的分配好的内存,这样才可以对这块内存操作,完成赋值的目的。

其实不止赋值操作,对于指针变量,如果没有分配内存,取值操作一样会报 nil 异常,因为没有可以操作的内存。

所以一个变量必须要经过声明、内存分配才能赋值,才可以在声明的时候进行初始化。指针类型在声明的时候,Go 语言并没有自动分配内存,所以不能对其进行赋值操作,这和值类型不一样。map 和 chan 也一样,因为它们本质上也是指针类型。

要分配内存,就引出来了内置函数new()和make()。 Go语言中new和make是内建的两个函数,主要用来分配内存。

new

new是一个内置的函数,它的函数签名如下:

1
2
3
4
go复制代码// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

其中,

  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

它的作用就是根据传入的类型申请一块内存,然后返回指向这块内存的指针,指针指向的数据就是该类型的零值:

1
2
3
4
5
6
7
8
go复制代码func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}

通过 new 函数分配内存并返回指向该内存的指针后,就可以通过该指针对这块内存进行赋值、取值等操作。

make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

1
go复制代码func make(t Type, size ...IntegerType) Type

在使用 make 函数创建 map 的时候,其实调用的是 makemap 函数:

1
2
3
4
go复制代码// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap{
//省略无关代码
}

makemap 函数返回的是 *hmap 类型,而 hmap 是一个结构体,它的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}

可以看到, map 关键字其实非常复杂,它包含 map 的大小 count、存储桶 buckets 等。要想使用这样的 hmap,不是简单地通过 new 函数返回一个 *hmap 就可以,还需要对其进行初始化,这就是 make 函数要做的事情:

1
go复制代码m:=make(map[string]int,10)

其实 make 函数就是 map 类型的工厂函数,它可以根据传递它的 K-V 键值对类型,创建不同类型的 map,同时可以初始化 map 的大小。

make 函数不只是 map 类型的工厂函数,还是 chan、slice 的工厂函数。它同时可以用于 slice、chan 和 map 这三种类型的初始化。

make函数是无可替代的,在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

new与make的区别

  1. 二者都是用来做内存分配的。
  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向对应类型零值的指针。

本文转载自: 掘金

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

web前端期末大作业❤️酷炫响应式汽车租赁网页设计❤️(HT

发表于 2021-10-20

​
小知识,大挑战!本文正在参与「程序员必备小知识」创作活动​

临近期末, 你还在为HTML网页设计结课作业,老师的作业要求感到头大?网页要求的总数量太多?HTML网页作业无从下手?没有合适的模板?等等一系列问题。你想要解决的问题,在这里都能解决
常见网页设计作业题材有 个人、 美食、 公司、体育、 化妆品、 物流、 环保、 书籍、 婚纱、 军事、 游戏、 节日、 戒烟、 电影、 摄影 学校、 旅游、 电商、 宠物、 电器、 茶叶、 家居、 酒店、 舞蹈、 动漫、 明星、 服装、 文化、 家乡、 鲜花、 礼品、 汽车、 其他等网页设计题目, A+水平作业, 可满足大学生网页大作业网页设计需求都能满足你的需求。原始HTML+CSS+JS页面设计, web大学生网页设计作业源码,这是一个不错的电竞博客网页制作,画面精明,非常适合初学者学习使用。

视频演示: Web前端期末大作业课程–响应式汽车租赁网页设计.mp4

网页实现截图:

网站首页

​)​

)​

关于我们

汽车租赁业被称为交通运输服务行,它因为无须办理保险、无须年检维修、车型可随意更换等优点,以租车代替买车来控制企业成本,这种在外企中十分流行的管理方式,正慢慢受到国内企事业单位和个人用户的青睐。汽车租赁是指将汽车的资产使用权从拥有权中分开,出租人具有资产所有权,承租人拥有资产使用权,出租人与承租人签订租赁合同,以交换使用权利的一种交易形式。

汽车租赁是指在约定时间内,租赁经营人将租赁汽车(包括载货汽车和载客汽车)交付承租人使用,不提供驾驶劳务的经营方式。汽车租赁的实质是在将汽车的产权与使用权分开的基础上,通过出租汽车的使用权而获取收益的一种经营行为,其出租标的除了实物汽车以外,还包含保证该车辆正常、合法上路行驶的所有手续与相关价值。不同于一般汽车出租业务的是,在租赁期间,承租人自行承担驾驶职责。汽车租赁业的核心思想是资源共享,服务社会。

)​

车量展示:

)​

)​

新闻资讯:

)​

​​

)​

客户案例:

)​

专业司机:

)​

)​

在线预约:

)​

联系我们:

)​

​
​

项目组织结构:

)​

主要源码展示:

index.html

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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
xml复制代码<!DOCTYPE html>

<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>(自适应手机版)黄色响应式车行汽车租赁网站模板 二手车销售出租公司网站</title>
<meta name="keywords" content="汽车租赁模板,二手车网站模板" />

</head>

<body>
<header class="index-header">
<div class="index-head">
<div class="wd1200"> <a href="/518/" class="logo"> <img src="/518/style/images/logo.png" alt="(自适应手机版)黄色响应式车行汽车租赁网站模板 二手车销售出租公司网站模板下载 - AB模板网"> </a>
<ul class="nav">
<li><a href="/518/index.html" target="_self">首 页</a> </li>

<li><a href="/518/about/">关于我们</a></li>

<li><a href="/518/car/">车辆展示</a></li>

<li><a href="/518/news/">新闻资讯</a></li>

<li><a href="/518/case/">客户案例</a></li>

<li><a href="/518/siji/">专业司机</a></li>

<li><a href="/518/yuyue/">在线预约</a></li>

<li><a href="/518/contact/">联系我们</a></li>

</ul>

<!-- 手机端导航 -->

<div class="m-header"> <span class="box"> <span class="line line1"></span> <span class="line line2"></span> <span class="line line3"></span> </span>
</div>
<div class="m-header-menu">
<div class="m-menu-top f-cb"> <a href="/518/" class="fl title"><img src="/518/style/images/logo2.png" alt="(自适应手机版)黄色响应式车行汽车租赁网站模板 二手车销售出租公司网站模板下载 - AB模板网" /></a> <span class="fr close"></span> </div>
<div class="m-header-list">
<ul>
<li> <a class="col-box" href="/S437/" target="_self"> <span class="tit">首 页</span> </a> </li>

<li> <a class="col-box" href="/518/about/" target="_self"> <span class="tit">关于我们</span> </a> </li>

<li> <a class="col-box" href="/518/car/" target="_self"> <span class="tit">车辆展示</span> </a> </li>

<li> <a class="col-box" href="/518/news/" target="_self"> <span class="tit">新闻资讯</span> </a> </li>

<li> <a class="col-box" href="/518/case/" target="_self"> <span class="tit">客户案例</span> </a> </li>

<li> <a class="col-box" href="/518/siji/" target="_self"> <span class="tit">专业司机</span> </a> </li>

<li> <a class="col-box" href="/518/yuyue/" target="_self"> <span class="tit">在线预约</span> </a> </li>

<li> <a class="col-box" href="/518/contact/" target="_self"> <span class="tit">联系我们</span> </a> </li>

</ul>
</div>
<div class="m-header-menu-sub">
<div class="boxs"></div>
<div class="boxs"></div>
<div class="boxs"></div>
<div class="boxs"></div>
<div class="boxs"></div>
<div class="boxs"></div>
<div class="boxs"></div>
</div>
</div>
<div class="m-shadow"></div>

<!-- 手机端导航 -->

</div>
</div>
<div class="swiper-container index-banner-swiper">
<div class="swiper-wrapper">
<div class="swiper-slide"> <img src="/518/uploads/allimg/210729/1-210H92019370-L.jpg" alt="幻灯片3"> </div>
<div class="swiper-slide"> <img src="/518/uploads/allimg/210729/1-210H92019230-L.jpg" alt="幻灯片2"> </div>
<div class="swiper-slide"> <img src="/518/uploads/allimg/210729/1-210H9201Z70-L.jpg" alt="幻灯片1"> </div>

</div>
<div class="swiper-pagination"></div>
</div>
</header>
<div class="index-search wow bounceInUp">
<div class="wd1200">
<div class="key"> <img src="/518/style/images/icon_hot.png" alt="" class="icon">
<div class="word"> <span class="title">搜索关键词:</span>
<div>

<a href='/518/car/c1/'>豪华型</a>

<a href='/518/car/c2/'>舒适型</a>

<a href='/518/car/c3/'>经济型</a>

<a href='/518/car/c4/'>越野SUV</a>

<a href='/518/car/c5/'>客车型</a>

</div>
</div>
</div>
<form name="formsearch" action="/518/plus/search.php">
<div class="search-box">
<div class="input">
<input type="text" name="q" id="kw" placeholder="请输入关键词" />
</div>
<div class="button">
<button type="submit"> <img src="/518/style/images/icon_search.png" alt=""> </button>
</div>
</div>
</form>
</div>
</div>
<div class="index-show">
<div class="wd1200">
<div class="index-Title">
<div class="title wow slideInUp">车辆展示</div>
<div class="intro wow slideInUp">给予客户清晰完美的用车解决方案!</div>
<div class="line wow slideInUp"></div>
</div>
<div class="show-nav wow slideInUp">
<ul>
<li class="li-active"> <a href="/518/car/">全部车型</a> </li>

<li><a href="/518/car/c1/">豪华型</a></li>

<li><a href="/518/car/c2/">舒适型</a></li>

<li><a href="/518/car/c3/">经济型</a></li>

<li><a href="/518/car/c4/">越野SUV</a></li>

<li><a href="/518/car/c5/">客车型</a></li>

</ul>
</div>
<div class="show-contain">
<div class="show-box wow slideInUp">
<div class="picture"> <img src="/518/uploads/allimg/210730/1-162L31146-F30.jpg" alt="迈巴赫"> </div>
<div class="info">
<div class="name">迈巴赫</div>
<div class="intro">迈巴赫(德文:Maybach)与迈巴赫引擎制造厂(德文:Maybach-Motorenbau GmbH)是曾经在1921年到1940年间活跃于欧洲地区的德国超豪华汽车品牌与制造厂。 车厂创始人卡尔迈巴赫(Karl Maybach)...</div>
<div class="photo"><img src='/518/uploads/allimg/210730/1-162L31146-6317.jpg'><img src='/518/uploads/allimg/210730/1-162L31146-2637.jpg'><img src='/518/uploads/allimg/210730/1-162L31146-F30.jpg'></div>
<div class="bot"> <a href="/518/car/c1/23.html" class="detail">查看详细+</a>
<div>¥<strong>1200</strong>/天</div>
</div>
</div>
</div>

<div class="show-car wow slideInUp">
<a href="/518/car/c3/22.html" class="car-item">
<div class="tu"> <img src="/518/uploads/allimg/210730/1-162L30923-9619.jpg" alt="宝马7系"> </div>
<div class="info"> <span>宝马7系</span>
<div>¥<strong>800</strong>/天</div>
</div>
</a>
<a href="/518/car/c1/2.html" class="car-item">
<div class="tu"> <img src="/518/uploads/allimg/210728/1-162JP4U-5634.jpg" alt="奔驰S级"> </div>
<div class="info"> <span>奔驰S级</span>
<div>¥<strong>800</strong>/天</div>
</div>
</a>
<a href="/518/car/c1/1.html" class="car-item">
<div class="tu"> <img src="/518/uploads/allimg/210728/1-162JP205-5921.jpg" alt="奥迪"> </div>
<div class="info"> <span>奥迪</span>
<div>¥<strong>600</strong>/天</div>
</div>
</a>

</div>
</div>
</div>
</div>
<div class="index-reason">
<div class="index-Title">
<div class="title wow slideInUp">选择我们的 <strong style="color: #3186E0 ;">四大理由</strong></div>
<div class="intro wow slideInUp">给予客户清晰完美的解决方案!</div>
<div class="line wow slideInUp"></div>
</div>
<div class="reason-nav wd1200 wow slideInUp">
<ul>
<li class='li-active'>
<div class="sanjiao"></div>
<img src="/518/style/images/ly1.png" alt="" class="icon icon1"> <img src="/518/style/images/ly1_h.png" alt="" class="icon icon2">
<div class="word">
<div class="title">实力雄厚</div>
<div class="intro">16年租车经验</div>
</div>
</li>
<li>
<div class="sanjiao"></div>
<img src="/518/style/images/ly2.png" alt="" class="icon icon1"> <img src="/518/style/images/ly2_h.png" alt="" class="icon icon2">
<div class="word">
<div class="title">质高价优</div>
<div class="intro">合理的价格</div>
</div>
</li>
<li>
<div class="sanjiao"></div>
<img src="/518/style/images/ly3.png" alt="" class="icon icon1"> <img src="/518/style/images/ly3_h.png" alt="" class="icon icon2">
<div class="word">
<div class="title">完善服务</div>
<div class="intro">优质服务体系</div>
</div>
</li>
<li>
<div class="sanjiao"></div>
<img src="/518/style/images/ly4.png" alt="" class="icon icon1"> <img src="/518/style/images/ly4_h.png" alt="" class="icon icon2">
<div class="word">
<div class="title">专业售后</div>
<div class="intro">专业团队保障</div>
</div>
</li>
</ul>
</div>
<div class="reason-contain" style='background-image: url(/518/style/images/ly.jpg);'>
<div class="wd1200">
<div class="promise">
<h3 class="wow slideInUp">四大服务承诺</h3>
<div class="intro wow slideInUp">免除您的后顾之忧</div>
<div class="line wow slideInUp"></div>
<div class="seave wow slideInUp">
<div class="strip">
<div></div>
<span>24小时客服问题响应服务</span> </div>
<div class="strip">
<div></div>
<span>7*24售后热线服务</span> </div>
<div class="strip">
<div></div>
<span>优质车源保证安全</span> </div>
<div class="strip">
<div></div>
<span>专家指导服务</span> </div>
</div>

<div class="wd1200">Copyright &copy; 2021 通用企业汽车租赁有限公司<a href="https://beian.miit.gov.cn/" target="_blank" rel="nofollow">苏ICP12345678</a> <a href="/sitemap.xml" target="_blank">XML地图</a></div>
</div>
</footer>
<img src="/518/style/images/go-top.png" alt="" id="go-top">
<script src="/518/style/js/swiper.min.js"></script>
<script src="/518/style/js/common.js"></script>
<script src="/518/style/js/wow.js"></script>
<script>
$(function() {
$('#tj2').click(function() {
//alert(1)
if ($('#name2').val() == '') {
alert('请输入您的姓名!');
$("#name2").focus();
return false;
}
if ($("#tel2").val() == "") {
alert("请输入你的手机!");
$("#tel2").focus();
return false;
}
if (!$("#tel2").val().match(/^(((13[0-9]{1})|(14[0-9]{1})|(15[0-9]{1})|(16[0-9]{1})|(17[0-9]{1})|(18[0-9]{1})|(19[0-9]{1}))+\d{8})$/)) {
alert("手机号码格式不正确!");
$("#tel2").focus();
return false;
}
})
})
</script>
<script src="/518/style/js/index.js"></script>
<script src="/518/style/js/jquery.kxbdmarquee.js"></script>
<script type="text/javascript">
(function() {

$("#marquee4").kxbdMarquee({
direction: "up",
isEqual: false
});

})();
</script>
</body>

</html>

style.css

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
css复制代码header .index-head .wd1200 {
display: flex;
align-items: center;
justify-content: space-between;
}

header .index-head .wd1200 .logo img {
height: 65px;
}

header .index-head .wd1200 .nav {
display: flex;
align-items: center;
}

header .index-head .wd1200 .nav li {
height: 90px;
padding: 0 20px;
border-bottom: 3px solid transparent;
transition: 0.3s;
}

header .index-head .wd1200 .nav li a {
font-size: 16px;
transition: 0.3s;
color: #fff
}

header .index-head .wd1200 .nav .li-active {
border-bottom: 3px solid #3186E0;
}

作品来自于网络收集整理、侵权立删

大家点赞、收藏、关注、评论啦 、*

打卡 文章 更新 83/ 100天

本文转载自: 掘金

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

你知不知道,线程之间是怎么通信的?

发表于 2021-10-20

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

简介:

线程开始运行,拥有自己的栈空间,就会如同一个脚本一样,按照既定的代码一步步的执行,直到终止。但是,如果每个线程之间都是孤立的,那么它们的价值就会很少;反之,如果多个线程能够配合着完成工作,将会带来各方面巨大的收益。

1、volatile和synchronized关键字

说明:(不做过多说明,需要的话可以看我的往期)

Java支持多线程访问一个对象或者对象的成员变量,由于每个线程都拥有这个变量的拷贝(为了执行速度更快),所以程序执行过程中读取的数据往往不是最新的。

关键字volatile可以用来修饰字段(成员变量),作用通俗来讲就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新到共享内存中,volatile能保证线程对变量的可见性。

关键字synchronized可以修饰方法或者同步代码块的形式来进行使用,它主要能确保多个线程在同一时刻,只有一个线程处于方法或者同步代码块中,它保证了线程对变量访问的可见性和排他性。

通过使用javap工具查看生成class文件信息来分析下synchronized关键字的实现细节,如下代码是使用了同步块和同步方法。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码 package com.lizba.p3;

/**
* <p>
* 同步方法和同步代码块示例代码
* </p>
*
* @Author: Liziba
* @Date: 2021/6/15 22:13
*/
public class Synchronized {

public static void main(String[] args) {

// 同步代码块
synchronized (Synchronized.class) {

}
// 静态方法
method();
}

public static synchronized void method() {}

}

在Synchronized.class同级目录执行javap -v Synchronized.class

1
arduino复制代码javap -v Synchronized.class

重点关注部分输出:

  • 同步代码块使用monitorenter和monitorexit指令

  • 同步方法使用了ACC_SYNCHRONIZED

总结:

同步代码块和同步方法使用了不同的方式来加锁,其本质上都是对一个对象的监视器(monitor)获取,而这个获取的过程是排他的,也就是说同一时刻只会有一个线程获取由synchronized所保护的监视器。我们知道,任意一个对象都拥有自己的监视器锁,当这个对象由同步代码块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该线程对象的监视器锁才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步代码块和同步方法的入口处,进入BLOCKED状态。

图示对象、对象的监视器、同步队列和执行线程之间的关系


总结上图:

任意线程对Object(受Synchronized保护)的访问,首先要获取Object的监视器。如果获取失败则进入同步队列,线程变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

2、等待通知机制

一个线程修改了一个对象的值,另一个对象感知到其的变化,然后进行相应的操作,这种类似于生产者-消费者模式的功能,在Java线程之间是怎么实现的呢?

最简单的做法:

1
2
3
4
5
6
scss复制代码// 使用while循环检测变量的值
while (flag) {
// 防止一直执行,未满足条件进行短暂睡眠
Thread.sleep(1000);
}
doSomething();

上述代码存在问题:

  1. 不能确保及时性,通过睡眠的方式来释放处理器资源,会导致时效性问题
  2. 难以降低开销,通过降低睡眠的时间来提升时效性又会带来过高的处理器资源开销

Java内置解决办法:

以上两个看似矛盾的问题,却可以通过Java内置的等待/通知机制很好的得以解决,等待/通知机制是Java任意对象都具备的,因为这些方法被定义在对象的超类Object中。

1
2
3
4
5
java复制代码public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException
方法名称 描述
notify() 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁
notifyAll() 通知所有等待在该对象上的线程
wait() 调用方的线程进入WAITING状态,只有等待其他线程的通知或被中断才会返回,需要注意,调用wait()方法会释放锁
wait(long) 超时等待一段时间,如果时间到没有通知就超时返回。单位ms
wait(long, int) 对于超时时间做更加细粒度的控制可以精确到纳秒

等待/通知机制描述:

等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。对象O上的wait()和notify()/notifyAll()就好比一个开关信号,用来完成等待方和通知方的交互工作(就好比一开始说的生产者-消费者模型)

示例代码:

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
java复制代码package com.lizba.p3;

import com.lizba.p2.SleepUtil;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* <p>
* wait()和notify()/notifyAll()示例代码
* </p>
*
* @Author: Liziba
* @Date: 2021/6/15 23:28
*/
public class WaitNotify {

static boolean flag = true;
static Object lock = new Object();
static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

public static void main(String[] args) {
Thread waitThread = new Thread(new Wait(), "waitThread");
waitThread.start();
SleepUtil.sleepSecond(1);
Thread notifyThread = new Thread(new Notify(), "notifyThread");
notifyThread.start();
}


/**
* wait线程,当条件不满足时wait()
*/
static class Wait implements Runnable{

@Override
public void run() {
// 加锁
synchronized(lock) {
// 当条件不满足时,继续wait
while (flag) {
System.out.println(Thread.currentThread()
+ " flag is true. wait at " +sdf.format(new Date()));
try {
// 此操作会释放锁
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 满足条件是完成工作
System.out.println(Thread.currentThread()
+ " flag is false. finished at " + sdf.format(new Date()));
}
}
}

static class Notify implements Runnable {

@Override
public void run() {
// 加锁
synchronized (lock) {
// 获取到锁或通知等待在锁上的线程
// 通知不会释放锁,直到当前线程执行完释放lock锁后,waitThread才能从wait方法返回
System.out.println(Thread.currentThread()
+ "hold lock. notify at " + sdf.format(new Date()));
lock.notifyAll();
flag = false;
SleepUtil.sleepSecond(5);
}
// 再次加锁
synchronized (lock) {
System.out.println(Thread.currentThread()
+ "hold lock again. notify at " + sdf.format(new Date()));
SleepUtil.sleepSecond(5);
}
}
}

}

查看输出:

注意上述的hold lock again 和 flag is flase这两行代码可能执行顺序会互换。

总结:

  1. 使用wait()、notify()和notifyAll()需要先对该对象加锁
  2. 调用wait()方法后线程由RUNNING状态变为WAITING状态,并且将当前线程放置到对象的等待队列中
  1. notify()方法和notifyAll()调用后,等待的线程需要等到调用notify()和notifyAll()的线程释放锁后,等待队列中的线程才有机会从wait()返回
  2. notify()移动一个线程从等待队列到同步队列,notifyAll()移动所有等待线程,过程是将线程从等待队列移动到同步队列中,被移动的线程由WAITING变为BLOCKED状态
  1. 从wait()方法返回的前提是获取了对象的锁
  2. wait()、notify()和notifyAll()机制依赖的是同步机制,其目的是为了从wait()方法返回的线程能感知到其他线程对变量作出的修改

图示上述过程:

总结上图:

WaitThread线程首先获取了锁,然后调用对象的wait()方法,从而释放了锁进入对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并且调用了对象的notify()方法,将处于等待队列WaitQueue的WaitThread移动到了SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁从wait()方法返回继续执行。

3、等待/通知的经典范式

等待/通知的经典范式,分为等待方和通知方,这两者需要分别遵循如下规则。

等待方遵循如下规则:

  1. 获取对象的锁
  2. 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
  1. 条件满足则执行对应的逻辑
1
2
3
4
5
6
7
scss复制代码// 示例等待方伪代码
synchronized(对象) {
while(条件不满足) {
对象.wait();
}
// ToDo...
}

通知方遵循如下规则:

  • 获取对象的锁
  • 改变条件
  • 通知所有等待在对象上的线程
1
2
3
4
5
scss复制代码// 示例通知方伪代码
synchronized(对象) {
改变条件
对象.notifyAll();
}

4、管道输入/输出流

管道输入/输出流和普通文件输入/输出流或者网络输入/输出流的不同之处在于,管道输出/输出流主要用于线程之间的数据传输,传输的媒介为内存。

管道输入/输出流的具体实现:

  1. PipedInputStream
  2. PipedOutputStream
  1. PipedReader
  2. PipedWriter

1、2为字节流,3、4为字符流。

示例代码:

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
java复制代码package com.lizba.p3;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

/**
* <p>
* 管道流
* </p>
*
* @Author: Liziba
* @Date: 2021/6/16 21:07
*/
public class Piped {

public static void main(String[] args) throws IOException {
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
// 输入输出流连接(不连接会报错)
out.connect(in);
Thread printThread = new Thread(new Print(in), "PrintThread");
printThread.start();

// 输入
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}



/**
* 单个字符读取并输出
*
*/
static class Print implements Runnable {
private PipedReader in;

public Print(PipedReader in) {
this.in = in;
}

@Override
public void run() {
int receive = 0;
try {
while (true) {
// 单个字符读取
if ((receive = in.read()) != -1){
System.out.print((char)receive);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

}

测试代码样例:

1
2
3
4
shell复制代码## 输入
hello liziba
## 输出
hello liziba

\

5、Thread.join()

Thread.join()的语义含义:当前线程A等待Thread线程终止之后才从Thread.join()处返回。线程提供的join()方法的api如下:

1
2
3
4
java复制代码public final void join() throws InterruptedException
// 下面两个具有超时等待,线程再给定的时间没有返回,那么超时的方法会返回
public final synchronized void join(long millis, int nanos)
public final synchronized void join(long millis)

示例代码:

设置十个线程,分别从0-9,每个线程需要调用前一个线程的join()方法, 比如线程0结束了,线程1才能从join()返回线,程1结束了,线程2才能从join()返回。

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
java复制代码package com.lizba.p3;

import com.lizba.p2.SleepUtil;

import java.util.concurrent.TimeUnit;

/**
* <p>
* join()等待通知机制
* </p>
*
* @Author: Liziba
* @Date: 2021/6/16 21:25
*/
public class Join {

public static void main(String[] args) {
// 前一个线程
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new Domino(previous), String.valueOf(i));
t.start();
previous = t;
}
SleepUtil.sleepSecond(5);
System.out.println(Thread.currentThread().getName() + " end.");

}

static class Domino implements Runnable {
private Thread thread;

public Domino(Thread thread) {
this.thread = thread;
}

@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end.");
}
}

}

查看输出结果:

总结上述代码:

每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()返回,这里涉及了等待/通知机制,具体原理我们可以通过看JDK的源码来了解:

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
arduino复制代码 public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 超时等待时间未设置则为0,也就是join()方法
if (millis == 0) {
// 判断当前线程是否终止
while (isAlive()) {
// 如果未终止,继续wait()
wait(0);
}
} else {
// 判断当前线程是否终止
while (isAlive()) {
long delay = millis - now;
// 判断超时等待时间是否已经到了,如果到了则返回
if (delay <= 0) {
break;
}
// 否则继续等待,计算新的时间传入
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}


// 尝试判断当前线程时候已经执行完毕(是否还活着)
public final native boolean isAlive();

\

6、ThreadLocal的使用

本文不会详细讲述ThreadLocal的核心原理,之后简单的介绍ThreadLocal的使用,后续会单独分一篇文章来详述其原理和使用。

ThreadLocal即线程变量,它是以ThreadLocal对象为键、任意对象为值的存储结构。这个存储结构可以附带在线程上,我们可以通过一个ThreadLocal对象来查询绑定在这个线程上的一个值。

示例代码:

如下代码构造一个计算方法调用时间计算的类。

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
typescript复制代码package com.lizba.p3;

import com.lizba.p2.SleepUtil;

/**
* <p>
*
* </p>
*
* @Author: Liziba
* @Date: 2021/6/16 22:04
*/
public class Profiler {

private static final ThreadLocal<Long> TIME_THREAD_LOCAL = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return System.currentTimeMillis();
}
};


public static final void begin() {
TIME_THREAD_LOCAL.set(System.currentTimeMillis());
}

public static final Long end() {
return System.currentTimeMillis() - TIME_THREAD_LOCAL.get();
}


public static void main(String[] args) {
Profiler.begin();
SleepUtil.sleepSecond(1);
System.out.println("Cost: " + Profiler.end());
}
}

查看执行结果:

本文转载自: 掘金

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

面试官:双亲委派模型你了解吗?

发表于 2021-10-20

面试官:要不你今天来详细讲讲双亲委派机制?

候选者:嗯,好的。

候选者:上次提到了:class文件是通过「类加载器」装载至JVM中的

候选者:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上)

候选者:JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。

候选者:这应该很好理解吧?

面试官:雀食(确实)!

面试官:顺着话题,我想问问,打破双亲委派机制是什么意思?

候选者:很好理解啊,意思就是:只要我加载类的时候,不是从APPClassLoader->Ext ClassLoader->BootStrap ClassLoader 这个顺序找,那就算是打破了啊

候选者:因为加载class核心的方法在LoaderClass类的loadClass方法上(双亲委派机制的核心实现)

候选者:那只要我自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。

面试官:这么简单?

候选者:嗯,就是这么简单

面试官:那你知道有哪个场景破坏了双亲委派机制吗?

候选者:最明显的就Tomcat啊

面试官:详细说说?

候选者:在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序(:

候选者:是吧?

面试官:嗯..

候选者:那假设我现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User。但是他们的具体实现是不一样的

候选者:那么Tomcat是如何保证它们是不会冲突的呢?

候选者:答案就是,Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找(:

候选者:那这样就做到了Web应用层级的隔离

面试官:嗯,那你还知道Tomcat还有别的类加载器吗?

候选者:嗯,知道的

候选者:并不是Web应用程序下的所有依赖都需要隔离的,比如Redis就可以Web应用程序之间共享(如果有需要的话),因为如果版本相同,没必要每个Web应用程序都独自加载一份啊。

候选者:做法也很简单,Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。

候选者:(无非就是把需要应用程序之间需要共享的类放到一个共享目录下嘛)

面试官:嗯..

候选者:为了隔绝Web应用程序与Tomcat本身的类,又有类加载器(CatalinaClassLoader)来装载Tomcat本身的依赖

候选者:如果Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享

候选者:各个类加载器的加载目录可以到tomcat的catalina.properties配置文件上查看

候选者:我稍微画下Tomcat的类加载结构图吧,不然有点抽象

面试官:嗯,还可以,我听懂了,有点意思。

面试官:顺便,我想问下,JDBC你不是知道吗,听说它也是破坏了双亲委派模型的,你怎么理解的。

候选者:Eumm,这个有没有破坏,见仁见智吧。

候选者:JDBC定义了接口,具体实现类由各个厂商进行实现嘛(比如MySQL)

候选者:类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

候选者:我们用JDBC的时候,是使用DriverManager进而获取Connection,DriverManager在java.sql包下,显然是由BootStrap类加载器进行装载

候选者:当我们使用DriverManager.getConnection()时,得到的一定是厂商实现的类。

候选者:但BootStrap ClassLoader会能加载到各个厂商实现的类吗?

候选者:显然不可以啊,这些实现类又没在java包中,怎么可能加载得到呢

面试官:嗯..

候选者:DriverManager的解决方案就是,在DriverManager初始化的时候,得到「线程上下文加载器」

候选者:去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,而这里的线程上下文加载器实际上还是App ClassLoader

候选者:所以在获取Connection的时候,还是先找Ext ClassLoader和BootStrap ClassLoader,只不过这俩加载器肯定是加载不到的,最终会由App ClassLoader进行加载

面试官:嗯..

候选者:那这种情况,有的人觉得破坏了双亲委派机制,因为本来明明应该是由BootStrap ClassLoader进行加载的,结果你来了一手「线程上下文加载器」,改掉了「类加载器」

候选者:有的人觉得没破坏双亲委派机制,只是改成由「线程上下文加载器」进行类加载,但还是遵守着:「依次往上找父类加载器进行加载,都找不到时才由自身加载」。认为”原则”上是没变的。

面试官:那我了解了

本文总结:

  • 前置知识: JDK中默认类加载器有三个:AppClassLoader、Ext ClassLoader、BootStrap ClassLoader。AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。这里的父子关系并不是通过继承实现的,而是组合。
  • 什么是双亲委派机制: 加载器在加载过程中,先把类交由父类加载器进行加载,父类加载器没找到才由自身加载。
  • 双亲委派机制目的: 为了防止内存中存在多份同样的字节码(安全)
  • 类加载规则: 如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。
  • 如何打破双亲委派机制: 自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)
  • 打破双亲委派机制案例: Tomcat
+ 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器
+ 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载
+ 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类
+ 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器
+ ShareClassLoader、CatalinaClassLoader、CommonClassLoader的目录可以在Tomcat的catalina.properties进行配置
  • 线程上下文加载器: 由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。

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

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

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

原创不易!!求三连!!

本文转载自: 掘金

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

Kubernetes-Service介绍(三)-Ingres

发表于 2021-10-20

前言

本篇是Kubernetes第十篇,大家一定要把环境搭建起来,看是解决不了问题的,必须实战。

Kubernetes系列文章:
  1. Kubernetes介绍
  2. Kubernetes环境搭建
  3. Kubernetes-kubectl介绍
  4. Kubernetes-Pod介绍(-)
  5. Kubernetes-Pod介绍(二)-生命周期
  6. Kubernetes-Pod介绍(三)-Pod调度
  7. Kubernetes-Pod介绍(四)-Deployment
  8. Kubernetes-Service介绍(一)-基本概念
  9. Kubernetes-Service介绍(二)-服务发现

什么需要Ingress

Service是基于四层TCP和UDP协议转发的,而Ingress可以基于七层的HTTP和HTTPS协议转发,可以通过域名和路径做到更细粒度的划分,如下图所示:

img

Ingress请求过程

用户请求首先到达Ingress Controller,Ingress Controller根据Ingress的路由规则,查找到对应的Service,进而通过Endpoint查询到Pod的IP地址,然后将请求转发给Pod。

img

Ingress与Ingress Controller

简单来说,Ingress Controller是负责具体转发的组件,通过各种方式将它暴露在集群入口,外部对集群的请求流量会先到Ingress Controller,而Ingress对象是用来告诉Ingress Controller该如何转发请求,比如哪些域名哪些Path要转发到哪些服务等等。

Ingress Controller

Ingress Controller并不是Kubernetes自带的组件,实际上Ingress Controller只是一个统称,用户可以选择不同的Ingress Controller实现,目前,由Kubernetes维护的Ingress Controller只有Google的GCE与Ingress Nginx两个,其他还有很多第三方维护的Ingress Controller。但是不管哪一种Ingress Controller,实现的机制都基本一致,只是在具体配置上有差异。一般来说,Ingress Controller的形式都是一个Pod,里面跑着daemon程序和反向代理程序。daemon负责不断监控集群的变化,根据Ingress对象生成配置并应用新配置到反向代理,比如Nginx Ingress就是动态生成Nginx配置,动态更新upstream,并在需要的时候重新加载应用新配置。

Ingress

Ingress是一个API对象,和其他对象一样,通过yaml文件来配置。Ingress通过Http或Https暴露集群内部Service,给Service提供外部URL、负载均衡、SSL/TLS能力以及基于Host代理。

Ingress的部署

Deployment+LoadBalancer模式的Service

如果要把Ingress部署在公有云,那可以选择这种方式。用Deployment部署Ingress Controller,创建一个type为LoadBalancer的Service关联这组Pod。大部分公有云,都会为LoadBalancer的Service自动创建一个负载均衡器,通常还绑定了公网地址。只要把域名解析指向该地址,就实现了集群服务的对外暴露。此方案确定需要在公有云上部署。

img

Deployment+NodePort模式的Service

使用Deployment模式部署Ingress Controller,并创建对应的服务,但是type为NodePort。这样,Ingress就会暴露在集群节点ip的特定端口上。由于Nodeport暴露的端口是随机端口,一般会在前面再搭建一套负载均衡器来转发请求。该方式一般用于宿主机是相对固定IP地址。

缺点:

  1. NodePort方式暴露Ingress虽然简单方便,但是NodePort多了一层转发,在请求量级很大时可能对性能会有一定影响;
  2. 请求节点会是类似www.a.com:3008,其中3008是Ingress Nginx的svc暴露出来的Nodeport端口,看起来不太专业;

image.png

DaemonSet+HostNetwork+NodeSelector模式

使用DaemonSet结合Nodeselector来部署Ingress Controller到特定的Node上,然后使用HostNetwork直接把该Pod与宿主机Node的网络打通,直接使用宿主机的80/433端口就能访问服务。这时Ingress Controller所在的Node机器就是流量入口。

优点

该方式整个请求链路最简单,性能相对NodePort减少一层转发,因此性能更好;

缺点

由于直接利用宿主机节点的网络和端口,一个Node只能部署一个Ingress Controller的pod;

image.png

部署实战

关于版本问题需要注意以下几点:

本篇采用ingress-nginx v1.0.0,最新版本 v1.0.0适用于 Kubernetes 版本 v1.19+ ;

Kubernetes-v1.22+ 需要使用 ingress-nginx>=1.0,因为 networking.k8s.io/v1beta 已经移除;

Deployment+NodePort模式
  1. 使用wget下载v1.0.0的deploy.yaml,该处可能由于网络的原因下载不下来,大家可以在第二步下载我改好的文件;
1
bash复制代码wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.0.0/deploy/static/provider/baremetal/deploy.yaml
  1. 接下来我们需要修改deploy.yaml里面的镜像文件,由于k8s.gcr.io相关的镜像国内都进行屏蔽了,这里我采用了从docker官方去下载相关的镜像文件,需要将k8s.gcr.io/ingress-nginx/controller:v1.0.0和k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.0镜像替换为willdockerhub/ingress-nginx-controller:v1.0.0和jettech/kube-webhook-certgen:v1.5.2,这里需要注意的是kube-webhook-certgen有两处不要漏替换,此外需要在args处增加–watch-ingress-without-class=true配置,这里将修改好的文件v1.0.0-deploy.yaml上传到网盘,链接 提取码: sksc ;

image.png

image.png

  1. 部署nginx-ingress-controller;
1
bash复制代码kubectl apply -f v1.0.0-deploy.yaml
  1. 检查nginx-ingress-controller创建的情况,这个我们会发现该Service是一个NodePort类型,并且被随机分配两个端口,分别是32368和32577,后续我们就需要通过这个端口访问改地址信息;
1
2
3
4
bash复制代码#查看相关pod状态
kubectl get pods -n ingress-nginx -owide
#查看service
kubectl get service -n ingress-nginx

image.png

  1. 创建Deployment和Service,这里我们就是就使用之前的nginx-deployment.yaml和nginx-service.yaml;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yaml复制代码apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: backend
replicas: 3
template:
metadata:
labels:
app: backend
spec:
containers:
- name: nginx
image: nginx:latest
resources:
limits:
memory: "128Mi"
cpu: "128m"
ports:
- containerPort: 80
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
#定义后端pod标签为app=backend
selector:
app: backend
ports:
#service端口号
- port: 80
#pod的端口号
targetPort: 80
1
2
bash复制代码kubectl apply -f nginx-service.yaml
kubectl apply -f nginx-nodeport-service.yaml
  1. 验证Service创建情况,并且可以通过Service地址访问相关服务;
1
2
3
4
bash复制代码#查看service状况
kubectl get svc
#访问服务
curl http://10.96.45.195
  1. 新建ingress策略nodeport-ingress.yaml,创建对应资源;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nodeport-ingress
namespace: default
spec:
rules:
- host: aa.bb.cc
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: nginx-service
port:
number: 80
1
2
3
4
bash复制代码#创建ingress资源
kubectl apply -f nodeport-ingress.yaml
#检查ingress资源
kubectl get ingress

image.png

  1. 检查资源的访问,这里使用nginx-ingress-controller暴露的端口32368访问服务的信息;
1
2
3
4
5
bash复制代码#第一种方案可以在对应Node上设置host文件
echo '172.21.122.231 aa.bb.cc' >> /etc/hosts
curl aa.bb.cc:32368
#使用curl方式
curl -v http://172.21.122.231:32368 -H 'host: aa.bb.cc'

image.png

DaemonSet+HostNetwork+nodeSelector模式
  1. 清除资源;
1
2
3
bash复制代码kubectl delete -f v.1.0.0-deploy.yaml
kubectl delete -f nginx-nodeport-service.yaml
kubectl delete -f nodeport-ingress.yaml
  1. 给Node节点打标签为ingress=nginx;
1
bash复制代码kubectl label nodes demo-work-1 ingress=nginx
  1. 编辑v1.0.0-deploy.yaml,找到deployment部分,将kind变为DaemonSet,nodeSelector节点选择器变为ingress等于nginx,增加网络为hostNetwork等于true,这里需要在args新增–watch-ingress-without-class,这样ingress规则才能生效,这个是新版的以后不太一样的地方,链接提取码: dbag;
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
yaml复制代码apiVersion: apps/v1
kind: DaemonSet
metadata:
labels:
helm.sh/chart: ingress-nginx-4.0.1
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/version: 1.0.0
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/component: controller
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/component: controller
revisionHistoryLimit: 10
minReadySeconds: 0
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/component: controller
spec:
dnsPolicy: ClusterFirst
containers:
- name: controller
image: willdockerhub/ingress-nginx-controller:v1.0.0
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
exec:
command:
- /wait-shutdown
args:
- /nginx-ingress-controller
- --election-id=ingress-controller-leader
- --controller-class=k8s.io/ingress-nginx
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --validating-webhook=:8443
- --validating-webhook-certificate=/usr/local/certificates/cert
- --validating-webhook-key=/usr/local/certificates/key
- --watch-ingress-without-class=true # 新增
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
runAsUser: 101
allowPrivilegeEscalation: true
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: LD_PRELOAD
value: /usr/local/lib/libmimalloc.so
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
- name: webhook
containerPort: 8443
protocol: TCP
volumeMounts:
- name: webhook-cert
mountPath: /usr/local/certificates/
readOnly: true
resources:
requests:
cpu: 100m
memory: 90Mi
nodeSelector:
ingress: nginx
hostNetwork: true
serviceAccountName: ingress-nginx
terminationGracePeriodSeconds: 300
volumes:
- name: webhook-cert
secret:
secretName: ingress-nginx-admission
  1. 部署和检查ingress-nginx;
1
2
3
4
5
6
bash复制代码#部署ingress-nginx
kubectl apply -f v1.0.0-deploy.yaml
#查看相关pod状态
kubectl get pods -n ingress-nginx -owide
#查看service
kubectl get service -n ingress-nginx
  1. 新建nginx-service.yaml,将方式改变为ClusterIP形式;
1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
#定义后端pod标签为app=backend
selector:
app: backend
ports:
#service端口号
- port: 80
#pod的端口号
targetPort: 80
  1. 创建nginx-service,并检查资源情况;
1
2
3
4
5
6
7
8
bash复制代码#创建nginx-service
kubectl apply -f nginx-service.yaml
#检查资源状况
kubectl get svc
#资源的绑定情况
kubectl describe service nginx-service
#通过curl访问
curl 10.96.53.148

image.png

  1. 创建hostnetwork-ingress.yaml文件;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nodeport-ingress
namespace: default
spec:
rules:
- host: aa.bb.cc
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: nginx-service
port:
number: 80
  1. 创建ingress资源;
1
2
3
4
bash复制代码#创建ingress
kubectl apply -f hostnetwork-ingress.yaml
#检查ingress资源
kubectl get ingress
  1. 检查资源的访问;
1
bash复制代码curl -v http://172.21.122.231 -H 'host: aa.bb.cc'

image.png

结束

欢迎大家点点关注,点点赞!

本文转载自: 掘金

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

1…481482483…956

开发者博客

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