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

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


  • 首页

  • 归档

  • 搜索

用了3年Apollo,这次我选择了Nacos,原因不多说了

发表于 2021-01-18

老板都开口了,我能说不么?

本文探讨一下如何实现不同环境(开发、测试、灰度、正式)的配置管理问题。

就像Maven用groupId、artifactId、version三者来定位jar包在仓库中的位置一样,Nacos也提供了 Namespace (命名空间) 、Data ID (配置集ID)、 Group (组) 来确定一个配置文件(或者叫配置集)。

由此,实现多环境配置的方案也有三种:

1、用命名空间(namespace)来区分不同的环境,一个命名空间对应一个环境;

2、用配置组(group)来区分不同的环境,命名空间用默认的public即可,一个组对应一种环境;

3、用配置集ID(Data ID)名称来区分不同的环境,命名空间和组用默认的即可,通过文件命名来区分;

接下来,逐个来看

http://{host}:{port}/nacos

http://{host}:{port}/nacos/index.html

默认用户名密码都是nacos

在这里插入图片描述

为了方便演示,这里建了一个名为example的Spring Boot项目

在这里插入图片描述

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
xml复制代码1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.3.6.RELEASE</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>com.example</groupId>
12 <artifactId>example</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>example</name>
15
16 <properties>
17 <java.version>1.8</java.version>
18 <spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
19 </properties>
20
21 <dependencies>
22 <dependency>
23 <groupId>org.springframework.boot</groupId>
24 <artifactId>spring-boot-starter-web</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>com.alibaba.cloud</groupId>
28 <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
29 </dependency>
30 </dependencies>
31
32 <dependencyManagement>
33 <dependencies>
34 <dependency>
35 <groupId>com.alibaba.cloud</groupId>
36 <artifactId>spring-cloud-alibaba-dependencies</artifactId>
37 <version>${spring-cloud-alibaba.version}</version>
38 <type>pom</type>
39 <scope>import</scope>
40 </dependency>
41 </dependencies>
42 </dependencyManagement>
43
44 <build>
45 <plugins>
46 <plugin>
47 <groupId>org.springframework.boot</groupId>
48 <artifactId>spring-boot-maven-plugin</artifactId>
49 </plugin>
50 </plugins>
51 </build>
52
53 </project>

bootstrap.yml

1
2
3
4
5
6
7
8
yaml复制代码spring:
application:
name: example
cloud:
nacos:
config:
server-addr: 192.168.100.10:8848
file-extension: yaml

HelloController.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
kotlin复制代码package com.example.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author ChengJianSheng
* @data 2020/11/19
*/
@RestController
@RequestMapping("/hello")
@RefreshScope
public class HelloController {

@Value("${greet.hello}")
private String greet;

@GetMapping("/sayHi")
public String sayHi() {
return greet;
}
}

1.利用 Data ID 命名 来区分环境

利用Data ID命名来区分环境,命名空间和组默认即可

在 Nacos Spring Cloud 中,dataId 的完整格式如下:

1
bash复制代码${prefix}-${spring.profiles.active}.${file-extension}
  • prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置
  • spring.profiles.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成

{prefix}.

prefix.{file-extension}

  • file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

用命令行启动也是一样的

在这里插入图片描述

例如:

java -Dspring.profiles.active=test -jar example-0.0.1-SNAPSHOT.jar

在这里插入图片描述

2.利用 Group 来区分环境

项目不变,我们把spring.application.name改成example2

命名空间用默认的public

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1
ini复制代码java -Dspring.cloud.nacos.config.group=DEV_GROUP -jar example-0.0.1-SNAPSHOT.jar

在这里插入图片描述

1
ini复制代码java -Dspring.cloud.nacos.config.group=TEST_GROUP -jar example-0.0.1-SNAPSHOT.jar

在这里插入图片描述

1
ini复制代码java -Dspring.profiles.active=test -Dspring.cloud.nacos.config.group=TEST_GROUP -jar example-0.0.1-SNAPSHOT.jar

如果是这样的话,这个时候,Data ID 命名就应该是 example2-test.yaml

3.利用 Namespace 区分环境

在这里插入图片描述

创建命名空间的时候,如果不指定ID,则自动生成的id就是这样的uuid字符串,我们还是自己指定一个有意义的ID吧

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1
arduino复制代码java -Dspring.cloud.nacos.config.namespace=ns_dev -jar example-0.0.1-SNAPSHOT.jar

在这里插入图片描述

1
arduino复制代码java -Dspring.profiles.active=dev -Dspring.cloud.nacos.config.namespace=ns_dev -jar example-0.0.1-SNAPSHOT.jar

在这里插入图片描述

1
arduino复制代码java -Dspring.cloud.nacos.config.namespace=ns_test -jar example-0.0.1-SNAPSHOT.jar

在这里插入图片描述

1
2
3
4
ini复制代码java -Dspring.profiles.active=test \
-Dspring.cloud.nacos.config.namespace=ns_test \
-Dspring.cloud.nacos.config.group=TEST_GROUP \
-jar example-0.0.1-SNAPSHOT.jar

在这里插入图片描述

4. 小结

第一种,用 Data ID 区分环境,虽然简单,但是每个项目要创建4个配置文件,随着项目的增多,都在一个命名空间下回显得很混乱,查找起来也不是很方便,而且不利于做权限控制

第二种,用Group区分,问题也是一样的

综上,最好的是用Namespace区分环境,清晰明了,而且有利于做权限控制

来源 | urlify.cn/jmY3Ez

本文转载自: 掘金

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

如何使用 Java 泛型来避免 ClassCastExcep

发表于 2021-01-18

如何使用 Java 泛型来避免 ClassCastException

泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。

一句话解释什么是泛型?

泛型是相关语言特性的集合,它允许类或方法对各种类型的对象进行操作,同时提供编译时类型安全性检查

引入泛型之前

泛型在Java集合框架中被广泛使用,我们不使用泛型,那么代码将会是这样:

1
2
3
java复制代码List doubleList = new LinkedList();
doubleList.add(3.5D);
Double d = (Double) doubleList.iterator().next(); //类型强制转换

doubleList中存储一个Double类型的值, 但是List并不能阻止我们往里面再添加一个String类型

比如:doubleList.add (“ Hello world ”);

最后一行的(Double)强制转换操作符将导致在遇到非 Double 对象时抛出 ClassCastException

引入泛型之后

因为直到运行时才检测到类型安全性的缺失,所以开发人员可能不会意识到这个问题,将其留给客户机(而不是编译器)来发现。泛型允许开发人员将List标记为只包含 Double 对象,从而帮助编译器提醒开发人员在列表中存储非 Double 类型的对象的问题,在编译和开发期间,就把问题解决掉

我们可以这样改造上面的代码:

1
2
3
java复制代码List<Double> doubleList = new LinkedList<Double>();
doubleList.add(3.5D);
Double d = doubleList.iterator().next();

这时 我们再添加String类型的参数 会提示需要的类型不符合需求.

深入探索泛型类

泛型的概念

泛型是通过类型参数引入一组类型的类或接口.

类型参数:是一对尖括号之间以逗号分隔的类型参数名列表。

一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型类型遵循语法

泛型类型遵循以下语法:

1
2
3
4
5
6
7
8
9
10
java复制代码class identifier<formalTypeParameterList>{

}

interface identifier<formalTypeParameterList>{

}

interface Map<K,V> {//多个用逗号分隔
}

类型参数命名原则

Java 编程约定要求类型参数名称为单个大写字母,例如 E 表示元素,K 表示键,V 表示值,T 表示类型。避免使用像A,B,C这样没有意义的名称。

List < E > 表示一个元素列表,但是 List < B > 的意思是什么呢?

实际类型参数 替换 类型参数

泛型的类型参数可以被替换为实际的类型参数(类型名称)。例如,List < String > 是一个参数化类型,其中 String 是替换类型参数 E 的实际类型参数。

JAVA支持的实际类型的参数有哪些

  • 类型参数: 类型参数 传递给 类型参数
1
2
3
java复制代码class Container<E> { 
Set<E> elements; //E传给E
}
  • 具体类:传递具体的类

例: List < Student > , Student为具体类 传给E

  • 参数化类:传递具体的参数化类

例: Set < List < Shape > >, List< Shape > 为具体的参数化类 传给E

  • 数组类型: 传递数组

例: Map < String, String[] >, String传给K String[]传给V

  • 通配符: 使用问号(?)传递

例: Class < ? > , ? 传给T

声明和使用泛型

泛型的声明涉及到指定形式类型参数列表,并在整个实现过程中访问这些类型参数。使用泛型时需要在实例化泛型时将实际类型参数传递给类型参数

定义泛型的例子

在本例子中,我们实现一个简易的容器Container,该容器类型存储相应参数类型的对象,使其能够存储各种类型

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
java复制代码class Container<E>  //也可以使用实际类型的参数
{
private E[] elements;
private int index;
Container(int size)
{
elements = (E[]) new Object[size];
//本例中我们传入的是String,将Object[]转化为String[]返回
index = 0;
}
void add(E element)
{
elements[index++] = element;
}
E get(int index)
{
return elements[index];
}
int size()
{
return index;
}
}
public class GenDemo
{

public static void main(String[] args)
{
Container<String> con = new Container<String>(5);//使用String传给E,指定E为String类型的
con.add("North");
con.add("South");
con.add("East");
con.add("West");
for (int i = 0; i < con.size(); i++)
System.out.println(con.get(i));
}
}

指定类型参数的泛型

Container < E > 中的 E 为无界类型参数,通俗的讲就是什么类型都可以,可以将任何实际的类型参数传递给 E
. 例如,可以指定 Container < Student > 、 Container < Employee > 或 Container < Person >

通过指定上限来限制传入的类

但是有时你想限制类型,比如你想 < E > 只接受 Employee 及其子类

1
java复制代码class Employees<E extends Employee>

此时传入的E 必须为 Employee子类, new Employees< String > 是无效的.

指定多个类型限制

当然我们还可以为一个类指定多个类型 使用&分隔 :

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
java复制代码abstract class Employee
{
private BigDecimal hourlySalary;
private String name;
Employee(String name, BigDecimal hourlySalary)
{
this.name = name;
this.hourlySalary = hourlySalary;
}
public BigDecimal getHourlySalary()
{
return hourlySalary;
}
public String getName()
{
return name;
}
public String toString()
{
return name + ": " + hourlySalary.toString();
}
}
class Accountant extends Employee implements Comparable<Accountant>
/*
Comparable < Accountant > 表明Accountant可以按照自然顺序进行比较
Comparable 接口声明为泛型类型,只有一个名为 t 的类型参数。
这个接口提供了一个 int compareTo (t o)方法,该方法将当前对象与参数(类型为 t)进行比较,
当该对象小于、等于或大于指定对象时返回负整数、零或正整数。
*/

{
Accountant(String name, BigDecimal hourlySalary)
{
super(name, hourlySalary);
}
public int compareTo(Accountant acct)
{
return getHourlySalary().compareTo(acct.getHourlySalary());
}
}

class SortedEmployees<E extends Employee & Comparable<E>>

//第一个必须为class 之后的必须为interface
{
private E[] employees;
private int index;
@SuppressWarnings("unchecked")
SortedEmployees(int size)
{
employees = (E[]) new Employee[size];
int index = 0;
}
void add(E emp)
{
employees[index++] = emp;
Arrays.sort(employees, 0, index);
}
E get(int index)
{
return employees[index];
}
int size()
{
return index;
}
}
public class GenDemo
{
public static void main(String[] args)
{
SortedEmployees<Accountant> se = new SortedEmployees<Accountant>(10);
se.add(new Accountant("John Doe", new BigDecimal("35.40")));
se.add(new Accountant("George Smith", new BigDecimal("15.20")));
se.add(new Accountant("Jane Jones", new BigDecimal("25.60")));
for (int i = 0; i < se.size(); i++)
System.out.println(se.get(i));
}
}

下界和泛型参数

假设你想要打印出一个对象列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码class Scratch_12{
public static void main(String[] args) {
{
List<String> directions = new ArrayList();
directions.add("north");
directions.add("south");
directions.add("east");
directions.add("west");
printList(directions);
List<Integer> grades = new ArrayList();
grades.add(new Integer(98));
grades.add(new Integer(63));
grades.add(new Integer(87));
printList(grades);
}

}
static void printList(List<Object> list)
{
Iterator<Object> iter = list.iterator();
while (iter.hasNext())
System.out.println(iter.next());
}
}

这个例子看似是合乎逻辑的,我们想通过将 List < object > 类型的对象传递给 printList ()方法,防止类型安全的这种冲突。然而,这样做并不是很有用。实际上编译器已经报出错误了,它告诉我们不能将字符串列表转换为对象列表

为什么会报这个错呢? 这和泛型的基本规则有关:

For a given subtype x of type y, and given G as a raw type declaration, G< x > is not a subtype of G < y >.
给定一个x类, x是y的子类, G作为原始类型声明,G(x)不是G(y)的子类

根据这个规则,尽管 String 和 Integer 是 java.lang.Object 的子类,但是List < string > 和 List < integer > 是 List < Object > 的子类就不对了.

为什么我们有这个规则?因为泛型的设计是为了在编译时捕获类型安全违规行为。如果没有泛型,我们可能会发生线上事故,因为程序抛出了 ClassCastException 并崩溃了!

作为演示,我们假设 List < string > 是 List < object > 的子类型。如果这是真的,你可能会得到以下代码:

1
2
3
4
java复制代码List<String> directions = new ArrayList<String>();
List<Object> objects = directions;
objects.add(new Integer());
String s = objects.get(0);

将一个整数添加到对象列表中,这违反了类型安全。问题发生在最后一行,该行抛出 ClassCastException,因为无法将存储的整数强制转换为字符串。

使用通配符来解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码class Scratch_13{
public static void main(String[] args) {
List<String> directions = new ArrayList<String>();
directions.add("north");
directions.add("south");
directions.add("east");
directions.add("west");
printList(directions);
List<Integer> grades = new ArrayList<Integer>();
grades.add(Integer.valueOf(98));
grades.add(Integer.valueOf(63));
grades.add(Integer.valueOf(87));
printList(grades);
}
static void printList (List < ? > list)
{
Iterator<?> iter = list.iterator();
while (iter.hasNext())
System.out.println(iter.next());
}

}

我使用了一个通配符(?)在参数列表和 printList ()的方法体中,因为此符号代表任何类型,所以将 List < string > 和 List < integer > 传递给此方法是合法的

深入探索泛型方法

假如你现在有一个业务逻辑需要你将一个List 复制到另外一个List,要传递任意类型的源和目标,需要使用通配符作为类型占位符
你可能会这样写:

1
2
3
4
5
6
java复制代码void copy(List<?> src, List<?> dest, Filter filter)
{
for (int i = 0; i < src.size(); i++)
if (filter.accept(src.get(i)))
dest.add(src.get(i));
}

这时编译器又又又报错了

< ? >意味着任何类型的对象都可以是列表的元素类型,并且源元素和目标元素类型可能是不兼容的

例:源列表是一个 Shape 的 List,而目标列表是一个 String 的 List,并且允许复制,那么在尝试检索目标列表的元素时将抛出 ClassCastException

指定类型上下界

1
2
3
4
5
6
java复制代码void copy(List<? extends String> src, List<? super String> dest, Filter filter)
{
for (int i = 0; i < src.size(); i++)
if (filter.accept(src.get(i)))
dest.add(src.get(i));
}

通过指定 extends 后跟类型名称,可以为通配符提供一个上限。类似地,可以通过指定 super 后跟类型名来为通配符提供一个下限。这些边界限制了可以作为实际类型参数传递的类型。

在这个例子中,因为 String 是 final,这意味着它不能被继承,所以只能传递 String 对象的源列表和 String 或 Object 对象的目标列表,这个问题只是解决了一部分,怎么办呢

使用泛型方法完全解决这个问题

泛型方法的语法规范:

1
java复制代码<formalTypeParameterList> returnType method(param)

类型参数可以用作返回类型,也可以出现在参数列表中

此时我们重写代码解决这个问题:

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
java复制代码public class Demo
{
public static void main(String[] args)
{
List<Integer> grades = new ArrayList<Integer>();
Integer[] gradeValues =
{
Integer.valueOf(96),
Integer.valueOf(95),
Integer.valueOf(27),
Integer.valueOf(100),
Integer.valueOf(43),
Integer.valueOf(68)
};

for (int i = 0; i < gradeValues.length; i++){
grades.add(gradeValues[i]);
}


List<Integer> failedGrades = new ArrayList<Integer>();

copy(grades, failedGrades, grade -> grade <= 50);//函数式编程,使用lambda表达式实现Filter<T>此时T为Integer类型

for (int i = 0; i < failedGrades.size(); i++){
System.out.println(failedGrades.get(i));
}

}

static <T> void copy(List<T> src, List<T> dest, Filter<T> filter)
{
for (int i = 0; i < src.size(); i++)
if (filter.accept(src.get(i)))
dest.add(src.get(i));
}
}
interface Filter<T>
{
boolean accept(T o);
}

此时我们为 src、 dest 和 filter 参数的类型都包含类型参数 T。这意味着在方法调用期间必须传递相同的实际类型参数,编译器自动通过调用来推断这个参数的类型是什么

泛型和类型推断

Java 编译器包含类型推断算法,用于在实例化泛型类、调用类的泛型构造函数或调用泛型方法时识别实际的类型参数。

泛型类实例化

在 Java SE 7之前,在实例化泛型类时,必须为变量的泛型类型和构造函数指定相同的实际类型参数。例子:

1
java复制代码Map<String, Set<String>> marbles = new HashMap<String, Set<String>>();

此时,代码显得非常混乱,为了消除这种混乱,Java SE 7修改了类型推断算法,以便可以用空列表< >替换构造函数的实际类型参数,前提是编译器可以从实例化上下文中推断类型参数。示例:

1
java复制代码Map<String, Set<String>> marbles = new HashMap<>();//使用<>替换<String, Set<String>>

要在泛型类实例化期间利用类型推断,必须指定<>:

1
java复制代码Map<String, Set<String>> marbles = new HashMap();

编译器生成一个“ unchecked conversion warning” ,因为 HashMap ()构造函数引用了 java.util。指定 HashMap 原始类型,而不是 HashMap<String, Set< String >>。

泛型构造函数

泛型类和非泛型类都可以声明泛型构造函数,其中构造函数具有形式类型参数列表。例如,你可以用泛型构造函数声明如下泛型类:

1
2
3
4
5
6
7
java复制代码public class Box<E>
{
public <T> Box(T t)
{
// ...
}
}

此声明使用形式类型参数 E 指定泛型类 Box < E > 。它还指定了一个具有形式类型参数 T 的泛型构造函数

那么在构造函数调用时是这样的:

1
java复制代码new Box<Marble>("Aggies");

进一步利用菱形运算符来消除构造函数调用中的 Marble 实际类型参数,只要编译器能够从实例化上下文中推断出这个类型参数:

1
java复制代码new Box<>("Aggies");

泛型方法调用

我们现在已经知道了 编译器会通过类型推断算法识别出我们使用的类型
那么对于我们之前,将一个list拷贝到另外一个List的例子,我们还可以继续改造一下

1
2
java复制代码//copy是静态方法 我们可以使用class.methodName的方式调用它
Demo.<Integer>copy(grades, failedGrades, grade -> grade <= 50);

对于实例方法,语法几乎完全相同。

1
java复制代码new Demo().<Integer>copy(grades, failedGrades, grade -> grade <= 50);

类型擦除

在泛型代码内部,无法获得任何有关泛型参数类型的信息 —《Java 编程思想》

举例说明

ArrayList< String > () 和 ArrayList< Integer > ()

很容易被认为是不同的类型,但是下面的打印结果却是 true

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

public static void main(String[] args) {

Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();

// output:true
System.out.println(c1 == c2);
}
}

System.out.println(Arrays.toString(c1.getTypeParameters()));
// output:[E]
System.out.println(Arrays.toString(c2.getTypeParameters()));
// output:[E]

分别打印它们的参数类型,可以发现,无论指定的是 Integer 类型还是 String 类型,最后输出结果都仅是一个 用作参数占位符的标识符 [E] 而已.

这意味着,在使用泛型时,任何具体的类型信息,比如上例中的 Integer 或 String,在泛型内部都是无法获得的,也就是,被擦除了。唯一知道的,就只是正在使用着的对象。由于 ArrayList< String >() 和 ArrayList< Integer >() 都会被擦除成“原生态”(即 List)

如果指定了边界,例如< T extends Integer>,类型参数会被擦除为边界(Integer),如果未指定边界,例如 ,类型参数会被擦除为 Object 。

堆污染( heap pollution)

在使用泛型时,可能会遇到堆污染,其中参数化类型的变量引用的对象不是该参数化类型(例如,如果原始类型与参数化类型混合)。在这种情况下,编译器报告“unchecked warning” ,因为无法验证涉及参数化类型的操作(如强制转换或方法调用)的正确性

堆污染示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码  import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
public class Scratch_15
{
public static void main(String[] args)
{
Set s = new TreeSet<Integer>();
Set<String> ss = s; // unchecked warning Unchecked assignment: 'java.util.Set' to 'java.util.Set<java.lang.String>'
s.add(42); // unchecked warning Unchecked call to 'add(E)' as a member of raw type 'java.util.Set'
Iterator<String> iter = ss.iterator();
while (iter.hasNext())
{
String str = iter.next(); //throw ClassCastException
System.out.println(str);
}
}
}
/*

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at Scratch_15.main(scratch_15.java:17)
*/
  • 第一个未检查的警告:变量 ss 具有参数化类型 Set < string > 。当 s 引用的 Set 被分配给 ss 时,编译器会生成一个未检查的警告。它因为编译器不能确定 s 引用 Set < string > 类型。结果就是堆污染
  • 第二个未检查的警告:由于泛型擦除,编译器也不能确定变量 s 是指 Set < string > 还是 Set < integer > 类型,这时就会产生 unchecked warning ,自然就会发生 堆污染了

@SafeVarargs的用法

@SafeVarargs在JDK 7中引入,主要目的是处理可变长参数中的泛型,此注解告诉编译器:在可变长参数中的泛型是类型安全的。可变长参数是使用数组存储的,而数组和泛型不能很好的混合使用
简单的说,数组元素的数据类型在编译和运行时都是确定的,而泛型的数据类型只有在运行时才能确定下来,因此当把一个泛型存储到数组中时,编译器在编译阶段无法检查数据类型是否匹配,因此会给出警告信息:存在可能的“堆污染”(heap pollution),即如果泛型的真实数据类型无法和参数数组的类型匹配,会导致ClassCastException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码import java.util.ArrayList;

public class SafeVarargsTest {
public static void main(String[] args) {
ArrayList<Integer> a1 = new ArrayList<>();
a1.add(new Integer(1));
a1.add(2);
showArgs(a1, 12);
}

//@SafeVarargs
public static <T> void showArgs(T... array) {
for (T arg : array) {
System.out.println(arg.getClass().getName() + ":" + arg);
}
}

}

如果使用IDE进行编译,需要修改编译参数,增加-Xlint:unchecked编译选项。
有如下的警告信息:

1
2
3
4
java复制代码$ javac -Xlint:unchecked SafeVarargsTest.java
SafeVarargsTest.java:18: 警告: [unchecked] 参数化 vararg 类型T的堆可能已受污染
public static < T> void showArgs(T… array) {
^

其中, T是类型变量:
T扩展已在方法 < T>showArgs(T…)中声明的Object

但是显然在这个示例中,可变参数的泛型是安全的,因此可以启用@SafeVarargs注解消除这个警告信息。

@SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。一个方法使用@SafeVarargs注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题,否则可能导致运行时的类型转换异常。下面给出一个“堆污染”的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
import java.util.Arrays;
import java.util.List;

public class UnsafeMethodTest {

public static void main(String[] args) {
List<String> list1 = Arrays.asList("one", "two");
List<String> list2 = Arrays.asList("three","four");
unsafeMethod(list1, list2);
}

@SafeVarargs //并不安全
static void unsafeMethod(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42, 56);
array[0] = tmpList; // tmpList是一个List对象(类型已经擦除),赋值给Object类型的对象是允许的(向上塑型),能够编译通过
String s = stringLists[0].get(0); // 运行时抛出ClassCastException!
}
}

运行UnsafeMethodTest的结果如下:

1
java复制代码Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

数组array和stringLists同时指向了参数数组,tmpList是一个包含两个Integer对象的list对象。

完

记得点赞 关注@Java宝典


关注公众号:java宝典

本文转载自: 掘金

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

在IDEA中通过Module管理多个项目

发表于 2021-01-18

你身边有没有这种顽固的Eclipse忠实用户:IDEA不能一个窗口管理多个项目!太不方便了!

对于一个窗口同时管理多个项目的需求,在我们日常开发时候是经常需要的。尤其当我们在分布式环境下,在一个窗口中调试起来就能方便很多。

如此强大的IDEA真的不支持吗?!当然不是!是你不会用!

下面我们就来说说如何在一个工作空间中管理多个项目的配置方式:

第一步:先创建一个新的空白工程

在弹出的项目名称和路径输入框中根据你的喜好输入即可。

第二步:添加模块

添加模块的方式有两种:

New Module:如果你要管理的是一个新项目,那么可以通过这个选项创建一个新项目,并纳入当前的项目管理界面中。

对于我们这些Spring开发者来说,可以继续用Spring Initializr来初始化你的项目,这样创建出来的项目会成为当前这个项目的模块来管理。

Import Module:如果你要管理的项目已经从git上拉下来了,可以直接用这个选项进行导入即可:

如果没有特殊配置,那就各种next完成导入。

第三步:在后续要继续添加一起管理的项目的时候,只需要在菜单中找到这两种方式。

  1. File -> New -> Module…:这个是新建项目
  2. File -> New -> Module from Existing Sources…:这个是导入项目

不断重复上面的动作,我们就可以把很多我们要一起调试的项目放到一起来使用了:

本文首发:blog.didispace.com/idea-multip… ,更多技术干货欢迎关注与收藏

欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源、日常干货及福利赠送。

本文转载自: 掘金

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

Redis 60新特性多线程连环13问

发表于 2021-01-18

干货分享最近将个人学习笔记整理成册,使用PDF分享主要包含了Java基础,数据结构,jvm,多线程等等,由于篇幅有限,以下只展示小部分面试题,

需要的朋友可以点一点领取:戳这里即可领取。。。暗号:JJ

Redis 6.0 多线程连环13问.

1.Redis6.0之前的版本真的是单线程吗?

Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。

2.Redis6.0之前为什么一直不使用多线程?

官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。

使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。

3.Redis6.0为什么要引入多线程呢?

Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。

但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。

从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:

• 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式

• 使用多线程充分利用多核,典型的实现比如 Memcached。

协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因:

• 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核

• 多线程任务可以分摊 Redis 同步 IO 读写负荷

4.Redis6.0默认是否开启了多线程?

Redis6.0的多线程默认是禁用的,只使用主线程。如需开启需要修改redis.conf配置文件:io-threads-do-reads yes

5.Redis6.0多线程开启时,线程数如何设置?

开启多线程后,还需要设置线程数,否则是不生效的。同样修改redis.conf配置文件

关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。

6.Redis6.0采用多线程后,性能的提升效果如何?

Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试,GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。

7.Redis6.0多线程的实现机制?

流程简述如下:

1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列

2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程

3、主线程阻塞等待 IO 线程读取 socket 完毕

4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行

5、主线程阻塞等待 IO 线程将数据回写 socket 完毕

6、解除绑定,清空等待队列

该设计有如下特点:

1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写

2、IO 线程只负责读写 socket 解析命令,不负责命令处理

8.开启多线程后,是否会存在线程并发安全问题?

从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。

9.Linux环境上如何安装Redis6.0.1(6.0的正式版是6.0.1)?

这个和安装其他版本的redis没有任何区别,整个流程跑下来也没有任何的坑,所以这里就不做描述了。唯一要注意的就是配置多线程数一定要小于cpu的核心数,查看核心数量命令:

1
2
3
4
5
6
css复制代码[root@centos7.5 ~]# lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3

10.Redis6.0的多线程和Memcached多线程模型进行对比

前些年memcached 是各大互联网公司常用的缓存方案,因此redis 和 memcached 的区别基本成了面试官缓存方面必问的面试题,最近几年memcached用的少了,基本都是 redis。不过随着Redis6.0加入了多线程特性,类似的问题可能还会出现,接下来我们只针对多线程模型来简单比较一下。

该设计有如下特点:

1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写

2、IO 线程只负责读写 socket 解析命令,不负责命令处理

Redis6.0与Memcached多线程模型对比:

相同点:都采用了 master线程-worker 线程的模型

不同点:Memcached 执行主逻辑也是在 worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。而 Redis 把处理逻辑交还给 master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。

11.Redis作者是如何点评 “多线程”这个新特性的?

关于多线程这个特性,在6.0 RC1时,Antirez曾做过说明:

Redis支持多线程有2种可行的方式:第一种就是像“memcached”那样,一个Redis实例开启多个线程,从而提升GET/SET等简单命令中每秒可以执行的操作。这涉及到I/O、命令解析等多线程处理,因此,我们将其称之为“I/O threading”。另一种就是允许在不同的线程中执行较耗时较慢的命令,以确保其它客户端不被阻塞,我们将这种线程模型称为“Slow commands threading”。

经过深思熟虑,Redis不会采用“I/O threading”,redis在运行时主要受制于网络和内存,所以提升redis性能主要是通过在多个redis实例,特别是redis集群。接下来我们主要会考虑改进两个方面:

  1. Redis集群的多个实例通过编排能够合理地使用本地实例的磁盘,避免同时重写AOF。

2.提供一个Redis集群代理,便于用户在没有较好的集群协议客户端时抽象出一个集群。

补充说明一下,Redis和memcached一样是一个内存系统,但不同于Memcached。多线程是复杂的,必须考虑使用简单的数据模型,执行LPUSH的线程需要服务其他执行LPOP的线程。

我真正期望的实际是“slow operations threading”,在redis6或redis7中,将提供“key-level locking”,使得线程可以完全获得对键的控制以处理缓慢的操作。

详见:antirez.com/news/126

12.Redis线程中经常提到IO多路复用,如何理解?

这是IO模型的一种,即经典的Reactor设计模式,有时也称为异步阻塞IO。

多路指的是多个socket连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

13.你知道Redis的彩蛋LOLWUT吗?

这个其实从Redis5.0就开始有了,但是原谅我刚刚知道。作者是这么描述这个功能的《LOLWUT: a piece of art inside a database command》,“数据库命令中的一件艺术品”。你可以把它称之为情怀,也可以称之为彩蛋,具体是什么,我就不透露了。和我一样不清楚是什么的小伙伴可以参见:antirez.com/news/123,每次…

个人简介:荡不羁,一生所爱。Java耕耘者(微信公众号ID:Java耕耘者),欢迎关注。可获得2000G详细的2020面试题的资料

本文转载自: 掘金

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

亿级大表在线不锁表变更字段与索引

发表于 2021-01-18

摘要:在业界中有一个比较成熟的工具,针对大表的场景,可以在线进行Alter变更,且不会出现锁表的风险。除此之外,它还有其他的一些优点,让我们开始探索吧。

背景

大家在日常工作中,往往需要对数据库的表结构做变更,一般涉及到增删字段,修改字段属性等ALTER的操作。然而,在大表场景下,特别是千万级、亿级的大表,如果处理不当。这些操作往往会引发锁表的巨大隐患,特别是在生产环境中,一旦在变更表结构过程中,出现了长时间锁表,会导致用户产生的数据长时间无法正常变更到表中,进而导致服务功能异常,结果将是灾难性的。

一般执行这种Alter类型的变更,我们可能有以下的想法:

1、停服,在停服期间做表结构的变更,自然就可以防止对用户产生影响。但是,很多场景是不允许停服的。并且如果表的数据量达到上亿,那么需要停服时间可能需要十几个小时,甚至更长,这是极不现实的;

2、凌晨执行,在用户较少的时间段内,做变更,尽量减少对用户产生影响。但是如果出现锁表的话,万一有用户使用服务,服务将不可用;

3、使用换表,但是缺点是复制数据到新表期间,如果用户在这期间做了update或delete操作,且数据发生在已经复制完成的部分,那么将无法感知到这部分数据,导致丢失掉用户的操作数据,风险太大;

4、使用存储过程,缺点是执行时间会很久,且有可能影响到用户的DDL操作。因为为了防止每次循环修改时,锁住太多数据行,我们需要控制每次更新数据的行数,粒度不能太大,否则很有可能会锁住用户正在操作的数据行。

那么针对以上实际的需求,就没有一个很好的工具,来解决我们的痛点吗?其实在业界中,就有一个比较成熟的工具,针对大表的场景,可以在线进行Alter变更,且不会出现锁表的风险。除此之外,它还有其他的一些优点,让我们开始探索吧。

一、pt-osc是什么

pt-online-schema-change是Percona-toolkit一员,通过改进原生ddl的方式,达到不锁表在线修改表结构的效果。在Percona的官网中,关于pt-osc工具,也特别提到了ALTER表不会出现锁表的特性。

针对上面谈到的避免锁表、感知用户更新删除动作等,ps-osc工具是怎么解决的呢?

pt-osc主要执行步骤如下:

1、创建一个跟原表一模一样的新表,命名方式为’_正式表名_new’;

2、使用alter语句将要变更的内容在新创建的新表上做变更,避免了对原表的alter操作;

3、在原表中创建3个触发器,分别是insert、update和delete,主要是用于原表在往新表复制数据时,如果用户有DDL操作,触发器能够将在这期间出现的DDL操作数据也写入到新表中,确保新表的数据是最新的,不会丢失掉用户的新操作数据;

4、按块拷贝数据到新表,拷贝过程对数据行持有S锁;

5、重命名,将原表重命名为老表,命名为“_正式表名_old”,将新表重命名为正式表,可通过配置决定执行完成后是否删除掉老表;

6、删除3个触发器;

二、pt-osc的安装

在linux系统中安装步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lua复制代码--下载安装包wget
http://szxge1-sw.artifactory.cd-cloud-artifact.tools.huawei.com/artifactory/CommonComponent/common/tool/percona-toolkit-3.1.0.tar.gz

--解压安装包
tar -zxvf percona-toolkit-3.1.0.tar.gz

--安装依赖环境
yum install perl-ExtUtils-CBuilder perl-ExtUtils-MakeMaker
yum -y install perl-Digest-MD5
cd percona-toolkit-3.1.0
perl Makefile.PL

--编译
make
make install
yum install mariadb

--安装Mysql
yum install perl-DBD-MySQL

三、pt-osc的使用

pt-osc工具使用起来很简单,直接在linux命令行输入pt-osc格式的命令,即可直接执行。

以Mysql数据库增加一个名字是MARK的字段为例:

1
2
3
4
5
ini复制代码pt-online-schema-change --user="root"
--password="*****" --host="数据库IP"
--port=3306 --alter "ADD COLUMN MARK TINYINT NULL
DEFAULT 1 COMMENT 'mark source region is 1';"
D=my_test,t=t_test --no-drop-old-table --execute --print --no-check-replication-filters --charset=utf8 --no-check-unique-key-change --max-load="Threads_running=100" --critical-load="Threads_running=300" --recursion-method=none;

在上面的语句中:

1、user和password分别为数据库执行变更操作的用户名、密码,需要高权限;

2、host为数据库的IP地址;

3、port为数据库的端口号;

4、alter后面跟上具体的alter语句;

5、D为database名字;

6、t为要执行变更的表名;

7、no-drop-old-table就是不要删除

8、charset,字符集,使用utf8;

9、max-load,在复制数据时,工具会监控数据库中正在运行的线程数,如果大于配置的Threads_running值,那么会暂停复制,直到小于该值。以此防止对数据库造成较大压力,影响现网业务正常使用;

10、critical-load,默认为50,在每个块之后检查SHOW GLOBAL STATUS,与max-load不同的是,如果负载太高,,直接中止,而不是暂停。可根据自己数据库情况斟酌配置阈值;

**注意:**在–alter后面跟着的变更语句中,列名不可以加符号,否则会出现报错。如--alter "ADD COLUMN MARK TINYINT NULL DEFAULT 1 COMMENT 'mark source region is 1';",MARK字段加了符号,就会出现错误,COMMENT后面有`符号无影响。

下面是使用pt-osc工具,实际执行一个作业时,打印出来的信息。为了安全起见,部分日志信息做了隐藏忽略。

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
less复制代码[root@ttt ~]#  `pt-online-schema-change
--user="root" --password="*****" --host="数据库IP" --port=3306 --alter "ADD
COLUMN MARK TINYINT NULL DEFAULT 1 COMMENT 'mark source region is 1';"
D=my_test,t=t_test --no-drop-old-table --execute --print
--no-check-replication-filters --charset=utf8 --no-check-unique-key-change
--max-load="Threads_running=100"
--critical-load="Threads_running=300" --recursion-method=none;`

No slaves found.
See --recursion-method if host EulerOS-BaseTemplate has slaves.

Not checking slave lag because no slaves were found and --check-slave-lag was not specified.

Operation, tries, wait:

analyze_table, 10, 1

copy_rows, 10, 0.25

create_triggers, 10, 1

drop_triggers, 10, 1

swap_tables, 10, 1

update_foreign_keys, 10, 1

Altering `my_test`.`t_test`...

Creating new table...

CREATE TABLE `my_test`.`_t_test_new` (

`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '递增ID',

.............建表语句数据................

Created new table my_test._t_test_new OK.

Altering new table...

ALTER TABLE `my_test`.`_t_test_new` ADD COLUMN MARK TINYINT NULL DEFAULT 1 COMMENT 'mark source region is 1';

Altered `my_test`.`_t_test_new` OK.

2020-10-14T11:14:48 Creating triggers...

2020-10-14T11:14:48 Created triggers OK.

2020-10-14T11:14:48 Copying approximately 346697 rows...

INSERT LOW_PRIORITY IGNORE INTO `my_test`.`_t_test_new` (`id`, ..建表语句信息....
FROM `my_test`.`_t_test_new` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) AND ((`id` <= ?)) LOCK IN SHARE MODE /*pt-online-schema-change
31340 copy nibble*/

SELECT /*!40001 SQL_NO_CACHE */ `id` FROM `my_test`.`t_test` FORCE INDEX(`PRIMARY`) WHERE ((`id` >= ?)) ORDER BY `id` LIMIT ?, 2 /*next chunk boundary*/

2020-10-14T11:14:53 Copied rows OK.

2020-10-14T11:14:53 Analyzing new table...

2020-10-14T11:14:53 Swapping tables...

RENAME TABLE `my_test`.`t_test` TO `my_test`.`_t_test_old`, `my_test`.`_t_test_new` TO `my_test`.`t_test`

2020-10-14T11:14:53 Swapped original and new tables OK.

Not dropping old table because --no-drop-old-table was specified.

2020-10-14T11:14:53 Dropping triggers...

DROP TRIGGER IF EXISTS `my_test`.`pt_osc_my_test_t_test_del`

DROP TRIGGER IF EXISTS `my_test`.`pt_osc_my_test_t_test_upd`

DROP TRIGGER IF EXISTS `my_test`.`pt_osc_my_test_t_test_ins`

2020-10-14T11:14:54 Dropped triggers OK.

Successfully altered `my_test`.`t_test`.

四、性能对比

前面介绍了很多pt-osc的优点,以及良好的特性。那么实际使用效果到底怎么样呢?在测试环境中,专门做了一个测试,让大家有更加直观的感受。

在测试库中,准备了一张1600万数据的大表,目标为对大表添加一个字段,分别使用存储过程和pt-osc工具,进行测试。

4.1 使用存储过程

首先使用存储过程做测试,为防止锁表,每次只更新200行。整个变更从开始到完成,需要耗费90分钟。其实,存储过程在执行过程中,如果恰好用户也在DDL操作存储过程正在变更的数据行,还有可能会锁住用户的数据,导致用户不能变更成功。

4.2 使用**pt-osc**工具

pt-osc从开始执行到变更完成,耗时7分钟左右,速度非常快。在执行的过程中,测试环境的服务连接到该数据库,并执行多个会操作该表的任务,整个过程中,任务能够正常执行,未出现异常情况。

五、结语

ps-osc的上述优点,在现网环境的不停服等要求下,能够优雅地帮助我们实施变更,且保证在变更期间,数据库不会受到锁表、过载等的影响,进而保证了业务能够正常运转。

本文分享自华为云社区《千万级、亿级大表在线不锁表变更字段与索引》,原文作者:active_zhao 。

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

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

万字长文爆肝 DNS 协议!

发表于 2021-01-18

试想一个问题,我们人类可以有多少种识别自己的方式?可以通过身份证来识别,可以通过社保卡号来识别,也可以通过驾驶证来识别,尽管我们有多种识别方式,但在特定的环境下,某种识别方法可能比另一种方法更为适合。因特网上的主机和人类一样,可以使用多种识别方式进行标识。互联网上主机的一种标识方法是使用它的 主机名(hostname) ,如 www.facebook.com、 www.google.com 等。但是这是我们人类的记忆方式,路由器不会这么理解,路由器喜欢定长的、有层次结构的 IP地址。

如果你还不理解 IP 的话,可以翻阅一下我的这篇文章计算机网络层

IP 地址现在简单表述一下,就是一个由 4 字节组成,并有着严格的层次结构。例如 121.7.106.83 这样一个 IP 地址,其中的每个字节都可以用 . 进行分割,表示了 0 - 255 的十进制数字。

然而,路由器喜欢的是 IP 地址进行解析,我们人类却便于记忆的是网址,那么路由器如何把 IP 地址解析为我们熟悉的网址地址呢?这时候就需要 DNS 出现了。


DNS 的全称是 Domain Name System,DNS ,它是一个由分层的 DNS 服务器(DNS server)实现的分布式数据库;它还是一个使得主机能够查询分布式数据库的应用层协议。DNS 服务器通常是运行 BIND(Berkeley Internet Name Domain) 软件的 UNIX 机器。DNS 协议运行在 UDP 之上,使用 53 端口。

DNS 基本概述

与 HTTP、FTP 和 SMTP 一样,DNS 协议也是应用层的协议,DNS 使用客户-服务器模式运行在通信的端系统之间,在通信的端系统之间通过下面的端到端运输协议来传送 DNS 报文。但是 DNS 不是一个直接和用户打交道的应用。DNS 是为因特网上的用户应用程序以及其他软件提供一种核心功能。

DNS 通常不是一门独立的协议,它通常为其他应用层协议所使用,这些协议包括 HTTP、SMTP 和 FTP,将用户提供的主机名解析为 IP 地址。

下面根据一个示例来描述一下这个 DNS 解析过程,这个和你输入网址后,浏览器做了什么操作有异曲同工之处

你在浏览器键入 www.someschool.edu/index.html 时会发生什么现象?为了使用户主机能够将一个 HTTP 请求报文发送到 Web 服务器 www.someschool.edu ,会经历如下操作

  • 同一台用户主机上运行着 DNS 应用的客户端
  • 浏览器从上述 URL 中抽取出主机名 www.someschool.edu ,并将这台主机名传给 DNS 应用的客户端
  • DNS 客户向 DNS 服务器发送一个包含主机名的请求。
  • DNS 客户最终会收到一份回答报文,其中包含该目标主机的 IP 地址
  • 一旦浏览器收到目标主机的 IP 地址后,它就能够向位于该 IP 地址 80 端口的 HTTP 服务器进程发起一个 TCP 连接。

除了提供 IP 地址到主机名的转换,DNS 还提供了下面几种重要的服务

  • 主机别名(host aliasing),有着复杂的主机名的主机能够拥有一个或多个其他别名,比如说一台名为 relay1.west-coast.enterprise.com 的主机,同时会拥有 enterprise.com 和 www.enterprise.com 的两个主机别名,在这种情况下,relay1.west-coast.enterprise.com 也称为 规范主机名,而主机别名要比规范主机名更加容易记忆。应用程序可以调用 DNS 来获得主机别名对应的规范主机名以及主机的 IP地址。
  • 邮件服务器别名(mail server aliasing),同样的,电子邮件的应用程序也可以调用 DNS 对提供的主机名进行解析。
  • 负载分配(load distribution),DNS 也用于冗余的服务器之间进行负载分配。繁忙的站点例如 cnn.com 被冗余分布在多台服务器上,每台服务器运行在不同的端系统之间,每个都有着不同的 IP 地址。由于这些冗余的 Web 服务器,一个 IP 地址集合因此与同一个规范主机名联系。DNS 数据库中存储着这些 IP 地址的集合。由于客户端每次都会发起 HTTP 请求,所以 DNS 就会在所有这些冗余的 Web 服务器之间循环分配了负载。

DNS 工作概述

假设运行在用户主机上的某些应用程序(如 Web 浏览器或邮件阅读器) 需要将主机名转换为 IP 地址。这些应用程序将调用 DNS 的客户端,并指明需要被转换的主机名。用户主机上的 DNS 收到后,会使用 UDP 通过 53 端口向网络上发送一个 DNS 查询报文,经过一段时间后,用户主机上的 DNS 会收到一个主机名对应的 DNS 回答报文。因此,从用户主机的角度来看,DNS 就像是一个黑盒子,其内部的操作你无法看到。但是实际上,实现 DNS 这个服务的黑盒子非常复杂,它由分布于全球的大量 DNS 服务器以及定义了 DNS 服务器与查询主机通信方式的应用层协议组成。

DNS 最早的设计是只有一台 DNS 服务器。这台服务器会包含所有的 DNS 映射。这是一种集中式的设计,这种设计并不适用于当今的互联网,因为互联网有着数量巨大并且持续增长的主机,这种集中式的设计会存在以下几个问题

  • 单点故障(a single point of failure),如果 DNS 服务器崩溃,那么整个网络随之瘫痪。
  • 通信容量(traaffic volume),单个 DNS 服务器不得不处理所有的 DNS 查询,这种查询级别可能是上百万上千万级
  • 远距离集中式数据库(distant centralized database),单个 DNS 服务器不可能 邻近 所有的用户,假设在美国的 DNS 服务器不可能临近让澳大利亚的查询使用,其中查询请求势必会经过低速和拥堵的链路,造成严重的时延。
  • 维护(maintenance),维护成本巨大,而且还需要频繁更新。

所以 DNS 不可能集中式设计,它完全没有可扩展能力,因此采用分布式设计,所以这种设计的特点如下

分布式、层次数据库

首先分布式设计首先解决的问题就是 DNS 服务器的扩展性问题,因此 DNS 使用了大量的 DNS 服务器,它们的组织模式一般是层次方式,并且分布在全世界范围内。没有一台 DNS 服务器能够拥有因特网上所有主机的映射。相反,这些映射分布在所有的 DNS 服务器上。

大致来说有三种 DNS 服务器:根 DNS 服务器、 顶级域(Top-Level Domain, TLD) DNS 服务器 和 权威 DNS 服务器 。这些服务器的层次模型如下图所示

假设现在一个 DNS 客户端想要知道 www.amazon.com 的 IP 地址,那么上面的域名服务器是如何解析的呢?首先,客户端会先根服务器之一进行关联,它将返回顶级域名 com 的 TLD 服务器的 IP 地址。该客户则与这些 TLD 服务器之一联系,它将为 amazon.com 返回权威服务器的 IP 地址。最后,该客户与 amazom.com 权威服务器之一联系,它为 www.amazom.com 返回其 IP 地址。

DNS 层次结构

我们现在来讨论一下上面域名服务器的层次系统

  • 根 DNS 服务器 ,有 400 多个根域名服务器遍及全世界,这些根域名服务器由 13 个不同的组织管理。根域名服务器的清单和组织机构可以在 root-servers.org/ 中找到,根域名服务器提供 TLD 服务器的 IP 地址。
  • 顶级域 DNS 服务器,对于每个顶级域名比如 com、org、net、edu 和 gov 和所有的国家级域名 uk、fr、ca 和 jp 都有 TLD 服务器或服务器集群。所有的顶级域列表参见 tld-list.com/ 。TDL 服务器提供了权威 DNS 服务器的 IP 地址。
  • 权威 DNS 服务器,在因特网上具有公共可访问的主机,如 Web 服务器和邮件服务器,这些主机的组织机构必须提供可供访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。一个组织机构的权威 DNS 服务器收藏了这些 DNS 记录。

DNS 查询步骤

下面我们描述一下 DNS 的查询步骤,从 DNS 解析 IP 再到 DNS 返回的一系列流程。

注意:通常情况下 DNS 会将查找的信息缓存在浏览器或者计算机本地中,如果有相同的请求到来时,就不再会进行 DNS 查找,而会直接返回结果。

通常情况下,DNS 的查找会经历下面这些步骤

  1. 用户在浏览器中输入网址 www.example.com 并点击回车后,查询会进入网络,并且由 DNS 解析器进行接收。
  2. DNS 解析器会向根域名发起查询请求,要求返回顶级域名的地址。
  3. 根 DNS 服务器会注意到请求地址的前缀并向 DNS 解析器返回 com 的顶级域名服务器(TLD) 的 IP 地址列表。
  4. 然后,DNS 解析器会向 TLD 服务器发送查询报文
  5. TLD 服务器接收请求后,会根据域名的地址把权威 DNS 服务器的 IP 地址返回给 DNS 解析器。
  6. 最后,DNS 解析器将查询直接发送到权威 DNS 服务器
  7. 权威 DNS 服务器将 IP 地址返回给 DNS 解析器
  8. DNS 解析器将会使用 IP 地址响应 Web 浏览器

一旦 DNS 查找的步骤返回了 example.com 的 IP 地址,浏览器就可以请求网页了。

整个流程如下图所示

DNS 解析器

进行 DNS 查询的主机和软件叫做 DNS 解析器,用户所使用的工作站和个人电脑都属于解析器。一个解析器要至少注册一个以上域名服务器的 IP 地址。DNS 解析器是 DNS 查找的第一站,其负责与发出初始请求的客户端打交道。解析器启动查询序列,最终使 URL 转换为必要的 IP 地址。

DNS 递归查询和 DNS 递归解析器不同,该查询是指向需要解析该查询的 DNS 解析器发出请求。DNS 递归解析器是一种计算机,其接受递归查询并通过发出必要的请求来处理响应。

DNS 查询类型

DNS 查找中会出现三种类型的查询。通过组合使用这些查询,优化的 DNS 解析过程可缩短传输距离。在理想情况下,可以使用缓存的记录数据,从而使 DNS 域名服务器能够直接使用非递归查询。

  1. 递归查询:在递归查询中,DNS 客户端要求 DNS 服务器(一般为 DNS 递归解析器)将使用所请求的资源记录响应客户端,或者如果解析器无法找到该记录,则返回错误消息。


2. 迭代查询:在迭代查询中,如果所查询的 DNS 服务器与查询名称不匹配,则其将返回对较低级别域名空间具有权威性的 DNS 服务器的引用。然后,DNS 客户端将对引用地址进行查询。此过程继续使用查询链中的其他 DNS 服务器,直至发生错误或超时为止。


3. 非递归查询:当 DNS 解析器客户端查询 DNS 服务器以获取其有权访问的记录时通常会进行此查询,因为其对该记录具有权威性,或者该记录存在于其缓存内。DNS 服务器通常会缓存 DNS 记录,查询到来后能够直接返回缓存结果,以防止更多带宽消耗和上游服务器上的负载。

DNS 缓存

DNS 缓存(DNS caching) 有时也叫做 DNS 解析器缓存,它是由操作系统维护的临时数据库,它包含有最近的网站和其他 Internet 域的访问记录。也就是说, DNS 缓存只是计算机为了满足快速的响应速度而把已加载过的资源缓存起来,再次访问时可以直接快速引用的一项技术和手段。那么 DNS 的缓存是如何工作的呢?

DNS 缓存的工作流程

在浏览器向外部发出请求之前,计算机会拦截每个请求并在 DNS 缓存数据库中查找域名,该数据库包含有最近的域名列表,以及 DNS 首次发出请求时 DNS 为它们计算的地址。

DNS 缓存方式

DNS 数据可缓存到各种不同的位置上,每个位置均将存储 DNS 记录,它的生存时间由 TTL(DNS 字段) 来决定。

浏览器缓存

现如今的 Web 浏览器设计默认将 DNS 记录缓存一段时间。因为越靠近 Web 浏览器进行 DNS 缓存,为检查缓存并向 IP 地址发出请求的次数就越少。发出对 DNS 记录的请求时,浏览器缓存是针对所请求的记录而检查的第一个位置。

在 chrome 浏览器中,你可以使用 chrome://net-internals/#dns 查看 DNS 缓存的状态。这是基于 Windows 下查询的,我的 Mac 电脑输入上面 url 后无法查看 DNS ,只能 clear host cache,我也不知道为啥,可能是哪里设置的原因?

操作系统内核缓存

在浏览器缓存查询后,会进行操作系统级 DNS 解析器的查询,操作系统级 DNS 解析器是 DNS 查询离开你的计算机前的第二站,也是本地查询的最后一个步骤。

DNS 报文

共同实现 DNS 分布式数据库的所有 DNS 服务器存储了资源记录(Resource Record, RR),RR 提供了主机名到 IP 地址的映射。每个 DNS 回答报文中会包含一条或多条资源记录。RR 记录用于回复客户端查询。

资源记录是一个包含了下列字段的 4 元组

1
python复制代码(Name, Value, Type, TTL)

RR 会有不同的类型,下面是不同类型的 RR 汇总表

DNS RR 类型 解释
A 记录 IPv4 主机记录,用于将域名映射到 IPv4 地址
AAAA 记录 IPv6 主机记录,用于将域名映射到 IPv6 地址
CNAME 记录 别名记录,用于映射 DNS 域名的别名
MX 记录 邮件交换器,用于将 DNS 域名映射到邮件服务器
PTR 记录 指针,用于反向查找(IP地址到域名解析)
SRV 记录 SRV记录,用于映射可用服务。

DNS 有两种报文,一种是查询报文,一种是响应报文,并且这两种报文有着相同的格式,下面是 DNS 的报文格式

上图显示了 DNS 的报文格式,其中事务 ID、标志、问题数量、回答资源记录数、权威名称服务器计数、附加资源记录数这六个字段是 DNS 的报文段首部,报文段首部一共有 12 个字节。

报文段首部

报文段首部是 DNS 报文的基础结构部分,下面我们对报文段首部中的每个字节进行描述

  • 事务 ID: 事务 ID 占用 2 个字节。它是 DNS 的标识,又叫做 标识符,对于请求报文和响应报文来说,这个字段的值是一样的,通过标识符可以区分 DNS 应答报文是对哪个请求进行响应的。
  • 标志:标志字段占用 2 个字节。标志字段有很多,而且也比较重要,下面列出来了所有的标志字段。

每个字段的含义如下

  • QR(Response): 1 bit 的 QR 标识报文是查询报文还是响应报文,查询报文时 QR = 0,响应报文时 QR = 1。
  • OpCode: 4 bit 的 OpCode 表示操作码,其中,0 表示标准查询,1 表示反向查询,2 表示服务器状态请求。
  • AA(Authoritative): 1 bit 的 AA 代表授权应答,这个 AA 只在响应报文中有效,值为 1 时,表示名称服务器是权威服务器;值为 0 时,表示不是权威服务器。
  • TC(Truncated): 截断标志位,值为 1 时,表示响应已超过 512 字节并且已经被截断,只返回前 512 个字节。
  • RD(Recursion Desired): 这个字段是期望递归字段,该字段在查询中设置,并在响应中返回。该标志告诉名称服务器必须处理这个查询,这种方式被称为一个递归查询。如果该位为 0,且被请求的名称服务器没有一个授权回答,它将返回一个能解答该查询的其他名称服务器列表。这种方式被称为迭代查询。
  • RA(Recursion Available): 可用递归字段,这个字段只出现在响应报文中。当值为 1 时,表示服务器支持递归查询。
  • zero: 保留字段,在所有的请求和应答报文中,它的值必须为 0。
  • AD: 这个字段表示信息是否是已授权。
  • CD: 这个字段表示是否禁用安全检查。
  • rcode(Reply code):这个字段是返回码字段,表示响应的差错状态。当值为 0 时,表示没有错误;当值为 1 时,表示报文格式错误(Format error),服务器不能理解请求的报文;当值为 2 时,表示域名服务器失败(Server failure),因为服务器的原因导致没办法处理这个请求;当值为 3 时,表示名字错误(Name Error),只有对授权域名解析服务器有意义,指出解析的域名不存在;当值为 4 时,表示查询类型不支持(Not Implemented),即域名服务器不支持查询类型;当值为 5 时,表示拒绝(Refused),一般是服务器由于设置的策略拒绝给出应答,如服务器不希望对某些请求者给出应答。

相信读者跟我一样,只看这些字段没什么意思,下面我们就通过抓包的方式,看一下具体的 DNS 报文。

现在我们可以看一下具体的 DNS 报文,通过 query 可知这是一个请求报文,这个报文的标识符是 0xcd28,它的标志如下

  • QR = 0 实锤了这就是一个请求。
  • 然后是四个字节的 OpCode,它的值是 0,表示这是一个标准查询。
  • 因为这是一个查询请求,所以没有 AA 字段出现。
  • 然后是截断标志位 Truncated,表示没有被截断。
  • 紧随其后的 RD = 1,表示希望得到递归回答。
  • 请求报文中没有 RA 字段出现。
  • 然后是保留字段 zero。
  • 紧随其后的 0 表示未经身份验证的数据是不可接受的。
  • 没有 rcode 字段的值

然后我们看一下响应报文

可以看到,标志位也是 0xcd28,可以说明这就是上面查询请求的响应。

查询请求已经解释过的报文我们这里就不再说明了,现在只解释一下请求报文中没有的内容

  • 紧随在 OpCode 后面的 AA 字段已经出现了,它的值为 0 ,表示不是权威 DNS 服务器的响应
  • 最后是 rcode 字段的响应,值为 0 时,表示没有错误。

问题区域

问题区域通常指报文格式中查询问题的区域部分。这部分用来显示 DNS 查询请求的问题,包括查询类型和查询类


这部分中每个字段的含义如下

  • 查询名:指定要查询的域名,有时候也是 IP 地址,用于反向查询。
  • 查询类型:DNS 查询请求的资源类型,通常查询类型为 A 类型,表示由域名获取对应的 IP 地址。
  • 查询类:地址类型,通常为互联网地址,值为 1 。

同样的,我们再使用 wireshark 查看一下问题区域

可以看到,这是对 mobile-gtalk.l.google.com 发起的 DNS 查询请求,查询类型是 A,那么得到的响应类型应该也是 A

如上图所示,响应类型是 A ,查询类的值通常是 1、254 和 255,分别表示互联网类、没有此类和所有类,这些是我们感兴趣的值,其他值通常不用于 TCP/IP 网络。

资源记录部分

资源记录部分是 DNS 报文的最后三个字段,包括回答问题区域、权威名称服务器记录、附加信息区域,这三个字段均采用一种称为资源记录的格式,如下图所示

资源记录部分的字段含义如下

  • 域名:DNS 请求的域名。
  • 类型:资源记录的类型,与问题部分中的查询类型值是一样的。
  • 类:地址类型、与问题中的查询类值一样的。
  • 生存时间:以秒为单位,表示资源记录的生命周期。
  • 资源数据长度:资源数据的长度。
  • 资源数据:表示按查询段要求返回的相关资源记录的数据。

资源记录部分只有在 DNS 响应包中才会出现。下面我们就来通过响应报文看一下具体的字段示例

其中,域名的值是 mobile-gtalk.l.google.com ,类型是 A,类是 1,生存时间是 5 秒,数据长度是 4 字节,资源数据表示的地址是 63.233.189.188。

SOA 记录

如果是权威 DNS 服务器的响应的话,会显示记录存储有关区域的重要信息,这种信息就是 SOA 记录。所有 的DNS 区域都需要一个 SOA 记录才能符合 IETF 标准。 SOA 记录对于区域传输也很重要。

SOA 记录除具有 DNS 解析器响应的字段外,还具有一些额外的字段,如下

具体字段含义

  • PNAME:即 Primary Name Server,这是区域的主要名称服务器的名称。
  • RNAME:即 Responsible authority’s mailbox,RNAME 代表管理员的电子邮件地址,@ 用 . 来表示,也就是说 admin.example.com 等同于 admin@example.com。
  • 序列号: 即 Serial Number ,区域序列号是该区域的唯一标识符。
  • 刷新间隔:即 Refresh Interval,在请求主服务器提供 SOA 记录以查看其是否已更新之前,辅助服务器应等待的时间(以秒为单位)。
  • 重试间隔:服务器应等待无响应的主要名称服务器再次请求更新的时间。
  • 过期限制:如果辅助服务器在这段时间内没有收到主服务器的响应,则应停止响应对该区域的查询。

上面提到了主要名称服务器和服务名称服务器,他们之间的关系如下

这块我们主要解释了 RR 类型为 A(IPv4) 和 SOA 的记录,除此之外还有很多类型,这篇文章就不再详细介绍了,读者朋友们可以阅读 《TCP/IP 卷一 协议》和 cloudflare 的官网 www.cloudflare.com/learning/dn… 查阅,值得一提的是,cloudflare 是一个学习网络协议非常好的网站。

DNS 安全

几乎所有的网络请求都会经过 DNS 查询,而且 DNS 和许多其他的 Internet 协议一样,系统设计时并未考虑到安全性,并且存在一些设计限制,这为 DNS 攻击创造了机会。

DNS 攻击主要有下面这几种方式

  • 第一种是 Dos 攻击,这种攻击的主要形式是使重要的 DNS 服务器比如 TLD 服务器或者根域名服务器过载,从而无法响应权威服务器的请求,使 DNS 查询不起作用。
  • 第二种攻击形式是 DNS 欺骗,通过改变 DNS 资源内容,比如伪装一个官方的 DNS 服务器,回复假的资源记录,从而导致主机在尝试与另一台机器连接时,连接至错误的 IP 地址。
  • 第三种攻击形式是 DNS 隧道,这种攻击使用其他网络协议通过 DNS 查询和响应建立隧道。攻击者可以使用 SSH、TCP 或者 HTTP 将恶意软件或者被盗信息传递到 DNS 查询中,这种方式使防火墙无法检测到,从而形成 DNS 攻击。
  • 第四种攻击形式是 DNS 劫持,在 DNS 劫持中,攻击者将查询重定向到其他域名服务器。这可以通过恶意软件或未经授权的 DNS 服务器修改来完成。尽管结果类似于 DNS 欺骗,但这是完全不同的攻击,因为它的目标是名称服务器上网站的 DNS 记录,而不是解析程序的缓存。
  • 第五章攻击形式是 DDoS 攻击,也叫做分布式拒绝服务带宽洪泛攻击,这种攻击形式相当于是 Dos 攻击的升级版

那么该如何防御 DNS 攻击呢?

防御 DNS 威胁的最广为人知的方法之一就是采用 DNSSEC 协议。

DNSSEC

DNSSEC 又叫做 DNS 安全扩展,DNSSEC 通过对数据进行数字签名来保护其有效性,从而防止受到攻击。它是由 IETF 提供的一系列 DNS 安全认证的机制。DNSSEC 不会对数据进行加密,它只会验证你所访问的站点地址是否有效。

DNS 防火墙

有一些攻击是针对服务器进行的,这就需要 DNS 防火墙的登场了,DNS 防火墙是一种可以为 DNS 服务器提供许多安全和性能服务的工具。DNS 防火墙位于用户的 DNS 解析器和他们尝试访问的网站或服务的权威名称服务器之间。防火墙提供 限速访问,以关闭试图淹没服务器的攻击者。如果服务器确实由于攻击或任何其他原因而导致停机,则 DNS 防火墙可以通过提供来自缓存的 DNS 响应来使操作员的站点或服务正常运行。

除了上述两种防御手段外,本身 DNS 区域的运营商就会采取进步一措施保护 DNS 服务器,比如配置 DNS 基础架构,来防止 DDoS 攻击。

更多关于 DNS 的攻击和防御就是网络安全的主题,这篇文章就不再详细介绍了。

总结

这篇文章我用较多的字数为你介绍了 DNS 的基本概述,DNS 的工作机制,DNS 的查询方式,DNS 的缓存机制,我们还通过 WireShark 抓包带你认识了一下 DNS 的报文,最后我为你介绍了 DNS 的攻击手段和防御方式。

这是一篇入门 DNS 较全的文章,花了我一周多的时间来写这篇文章,这篇文章了解清楚后,基本上 DNS 的大部分问题你应该都能够回答,面试我估计也稳了。

另外,添加我的微信 becomecxuan,加入每日一题群,每天一道面试题分享,更多内容请参见我的 Github,成为最好的 bestJavaer

我自己肝了六本 PDF,微信搜索「程序员cxuan」关注公众号后,在后台回复 cxuan ,领取全部 PDF,这些 PDF 如下

六本 PDF 链接

本文转载自: 掘金

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

JVM性能调优实战:让你的IntelliJ Idea纵享丝滑

发表于 2021-01-18

本文已被Github仓库收录 github.com/silently952…

微信公众号:贝塔学Java

前言

在前面整理了一篇关于JVM故障诊断和处理工具,考虑到大部分的Java程序员都使用的时IntelliJ Idea,本篇就使用工具来实战演练对IntelliJ Idea运行速度调优

调优前的运行状态

原始配置内容

要查询idea原始配置文件的路径可以在VisualVM中的概述中查看

原始配置内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码-XX:ReservedCodeCacheSize=240m
-XX:+UseCompressedOops
-Dfile.encoding=UTF-8
-XX:SoftRefLRUPolicyMSPerMB=50
-ea
-Dsun.io.useCanonCaches=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow

-XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log
-XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof

-Xmx512m

打印启动时间插件开发

需要直观的看到优化前和优化后启动时间的变化,所以需要简单做一个Idea的插件开发,关于Idea插件开发的流程建议参考我以前的文章《IDEA插件:多线程文件下载插件开发》

JVM的启动时间到所有组件初始化完成后的时间就看做是IDEA的启动时间,代码如下

1
2
3
4
5
6
7
8
9
10
java复制代码public class MyApplicationInitializedListener implements ApplicationInitializedListener {
@Override
public void componentsInitialized() {
RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
long startTime = bean.getStartTime();
long costTime = System.currentTimeMillis() - startTime;

Messages.showMessageDialog("毫秒:" + costTime, "启动耗时", Messages.getInformationIcon());
}
}

plugin.xml中添加如下代码:

1
2
3
4
ini复制代码<extensions defaultExtensionNs="com.intellij">
<applicationInitializedListener id="MyApplicationInitializedListener"
implementation="cn.silently9527.MyApplicationInitializedListener"/>
</extensions>

优化前的启动信息与时间消耗

根据VisualGC和IDEA启动插件收集到的信息:

  • IDEA启动耗时 15s
  • 总共垃圾收集22次,耗时1.2s,其中新生代GC 17次,耗时324ms; 老年代GC 5次,耗时953ms
  • 加载类27526个,耗时 21s

按照这个数据来看也算是正常,15s 其实也在接受范围内,由于本文主要演示性能调优,所以需要测试能否在快一些

开始尝试优化

调整内存来控制垃圾回收频率

图上我们可以看出,启动参数指定的512m的内存被分配到新生代的只有169m,由于IDEA是我们开发常用的工具,平时的编译过程也需要足够的内存,所以我们需要先把总的内存扩大,这里我设置最大的内存-Xmx1024m,为了让JVM在GC期间不需要再浪费时间再动态计算扩容大小,同时也设置了-Xms1024m;

在启动的过程中Eden共发生了17次GC,为了减少新生代gc次数,我把新生代的内存大小设置成-Xmn256m;

重新启动之后查看VisualGC,新生代gc次数从 17次 降低到了 7次,耗时从 324ms 降低到了 152ms。

在调整内存前发生了5次Full GC,调整内存后的依然还是有4次Full GC,但是从两张图我们可以看出,老年代的空间还有很多剩余,是不应该发生Full GC的;考虑是否是代码中有地方手动调用System.gc()出发了Full GC,所以添加了参数-XX:+DisableExplicitGC,再次重新启动IDEA,结果很失望,依然还有4次Full GC;

再次仔细观察优化前的图,注意看 Last Cause: Metadata GC Threshold , 最后一次gc是应该Metaspace区域内存不够发生的GC,为了验证我们的猜想,打印出GC日志来看看。在idea.vmoptions中添加打印日志相关的参数:

1
2
3
ruby复制代码-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:../gc.log

JVM的GC日志的主要参数包括如下几个:

  • -XX:+PrintGC 输出GC日志
  • -XX:+PrintGCDetails 输出GC的详细日志
  • -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
  • -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
  • -Xloggc:../logs/gc.log 日志文件的输出路径

重新启动idea,查看gc.log

其中PSYoungGen:表示新生代使用的ParallelScavenge垃圾收集器,31416K->0K(181248K)表示 gc前已使用的内存大小 -> gc后已使用内存大小(该区域的总内存大小)

从日志中我们看出每次Full GC都是因为Metadata GC Threshold,而Metaspace每次gc回收的内存几乎没有,仅仅是扩大了该区域的容量;找到了原因那就好办了,添加如下的参数调整Metaspace的大小:

1
ini复制代码-XX:MetaspaceSize=256m

再次重启Idea之后,发现Full GC没有了,心情很爽

测试打开大项目点击编译代码,发现自己的idea卡死了,查看VisualGC之后发现堆内存都还有空闲,只有Metaspace被全部占满了,所以是自己给的最大空间设置太小,所以直接去掉了-XX:MaxMetaspaceSize=256m

选择垃圾收集器

从刚才的gc日志中,我们可以发现默认使用的是ParallelScavenge + Parallel Old垃圾收集器,这个组合注重的是吞吐量,这里我们尝试换一个注重低延时的垃圾收集器试一试

  • ParNew + CMS
    在idea.vmoptions中添加如下配置:
1
2
ruby复制代码-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC

重启IDEA之后查看VisualGC

很尴尬,同样发生了6次gc,ParallelScavenge + Parallel Old的组合耗时197ms,而ParNew + CMS的组合耗时379ms;虽然是这个结果,但是我们需要考虑当前只发生了MinorGC,如果发生FullGC了结果又会如何了,大家可以自己测试一下

  • G1
    我们在换一个最新的G1垃圾回收器试试,在idea.vmoptions中添加如下配置:
1
ruby复制代码-XX:+UseG1GC

这个结果好像也还是要慢一点点,自己多次测试过这两个垃圾回收器,虽然每次结果都不一样,相差不远,所以垃圾回收器可以自己选择,这里我们选择的是G1

类加载时间优化

根据之前的分析,idea启动加载类27526个,耗时 21s,这个我们有办法能优化一下吗?因为idea是常用的开发工具,经常很多人的使用,我们可以认为它的代码是安全的,是否符合当前虚拟机的要求,不会危害虚拟机的安全,所以我们使用参数-Xverify:none来禁用字节码的验证过程

重启IDEA

耗时下降到了11s,效果还是比较明显的

总结

做完了所有优化之后,经过多次重启测试,平均的启动时间下降到了11s,为了安慰我本次操作没有白辛苦,搞一张11s以下的图


写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,白嫖不好,创作不易,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏


我已经从零开始手写了简易版springmvc,以及编写了详细的说明文档,帮助伙伴们深入理解springmvc核心原理,有需要的朋友欢迎关注公众号:贝塔学JAVA ,回复源码即可


本文转载自: 掘金

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

你都用过SpringCloud的哪些组件,它们的原理是什么?

发表于 2021-01-18

前言

看到文章的题目了吗?就是这么抽象和笼统的一个问题,确实是我面试中真实被问到的,某共享货车平台的真实面试问题。
SpringCloud确实是用过,但是那是三四年前了,那个时候SpringCloud刚开始流行没多久,我们技术总监让我们调研一下,然后算上我在内的三个同事就一人买了一本SpringCloud的书籍,开始看,开始研究,正好那个时候DDD也比较火,然后我们就一边研究的SpringCloud一边按照DDD的模型搭建自己的项目。
但是这个项目最后做了三个月,才完成了一期。后面二期还没开始,我就撤了。所以SpringCloud总共的使用时间就两三个月,所以对这部分知识掌握的并不扎实,而且入职了新公司之后,都是使用公司自己封装的框架,也已经三年没有用过SpringCloud了,这次是要面试换工作了,所以决定将这方面的知识,总结一下。

服务治理 Spring Cloud Eureka

我们之前在使用服务之间相互调用的时候,一般是靠一些静态配置来完成的。比如服务A,要调用服务B来完成一个业务操作时,为了实现服务B的高可用,一般是通过手动配置来完成服务B的服务实例清单的维护。
随着业务的发展,系统功能越来越复杂,相应的服务不断增加,而且服务的IP还一直在变化,静态配置来维护各服务,就会变得越来越困难。
在这里插入图片描述
这个时候就出现了服务治理框架,Spring Cloud Eureka。

Spring Cloud Eureka 主要是围绕着服务注册与服务发现机制来完成对微服务的自动化管理的。

服务注册

Eureka提供了服务端组件,我们也称为注册中心。每个服务都向Eureka的服务注册中心,登记自己提供服务的元数据,包括服务的ip地址、端口号、版本号、通信协议等。这样注册中心,就将各个服务维护在了一个服务清单中(双层Map,第一层key是服务名,第二层key是实例名,value是服务地址加端口)。
在这里插入图片描述
服务注册中心,还会以心跳的方式去监听清单中的服务是否可用(默认30秒),若不可用(服务续约时间默认90秒),需从清单中剔除,达到排除故障服务的效果。
Eureka注册中心提供了高可用方案,可以支持集群部署注册中心,然后多个注册中心实例之间又相互注册,这样每个实例中都有一样的服务清单了。

服务发现

Eureka不但提供服务端,还提供了客户端,客户端是在各个服务中运行的。
Eureka客户端主要有两个作用:

  • 向注册中心注册自身提供的服务,并周期性的发送心跳来更新它非服务租约。
  • 同时,也能从服务端查询当前注册的服务信息,并把他们缓存到本地,并周期性的刷新服务状态。

在Eureka Server中注册的服务,相互之间调用,不再是通过指定的具体实例地址,而是通过向服务名发请求实现调用,因为每个服务服务都是多实例,并且实例地址还有可能经常变。
但是通过服务名称调用,并不知道具体的服务实例位置,因此需要向注册中心咨询,并获取所有服务实例清单,然后实现服务的请求访问。

举例

在这里插入图片描述
服务A的一个业务操作,需要调用服务B和服务C来完成。
那么服务A和服务B和服务C都将自己注册到Eureka的注册中心,然后服务A通过咨询注册中心,将注册中心的服务列表清单缓存到自己本地。
通过服务名称获取到服务B和服务C的服务实例地址,最后通过一种轮询策略取出一个具体的服务实例地址来进行调用。

总结一下
Eureka Client : 主要是将服务本身注册到Eureka Server中,同时查询Eureka Server的注册服务列表缓存到本地。
Eureka Server:注册中心,保存了所有注册服务的元数据,包括ip地址,端口等信息。

客户端负载均衡 Spring Cloud Ribbon

服务的调用方,在通过Eureka Client缓存到本地的注册表之后,通过服务名称,找到具体的服务对应的实例地址,但是被调用方的服务地址是有多个的,那么该用那个地址去进行调用呢?

1
2
3
4
java复制代码服务A:
192.168.12.10:9001
192.168.12.11:9001
192.168.12.12:9001

这个时候Spring Cloud Ribbon就出现了,它是专门解决这个问题的,它的作用就是做负载均衡,会均匀的把请求分发到每台机器上。

Ribbon默认使用Round Ribbon的策略进行负载均衡,具体就是采用轮询的方式进行请求。

Ribbon除了有Round Ribbon这种轮询策略,还有其他策略以及自定义策略。
主要有:

  • RandomRole: 从服务实例清单中随机选择一个服务实例。
  • RoundRobinRule: 按照线性轮询的方式依次选择每个服务实例。
  • RetryRule:根据轮询方式进行,且具备重试机制进行选择实例。
  • WeightedResponseTimeRule:对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例。
  • ZoneAvoidanceRule:根据服务方的zone区域和可用性来轮询选择。

Spring Cloud Ribbon具体的执行示例如下:
在这里插入图片描述

实例代码

下面的代码就是通过Ribbon调用服务的代码实例。

1
2
3
4
5
6
7
8
9
java复制代码@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/ribbon-consumer")
public String helloConsumer(){
return restTemplate.getForEntity("http://example-service/index",String.class).getBody();
}
}

可以看到Ribbon也是通过发起http请求,来进行的调用,只不过是通过调用服务名的地址来实现的。虽然说Ribbon不用去具体请求服务实例的ip地址或域名了,但是每调用一个接口都还要手动去发起Http请求,也是比较繁琐的,而且返回类型也比较抽象,所以Spring Cloud对调用方式进行了升级封装。

声明式服务调用 Spring Cloud Feign

Spring Cloud 为了简化服务间的调用,在Ribbon的基础上进行了进一步的封装。单独抽出了一个组件,就是Spring Cloud Feign。在引入Spring Cloud Feign后,我们只需要创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。

Spring Cloud Feign具备可插拔的注解支持,并扩展了Spring MVC的注解支持。

下面我们来看一个具体的例子:

服务方具体的接口定义与实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 接口定义
*/
@FeignClient(value="service-hi",fallback = TestFeignServiceImpl.class)
public interface TestFeignService {

@RequestMapping(value="/hi",method = RequestMethod.GET)
String sayHi(@RequestParam("name") String name);
}
/**
* 具体的服务实现
*/
@Component
public class TestFeignServiceImpl implements TestFeignService {
@Override
public String sayHi(String name) {
return "你好,"+name;
}
}

调用方的使用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RestController
public class TestController
{
@Resource
private TestFeignService testFeignService;

@RequestMapping(value="/hi",method = RequestMethod.GET)
public String sayHi(@RequestParam String name)
{
// 调用远程服务
return testFeignService.sayHi(name);
}
}

通过上面的代码,我们可以看到,调用方通过Feign进程远程服务调用的时候,非常简单,就向是在调用本地服务一样。

像之前的建立连接,构造请求,发起请求,获取响应,解析响应等等操作,对使用者来说都是透明化的,使用者不用关心服务是怎么实现调用的,直接使用即可。

那么Feign是如何实现这套封装逻辑的呢?

其实Feign底层主要是靠动态代理来实现这整个服务的调用过程的。
主要逻辑如下:

  • 如果一个接口上定义了@FeignClient注解,Feign就会根据这个接口生成一个动态代理类。
  • 如果调用方,在调用这个定义了@FeignClient注解的接口时,本质上是会调用Feign生成的代理类。
  • Feign生成的动态代理类,会根据具体接口方法中的@RequestMapping等注解,来动态构造出需要请求的服务地址。
  • 最后针对这个地址,再通过Ribbon发起服务调用,解析响应等操作。

在这里插入图片描述
因为Spring Cloud Feign的使用方式比Spring Cloud Ribbon更方便,所以一般项目中都是使用Feign,而且Feign还有继承特性,可以将远程服务接口继承过来然后再进行自己的个性化扩展。因此Feign的使用范围以及普及率更高一些。

服务容错保护 Spring Cloud Hystrix

在微服务架构中,我们将系统拆分成多个服务单元,各个服务之间通过服务注册与订阅的方式互相依赖。

我们以一个电商网站下单的过程来举例,在下单的业务操作过程中需要调用库存服务,支付服务,积分、物流等服务。假设订单服务最多同一时间只能处理50个请求,这个时候如果积分服务挂了,那么每次订单服务去调用积分服务的时候,都会卡这么一段时间,然后才返回超时异常。

在这种场景下会有什么问题呢?

如果目前电商网站正在搞活动,进行抢购活动,下单的人非常多,这种高并发的场景下,订单服务的已经同时在处理50个下单请求了,并且都卡在了请求积分服务的过程中。订单服务已经没有能力去处理其他请求了。

那么其他服务再来调用订单服务时,发订单服务无响应,这样就导致订单服务也不可用了。然后其他依赖订单服务的服务,也最终会导致不可用。这就是微服务架构中的服务雪崩。

在这里插入图片描述

就上图所示,如果多个服务之间相互调用,而不做任何保护措施的话,那么一个服务挂了,就会产生连锁反应,导致其他服务也挂了。

其实就算是积分服务挂了,也并不应该导致订单服务也挂了,积分服务挂了,我们可以跳过积分服务,或者是放一个默认值,然后继续往下走,等着积分服务恢复了,可以手动恢复一下数据。

那么Spring Cloud Hystrix就是解决这个问题的组件,他主要是起到熔断,隔离,降级的作用。

Spring Cloud Hystrix其实是会为每一个服务开辟一个线程池,然后每个线程池中的线程用于对服务的调用请求。这样就算是积分服务挂了,那也只是调用积分服务的线程池出现问题了,而其他服务的线程池还正常工作。这就是服务的隔离。

这样订单服务在的调用积分服务的时候,如果发现有问题了,积分服务可以通过Hystrix返回一个默认值(默认是5秒内20次调用失败就熔断)。这样订单服务就不用在这里卡住了,可以继续往下调用其他服务进行业务操作了。这就是服务的熔断。

虽然说是积分服务挂了,并且也返回了默认值了,但是后续如果积分服务恢复了,想恢复数据怎么办呢?这个时候积分服务可以将姐收到的请求记录下来,或者是打到日志中,能为后面恢复数据提供依据就行。这就是服务的降级。

整个过程大致如下图所示:
在这里插入图片描述

API网关服务Spring Cloud Zuul

通过上面几个组件的结合使用,已经能够完成一个基本的微服务架构了。但是当一个系统中微服务的数量逐渐增多时,一些通用的逻辑,例如:权限校验机制,请求过滤,请求路由,限流等等,这些每个服务对外提供能力的时候都要考虑到的逻辑,就会变得冗余。

这个时候API网关的概念应运而生,它类似于面向对象设计模式中的Facade模式(门面模式/外观模式),所有的外部客户端访问都需要经过它来进行调度和过滤。主要实现请求路由、负载均衡、校验过滤、服务限流等功能。

Spring Cloud Zuul就是Spring Cloud提供的API网关组件,它通过与Eureka进行整合,将自身注册为Eureka下的应用,从Eureka下获取所有服务的实例,来进行服务的路由。

Zuul还提供了一套过滤器机制,开发者可以自己指定哪些规则的请求需要执行校验逻辑,只有通过校验逻辑的请求才会被路由到具体服务实例上,否则返回错误提示。
在这里插入图片描述

Spring Cloud Zuul的依赖包spring-cloud-starter-zuul本身就包含了对spring-cloud-starter-hystrix和spring-cloud-starter-ribbon模块的依赖,所以Zuul天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载功能。

Zuul的路由实现是通过Path和serviceId还实现的,path是一个http请求去除ip和端口号后的方法路径,例如:http://192.168.20.12:9001/api-zuul/123,那么path就是/api-zuul/123,Zuul在配置时支持模糊匹配,若123是动态参数,可以将path配置成/pai-zuul/**,serviceId就是服务在Eureka中注册的服务名称。

1
2
yaml复制代码zuul.routes.api-zuul.path= /api-zuul/**
zuul.routes.api-zuul.serviceId= service-jimoer

有了统一的网关后,再做统一的鉴权、限流、认证授权、安全等方面的工作就会变的更加方便了。

总结

上面总结了Spring Cloud的几个核心组件,其实Spring Cloud 除了这几个组件还有一些其他的组件,例如:

  • 分布式配置中心:Spring Cloud Config;
  • 消息总线:Spring Cloud Bus;
  • 消息驱动:Spring Cloud Stream;
  • 分布式服务跟踪:Spring Cloud Sleuth。

主要是后面这些组件我们平时用的不多,而且对于微服务来说有些是有替代品的,所以我暂时就没有总结。还有一点毕竟我这次总结是为了解决面试的问题,所以后面如果在实际的工作中用到了剩下的这些组件,我会继续总结的。

好了,总结一下这次的几个组件的内容吧。

  • Spring Cloud Eureka 各个微服务在启动时将自己注册到Eureka Server中,并且各个服务中的Eureka Client又能从注册中心获取各个服务的实例地址清单。
  • Spring Cloud Ribbon 各个服务相互调用的时候,通过Ribbon来进行客户端的负载均衡,从多个实例中根据一定的策略选择一台进行请求。
  • Spring Cloud Feign 基于动态代理机制,根据注解和参数拼接URL,选择具体的服务实例发起请求,简化了服务间相互调用的开发工作。
  • Spring Cloud Hystrix 调用每个服务的时候都是通过线程池中的线程来发起的,不同的服务走不同的线程池,实现了服务的隔离,而且服务不可用时还提供了熔断机制以及支持降低措施。
  • Spring Cloud Zuul 外部请求统一通过Zuul网关来进入,支持自定义路由规则,自定义过滤规则,可以实现同一的鉴权、限流、认证等功能。

最后来一个整体的架构图,将各个组件串起来。

在这里插入图片描述

本文转载自: 掘金

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

Spring Boot 单元测试实践 一、单元测试 二、相关

发表于 2021-01-18

最近在琢磨并写了不少单元测试,参考了很多关于 Spring Boot 的单元测试的文章, 但是绝大部分的案例都是通过 @SpringBootTest 来跑单测,多次实践后,发现对于单元测试来说其实这不是一个最优解,此方式更适用于集成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class FooServiceTest {

@Autowired
private FooService service;

@Test
public void get() {
FooVO foo = service.get("id");
assertThat(foo).isNotNull();
}

}

一、单元测试

原则

AIR 原则

  • Automatic(自动化的):自动通过一系列的断言给出执行结果,而不需要人为去判断,在几十上百的测试用例下很难人为的去判断
  • Independent(独立的):测试用例之间不能相互依赖影响,是独立的
  • Repeatable(可重复的):单元测试是可以重复执行的,不能受到外界环境的影响,如数据库、远程调用、中间件等外部依赖不能影响测试用例的执行

可测性

目前在实际开发中来说单元测试更多是针对方法(函数)进行一个测试,而对方法来说最重要的就是方法是要可测的,如果某个方法是不可测的,或者说很难测,那么就代表此方法的结构存在有问题的,需要进行调整,可见进行单元测试对代码结构设计是有好处的

益处

不进行单元测试,那么在将来对此方法进行一个逻辑的修改或者重构将会付出更多的成本,因为没有一种快速高效可靠的手段去保证修改后的结果是正确的且不会影响到其它业务逻辑

而当对此方法有了充分的测试用例后,在后续的逻辑修改或者重构调整都可以放心大胆的进行,因为可以通过单元测试来验证调整重构后的结果是否依然符合期望

不足

写单元测试的成本太高,特别是在开发任务紧急的情况下很难做到,这也是 TDD 为何难以推行的原因之一

二、相关概念

注解

@RunWith(SpringRunner.class)

表明在 Spring 测试环境下执行测试用例,SpringRunner 是 SpringJUnit4ClassRunner 的别名,此 Runner 提供了一个 Spring 容器环境

@RunWith:When a class is annotated with @RunWith or extends a class annotated with @RunWith, JUnit will invoke the class it references to run the tests in that class instead of the runner built into JUnit.

SpringJUnit4ClassRunner:SpringJUnit4ClassRunner is a custom extension of JUnit’s BlockJUnit4ClassRunner which provides functionality of the Spring TestContext Framework to standard JUnit tests by means of the TestContextManager and associated support classes and annotations.

@SpringBootTest

此注解用于启动一个真实的 Spring 容器用于测试,具有加载 ApplicationContext 的能力,因此可以随心所欲的注入和使用 Spring 容器里的 Bean, 如下所示
image.png
然而实际上的服务会依赖数据库、Redis、MQ等等之类的各种外部服务,此注解也需要配置相关的信息才能正常启动后才能执行单元测试,这违反了单元测试的可重复的原则

而引入这些依赖,服务的体积会变大,导致完全启动起来需要较长的时间,特别是在机器性能内存不够的情况下需要的时间就更久了,可实际上需要的只是简单的调试一个测试用例而已,尤其是这个测试用例需要频繁调试的时候,都需要等待服务慢悠悠的连完数据库、Redis、MQ,再去执行测试用例,不得不说这是一个及其低效的方式(网上的参考文章里的例子都不会提到这个,毕竟 demo 不需要依赖外部服务和中间件,秒秒钟就启动完成)

单元测试的 R 原则是要遵守的,不应该依赖外部服务和中间件,毕竟大部分情况下在单元测试阶段是没有这些中间件的,尤其是在 CI/CD 流程中

@MockBean

spring-boot-test 包提供的注解,用于在 Spring 容器中 Mock 一些 Bean,当测试目标依赖了 下层的 Bean 时,可通过该注解 mock 注入,避免真正去调用 Bean,毕竟不一定会有真实数据库或者其它外部依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Service
public class FooService {
@Autowired
private FooRepository fooRepository;
@Autowired
private BarRepository barRepository;

// some method
}

@RunWith(SpringRunner.class)
public class FooServiceTest {
@Autowired
private FooService fooService;
@MockBean
private FooRepository fooRepository;
@MockBean
private BarRepository barRepository;

// some test method
}

@Import

提供一种快速的方式把 Bean 加入 Spring 的容器中,使得该 Bean 能够被注入(此注解貌似跟单元测试没得关系,实际上也没得关系,但是也可以在单元测试中使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码// 快速导入
@Import({FooService.class})
@RunWith(SpringRunner.class)
public class BarServiceTest extends BaseTest {
@Autowired
private FooService fooService;

@MockBean
private FooRepository fooRepository;
@MockBean
private BarRepository barRepository;
// some test method
}

// 正常使用
@RunWith(SpringRunner.class)
public class OtherServiceTest extends BaseTest {
@Autowired
private FooService fooService;

@MockBean
private FooRepository fooRepository;
@MockBean
private BarRepository barRepository;

// 提供一些测试相关的配置入口,也仅限于 test,ComponentScan 会跳过此类的
@TestConfiguration
static class TestContextConfiguration {
@Bean
public FooService fooService() {
return new FooService();
}
}
// some test method
}

Mockito

单测很重要的一个思想就是 Mock,通过 Mock 能够做到不依赖任何外部的服务或中间件,只关注于方法本身的逻辑,任何外部依赖都应该通过 Mock 的手段完成,如果无法做到 Mock,那么代表方法和类是不可测的,代码结构存在问题,需要进行结构上的调整

Mockito 是 Java 中一个比较强大的 Mock 框架,关于此框架的用法网上文章很多,就此略过

手把手教你 Mockito 的使用

单元测试利器Mockito框架

Mockito API Docs

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.

Mock数据填充

构造真实的 Mock 数据,以下列出了两种,看个人喜好,选一种即可

jfairy

github.com/Codearte/jf…

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Test
public void name() {
Fairy fairy = Fairy.create();
fairy.person();
fairy.company();
fairy.creditCard();
fairy.textProducer();
fairy.baseProducer();
fairy.dateProducer();
fairy.networkProducer();
}

java-faker

github.com/DiUS/java-f…

1
2
3
4
5
6
7
8
java复制代码@Test
public void name() {
Faker faker = new Faker();
String name = faker.name().fullName(); // Miss Samanta Schmidt
String firstName = faker.name().firstName(); // Emory
String lastName = faker.name().lastName(); // Barton
String streetAddress = faker.address().streetAddress(); // 60018 Sawayn Brooks Suite 449
}

断言

判断一个方法执行的结果是否符合期望

单元测试如果不进行断言那是没有意义的,每一次代码逻辑的改动,通过断言都能够及时的接收到反馈,所以断言是必须有的

三、示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// FooService
public FooVO get(String id) {
BarDO barDO = barRepository.getByFooId(id);
FooDO fooDO = fooRepository.get(id);
FooVO foo = new FooVO();
foo.setStatus(barDO.getType());
foo.setId(id);
foo.setName(fooDO.getName() + "-suffix");
switch (barDO.getType()) {
case FAILED:
foo.setContent("this is failed result");
break;
case SUCCESS:
foo.setContent("this is success result");
break;
default:
throw new DummyException("some exception happened!");
}
return foo;
}

正常流

1
2
3
4
5
6
7
8
9
10
11
java复制代码// BarServiceTest
@Test
public void getSuccess() {
String name = getName();
when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.SUCCESS));
when(fooRepository.get(ID)).thenReturn(mockFoo(name));
FooVO fooVO = fooService.get(ID);
assertThat(fooVO).isNotNull();
assertThat(fooVO.getContent()).isEqualTo("this is success result");
assertThat(fooVO.getName()).isEqualTo(name + "-suffix");
}

image.png
当逻辑稍微调整,运行单元测试,断言就会失败

1
java复制代码foo.setContent("this is success result...");

image.png

异常流

1
2
3
4
5
6
7
8
9
10
11
java复制代码// BarServiceTest
@Rule
public ExpectedException expected = ExpectedException.none();
@Test
public void getException() {
expected.expect(DummyException.class);
expected.expectMessage("exception happened"); // 包含
when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.EXCEPTION));
when(fooRepository.get(ID)).thenReturn(mockFoo(getName()));
fooService.get(ID);
}

完整案例

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
java复制代码@Import({FooService.class})
@RunWith(SpringRunner.class)
public class BarServiceTest extends BaseTest {
@Autowired
private FooService fooService;

@MockBean
private FooRepository fooRepository;
@MockBean
private BarRepository barRepository;

@Rule
public ExpectedException expected = ExpectedException.none();

// ignore setUp/tearDown

@Test
public void getSuccess() {
String name = getName();
when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.SUCCESS));
when(fooRepository.get(ID)).thenReturn(mockFoo(name));
FooVO fooVO = fooService.get(ID);
assertThat(fooVO).isNotNull();
assertThat(fooVO.getContent()).isEqualTo("this is success result");
assertThat(fooVO.getName()).isEqualTo(name + "-suffix");
}

@Test
public void getFailed() {
String name = getName();
when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.FAILED));
when(fooRepository.get(ID)).thenReturn(mockFoo(name));
FooVO fooVO = fooService.get(ID);
assertThat(fooVO).isNotNull();
assertThat(fooVO.getContent()).isEqualTo("this is failed result");
assertThat(fooVO.getName()).isEqualTo(name + "-suffix");
}

@Test
public void getException() {
expected.expect(DummyException.class);
expected.expectMessage("exception happened");
when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.EXCEPTION));
when(fooRepository.get(ID)).thenReturn(mockFoo(getName()));
fooService.get(ID);
}

}

@Service
public class FooService {
@Autowired
private FooRepository fooRepository;
@Autowired
private BarRepository barRepository;

// 当此方法的逻辑有任何的调整,测试用例都有可能执行失败
public FooVO get(String id) {
BarDO barDO = barRepository.getByFooId(id);
FooDO fooDO = fooRepository.get(id);
FooVO foo = new FooVO();
foo.setStatus(barDO.getType());
foo.setId(id);
foo.setName(fooDO.getName() + "-suffix");
switch (barDO.getType()) {
case FAILED:
foo.setContent("this is failed result");
break;
case SUCCESS:
foo.setContent("this is success result");
break;
default:
throw new DummyException("some exception happened!");
}
return foo;
}
}

详细代码,传送门

示例代码基于 Junit4,Junit5在4的基础上优化增强了很多,但是这不是最重要的,工具再好用,如果方法不可测、用例断言没设计好依旧是徒然的

@Rule 和 @TestConfiguration 等其它概念后续有机会实践后再聊聊

四、结语

敏捷宣言有一句就是要响应变化,而对于开发人员来说,单元测试就是一种高效、可靠的方式去响应需求或是其它层面的变化,对代码逻辑进行的任何改动都能够快速的在测试用例中得到反馈

本文转载自: 掘金

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

Kafka 探险 - 源码环境搭建

发表于 2021-01-17

这个 Kafka 的专题,我会从系统整体架构,设计到代码落地。和大家一起杠源码,学技巧,涨知识。希望大家持续关注一起见证成长!

我相信:技术的道路,十年如一日!十年磨一剑!

前言

在阅读源码之前,首先要做的就是搭建一套源码调试环境,这是最基本的一步,不要觉得麻烦或者简单就不去做,也许你会像我一样搭源码的过程中得到一些教训和经验。同时在后面阅读源码的过程中,很多看不懂的地方 debug 一下也许就明朗了。

记录了搭建 Kafka 源码环境的简单过程,为大家提供一个步骤参考,同时记录搭建环境中可能会遇到的问题及解决方案。

这个环境搭建过程也会提到一个非常实用,并且很多人都不知道的源码 debug 技巧,对阅读源码和 debug 系统很有帮助哦!

源码下载

笔者下载的 Kafka 版本是 0.11.0.1 ,源码下载地址是 :kafka.apache.org/downloads

下载时选择,源码下载:

image.png

解压工程&安装插件

解压下载好的源码包,直接使用 Idea 打开项目即可。另外由于 Kafka 代码是 Scala 写的,所以需要安装一个 Scala 插件。

到 Idea 的插件市场下载 Scala 插件,这个插件不仅仅有语法提示而且可以帮你下载 Scala SDK,切换 SDK 非常方便,必装!

image.png

仓库初始化

养成一个好习惯,对于这种直接下载的源码包,先用 git 进行初始化,后续有什么改动也能够进行回溯,防止直接把源码改瓢了,之前做的注释也很难再拷贝出来。

1
sql复制代码git add . && git commit -m 'init'

构建项目

修改项目根目录下的 build.gradle ,将所有的 mavenCentral() 替换成 maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'} 加快 gradle 导入包的速度。

完事以后开始进行 Gradle 构建

image.png

构建完成后,所有的 Kafka 些模块会被自动导入,如下图是导入完成时的工程模块结构

image.png

启动

找到 kafka.Kafka 这个类,然后运行 Main 方法,添加启动参数

1
2
3
bash复制代码vmOptions ->  -Dkafka.logs.dir=/Users/lwen/logs/kafka   # 这个目录需要修改一下,是 kafka 消息文件目录

program arguments -> config/server.properties # kafka 的配置文件路径

下图展示配置完毕时的参数

image.png

我遇到了很多编译警告⚠️,不过只要还能继续编译就不用 care。

令人悲伤的是程序启动不起来,main 方法直接退出了,没有任何的提示。

image.png

排查问题

遇到上面那个问题后,找不到任何的日志看出是因为什么导致的,当时看网上的教程是把 log4j 配置文件拷贝到 kafka 目录,日志就能生效,但是我尝试过了也不 OK。

所以我就开始 debug,找出为什么这个地方会出现 exit with 1 ,这里介绍一个调试源码的技巧:我们看到代码是遇到了异常才退出的,但是我们没有异常堆栈和错误提示,可以肯定的是程序肯定遇到异常了。

所以我们在 Idea 中,断点所有会发生异常的位置具体操作:

cmd+shift+f8 打开断点窗口

image.png

勾选上 Any Exception ,并在 Catch Class Filter 中去掉 ClassNotFoundException 因为在程序运行的时候会有双亲委派的类加载过程,肯定会触发 ClassNotFoundException 。这样配置以后,程序抛出任何非 ClassNotFoundException 的位置都会停下来

image.png

以 debug 的方式启动程序,最后我发现程序在 initZk() 的地方异常了,那就很清晰了,zk 配置问题

image.png

这个有点坑!主要是因为没有开启日志,所以一行日志没有直接抛出异常结束进程了,后来我也找到打印日志的方法,按照我上面的启动参数配置就可以。

所以原因是没有启动 zk,那么下一步就是安装 zk。

安装 ZK

1
复制代码brew install  zookeeper

安装完了以后启动 zk ,我采用的是 后台运行的方式:

1
sql复制代码brew services start zookeeper

当然也可以直接前台启动,看到日志输出:

1
sql复制代码zkServer start

再次启动

image.png

唠叨

本来以为搭建源码挺简单的,但是还是自己把自己坑了一把。日志没配,zk 没配。不过好在这个过程中,就算没有任何日志和堆栈也能分析到问题的原因,也是调试的一个小技巧,相当实用。

下篇文章要开始分析 Producer 的架构啦,首先我们会尝试自己实现一个 Producer ,然后再和官方的对比,看看优秀的代码在设计中更关注的点以及是如何实现的。

另外:大家也可以关注下我的微信公众号哦~ 技术分享和个人思考都会第一时间同步!

本文转载自: 掘金

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

1…734735736…956

开发者博客

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