diff --git a/README.md b/README.md index 1409491..c6e71f5 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ **目的**:本着开源的精神,为了让广大 Javaer 不浪费搜索的时间和金钱,可以快速的浏览/学习面试最常见的知识点,我创建了这个 Repository。 **适合阅读人群:** - -* 面临校招的相关专业大学生。 + +* 面临校招的相关专业大学生。 * 准备面试的 Java 初/中级工程师。 * 希望掌握 Java 最流行知识点的编程爱好者。 @@ -19,11 +19,12 @@ * 鉴于 GitHub 网页端对于目录的支持性问题,我在单个章节没有创建目录,如有需要,建议使用 Chrome 的插件 **简悦** 阅读,当然你也可以 download 后使用 Typora 阅读。 * 如果对你有帮助,请轻点 Star 支持,给我更大的动力 :D -* **金三要到了,准备组建一个由最近在面试的人组成的群,在群里分享每次面试的第一手面试题,star 后即可进群,请加微信:naerjiajia207** +* **微信:naerjiajia207** # Java 知识点及面试题 -## Java 基础 +## Java 基础 - [Java 基础](https://github.com/lvminghui/Java-Notes/blob/master/docs/Java%E5%9F%BA%E7%A1%80.md) +- [计算机网络面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%A2%98.md) ## Java 容器 - [Java 容器知识点梳理](https://github.com/lvminghui/Java-Notes/blob/master/docs/Java%20%E5%AE%B9%E5%99%A8.md) - [集合框架面试题及解析](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E9%9D%A2%E8%AF%95%E7%9F%A5%E8%AF%86%E7%82%B9.md) @@ -43,6 +44,11 @@ - [MySQL 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/MySQL%E9%9D%A2%E8%AF%95%E9%A2%98.md) - [MySQL 常见知识点总结](https://github.com/lvminghui/Java-Notes/blob/master/docs/MySQL%E5%B8%B8%E8%A7%81%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93.md) +### 深入理解 MySQL +- [基础篇](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3MySQL%E5%9F%BA%E7%A1%80%E7%AF%87.md) + +- [高性能实战篇](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E9%AB%98%E6%80%A7%E8%83%BD%E5%AE%9E%E8%B7%B5%E7%AF%87.md) + ## Redis - [Redis 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/Redis%20%E9%9D%A2%E8%AF%95%E9%A2%98.md) @@ -50,12 +56,13 @@ - [SpringBean 的生命周期](https://github.com/lvminghui/Java-Notes/blob/master/docs/SpringBean%20%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.md) - [Spring 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%20%E9%9D%A2%E8%AF%95%E9%A2%98.md) - [Spring 的设计理念和设计模式分析](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%E7%9A%84%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5%E5%92%8C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%88%86%E6%9E%90.md) -## Mybatis +### Mybatis - [Mybatis](https://github.com/lvminghui/Java-Notes/blob/master/docs/Mybatis.md) -## Spring Boot +### Spring Boot - [Spring Boot 面试题](https://github.com/lvminghui/Java-Notes/blob/master/docs/SpringBoot.md) ## Spring Cloud - [Spring Cloud](https://github.com/lvminghui/Java-Notes/blob/master/docs/Spring%20Cloud.md) +- [项目常用工具](https://github.com/lvminghui/Java-Notes/blob/master/docs/%E9%A1%B9%E7%9B%AE%E5%B8%B8%E7%94%A8%E5%B7%A5%E5%85%B7.md) diff --git a/docs/Effective Java.md b/docs/Effective Java.md new file mode 100644 index 0000000..3e5e3f9 --- /dev/null +++ b/docs/Effective Java.md @@ -0,0 +1,1088 @@ +20 为后代设计接口  + + 在 Java 8 之前,不可能在不破坏现有实现的情况下为接口添加方法。 如果向接口添加了一个新方法,现有的实现通常会缺少该方法,从而导致编译时错误。 在 Java 8 中,添加了默认方法(default method)构造[JLS 9.4],目的是允许将方法添加到现有的接口。 但是增加新的方法到现有的接口是充满风险的。 + +  默认方法的声明包含一个默认实现,该方法允许实现接口的类直接使用,而不必实现默认方法。 虽然在 Java 中添加默认方法可以将方法添加到现有接口,但不能保证这些方法可以在所有已有的实现中使用。 默认的方法被「注入(injected)」到现有的实现中,没有经过实现类的知道或同意。 在 Java 8 之前,这些实现是用默认的接口编写的,它们的接口永远不会获得任何新的方法。 + +  许多新的默认方法被添加到 Java 8 的核心集合接口中,主要是为了方便使用 lambda 表达式(第 6 章)。 Java 类库的默认方法是高质量的通用实现,在大多数情况下,它们工作正常。 但是,编写一个默认方法并不总是可能的,它保留了每个可能的实现的所有不变量。 + +  例如,考虑在 Java 8 中添加到 Collection 接口的 removeIf 方法。此方法删除给定布尔方法(或 Predicate 函数式接口)返回 true 的所有元素。默认实现被指定为使用迭代器遍历集合,调用每个元素的谓词,并使用迭代器的 remove 方法删除谓词返回 true 的元素。 据推测,这个声明看起来像这样:默认实现被指定为使用迭代器遍历集合,调用每个元素的 Predicate 函数式接口,并使用迭代器的 remove 方法删除 Predicate 函数式接口返回 true 的元素。 根据推测,这个声明看起来像这样: + +// Default method added to the Collection interface in Java 8 +default boolean removeIf(Predicate filter) { + Objects.requireNonNull(filter); + boolean result = false; + for (Iterator it = iterator(); it.hasNext(); ) { + if (filter.test(it.next())) { + it.remove(); + result = true; + } + } + return result; +} +  这是可能为 removeIf 方法编写的最好的通用实现,但遗憾的是,它在一些实际的 Collection 实现中失败了。 例如,考虑 org.apache.commons.collections4.collection.SynchronizedCollection 方法。 这个类出自 Apache Commons 类库中,与 java.util 包中的静态工厂 Collections.synchronizedCollection 方法返回的类相似。 Apache 版本还提供了使用客户端提供的对象进行锁定的能力,以代替集合。 换句话说,它是一个包装类(条目 18),它们的所有方法在委托给包装集合类之前在一个锁定对象上进行同步。 + +  Apache 的 SynchronizedCollection 类仍然在积极维护,但在撰写本文时,并未重写 removeIf 方法。 如果这个类与 Java 8 一起使用,它将继承 removeIf 的默认实现,但实际上不能保持类的基本承诺:自动同步每个方法调用。 默认实现对同步一无所知,并且不能访问包含锁定对象的属性。 如果客户端在另一个线程同时修改集合的情况下调用 SynchronizedCollection 实例上的 removeIf 方法,则可能会导致 ConcurrentModificationException 异常或其他未指定的行为。 + +  为了防止在类似的 Java 平台类库实现中发生这种情况,比如 Collections.synchronizedCollection 返回的包级私有的类,JDK 维护者必须重写默认的 removeIf 实现和其他类似的方法来在调用默认实现之前执行必要的同步。 原来不属于 Java 平台的集合实现没有机会与接口更改进行类似的改变,有些还没有这样做。 + +  在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时会失败。 虽然不是非常普遍,但这个问题也不是一个孤立的事件。 在 Java 8 中添加到集合接口的一些方法已知是易受影响的,并且已知一些现有的实现会受到影响。 + +  应该避免使用默认方法向现有的接口添加新的方法,除非这个需要是关键的,在这种情况下,你应该仔细考虑,以确定现有的接口实现是否会被默认的方法实现所破坏。然而,默认方法对于在创建接口时提供标准的方法实现非常有用,以减轻实现接口的任务(详见第 20 条)。 + +  还值得注意的是,默认方法不是被用来设计,来支持从接口中移除方法或者改变现有方法的签名的目的。在不破坏现有客户端的情况下,这些接口都不可能发生更改。 + +  准则是清楚的。 尽管默认方法现在是 Java 平台的一部分,但是非常悉心地设计接口仍然是非常重要的。 虽然默认方法可以将方法添加到现有的接口,但这样做有很大的风险。 如果一个接口包含一个小缺陷,可能会永远惹怒用户。 如果一个接口严重缺陷,可能会破坏包含它的 API。 + +  因此,在发布之前测试每个新接口是非常重要的。 多个程序员应该以不同的方式实现每个接口。 至少,你应该准备三种不同的实现。 编写多个使用每个新接口的实例来执行各种任务的客户端程序同样重要。 这将大大确保每个接口都能满足其所有的预期用途。 这些步骤将允许你在发布之前发现接口中的缺陷,但仍然可以轻松地修正它们。 虽然在接口被发布后可能会修正一些存在的缺陷,但不要太指望这一点。 + +22. 接口仅用来定义类型 +  当类实现接口时,该接口作为一种类型(type),可以用来引用类的实例。因此,一个类实现了一个接口,因此表明客户端可以如何处理类的实例。为其他目的定义接口是不合适的。 + +  一种失败的接口就是所谓的常量接口(constant interface)。 这样的接口不包含任何方法; 它只包含静态 final 属性,每个输出一个常量。 使用这些常量的类实现接口,以避免需要用类名限定常量名。 这里是一个例子: + +// Constant interface antipattern - do not use! +public interface PhysicalConstants { + // Avogadro's number (1/mol) + static final double AVOGADROS_NUMBER = 6.022_140_857e23; + + // Boltzmann constant (J/K) + static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23; + + // Mass of the electron (kg) + static final double ELECTRON_MASS = 9.109_383_56e-31; +} +  常量接口模式是对接口的糟糕使用。 类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须实现接口,以确保二进制兼容性。如果一个非 final 类实现了常量接口,那么它的所有子类的命名空间都会被接口中的常量所污染。 + +  Java 平台类库中有多个常量接口,如 java.io.ObjectStreamConstants。 这些接口应该被视为不规范的,不应该被效仿。 + +  如果你想导出常量,有几个合理的选择方案。 如果常量与现有的类或接口紧密相关,则应将其添加到该类或接口中。 例如,所有数字基本类型的包装类,如 Integer 和 Double,都会导出 MIN_VALUE 和 MAX_VALUE 常量。 如果常量最好被看作枚举类型的成员,则应该使用枚举类型(详见第 34 条)导出它们。 否则,你应该用一个不可实例化的工具类来导出常量(详见第 4 条)。 下是前面所示的 PhysicalConstants 示例的工具类的版本: + +// Constant utility class +package com.effectivejava.science; + +public class PhysicalConstants { + private PhysicalConstants() { } // Prevents instantiation + + public static final double AVOGADROS_NUMBER = 6.022_140_857e23; + public static final double BOLTZMANN_CONST = 1.380_648_52e-23; + public static final double ELECTRON_MASS = 9.109_383_56e-31; +} +  顺便提一下,请注意在数字文字中使用下划线字符(_)。 从 Java 7 开始,合法的下划线对数字字面量的值没有影响,但是如果使用得当的话可以使它们更容易阅读。 无论是固定的浮点数,如果他们包含五个或更多的连续数字,考虑将下划线添加到数字字面量中。 对于底数为 10 的数字,无论是整型还是浮点型的,都应该用下划线将数字分成三个数字组,表示一千的正负幂。 + +  通常,实用工具类要求客户端使用类名来限定常量名,例如 PhysicalConstants.AVOGADROS_NUMBER。 如果大量使用实用工具类导出的常量,则通过使用静态导入来限定具有类名的常量: + +// Use of static import to avoid qualifying constants +import static com.effectivejava.science.PhysicalConstants.*; + +public class Test { + double atoms(double mols) { + return AVOGADROS_NUMBER * mols; + } + ... + // Many more uses of PhysicalConstants justify static import +} +  总之,接口只能用于定义类型。 它们不应该仅用于导出常量。 + + 23. 类层次结构优于标签类 +  有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形: + +// Tagged class - vastly inferior to a class hierarchy! +class Figure { + enum Shape { RECTANGLE, CIRCLE }; + + // Tag field - the shape of this figure + final Shape shape; + + // These fields are used only if shape is RECTANGLE + double length; + double width; + + // This field is used only if shape is CIRCLE + double radius; + + // Constructor for circle + Figure(double radius) { + shape = Shape.CIRCLE; + this.radius = radius; + } + + // Constructor for rectangle + Figure(double length, double width) { + shape = Shape.RECTANGLE; + this.length = length; + this.width = width; + } + + double area() { + switch(shape) { + case RECTANGLE: + return length * width; + case CIRCLE: + return Math.PI * (radius * radius); + default: + throw new AssertionError(shape); + } + } +} +  这样的标签类具有许多缺点。 它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 switch 语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 字段不能成为 final,除非构造方法初始化不相关的字段,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 switch 语句添加一个 case,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,标签类是冗长的,容易出错的,而且效率低下。 + +  幸运的是,像 Java 这样的面向对象的语言为定义一个能够表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。标签类仅仅是一个类层次的简单的模仿。 + +  要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 Figure 类中,只有一个这样的方法,就是 area 方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据字段,把它们放在这个类。Figure 类中不存在这种与类型无关的方法或字段。 + +  接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径字段是属于圆的,长度和宽度字段都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 Figure 类的类层次: + +// Class hierarchy replacement for a tagged class +abstract class Figure { + abstract double area(); +} + +class Circle extends Figure { + final double radius; + + Circle(double radius) { this.radius = radius; } + + @Override double area() { return Math.PI * (radius * radius); } +} +class Rectangle extends Figure { + final double length; + final double width; + + Rectangle(double length, double width) { + this.length = length; + this.width = width; + } + @Override double area() { return length * width; } +} +  这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据字段所占用。 所有的字段是 final 的。 编译器确保每个类的构造方法初始化其数据字段,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 switch-case 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。 + +  类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的): + +class Square extends Rectangle { + Square(double side) { + super(side, side); + } +} +  请注意,上述层次结构中的字段是直接访问的,而不是通过访问器方法访问的。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。 + +  总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑标签字段是否可以被删除,并是否能被类层次结构替换。 当遇到一个带有标签字段的现有类时,可以考虑将其重构为一个类层次结构。 + +24. 支持使用静态成员类而不是非静态类 +  嵌套类(nested class)是在另一个类中定义的类。 嵌套类应该只存在于其宿主类(enclosing class)中。 如果一个嵌套类在其他一些情况下是有用的,那么它应该是一个顶级类。 有四种嵌套类:静态成员类,非静态成员类,匿名类和局部类。 除了第一种以外,剩下的三种都被称为内部类(inner class)。 这个条目告诉你什么时候使用哪种类型的嵌套类以及为什么使用。 + +  静态成员类是最简单的嵌套类。 最好把它看作是一个普通的类,恰好在另一个类中声明,并且可以访问所有宿主类的成员,甚至是那些被声明为私有类的成员。 静态成员类是其宿主类的静态成员,并遵循与其他静态成员相同的可访问性规则。 如果它被声明为 private,则只能在宿主类中访问,等等。 + +  静态成员类的一个常见用途是作为公共帮助类,仅在与其外部类一起使用时才有用。 例如,考虑一个描述计算器支持的操作的枚举类型(详见第 34 条)。 Operation 枚举应该是 Calculator 类的公共静态成员类。 Calculator 客户端可以使用 Calculator.Operation.PLUS 和 Calculator.Operation.MINUS 等名称来引用操作。 + +  在语法上,静态成员类和非静态成员类之间的唯一区别是静态成员类在其声明中具有 static 修饰符。 尽管句法相似,但这两种嵌套类是非常不同的。 非静态成员类的每个实例都隐含地与其包含的类的宿主实例相关联。 在非静态成员类的实例方法中,可以调用宿主实例上的方法,或者使用限定的构造[JLS,15.8.4] 获得对宿主实例的引用。 如果嵌套类的实例可以与其宿主类的实例隔离存在,那么嵌套类必须是静态成员类:不可能在没有宿主实例的情况下创建非静态成员类的实例。 + +  非静态成员类实例和其宿主实例之间的关联是在创建成员类实例时建立的,并且之后不能被修改。 通常情况下,通过在宿主类的实例方法中调用非静态成员类构造方法来自动建立关联。 尽管很少有可能使用表达式 enclosingInstance.new MemberClass(args) 手动建立关联。 正如你所预料的那样,该关联在非静态成员类实例中占用了空间,并为其构建添加了时间开销。 + +  非静态成员类的一个常见用法是定义一个 Adapter [Gamma95],它允许将外部类的实例视为某个不相关类的实例。 例如,Map 接口的实现通常使用非静态成员类来实现它们的集合视图,这些视图由 Map 的 keySet,entrySet 和 values 方法返回。 同样,集合接口(如 Set 和 List)的实现通常使用非静态成员类来实现它们的迭代器: + +// Typical use of a nonstatic member class +public class MySet extends AbstractSet { + ... // Bulk of the class omitted + + @Override + public Iterator iterator() { + return new MyIterator(); + } + + private class MyIterator implements Iterator { + ... + } +} +  如果你声明了一个不需要访问宿主实例的成员类,总是把 static 修饰符放在它的声明中,使它成为一个静态成员类,而不是非静态的成员类。 如果你忽略了这个修饰符,每个实例都会有一个隐藏的外部引用给它的宿主实例。 如前所述,存储这个引用需要占用时间和空间。 更严重的是,并且会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中(详见第 7 条)。 由此产生的内存泄漏可能是灾难性的。 由于引用是不可见的,所以通常难以检测到。 + +  私有静态成员类的常见用法是表示由它们的宿主类表示的对象的组件。 例如,考虑将键与值相关联的 Map 实例。 许多 Map 实现对于映射中的每个键值对都有一个内部的 Entry 对象。 当每个 entry 都与 Map 关联时,entry 上的方法 (getKey,getValue 和 setValue) 不需要访问 Map。 因此,使用非静态成员类来表示 entry 将是浪费的:私有静态成员类是最好的。 如果意外地忽略了 entry 声明中的 static 修饰符,Map 仍然可以工作,但是每个 entry 都会包含对 Map 的引用,浪费空间和时间。 + +  如果所讨论的类是导出类的公共或受保护成员,则在静态和非静态成员类之间正确选择是非常重要的。 在这种情况下,成员类是导出的 API 元素,如果不违反向后兼容性,就不能在后续版本中从非静态变为静态成员类。 + +  正如你所期望的,一个匿名类没有名字。 它不是其宿主类的成员。 它不是与其他成员一起声明,而是在使用时同时声明和实例化。 在表达式合法的代码中,匿名类是允许的。 当且仅当它们出现在非静态上下文中时,匿名类才会封装实例。 但是,即使它们出现在静态上下文中,它们也不能有除常量型变量之外的任何静态成员,这些常量型变量包括 final 的基本类型,或者初始化常量表达式的字符串属性[JLS,4.12.4]。 + +  匿名类的适用性有很多限制。 除了在声明的时候之外,不能实例化它们。 你不能执行 instanceof 方法测试或者做任何其他需要你命名的类。 不能声明一个匿名类来实现多个接口,或者继承一个类并同时实现一个接口。 匿名类的客户端不能调用除父类型继承的成员以外的任何成员。 因为匿名类在表达式中出现,所以它们必须保持简短 —— 约十行或更少 —— 否则可读性将受到影响。 + +  在将 lambda 表达式添加到 Java(第 6 章)之前,匿名类是创建小函数对象和处理对象的首选方法,但 lambda 表达式现在是首选(详见第 42 条)。 匿名类的另一个常见用途是实现静态工厂方法(请参阅条目 20 中的 intArrayAsList)。 + +  局部类是四种嵌套类中使用最少的。 一个局部类可以在任何可以声明局部变量的地方声明,并遵守相同的作用域规则。 局部类与其他类型的嵌套类具有共同的属性。 像成员类一样,他们有名字,可以重复使用。 就像匿名类一样,只有在非静态上下文中定义它们时,它们才会包含实例,并且它们不能包含静态成员。 像匿名类一样,应该保持简短,以免损害可读性。 + +  回顾一下,有四种不同的嵌套类,每个都有它的用途。 如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。 如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的; 否则,使其静态。 假设这个类属于一个方法内部,如果你只需要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类;否则,把它变成局部类。 + + 25. 将源文件限制为单个顶级类 +  虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。 风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。 使用哪个定义会受到源文件传递给编译器的顺序的影响。 + +  为了具体说明,请考虑下面源文件,其中只包含一个引用其他两个顶级类(Utensil 和 Dessert 类)的成员的 Main 类: + +public class Main { + public static void main(String[] args) { + System.out.println(Utensil.NAME + [Dessert.NAME](http://Dessert.NAME)); + } +} +复制ErrorOK! +  现在假设在 Utensil.java 的源文件中同时定义了 Utensil 和 Dessert: + +// Two classes defined in one file. Don't ever do this! +class Utensil { + static final String NAME = "pan"; +} + +class Dessert { + static final String NAME = "cake"; +} +复制ErrorOK! +  当然,main 方法会打印 pancake。 + +  现在假设你不小心创建了另一个名为 Dessert.java 的源文件,它定义了相同的两个类: + +// Two classes defined in one file. Don't ever do this! +class Utensil { + static final String NAME = "pot"; +} + +class Dessert { + static final String NAME = "pie"; +} +复制ErrorOK! +  如果你足够幸运,使用命令 javac Main.java Dessert.java 编译程序,编译将失败,编译器会告诉你,你已经多次定义了类 Utensil 和 Dessert。 这是因为编译器首先编译 Main.java,当它看到对 Utensil 的引用(它在 Dessert 的引用之前)时,它将在 Utensil.java 中查找这个类并找到 Utensil 和 Dessert。 当编译器在命令行上遇到 Dessert.java 时,它也将拉入该文件,导致它遇到 Utensil 和 Dessert 的定义。 + +  如果使用命令 javac Main.java 或 javac Main.java Utensil.java 编译程序,它的行为与在编写 Dessert.java 文件(即打印 pancake)之前的行为相同。 但是,如果使用命令 javac Dessert.java Main.java 编译程序,它将打印 potpie。 程序的行为因此受到源文件传递给编译器的顺序的影响,这显然是不可接受的。 + +  解决这个问题很简单,将顶层类(如我们的例子中的 Utensil 和 Dessert)分割成单独的源文件。 如果试图将多个顶级类放入单个源文件中,请考虑使用静态成员类(详见第 24 条)作为将类拆分为单独的源文件的替代方法。 如果这些类从属于另一个类,那么将它们变成静态成员类通常是更好的选择,因为它提高了可读性,并且可以通过声明它们为私有(详见第 15 条)来减少类的可访问性。下面是我们的例子看起来如何使用静态成员类: + +// Static member classes instead of multiple top-level classes +public class Test { + public static void main(String[] args) { + System.out.println(Utensil.NAME + [Dessert.NAME](http://Dessert.NAME)); + } + + private static class Utensil { + static final String NAME = "pan"; + } + + private static class Dessert { + static final String NAME = "cake"; + } +} +复制ErrorOK! +  这个教训很清楚:永远不要将多个顶级类或接口放在一个源文件中。 遵循这个规则保证在编译时不能有多个定义。 这又保证了编译生成的类文件以及生成的程序的行为与源文件传递给编译器的顺序无关。 + +  自 Java 5 以来,泛型已经成为该语言的一部分。 在泛型之前,你必须转换从集合中读取的每个对象。 如果有人不小心插入了错误类型的对象,则在运行时可能会失败。 使用泛型,你告诉编译器在每个集合中允许哪些类型的对象。 编译器会自动插入强制转换,并在编译时告诉你是否尝试插入错误类型的对象。 这样做的结果是既安全又清晰的程序,但这些益处,不限于集合,是有代价的。 本章告诉你如何最大限度地提高益处,并将并发症降至最低。 + +26. 不要使用原始类型 +  首先,有几个术语。一个类或接口,它的声明有一个或多个类型参数(type parameters ),被称之为泛型类或泛型接口[JLS,8.1.2,9.1.2]。 例如,List 接口具有单个类型参数 E,表示其元素类型。 接口的全名是 List(读作「E」的列表),但是人们经常称它为 List。 泛型类和接口统称为泛型类型(generic types)。 + +  每个泛型定义了一组参数化类型(parameterized types),它们由类或接口名称组成,后跟一个与泛型类型的形式类型参数[JLS,4.4,4.5] 相对应的实际类型参数的尖括号「<>」列表。 例如,List(读作「字符串列表」)是一个参数化类型,表示其元素类型为 String 的列表。 (String 是与形式类型参数 E 相对应的实际类型参数)。 + +  最后,每个泛型定义了一个原始类型(raw type),它是没有任何类型参数的泛型类型的名称[JLS,4.8]。 例如,对应于 List 的原始类型是 List。 原始类型的行为就像所有的泛型类型信息都从类型声明中被清除一样。 它们的存在主要是为了与没有泛型之前的代码相兼容。 + +  在泛型被添加到 Java 之前,这是一个典型的集合声明。 从 Java 9 开始,它仍然是合法的,但并不是典型的声明方式了: + +// Raw collection type - don't do this! + +// My stamp collection. Contains only Stamp instances. +private final Collection stamps = ... ; +复制ErrorOK! +  如果你今天使用这个声明,然后不小心把 coin 实例放入你的 stamp 集合中,错误的插入编译和运行没有错误(尽管编译器发出一个模糊的警告): + +// Erroneous insertion of coin into stamp collection +stamps.add(new Coin( ... )); // Emits "unchecked call" warning +复制ErrorOK! +  直到您尝试从 stamp 集合中检索 coin 实例时才会发生错误: + +// Raw iterator type - don't do this! +for (Iterator i = stamps.iterator(); i.hasNext(); ) + Stamp stamp = (Stamp) i.next(); // Throws ClassCastException + stamp.cancel(); +复制ErrorOK! +  正如本书所提到的,在编译完成之后尽快发现错误是值得的,理想情况是在编译时。 在这种情况下,直到运行时才发现错误,在错误发生后的很长一段时间,以及可能远离包含错误的代码的代码中。 一旦看到 ClassCastException,就必须搜索代码类库,查找将 coin 实例放入 stamp 集合的方法调用。 编译器不能帮助你,因为它不能理解那个说「仅包含 stamp 实例」的注释。 + +  对于泛型,类型声明包含的信息,而不是注释: + +// Parameterized collection type - typesafe +private final Collection stamps = ... ; +复制ErrorOK! +  从这个声明中,编译器知道 stamps 集合应该只包含 Stamp 实例,并保证它是 true,假设你的整个代码类库编译时不发出(或者抑制;参见条目 27)任何警告。 当使用参数化类型声明声明 stamps 时,错误的插入会生成一个编译时错误消息,告诉你到底发生了什么错误: + +Test.java:9: error: incompatible types: Coin cannot be converted +to Stamp + c.add(new Coin()); + ^ +复制ErrorOK! +  当从集合中检索元素时,编译器会为你插入不可见的强制转换,并保证它们不会失败(再假设你的所有代码都不会生成或禁止任何编译器警告)。 虽然意外地将 coin 实例插入 stamp 集合的预期可能看起来很牵强,但这个问题是真实的。 例如,很容易想象将 BigInteger 放入一个只包含 BigDecimal 实例的集合中。 + +  如前所述,使用原始类型(没有类型参数的泛型)是合法的,但是你不应该这样做。 如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势。 鉴于你不应该使用它们,为什么语言设计者首先允许原始类型呢? 答案是为了兼容性。 泛型被添加时,Java 即将进入第二个十年,并且有大量的代码没有使用泛型。 所有这些代码都是合法的,并且与使用泛型的新代码进行交互操作被认为是至关重要的。 将参数化类型的实例传递给为原始类型设计的方法必须是合法的,反之亦然。 这个需求,被称为迁移兼容性,驱使决策支持原始类型,并使用擦除来实现泛型(详见第 28 条)。 + +  虽然不应使用诸如 List 之类的原始类型,但可以使用参数化类型来允许插入任意对象(如 List)。 原始类型 List 和参数化类型 List 之间有什么区别? 松散地说,前者已经选择了泛型类型系统,而后者明确地告诉编译器,它能够保存任何类型的对象。 虽然可以将 List 传递给 List 类型的参数,但不能将其传递给 List 类型的参数。 泛型有子类型的规则,List 是原始类型 List 的子类型,但不是参数化类型 List 的子类型(条目 28)。 因此,如果使用诸如 List 之类的原始类型,则会丢失类型安全性,但是如果使用参数化类型(例如 List)则不会。 + +  为了具体说明,请考虑以下程序: + +// Fails at runtime - unsafeAdd method uses a raw type (List)! +public static void main(String[] args) { + List strings = new ArrayList<>(); + unsafeAdd(strings, Integer.valueOf(42)); + String s = strings.get(0); // Has compiler-generated cast +} + +private static void unsafeAdd(List list, Object o) { + list.add(o); +} +复制ErrorOK! +  此程序可以编译,它使用原始类型列表,但会收到警告: + +Test.java:10: warning: [unchecked] unchecked call to add(E) as a +member of the raw type List + list.add(o); + ^ +复制ErrorOK! +  实际上,如果运行该程序,则当程序尝试调用 strings.get(0) 的结果(一个 Integer)转换为一个 String 时,会得到 ClassCastException 异常。 这是一个编译器生成的强制转换,因此通常会保证成功,但在这种情况下,我们忽略了编译器警告并付出了代价。 + +  如果用 unsafeAdd 声明中的参数化类型 List 替换原始类型 List,并尝试重新编译该程序,则会发现它不再编译,而是发出错误消息: + +Test.java:5: error: incompatible types: List cannot be +converted to List + unsafeAdd(strings, Integer.valueOf(42)); +复制ErrorOK! +  你可能会试图使用原始类型来处理元素类型未知且无关紧要的集合。 例如,假设你想编写一个方法,它需要两个集合并返回它们共同拥有的元素的数量。 如果是泛型新手,那么您可以这样写: + +// Use of raw type for unknown element type - don't do this! +static int numElementsInCommon(Set s1, Set s2) { + int result = 0; + for (Object o1 : s1) + if (s2.contains(o1)) + result++; + return result; +} +复制ErrorOK! +  这种方法可以工作,但它使用原始类型,这是危险的。 安全替代方式是使用无限制通配符类型(unbounded wildcard types)。 如果要使用泛型类型,但不知道或关心实际类型参数是什么,则可以使用问号来代替。 例如,泛型类型 Set 的无限制通配符类型是 Set(读取「某种类型的集合」)。 它是最通用的参数化的 Set 类型,能够保持任何集合。 下面是 numElementsInCommon 方法使用无限制通配符类型声明的情况: + +// Uses unbounded wildcard type - typesafe and flexible +static int numElementsInCommon(Set s1, Set s2) { ... } +复制ErrorOK! +  无限制通配符 Set 与原始类型 Set 之间有什么区别? 问号真的给你放任何东西吗? 这不是要点,但通配符类型是安全的,原始类型不是。 你可以将任何元素放入具有原始类型的集合中,轻易破坏集合的类型不变性(如第 119 页上的 unsafeAdd 方法所示); 你不能把任何元素(除 null 之外)放入一个 Collection 中。 试图这样做会产生一个像这样的编译时错误消息: + +WildCard.java:13: error: incompatible types: String cannot be +converted to CAP#1 + c.add("verboten"); + ^ + where CAP#1 is a fresh type-variable: + CAP#1 extends Object from capture of ? +复制ErrorOK! +  不可否认的是,这个错误信息留下了一些需要的东西,但是编译器已经完成了它的工作,不管它的元素类型是什么,都不会破坏集合的类型不变性。 你不仅不能将任何元素(除 null 以外)放入一个 Collection 中,并且根本无法猜测你会得到那种类型的对象。 如果这些限制是不可接受的,可以使用泛型方法(详见第 30 条)或有限制的通配符类型(详见第 31 条)。 + +  对于不应该使用原始类型的规则,有一些小例外。 你必须在类字面值(class literals)中使用原始类型。 规范中不允许使用参数化类型(尽管它允许数组类型和基本类型)[JLS,15.8.2]。 换句话说,List.class,String[].class 和 int.class 都是合法的,但 List.class 和 List.class 都是不合法的。 + +  规则的第二个例外与 instanceof 操作符有关。 因为泛型类型信息在运行时被擦除,所以在无限制通配符类型以外的参数化类型上使用 instanceof 运算符是非法的。 使用无限制通配符类型代替原始类型,不会对 instanceof 运算符的行为产生任何影响。 在这种情况下,尖括号(<>)和问号(?)就显得多余。 以下是使用泛型类型的 instanceof 运算符的首选方法: + +// Legitimate use of raw type - instanceof operator +if (o instanceof Set) { // Raw type + Set s = (Set) o; // Wildcard type + ... +} +复制ErrorOK! +  请注意,一旦确定 o 对象是一个 Set,则必须将其转换为通配符 Set,而不是原始类型 Set。 这是一个受检查的(checked)转换,所以不会导致编译器警告。 + +  总之,使用原始类型可能导致运行时异常,所以不要使用它们。 原始类型只是为了与引入泛型机制之前的遗留代码进行兼容和互用而提供的。 作为一个快速回顾,Set 是一个参数化类型,表示一个可以包含任何类型对象的集合,Set 是一个通配符类型,表示一个只能包含某些未知类型对象的集合,Set 是一个原始类型,它不在泛型类型系统之列。 前两个类型是安全的,最后一个不是。 + +  为了快速参考,下表中总结了本条目(以及本章稍后介绍的一些)中介绍的术语: + +术语 中文含义 举例 所在条目 +Parameterized type 参数化类型 List 条目 26 +Actual type parameter 实际类型参数 String 条目 26 +Generic type 泛型类型 List 条目 26 和 条目 29 +Formal type parameter 形式类型参数 E 条目 26 +Unbounded wildcard type 无限制通配符类型 List 条目 26 +Raw type 原始类型 List 条目 26 +Bounded type parameter 限制类型参数 条目 29 +Recursive type bound 递归类型限制 > 条目 30 +Bounded wildcard type 限制通配符类型 List 条目 31 +Generic method 泛型方法 static List asList(E[] a) 条目 30 +Type token 类型令牌 String.class 条目 33 + + + +// +27. 消除非检查警告 +  使用泛型编程时,会看到许多编译器警告:未经检查的强制转换警告,未经检查的方法调用警告,未经检查的参数化可变长度类型警告以及未经检查的转换警告。 你使用泛型获得的经验越多,获得的警告越少,但不要期望新编写的代码能够干净地编译。 + +  许多未经检查的警告很容易消除。 例如,假设你不小心写了以下声明: + +Set exaltation = new HashSet(); +复制ErrorOK! +  编译器会提醒你你做错了什么: + +Venery.java:4: warning: [unchecked] unchecked conversion + Set exaltation = new HashSet(); + ^ + required: Set + found: HashSet。 +复制ErrorOK! +  然后可以进行指示修正,让警告消失。 请注意,实际上并不需要指定类型参数,只是为了表明它与 Java 7 中引入的钻石运算符(「<>」)一同出现。然后编译器会推断出正确的实际类型参数(在本例中为 Lark): + +Set exaltation = new HashSet<>(); +复制ErrorOK! +  但一些警告更难以消除。 本章充满了这种警告的例子。 当你收到需要进一步思考的警告时,坚持不懈! 尽可能地消除每一个未经检查的警告。 如果你消除所有的警告,你可以放心,你的代码是类型安全的,这是一件非常好的事情。 这意味着在运行时你将不会得到一个 ClassCastException 异常,并且增加了你的程序将按照你的意图行事的信心。 + +  如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用 @SuppressWarnings("unchecked") 注解来抑制警告。 如果你在没有首先证明代码是类型安全的情况下压制警告,那么你给自己一个错误的安全感。 代码可能会在不发出任何警告的情况下进行编译,但是它仍然可以在运行时抛出 ClassCastException 异常。 但是,如果你忽略了你认为是安全的未经检查的警告(而不是抑制它们),那么当一个新的警告出现时,你将不会注意到这是一个真正的问题。 新出现的警告就会淹没在所有的错误警告当中。 + +  SuppressWarnings 注解可用于任何声明,从单个局部变量声明到整个类。 始终在尽可能最小的范围内使用 SuppressWarnings 注解。 通常这是一个变量声明或一个非常短的方法或构造方法。 切勿在整个类上使用 SuppressWarnings 注解。 这样做可能会掩盖重要的警告。 + +  如果你发现自己在长度超过一行的方法或构造方法上使用 SuppressWarnings 注解,则可以将其移到局部变量声明上。 你可能需要声明一个新的局部变量,但这是值得的。 例如,考虑这个来自 ArrayList 的 toArray 方法: + +public T[] toArray(T[] a) { + if (a.length < size) + return (T[]) Arrays.copyOf(elements, size, a.getClass()); + System.arraycopy(elements, 0, a, 0, size); + if (a.length > size) + a[size] = null; + return a; +} + +如果编译 ArrayList 类,则该方法会生成此警告: +ArrayList.java:305: warning: [unchecked] unchecked cast + return (T[]) Arrays.copyOf(elements, size, a.getClass()); + ^ + required: T[] + found: Object[] +复制ErrorOK! +  在返回语句中设置 SuppressWarnings 注解是非法的,因为它不是一个声明[JLS,9.7]。 你可能会试图把注释放在整个方法上,但是不要这要做。 相反,声明一个局部变量来保存返回值并标注它的声明,如下所示: + +// Adding local variable to reduce scope of @SuppressWarnings +public T[] toArray(T[] a) { + if (a.length < size) { + // This cast is correct because the array we're creating + // is of the same type as the one passed in, which is T[]. + @SuppressWarnings("unchecked") T[] result = + (T[]) Arrays.copyOf(elements, size, a.getClass()); + return result; + } + System.arraycopy(elements, 0, a, 0, size); + if (a.length > size) + a[size] = null; + return a; +} +复制ErrorOK! +  所产生的方法干净地编译,并最小化未经检查的警告被抑制的范围。 + +  每当使用 @SuppressWarnings(“unchecked”) 注解时,请添加注释,说明为什么是安全的。 这将有助于他人理解代码,更重要的是,这将减少有人修改代码的可能性,从而使计算不安全。 如果你觉得很难写这样的注释,请继续思考。 毕竟,你最终可能会发现未经检查的操作是不安全的。 + +  总之,未经检查的警告是重要的。 不要忽视他们。 每个未经检查的警告代表在运行时出现 ClassCastException 异常的可能性。 尽你所能消除这些警告。 如果无法消除未经检查的警告,并且可以证明引发该警告的代码是安全类型的,则可以在尽可能小的范围内使用 @SuppressWarnings(“unchecked”) 注解来禁止警告。 记录你决定在注释中抑制此警告的理由。 + + 28. 列表优于数组 +  数组在两个重要方面与泛型不同。 首先,数组是协变的(covariant)。 这个吓人的单词意味着如果 Sub 是 Super 的子类型,则数组类型 Sub[] 是数组类型 Super[] 的子类型。 相比之下,泛型是不变的(invariant):对于任何两种不同的类型 Type1 和 Type2,List 既不是 List 的子类型也不是父类型。[JLS,4.10; Naftalin07, 2.5]。 你可能认为这意味着泛型是不足的,但可以说是数组缺陷。 这段代码是合法的: + +// Fails at runtime! +Object[] objectArray = new Long[1]; +objectArray[0] = "I don't fit in"; // Throws ArrayStoreException +  但这个不是: + +// Won't compile! +List ol = new ArrayList(); // Incompatible types +ol.add("I don't fit in"); +  无论哪种方式,你不能把一个 String 类型放到一个 Long 类型容器中,但是用一个数组,你会发现在运行时产生了一个错误;对于列表,可以在编译时就能发现错误。 当然,你宁愿在编译时找出错误。 + +  数组和泛型之间的第二个主要区别是数组被具体化了(reified)[JLS,4.7]。 这意味着数组在运行时知道并强制执行它们的元素类型。 如前所述,如果尝试将一个 String 放入 Long 数组中,得到一个 ArrayStoreException 异常。 相反,泛型通过擦除(erasure)来实现[JLS,4.6]。 这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)它们的元素类型信息。 擦除是允许泛型类型与不使用泛型的遗留代码自由互操作(详见第 26 条),从而确保在 Java 5 中平滑过渡到泛型。 + +  由于这些基本差异,数组和泛型不能很好地在一起混合使用。 例如,创建泛型类型的数组,参数化类型的数组,以及类型参数的数组都是非法的。 因此,这些数组创建表达式都不合法:new List[],new List[],new E[]。 所有将在编译时导致泛型数组创建错误。 + +  为什么创建一个泛型数组是非法的? 因为它不是类型安全的。 如果这是合法的,编译器生成的强制转换程序在运行时可能会因为 ClassCastException 异常而失败。 这将违反泛型类型系统提供的基本保证。 + +  为了具体说明,请考虑下面的代码片段: + +// Why generic array creation is illegal - won't compile! +List[] stringLists = new List[1]; // (1) +List intList = List.of(42); // (2) +Object[] objects = stringLists; // (3) +objects[0] = intList; // (4) +String s = stringLists[0].get(0); // (5) +  让我们假设第 1 行创建一个泛型数组是合法的。第 2 行创建并初始化包含单个元素的 List。第 3 行将 List 数组存储到 Object 数组变量中,这是合法的,因为数组是协变的。第 4 行将 List 存储在 Object 数组的唯一元素中,这是因为泛型是通过擦除来实现的:List 实例的运行时类型仅仅是 List,而 List[] 实例是 List[],所以这个赋值不会产生 ArrayStoreException 异常。现在我们遇到了麻烦。将一个 List 实例存储到一个声明为仅保存 List 实例的数组中。在第 5 行中,我们从这个数组的唯一列表中检索唯一的元素。编译器自动将检索到的元素转换为 String,但它是一个 Integer,所以我们在运行时得到一个 ClassCastException 异常。为了防止发生这种情况,第 1 行(创建一个泛型数组)必须产生一个编译时错误。 + +  类型 E,List 和 List 等在技术上被称为不可具体化的类型(nonreifiable types)[JLS,4.7]。 直观地说,不可具体化的类型是其运行时表示包含的信息少于其编译时表示的类型。 由于擦除,可唯一确定的参数化类型是无限定通配符类型,如 List 和 Map(详见第 26 条)。 尽管很少有用,创建无限定通配符类型的数组是合法的。 + +  禁止泛型数组的创建可能会很恼人的。 这意味着,例如,泛型集合通常不可能返回其元素类型的数组(但是参见条目 33 中的部分解决方案)。 这也意味着,当使用可变参数方法(详见第 53 条)和泛型时,会产生令人困惑的警告。 这是因为每次调用可变参数方法时,都会创建一个数组来保存可变参数。 如果此数组的元素类型不可确定,则会收到警告。 SafeVarargs 注解可以用来解决这个问题(详见第 32 条)。 + +  当你在强制转换为数组类型时,得到泛型数组创建错误,或是未经检查的强制转换警告时,最佳解决方案通常是使用集合类型 List 而不是数组类型 E[]。 这样可能会牺牲一些简洁性或性能,但作为交换,你会获得更好的类型安全性和互操作性。 + +  例如,假设你想用带有集合的构造方法来编写一个 Chooser 类,并且有个方法返回随机选择的集合的一个元素。 根据传递给构造方法的集合,可以使用该类的实例对象作为游戏骰子,魔力 8 号球或蒙特卡罗模拟的数据源。 这是一个没有泛型的简单实现: + +// Chooser - a class badly in need of generics! +public class Chooser { + private final Object[] choiceArray; + + + public Chooser(Collection choices) { + choiceArray = choices.toArray(); + } + + + public Object choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceArray[rnd.nextInt(choiceArray.length)]; + } +} +  要使用这个类,每次调用方法时,都必须将 choose 方法的返回值从 Object 转换为所需的类型,如果类型错误,则转换在运行时失败。 我们先根据条目 29 的建议,试图修改 Chooser 类,使其成为泛型的。 + +// A first cut at making Chooser generic - won't compile +public class Chooser { + private final T[] choiceArray; + + public Chooser(Collection choices) { + choiceArray = choices.toArray(); + } + + // choose method unchanged +} +  如果你尝试编译这个类,会得到这个错误信息: + +Chooser.java:9: error: incompatible types: Object[] cannot be +converted to T[] + choiceArray = choices.toArray(); + ^ + where T is a type-variable: + T extends Object declared in class Chooser +  没什么大不了的,将 Object 数组转换为 T 数组: + +choiceArray = (T[]) choices.toArray(); +  这没有了错误,而是得到一个警告: + +Chooser.java:9: warning: [unchecked] unchecked cast + choiceArray = (T[]) choices.toArray(); + ^ + required: T[], found: Object[] + where T is a type-variable: +T extends Object declared in class Chooser +  编译器告诉你在运行时不能保证强制转换的安全性,因为程序不会知道 T 代表什么类型——记住,元素类型信息在运行时会被泛型删除。 该程序可以正常工作吗? 是的,但编译器不能证明这一点。 你可以证明这一点,在注释中提出证据,并用注解来抑制警告,但最好是消除警告的原因(详见第 27 条)。 + +  要消除未经检查的强制转换警告,请使用列表而不是数组。 下面是另一个版本的 Chooser 类,编译时没有错误或警告: + +// List-based Chooser - typesafe +public class Chooser { + private final List choiceList; + + + public Chooser(Collection choices) { + choiceList = new ArrayList<>(choices); + } + + + public T choose() { + Random rnd = ThreadLocalRandom.current(); + return choiceList.get(rnd.nextInt(choiceList.size())); + } +} +  这个版本有些冗长,也许运行比较慢,但是值得一提的是,在运行时不会得到 ClassCastException 异常。 + +  总之,数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,对于泛型则是相反。 一般来说,数组和泛型不能很好地混合工作。 如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。 + + 29. 优先考虑泛型 +  参数化声明并使用 JDK 提供的泛型类型和方法通常不会太困难。 但编写自己的泛型类型有点困难,但值得努力学习。 + +  考虑条目 7 中的简单堆栈实现: + +// Object-based collection - a prime candidate for generics +public class Stack { + private Object[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + public Stack() { + elements = new Object[DEFAULT_INITIAL_CAPACITY]; + } + + public void push(Object e) { + ensureCapacity(); + elements[size++] = e; + } + + public Object pop() { + if (size == 0) + throw new EmptyStackException(); + Object result = elements[--size]; + elements[size] = null; // Eliminate obsolete reference + return result; + } + + public boolean isEmpty() { + return size == 0; + } + + private void ensureCapacity() { + if (elements.length == size) + elements = Arrays.copyOf(elements, 2 * size + 1); + } +} +  这个类应该已经被参数化了,但是由于事实并非如此,我们可以对它进行泛型化。 换句话说,我们可以参数化它,而不会损害原始非参数化版本的客户端。 就目前而言,客户端必须强制转换从堆栈中弹出的对象,而这些强制转换可能会在运行时失败。 泛型化类的第一步是在其声明中添加一个或多个类型参数。 在这种情况下,有一个类型参数,表示堆栈的元素类型,这个类型参数的常规名称是 E(详见第 68 条)。 + +  下一步是用相应的类型参数替换所有使用的 Object 类型,然后尝试编译生成的程序: + +// Initial attempt to generify Stack - won't compile! +public class Stack { + private E[] elements; + private int size = 0; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + public Stack() { + elements = new E[DEFAULT_INITIAL_CAPACITY]; + } + + public void push(E e) { + ensureCapacity(); + elements[size++] = e; + } + + public E pop() { + if (size == 0) + throw new EmptyStackException(); + E result = elements[--size]; + elements[size] = null; // Eliminate obsolete reference + return result; + } + ... // no changes in isEmpty or ensureCapacity +} +  你通常会得到至少一个错误或警告,这个类也不例外。 幸运的是,这个类只产生一个错误: + +Stack.java:8: generic array creation + elements = new E[DEFAULT_INITIAL_CAPACITY]; + ^ +  如条目 28 所述,你不能创建一个不可具体化类型的数组,例如类型 E。每当编写一个由数组支持的泛型时,就会出现此问题。 有两种合理的方法来解决它。 第一种解决方案直接规避了对泛型数组创建的禁用:创建一个 Object 数组并将其转换为泛型数组类型。 现在没有了错误,编译器会发出警告。 这种用法是合法的,但不是(一般)类型安全的: + +Stack.java:8: warning: [unchecked] unchecked cast +found: Object[], required: E[] + elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; + ^ +  编译器可能无法证明你的程序是类型安全的,但你可以。 你必须说服自己,不加限制的类型强制转换不会损害程序的类型安全。 有问题的数组(元素)保存在一个私有属性中,永远不会返回给客户端或传递给任何其他方法。 保存在数组中的唯一元素是那些传递给 push 方法的元素,它们是 E 类型的,所以未经检查的强制转换不会造成任何伤害。 + +  一旦证明未经检查的强制转换是安全的,请尽可能缩小范围(条目 27)。 在这种情况下,构造方法只包含未经检查的数组创建,所以在整个构造方法中抑制警告是合适的。 通过添加一个注解来执行此操作,Stack 可以干净地编译,并且可以在没有显式强制转换或担心 ClassCastException 异常的情况下使用它: + +// The elements array will contain only E instances from push(E). +// This is sufficient to ensure type safety, but the runtime +// type of the array won't be E[]; it will always be Object[]! +@SuppressWarnings("unchecked") +public Stack() { + elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; +} +  消除 Stack 中的泛型数组创建错误的第二种方法是将属性元素的类型从 E[] 更改为 Object[]。 如果这样做,会得到一个不同的错误: + +Stack.java:19: incompatible types +found: Object, required: E + E result = elements[--size]; + ^ +  可以通过将从数组中检索到的元素转换为 E 来将此错误更改为警告: + +Stack.java:19: warning: [unchecked] unchecked cast +found: Object, required: E + E result = (E) elements[--size]; + ^ +  因为 E 是不可具体化的类型,编译器无法在运行时检查强制转换。 再一次,你可以很容易地向自己证明,不加限制的转换是安全的,所以可以适当地抑制警告。 根据条目 27 的建议,我们只在包含未经检查的强制转换的分配上抑制警告,而不是在整个 pop 方法上: + +// Appropriate suppression of unchecked warning +public E pop() { + if (size == 0) + throw new EmptyStackException(); + + // push requires elements to be of type E, so cast is correct + @SuppressWarnings("unchecked") E result = + (E) elements[--size]; + + elements[size] = null; // Eliminate obsolete reference + return result; +} +  两种消除泛型数组创建的技术都有其追随者。 第一个更可读:数组被声明为 E[] 类型,清楚地表明它只包含 E 实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组;第一种技术只需要一次转换(创建数组的地方),而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)(详见第 32 条):数组的运行时类型与编译时类型不匹配(除非 E 碰巧是 Object)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。 + +  下面的程序演示了泛型 Stack 类的使用。 该程序以相反的顺序打印其命令行参数,并将其转换为大写。 对从堆栈弹出的元素调用 String 的 toUpperCase 方法不需要显式强制转换,而自动生成的强制转换将保证成功: + +// Little program to exercise our generic Stack +public static void main(String[] args) { + Stack stack = new Stack<>(); + for (String arg : args) + stack.push(arg); + while (!stack.isEmpty()) + System.out.println(stack.pop().toUpperCase()); +} +  上面的例子似乎与条目 28 相矛盾,条目 28 中鼓励使用列表优先于数组。 在泛型类型中使用列表并不总是可行或可取的。 Java 本身生来并不支持列表,所以一些泛型类型(如 ArrayList)必须在数组上实现。 其他的泛型类型,比如 HashMap,是为了提高性能而实现的。 + +  绝大多数泛型类型就像我们的 Stack 示例一样,它们的类型参数没有限制:可以创建一个 Stack, Stack,Stack> 或者其他任何对象的 Stack 引用类型。 请注意,不能创建基本类型的堆栈:尝试创建 Stack 或 Stack 将导致编译时错误。 这是 Java 泛型类型系统的一个基本限制。 可以使用基本类型的包装类(详见第 61 条)来解决这个限制。 + +  有一些泛型类型限制了它们类型参数的允许值。 例如,考虑 java.util.concurrent.DelayQueue,它的声明如下所示: + +class DelayQueue implements BlockingQueue +  类型参数列表()要求实际的类型参数 E 是 java.util.concurrent.Delayed 的子类型。 这使得 DelayQueue 实现及其客户端可以利用 DelayQueue 元素上的 Delayed 方法,而不需要显式的转换或 ClassCastException 异常的风险。 类型参数 E 被称为限定类型参数。 请注意,子类型关系被定义为每个类型都是自己的子类型[JLS,4.10],因此创建 DelayQueue 是合法的。 + +  总之,泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用。 当你设计新的类型时,确保它们可以在没有这种强制转换的情况下使用。 这通常意味着使类型泛型化。 如果你有任何现有的类型,应该是泛型的但实际上却不是,那么把它们泛型化。 这使这些类型的新用户的使用更容易,而不会破坏现有的客户端(条目 26)。 + + 31. 使用限定通配符来增加 API 的灵活性 +  如条目 28 所述,参数化类型是不变的。换句话说,对于任何两个不同类型的 Type1 和 Type2,List 既不是 List 的子类型也不是其父类型。尽管 List 不是 List 的子类型是违反直觉的,但它确实是有道理的。 可以将任何对象放入 List 中,但是只能将字符串放入 List 中。 由于 List 不能做 List 所能做的所有事情,所以它不是一个子类型(条目 10 中的里氏替代原则)。 + +  相对于提供的不可变的类型,有时你需要比此更多的灵活性。 考虑条目 29 中的 Stack 类。下面是它的公共 API: + +public class Stack { + + public Stack(); + + public void push(E e); + + public E pop(); + + public boolean isEmpty(); + +} +  假设我们想要添加一个方法来获取一系列元素,并将它们全部推送到栈上。 以下是第一种尝试: + +// pushAll method without wildcard type - deficient! +public void pushAll(Iterable src) { + for (E e : src) + push(e); +} +  这种方法可以干净地编译,但不完全令人满意。 如果可遍历的 src 元素类型与栈的元素类型完全匹配,那么它工作正常。 但是,假设有一个 Stack,并调用 push(intVal),其中 intVal 的类型是 Integer。 这是因为 Integer 是 Number 的子类型。 从逻辑上看,这似乎也应该起作用: + +Stack numberStack = new Stack<>(); +Iterable integers = ... ; +numberStack.pushAll(integers); +  但是,如果你尝试了,会得到这个错误消息,因为参数化类型是不变的: + +StackTest.java:7: error: incompatible types: Iterable +cannot be converted to Iterable + numberStack.pushAll(integers); + ^ +  幸运的是,有对应的解决方法。 该语言提供了一种特殊的参数化类型来调用一个限定通配符类型来处理这种情况。 pushAll 的输入参数的类型不应该是「E 的 Iterable 接口」,而应该是「E 的某个子类型的 Iterable 接口」,并且有一个通配符类型,这意味着:Iterable。 (关键字 extends 的使用有点误导:回忆条目 29 中,子类型被定义为每个类型都是它自己的子类型,即使它本身没有继承。)让我们修改 pushAll 来使用这个类型: + +// Wildcard type for a parameter that serves as an E producer +public void pushAll(Iterable src) { + for (E e : src) + push(e); +} +  有了这个改变,Stack 类不仅可以干净地编译,而且客户端代码也不会用原始的 pushAll 声明编译。 因为 Stack 和它的客户端干净地编译,你知道一切都是类型安全的。 + +  现在假设你想写一个 popAll 方法,与 pushAll 方法相对应。 popAll 方法从栈中弹出每个元素并将元素添加到给定的集合中。 以下是第一次尝试编写 popAll 方法的过程: + +// popAll method without wildcard type - deficient! +public void popAll(Collection dst) { + while (!isEmpty()) + dst.add(pop()); +} +  同样,如果目标集合的元素类型与栈的元素类型完全匹配,则干净编译并且工作正常。 但是,这又不完全令人满意。 假设你有一个 Stac 和 Object 类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,它将编译并运行而不会出错。 你不应该也这样做吗? + +Stack numberStack = new Stack(); + +Collection objects = ... ; + +numberStack.popAll(objects); +  如果尝试将此客户端代码与之前显示的 popAll 版本进行编译,则会得到与我们的第一版 pushAll 非常类似的错误:Collection 不是 Collection 的子类型。 通配符类型再一次提供了一条出路。 popAll 的输入参数的类型不应该是「E 的集合」,而应该是「E 的某个父类型的集合」(其中父类型被定义为 E 是它自己的父类型[JLS,4.10])。 再次,有一个通配符类型,正是这个意思:Collection。 让我们修改 popAll 来使用它: + +// Wildcard type for parameter that serves as an E consumer +public void popAll(Collection dst) { + while (!isEmpty()) + dst.add(pop()); +} +  通过这个改动,Stack 类和客户端代码都可以干净地编译。 + +  这个结论很清楚。 为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。 如果一个输入参数既是一个生产者又是一个消费者,那么通配符类型对你没有好处:你需要一个精确的类型匹配,这就是没有任何通配符的情况。 + +  这里有一个助记符来帮助你记住使用哪种通配符类型: PECS 代表: producer-extends,consumer-super。 + +  换句话说,如果一个参数化类型代表一个 T 生产者,使用 ;如果它代表 T 消费者,则使用 。 在我们的 Stack 示例中,pushAll 方法的 src 参数生成栈使用的 E 实例,因此 src 的合适类型为 Iterable;popAll 方法的 dst 参数消费 Stack 中的 E 实例,因此 dst 的合适类型是 Collection 。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则(Get and Put Principle)[Naftalin07,2.4]。 + +  记住这个助记符之后,让我们来看看本章中以前项目的一些方法和构造方法声明。 条目 28 中的 Chooser 类构造方法有这样的声明: + +public Chooser(Collection choices) +  这个构造方法只使用集合选择来生产类型 T 的值(并将它们存储起来以备后用),所以它的声明应该使用一个 extends T 的通配符类型。下面是得到的构造方法声明: + +// Wildcard type for parameter that serves as an T producer + +public Chooser(Collection choices) +  这种改变在实践中会有什么不同吗? 是的,会有不同。 假你有一个 List,并且想把它传递给 Chooser 的构造方法。 这不会与原始声明一起编译,但是它只会将限定通配符类型添加到声明中。 + +  现在看看条目 30 中的 union 方法。下是声明: + +public static Set union(Set s1, Set s2) +  两个参数 s1 和 s2 都是 E 的生产者,所以 PECS 助记符告诉我们该声明应该如下: + +public static Set union(Set s1, Set s2) +  请注意,返回类型仍然是 Set。 不要使用限定通配符类型作为返回类型。除了会为用户提供额外的灵活性,还强制他们在客户端代码中使用通配符类型。 通过修改后的声明,此代码将清晰地编译: + +Set integers = Set.of(1, 3, 5); + +Set doubles = Set.of(2.0, 4.0, 6.0); + +Set numbers = union(integers, doubles); +  如果使用得当,类的用户几乎不会看到通配符类型。 他们使方法接受他们应该接受的参数,拒绝他们应该拒绝的参数。 如果一个类的用户必须考虑通配符类型,那么它的 API 可能有问题。 + +  在 Java 8 之前,类型推断规则不够聪明,无法处理先前的代码片段,这要求编译器使用上下文指定的返回类型(或目标类型)来推断 E 的类型。union 方法调用的目标类型如前所示是 Set。 如果尝试在早期版本的 Java 中编译片段(以及适合的 Set.of 工厂替代版本),将会看到如此长的错综复杂的错误消息: + +Union.java:14: error: incompatible types + Set numbers = union(integers, doubles); + ^ + required: Set + found: Set + where INT#1,INT#2 are intersection types: + INT#1 extends Number,Comparable + INT#2 extends Number,Comparable +  幸运的是有办法来处理这种错误。 如果编译器不能推断出正确的类型,你可以随时告诉它使用什么类型的显式类型参数[JLS,15.12]。 甚至在 Java 8 中引入目标类型之前,这不是你必须经常做的事情,这很好,因为显式类型参数不是很漂亮。 通过添加显式类型参数,如下所示,代码片段在 Java 8 之前的版本中进行了干净编译: + +// Explicit type parameter - required prior to Java 8 +Set numbers = Union.union(integers, doubles); +  接下来让我们把注意力转向条目 30 中的 max 方法。这里是原始声明: + +public static > T max(List list) +  为了从原来到修改后的声明,我们两次应用了 PECS。首先直接的应用是参数列表。 它生成 T 实例,所以将类型从 List 更改为 List。 棘手的应用是类型参数 T。这是我们第一次看到通配符应用于类型参数。 最初,T 被指定为继承 Comparable,但 Comparable 的 T 消费 T 实例(并生成指示顺序关系的整数)。 因此,参数化类型 Comparable 被替换为限定通配符类型 Comparable。 Comparable 实例总是消费者,所以通常应该使用 Comparable 优于 Comparable。 Comparator 也是如此。因此,通常应该使用 Comparator 优于 Comparator。 + +  修改后的 max 声明可能是本书中最复杂的方法声明。 增加的复杂性是否真的起作用了吗? 同样,它的确如此。 这是一个列表的简单例子,它被原始声明排除,但在被修改后的版本里是允许的: + +List> scheduledFutures = ... ; +  无法将原始方法声明应用于此列表的原因是 ScheduledFuture 不实现 Comparable。 相反,它是 Delayed 的子接口,它继承了 Comparable。 换句话说,一个 ScheduledFuture 实例不仅仅和其他的 ScheduledFuture 实例相比较: 它可以与任何 Delayed 实例比较,并且足以导致原始的声明拒绝它。 更普遍地说,通配符要求来支持没有直接实现 Comparable(或 Comparator)的类型,但继承了一个类型。 + +  还有一个关于通配符相关的话题。 类型参数和通配符之间具有双重性,许多方法可以用一个或另一个声明。 例如,下面是两个可能的声明,用于交换列表中两个索引项目的静态方法。 第一个使用无限制类型参数(条目 30),第二个使用无限制通配符: + +// Two possible declarations for the swap method +public static void swap(List list, int i, int j); +public static void swap(List list, int i, int j); +  这两个声明中的哪一个更可取,为什么? 在公共 API 中,第二个更好,因为它更简单。 你传入一个列表(任何列表),该方法交换索引的元素。 没有类型参数需要担心。 通常, 如果类型参数在方法声明中只出现一次,请将其替换为通配符。 如果它是一个无限制的类型参数,请将其替换为无限制的通配符; 如果它是一个限定类型参数,则用限定通配符替换它。 + +  第二个 swap 方法声明有一个问题。 这个简单的实现不会编译: + +public static void swap(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); +} +  试图编译它会产生这个不太有用的错误信息: + +Swap.java:5: error: incompatible types: Object cannot be +converted to CAP#1 + list.set(i, list.set(j, list.get(i))); + ^ + where CAP#1 is a fresh type-variable: + CAP#1 extends Object from capture of ? +  看起来我们不能把一个元素放回到我们刚刚拿出来的列表中。 问题是列表的类型是 List,并且不能将除 null 外的任何值放入 List 中。 幸运的是,有一种方法可以在不使用不安全的转换或原始类型的情况下实现此方法。 这个想法是写一个私有辅助方法来捕捉通配符类型。 辅助方法必须是泛型方法才能捕获类型。 以下是它的定义: + +public static void swap(List list, int i, int j) { + swapHelper(list, i, j); +} + +// Private helper method for wildcard capture +private static void swapHelper(List list, int i, int j) { + list.set(i, list.set(j, list.get(i))); +} +  swapHelper 方法知道该列表是一个 List。 因此,它知道从这个列表中获得的任何值都是 E 类型,并且可以安全地将任何类型的 E 值放入列表中。 这个稍微复杂的 swap 的实现可以干净地编译。 它允许我们导出基于通配符的漂亮声明,同时利用内部更复杂的泛型方法。 swap 方法的客户端不需要面对更复杂的 swapHelper 声明,但他们从中受益。 辅助方法具有我们认为对公共方法来说过于复杂的签名。 + +  总之,在你的 API 中使用通配符类型,虽然棘手,但使得 API 更加灵活。 如果编写一个将被广泛使用的类库,正确使用通配符类型应该被认为是强制性的。 记住基本规则: producer-extends, consumer-super(PECS)。 还要记住,所有 Comparable 和 Comparator 都是消费者。 + + 32. 合理地结合泛型和可变参数 +  在 Java 5 中,可变参数方法(详见第 53 条)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有。 可变参数的目的是允许客户端将一个可变数量的参数传递给一个方法,但这是一个脆弱的抽象(leaky abstraction):当你调用一个可变参数方法时,会创建一个数组来保存可变参数;那个应该是实现细节的数组是可见的。 因此,当可变参数具有泛型或参数化类型时,会导致编译器警告混淆。 + +  回顾条目 28,非具体化(non-reifiable)的类型是其运行时表示比其编译时表示具有更少信息的类型,并且几乎所有泛型和参数化类型都是不可具体化的。 如果某个方法声明其可变参数为非具体化的类型,则编译器将在该声明上生成警告。 如果在推断类型不可确定的可变参数参数上调用该方法,那么编译器也会在调用中生成警告。 警告看起来像这样: + +warning: [unchecked] Possible heap pollution from + parameterized vararg type List +复制ErrorOK!复制ErrorOK! +  当参数化类型的变量引用不属于该类型的对象时会发生堆污染(Heap pollution)[JLS,4.12.2]。 它会导致编译器的自动生成的强制转换失败,违反了泛型类型系统的基本保证。 + +  例如,请考虑以下方法,该方法是第 127 页上的代码片段的一个不太明显的变体: + +// Mixing generics and varargs can violate type safety! +static void dangerous(List... stringLists) { + List intList = List.of(42); + Object[] objects = stringLists; + objects[0] = intList; // Heap pollution + String s = stringLists[0].get(0); // ClassCastException +} +复制ErrorOK!复制ErrorOK! +  此方法没有可见的强制转换,但在调用一个或多个参数时抛出 ClassCastException 异常。 它的最后一行有一个由编译器生成的隐形转换。 这种转换失败,表明类型安全性已经被破坏,并且将值保存在泛型可变参数数组参数中是不安全的。 + +  这个例子引发了一个有趣的问题:为什么声明一个带有泛型可变参数的方法是合法的,当明确创建一个泛型数组是非法的时候呢? 换句话说,为什么前面显示的方法只生成一个警告,而 127 页上的代码片段会生成一个错误? 答案是,具有泛型或参数化类型的可变参数参数的方法在实践中可能非常有用,因此语言设计人员选择忍受这种不一致。 事实上,Java 类库导出了几个这样的方法,包括 Arrays.asList(T... a),Collections.addAll(Collection c, T... elements),EnumSet.of(E first, E... rest)。 与前面显示的危险方法不同,这些类库方法是类型安全的。 + +  在 Java 7 中,@SafeVarargs 注解已添加到平台,以允许具有泛型可变参数的方法的作者自动禁止客户端警告。 实质上,@SafeVarargs 注解构成了作者对类型安全的方法的承诺。 为了交换这个承诺,编译器同意不要警告用户调用可能不安全的方法。 + +  除非它实际上是安全的,否则注意不要使用 @SafeVarargs 注解标注一个方法。 那么需要做些什么来确保这一点呢? 回想一下,调用方法时会创建一个泛型数组,以容纳可变参数。 如果方法没有在数组中存储任何东西(它会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它是安全的。 换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法是安全的。 + +  值得注意的是,你可以违反类型安全性,即使不会在可变参数数组中存储任何内容。 考虑下面的泛型可变参数方法,它返回一个包含参数的数组。 乍一看,它可能看起来像一个方便的小工具: + +// UNSAFE - Exposes a reference to its generic parameter array! +static T[] toArray(T... args) { + return args; +} +复制ErrorOK!复制ErrorOK! +  这个方法只是返回它的可变参数数组。 该方法可能看起来并不危险,但它是! 该数组的类型由传递给方法的参数的编译时类型决定,编译器可能没有足够的信息来做出正确的判断。 由于此方法返回其可变参数数组,它可以将堆污染传播到调用栈上。 + +  为了具体说明,请考虑下面的泛型方法,它接受三个类型 T 的参数,并返回一个包含两个参数的数组,随机选择: + +static T[] pickTwo(T a, T b, T c) { + switch(ThreadLocalRandom.current().nextInt(3)) { + case 0: return toArray(a, b); + case 1: return toArray(a, c); + case 2: return toArray(b, c); + } + throw new AssertionError(); // Can't get here +} +复制ErrorOK!复制ErrorOK! +  这个方法本身不是危险的,除了调用具有泛型可变参数的 toArray 方法之外,不会产生警告。 + +  编译此方法时,编译器会生成代码以创建一个将两个 T 实例传递给 toArray 的可变参数数组。 这段代码分配了一个 Object[] 类型的数组,它是保证保存这些实例的最具体的类型,而不管在调用位置传递给 pickTwo 的对象是什么类型。 toArray 方法只是简单地将这个数组返回给 pickTwo,然后 pickTwo 将它返回给调用者,所以 pickTwo 总是返回一个 Object[] 类型的数组。 + +public static void main(String[] args) { + String[] attributes = pickTwo("Good", "Fast", "Cheap"); +} +复制ErrorOK!复制ErrorOK! +  这种方法没有任何问题,因此它编译时不会产生任何警告。 但是当运行它时,抛出一个 ClassCastException 异常,尽管不包含可见的转换。 你没有看到的是,编译器已经生成了一个隐藏的强制转换为由 pickTwo 返回的值的 String[] 类型,以便它可以存储在属性中。 转换失败,因为 Object[] 不是 String[] 的子类型。 这种故障相当令人不安,因为它从实际导致堆污染(toArray)的方法中移除了两个级别,并且在实际参数存储在其中之后,可变参数数组未被修改。 + +  这个例子是为了让人们认识到给另一个方法访问一个泛型的可变参数数组是不安全的,除了两个例外:将数组传递给另一个可变参数方法是安全的,这个方法是用 @SafeVarargs 正确标注的, 将数组传递给一个非可变参数的方法是安全的,该方法仅计算数组内容的一些方法。 + +  这里是安全使用泛型可变参数的典型示例。 此方法将任意数量的列表作为参数,并按顺序返回包含所有输入列表元素的单个列表。 由于该方法使用 @SafeVarargs 进行标注,因此在声明或其调用站位置上不会生成任何警告: + +// Safe method with a generic varargs parameter +@SafeVarargs +static List flatten(List... lists) { + List result = new ArrayList<>(); + for (List list : lists) + result.addAll(list); + return result; +} +复制ErrorOK!复制ErrorOK! +  决定何时使用 @SafeVarargs 注解的规则很简单:在每种方法上使用 @SafeVarargs,并使用泛型或参数化类型的可变参数,这样用户就不会因不必要的和令人困惑的编译器警告而担忧。 这意味着你不应该写危险或者 toArray 等不安全的可变参数方法。 每次编译器警告你可能会受到来自你控制的方法中泛型可变参数的堆污染时,请检查该方法是否安全。 提醒一下,在下列情况下,泛型可变参数方法是安全的: + +它不会在可变参数数组中存储任何东西 +它不会使数组(或克隆)对不可信代码可见。 如果违反这些禁令中的任何一项,请修复。 +  请注意,SafeVarargs 注解只对不能被重写的方法是合法的,因为不可能保证每个可能的重写方法都是安全的。 在 Java 8 中,注解仅在静态方法和 final 实例方法上合法; 在 Java 9 中,它在私有实例方法中也变为合法。 + +  使用 SafeVarargs 注解的替代方法是采用条目 28 的建议,并用 List 参数替换可变参数(这是一个变相的数组)。 下面是应用于我们的 flatten 方法时,这种方法的样子。 请注意,只有参数声明被更改了: + +// List as a typesafe alternative to a generic varargs parameter +static List flatten(List> lists) { + List result = new ArrayList<>(); + for (List list : lists) + result.addAll(list); + return result; +} +复制ErrorOK!复制ErrorOK! +  然后可以将此方法与静态工厂方法 List.of 结合使用,以允许可变数量的参数。 请注意,这种方法依赖于 List.of 声明使用 @SafeVarargs 注解: + +audience = flatten(List.of(friends, romans, countrymen)); +复制ErrorOK!复制ErrorOK! +  这种方法的优点是编译器可以证明这种方法是类型安全的。 不必使用 @SafeVarargs 注解来证明其安全性,也不用担心在确定安全性时可能会犯错。 主要缺点是客户端代码有点冗长,运行可能会慢一些。 + +  这个技巧也可以用在不可能写一个安全的可变参数方法的情况下,就像第 147 页的 toArray 方法那样。它的列表模拟是 List.of 方法,所以我们甚至不必编写它;Java 类库作者已经为我们完成了这项工作。 pickTwo 方法然后变成这样: + +static List pickTwo(T a, T b, T c) { + switch(rnd.nextInt(3)) { + case 0: return List.of(a, b); + case 1: return List.of(a, c); + case 2: return List.of(b, c); + } + throw new AssertionError(); +} +复制ErrorOK!复制ErrorOK! +  main 方变成这样: + +public static void main(String[] args) { + List attributes = pickTwo("Good", "Fast", "Cheap"); +} +复制ErrorOK!复制ErrorOK! +  生成的代码是类型安全的,因为它只使用泛型,不是数组。 + +  总而言之,可变参数和泛型不能很好地交互,因为可变参数机制是在数组上面构建的脆弱的抽象,并且数组具有与泛型不同的类型规则。 虽然泛型可变参数不是类型安全的,但它们是合法的。 如果选择使用泛型(或参数化)可变参数编写方法,请首先确保该方法是类型安全的,然后使用 @SafeVarargs 注解对其进行标注,以免造成使用不愉快。 + + 33. 优先考虑类型安全的异构容器 +  泛型的常见用法包括集合,如 Set 和 Map 和单个元素容器,如 ThreadLocal 和 AtomicReference。 在所有这些用途中,它都是参数化的容器。 这限制了每个容器只能有固定数量的类型参数。 通常这正是你想要的。 一个 Set 有单一的类型参数,表示它的元素类型; 一个 Map 有两个,代表它的键和值的类型;等等。 + +  然而有时候,你需要更多的灵活性。 例如,数据库一行记录可以具有任意多列,并且能够以类型安全的方式访问它们是很好的。 幸运的是,有一个简单的方法可以达到这个效果。 这个想法是参数化键(key)而不是容器。 然后将参数化的键提交给容器以插入或检索值。 泛型类型系统用于保证值的类型与其键一致。 + +  作为这种方法的一个简单示例,请考虑一个 Favorites 类,它允许其客户端保存和检索任意多种类型的 favorite 实例。 该类型的 Class 对象将扮演参数化键的一部分。其原因是这 Class 类是泛型的。 类的类型从字面上来说不是简单的 Class,而是 Class。 例如,String.class 的类型为 Class,Integer.class 的类型为 Class。 当在方法中传递字面类传递编译时和运行时类型信息时,它被称为类型令牌(type token)[Bracha04]。 + +  Favorites 类的 API 很简单。 它看起来就像一个简单 Map 类,除了该键是参数化的以外。 客户端在设置和获取 favorites 实例时呈现一个 Class 对象。 如下是 API: + +// Typesafe heterogeneous container pattern - API +public class Favorites { + public void putFavorite(Class type, T instance); + public T getFavorite(Class type); +} +复制ErrorOK! +  下面是一个演示 Favorites 类,保存,检索和打印喜欢的 String,Integer 和 Class 实例: + +// Typesafe heterogeneous container pattern - client +public static void main(String[] args) { + Favorites f = new Favorites(); + f.putFavorite(String.class, "Java"); + f.putFavorite(Integer.class, 0xcafebabe); + f.putFavorite(Class.class, Favorites.class); + + String favoriteString = f.getFavorite(String.class); + int favoriteInteger = f.getFavorite(Integer.class); + Class favoriteClass = f.getFavorite(Class.class); + System.out.printf("%s %x %s%n", favoriteString, + favoriteInteger, favoriteClass.getName()); +} +复制ErrorOK! +  正如你所期望的,这个程序打印 Java cafebabe Favorites。 请注意,顺便说一下,Java 的 printf 方法与 C 语言的不同之处在于,你应该在 C 中使用 \n 的地方改用 %n。%n 用于生成适用于特定平台的行分隔符,在大多数平台上面的值为 \n,但并不是所有平台的分隔符都为 \n。 + +  Favorites 实例是类型安全的:当你请求一个字符串时它永远不会返回一个整数。 它也是异构的:与普通 Map 不同,所有的键都是不同的类型。 因此,我们将 Favorites 称为类型安全异构容器(typesafe heterogeneous container)。 + +  Favorites 的实现非常小巧。 这是完整的代码: + +// Typesafe heterogeneous container pattern - implementation +public class Favorites { + private Map, Object> favorites = new HashMap<>(); + + public void putFavorite(Class type, T instance) { + favorites.put(Objects.requireNonNull(type), instance); + } + + public T getFavorite(Class type) { + return type.cast(favorites.get(type)); + } +} +复制ErrorOK! +  这里有一些微妙的事情发生。 每个 Favorites 实例都由一个名为 favorites 私有的 Map, Object> 来支持。 你可能认为无法将任何内容放入此 Map 中,因为这是无限定的通配符类型,但事实恰恰相反。 需要注意的是通配符类型是嵌套的:它不是通配符类型的 Map 类型,而是键的类型。 这意味着每个键都可以有不同的参数化类型:一个可以是 Class,下一个 Class 等等。 这就是异构的由来。 + +  接下来要注意的是,favorites 的 Map 的值类型只是 Object。 换句话说,Map 不保证键和值之间的类型关系,即每个值都是由其键表示的类型。 事实上,Java 的类型系统并不足以表达这一点。 但是我们知道这是真的,并在检索一个 favorite 时利用了这点。 + +  putFavorite 实现很简单:只需将给定的 Class 对象映射到给定的 favorites 的实例即可。 如上所述,这丢弃了键和值之间的“类型联系(type linkage)”;无法知道这个值是不是键的一个实例。 但没关系,因为 getFavorites 方法可以并且确实重新建立这种关联。 + +  getFavorite 的实现比 putFavorite 更复杂。 首先,它从 favorites Map 中获取与给定 Class 对象相对应的值。 这是返回的正确对象引用,但它具有错误的编译时类型:它是 Object(favorites map 的值类型),我们需要返回类型 T。因此,getFavorite 实现动态地将对象引用转换为 Class 对象表示的类型,使用 Class 的 cast 方法。 + +  cast 方法是 Java 的 cast 操作符的动态模拟。它只是检查它的参数是否由 Class 对象表示的类型的实例。如果是,它返回参数;否则会抛出 ClassCastException 异常。我们知道,假设客户端代码能够干净地编译,getFavorite 中的强制转换不会抛出 ClassCastException 异常。 也就是说,favorites map 中的值始终与其键的类型相匹配。 + +  那么这个 cast 方法为我们做了什么,因为它只是返回它的参数? cast 的签名充分利用了 Class 类是泛型的事实。 它的返回类型是 Class 对象的类型参数: + +public class Class { + T cast(Object obj); +} +复制ErrorOK! +  这正是 getFavorite 方法所需要的。 这正是确保 Favorites 类型安全,而不用求助一个未经检查的强制转换的 T 类型。 + +  Favorites 类有两个限制值得注意。 首先,恶意客户可以通过使用原始形式的 Class 对象,轻松破坏 Favorites 实例的类型安全。 但生成的客户端代码在编译时会生成未经检查的警告。 这与正常的集合实现(如 HashSet 和 HashMap)没有什么不同。 通过使用原始类型 HashSet(条目 26),可以轻松地将字符串放入 HashSet 中。 也就是说,如果你愿意为此付出一点代价,就可以拥有运行时类型安全性。 确保 Favorites 永远不违反类型不变的方法是,使 putFavorite 方法检查该实例是否由 type 表示类型的实例,并且我们已经知道如何执行此操作。只需使用动态转换: + +// Achieving runtime type safety with a dynamic cast +public void putFavorite(Class type, T instance) { + favorites.put(type, type.cast(instance)); +} +复制ErrorOK! +  java.util.Collections 中有一些集合包装类,可以发挥相同的诀窍。 它们被称为 checkedSet,checkedList,checkedMap 等等。 他们的静态工厂除了一个集合(或 Map)之外还有一个 Class 对象(或两个)。 静态工厂是泛型方法,确保 Class 对象和集合的编译时类型匹配。 包装类为它们包装的集合添加了具体化。 例如,如果有人试图将 Coin 放入你的 Collection 中,则包装类在运行时会抛出 ClassCastException。 这些包装类对于追踪在混合了泛型和原始类型的应用程序中添加不正确类型的元素到集合的客户端代码很有用。 + +  Favorites 类的第二个限制是它不能用于不可具体化的(non-reifiable)类型(详见第 28 条)。 换句话说,你可以保存你最喜欢的 String 或 String[],但不能保存 List。 如果你尝试保存你最喜欢的 List,程序将不能编译。 原因是无法获取 List 的 Class 对象。 List.class 是语法错误,也是一件好事。 List 和 List 共享一个 Class 对象,即 List.class。 如果“字面类型(type literals)”List .class 和 List.class 合法并返回相同的对象引用,那么它会对 Favorites 对象的内部造成严重破坏。 对于这种限制,没有完全令人满意的解决方法。 + +  Favorites 使用的类型令牌 type tokens) 是无限制的:getFavorite 和 putFavorite 接受任何 Class 对象。 有时你可能需要限制可传递给方法的类型。 这可以通过一个有限定的类型令牌来实现,该令牌只是一个类型令牌,它使用限定的类型参数(详见第 30 条)或限定的通配符(详见第 31 条)来放置可以表示的类型的边界。 + +  注解 API(详见第 39 条)广泛使用限定类型的令牌。 例如,以下是在运行时读取注解的方法。 此方法来自 AnnotatedElement 接口,该接口由表示类,方法,属性和其他程序元素的反射类型实现: + +public + T getAnnotation(Class annotationType); +复制ErrorOK! +  参数 annotationType 是表示注解类型的限定类型令牌。 该方法返回该类型的元素的注解(如果它有一个);如果没有,则返回 null。 本质上,注解元素是一个类型安全的异构容器,其键是注解类型。 + +  假设有一个 Class 类型的对象,并且想要将它传递给需要限定类型令牌(如 getAnnotation)的方法。 可以将对象转换为 Class,但是这个转换没有被检查,所以它会产生一个编译时警告(详见第 52 条)。 幸运的是,Class 类提供了一种安全(动态)执行这种类型转换的实例方法。 该方法被称为 asSubclass,并且它转换所调用的 Class 对象来表示由其参数表示的类的子类。 如果转换成功,该方法返回它的参数;如果失败,则抛出 ClassCastException 异常。 + +  以下是如何使用 asSubclass 方法在编译时读取类型未知的注解。 此方法编译时没有错误或警告: + +// Use of asSubclass to safely cast to a bounded type token +static Annotation getAnnotation(AnnotatedElement element, + String annotationTypeName) { + Class annotationType = null; // Unbounded type token + try { + annotationType = Class.forName(annotationTypeName); + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + return element.getAnnotation( + annotationType.asSubclass(Annotation.class)); +} +复制ErrorOK! +  总之,泛型 API 的通常用法(以集合 API 为例)限制了每个容器的固定数量的类型参数。 你可以通过将类型参数放在键上而不是容器上来解决此限制。 可以使用 Class 对象作为此类型安全异构容器的键。 以这种方式使用的 Class 对象称为类型令牌。 也可以使用自定义键类型。 例如,可以有一个表示数据库行(容器)的 DatabaseRow 类型和一个泛型类型 Column 作为其键。 diff --git "a/docs/JVM \351\235\242\350\257\225\351\242\230.md" "b/docs/JVM \351\235\242\350\257\225\351\242\230.md" index 0b6ff25..c5b483b 100644 --- "a/docs/JVM \351\235\242\350\257\225\351\242\230.md" +++ "b/docs/JVM \351\235\242\350\257\225\351\242\230.md" @@ -1,13 +1,13 @@ - ## jvm 的主要组成部分及其作用 + ## JVM 的主要组成部分及其作用 - 类加载器(ClassLoader) - 运行时数据区(Runtime Data Area) - 执行引擎(Execution Engine) - 本地库接口(Native Interface) -组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。 +组件的作用: 首先通过类加载器(ClassLoader)会加载类文件到内存,Class loader只管加载,只要符合文件结构就加载。运行时数据区(Runtime Data Area)是jvm的重点,我们所有所写的程序都被加载到这里,之后才开始运行。而字节码文件只是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来融合不同的语言为java所用,从而实现整个程序的功能。 -## jvm 运行时数据区 +## JVM 运行时数据区⭐ - 程序计数器 @@ -15,19 +15,19 @@ - 虚拟机栈 - 存储当前线程执行方法时所需要的数据,指令,返回地址。因此一个线程独享一块虚拟机栈 ,栈中内存的单位是栈帧。 每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。 + 存储当前线程执行方法时所需要的数据,指令,返回地址。因此一个线程独享一块虚拟机栈 ,栈中内存的单位是栈帧。 每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。 - 本地方法栈 - 本地方法栈为虚拟机使用到的 Native 方法服务。 + 本地方法栈为虚拟机使用到的 Native 方法服务。 - 堆 - 详见堆内存模型。 + 唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 详见堆内存模型。 - 方法区 - 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。 + 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。 ### 堆 @@ -40,14 +40,25 @@ ## 堆和栈的区别 1. 栈内存存储的是局部变量而堆内存存储的是实体; - 2. 栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短; +3. 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。、 + +### 分代回收 + +HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。 + +因为年轻代中的对象基本都是朝生夕死的,所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。 -3. 栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。 +在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。 -## 什么是双亲委派模型? +## 什么是双亲委派模型?⭐ -如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。 +双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。 + +## 双亲委派机制的作用 + +1、防止重复加载同一个`.class`。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 +2、保证核心`.class`不能被篡改。通过委托方式,不会去篡改核心`.class`,不同的加载器加载同一个`.class`也不是同一个`Class`对象。这样保证了`Class`执行安全。 ## 对象的创建过程? @@ -91,12 +102,53 @@ - 弱引用 - 虚引用 - ## jvm 有哪些垃圾回收算法 +## 常见的垃圾回收机制⭐ + +1. 引用计数法:引用计数法是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象引用计数为0时,就释放其占用的空间。 +2. 可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。 + + + + ## jvm 有哪些垃圾回收算法⭐ + +- 停止-复制:先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的对象全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单,直接的分配了。缺点是一浪费空间,两个堆之间要来回倒腾,二是当程序进入稳定态时,可能只会产生极少的垃圾,甚至不产生垃圾,尽管如此,复制式回收器仍会将所有内存自一处复制到另一处。 +- 标记-清除:同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活的对象,就会给对象一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象会被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器如果要希望得到连续空间的话,就得重新整理剩下的对象。 +- 标记-整理:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。 +- 分代收集算法:把Java堆分为新生代和老年代,然后根据各个年代的特点采用最合适的收集算法。新生代中,对象的存活率比较低,所以选用复制算法,老年代中对象存活率高且没有额外空间对它进行分配担保,所以使用“标记-清除”或“标记-整理”算法进行回收。 + +### Minor GC和Full GC触发条件⭐ + +- Minor GC触发条件:当Eden区满时,触发Minor GC。 +- Full GC触发条件: + 1. 调用System.gc时,系统建议执行Full GC,但是不必然执行 + 2. 老年代空间不足 + 3. 方法区空间不足 + 4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存 + 5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 + +### GC中Stop the world(STW)⭐ + +在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下来,这对于很多的应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。 + +但不是说GC必须STW,你也可以选择降低运行速度但是可以并发执行的收集算法,这取决于你的业务。 + +### 哪些对象可以作为GC Roots + +1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。 +2. 方法区中类静态属性引用的对象。 +3. 方法区中常量引用的对象。 +4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。 + +### JVM锁优化和膨胀过程⭐ + +1. 自旋锁:自旋锁其实就是在拿锁时发现已经有线程拿了锁,自己如果去拿会阻塞自己,这个时候会选择进行一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放锁。自适应自旋锁指的是例如第一次设置最多自旋10次,结果在自旋的过程中成功获得了锁,那么下一次就可以设置成最多自旋20次。 +2. 锁粗化:虚拟机通过适当扩大加锁的范围以避免频繁的拿锁释放锁的过程。 +3. 锁消除:通过逃逸分析发现其实根本就没有别的线程产生竞争的可能(别的线程没有临界量的引用),或者同步块内进行的是原子操作,而“自作多情”地给自己加上了锁。有可能虚拟机会直接去掉这个锁。 +4. 偏向锁:在大多数的情况下,锁不仅不存在多线程的竞争,而且总是由同一个线程获得。因此为了让线程获得锁的代价更低引入了偏向锁的概念。偏向锁的意思是如果一个线程获得了一个偏向锁,如果在接下来的一段时间中没有其他线程来竞争锁,那么持有偏向锁的线程再次进入或者退出同一个同步代码块,不需要再次进行抢占锁和释放锁的操作。 +5. 轻量级锁:当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁。 +6. 重量级锁:重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁)。当系统检查到是重量级锁之后,会把等待想要获取锁的线程阻塞,被阻塞的线程不会消耗CPU,但是阻塞或者唤醒一个线程,都需要通过操作系统来实现。 + -- 标记-清除算法 -- 标记-整理算法 -- 复制算法 -- 分代算法 ## jvm 有哪些垃圾回收器? @@ -128,12 +180,30 @@ - ## 详细介绍一下 CMS 垃圾回收器 + ## 详细介绍一下 CMS 垃圾回收器⭐ CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。 CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。 +### G1和CMS的比较 + +1. CMS收集器是获取最短回收停顿时间为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低手机停顿时间的目的(只有初始标记和重新标记会STW)。但是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。 +2. CMS仅作用于老年代,是基于标记清除算法,所以清理的过程中会有大量的空间碎片。 +3. CMS收集器无法处理浮动垃圾,由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。 +4. G1是一款面向服务端应用的垃圾收集器,适用于多核处理器、大内存容量的服务端系统。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。 +5. 从JDK 9开始,G1成为默认的垃圾回收器。当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5s甚至1s)。 +6. G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法。 +7. G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。 + +### i++操作的字节码指令⭐ + +1. 将int类型常量加载到操作数栈顶 +2. 将int类型数值从操作数栈顶取出,并存储到到局部变量表的第1个Slot中 +3. 将int类型变量从局部变量表的第1个Slot中取出,并放到操作数栈顶 +4. 将局部变量表的第1个Slot中的int类型变量加1 +5. 表示将int类型数值从操作数栈顶取出,并存储到到局部变量表的第1个Slot中,即i中 + ## 说一下 jvm 调优的工具 JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。 @@ -152,3 +222,17 @@ JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常 - -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合; - -XX:+PrintGC:开启打印 gc 信息; - -XX:+PrintGCDetails:打印 gc 详细信息。 + +## TODO: + +- CMS GC回收分为哪几个阶段?分别做了什么事情? +- CMS有哪些重要参数? +- Concurrent Model Failure和ParNew promotion failed什么情况下会发生? +- CMS的优缺点? +- 有做过哪些GC调优? +- 为什么要划分成年轻代和老年代? +- 年轻代为什么被划分成eden、survivor区域? +- 年轻代为什么采用的是复制算法? +- 老年代为什么采用的是标记清除、标记整理算法 +- 什么情况下使用堆外内存?要注意些什么? +- 堆外内存如何被回收? diff --git "a/docs/Linux\351\235\242\350\257\225\351\242\230.md" "b/docs/Linux\351\235\242\350\257\225\351\242\230.md" index 635f72c..181e32e 100644 --- "a/docs/Linux\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/Linux\351\235\242\350\257\225\351\242\230.md" @@ -17,11 +17,7 @@ ls/ll、cd、mkdir、rm-rf、cp、mv、ps -ef | grep xxx、kill、free-m、tar - ​ ps -aux | grep xxx(-aux显示所有状态) -**查看日志:** - -tail -f *.log : 适用于实时查看日志,开发环境还行,生产就算了,日志会很多。 -**tail -f error.log** :生产中一般用这个实时看异常日志 **编辑 vi/vim : ** @@ -35,7 +31,37 @@ i 写入 Shift+g 跳至当前文本最后一行,看最新的日志,都在最下面 -## grep 查找(重要) +## top⭐ + +显示系统中各个进程的资源占用状况,可以看是否有 CPU 占用过大的进程。 + +## less 和 more + +less 与 more 类似,但使用 less 可以随意浏览文件,而 more 仅能向前移动,却不能向后移动,而且 less 在查看之前不会加载整个文件。 + +## tail⭐ + +**查看日志:** + +tail -f *.log : 适用于实时查看日志,开发环境还行,生产就算了,日志会很多。 + +**tail -f error.log** :生产中一般用这个实时看异常日志 + +**-f :循环读取 ,用于查阅正在改变的日志文件。** + +## netstat⭐ + +用于显示网络状态。 + +``` +-a (all)显示所有选项,netstat默认不显示LISTEN相关 +-n 拒绝显示别名,能显示数字的全部转化成数字。(重要) +-r 显示路由信息,路由表 +-e 显示扩展信息,例如uid等 +-s 按各个协议进行统计 (重要) +``` + +## grep 查找⭐ **grep 是必备日志分析命令** @@ -67,9 +93,17 @@ ps -ef | grep java 【先查java进程ID】 kill -9 java进程ID 【生产环境谨慎使用】 -## 对文件内容做统计 awk +## 对文件内容做统计 awk ⭐ + +依次处理文件的每一行,并读取里面的每一个字段,可用作统计。 +$ awk 动作 文件名 ## 批量替换 sed sed 配合正则表达式批量替换文本内容 + +## 你经常使用哪些 Linux 命令,主要用来解决什么问题? + + + diff --git "a/docs/MySQL\351\235\242\350\257\225\351\242\230.md" "b/docs/MySQL\351\235\242\350\257\225\351\242\230.md" index 27f1182..0b1db96 100644 --- "a/docs/MySQL\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/MySQL\351\235\242\350\257\225\351\242\230.md" @@ -9,7 +9,7 @@ ## 数据库的三范式是什么? - 第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。 -- 第二范式:要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。 +- 第二范式:要求实体的属性完全依赖于主关键字。所谓完全 依赖是指不能存在仅依赖主关键字一部分的属性。 - 第三范式:任何非主属性不依赖于其它非主属性。 ## 事务的基本要素 ACID⭐ @@ -18,7 +18,7 @@ Atomicity(原子性):事务是一个原子操作单元,其对数据的 Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。 -Isolation(隔离性):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。 事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。 +Isolation(隔离性):同一时间,只允许一个事务操作同一数据,不同的事务之间彼此没有任何干扰。 事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。 Durability(持久性):事务处理结束后,对数据的修改是永久的。 @@ -26,7 +26,7 @@ Durability(持久性):事务处理结束后,对数据的修改是永久 char(n) :固定长度类型,比如订阅 char(10),当你输入"abc"三个字符的时候,它们占的空间还是 10 个字节,其他 7 个是空字节。 -chat 优点:效率高;缺点:占用空间;适用场景:存储密码的 md5 值,固定长度的,使用 char 非常合适。 +char 优点:效率高;缺点:占用空间;适用场景:存储密码的 md5 值,固定长度的,使用 char 非常合适。 varchar(n) :可变长度,存储的值是每个值占用的字节再加上一个用来记录其长度的字节的长度。 @@ -60,7 +60,11 @@ REPEATABLE-READ:可重复读,mysql默认级别,保证多次读取同一个 SERIALIZABLE:序列化,代价最高最可靠的隔离级别,该隔离级别能防止脏读、不可重复读、幻读。 -在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决是不彻底的。 **通过索引加锁,间隙锁,next key lock解决幻了读的问题。** +在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决是不彻底的。 **通过next key lock解决了幻读的问题。** + +- Record lock:单个行记录上的锁 +- Gap lock:间隙锁,锁定一个范围,不包括记录本身 +- Next-key lock:record+gap 锁定一个范围,包含记录本身 **补充:** @@ -118,6 +122,24 @@ UPDATE table SET status = 1 WHERE id=1 AND status = 0; 乐观锁不能解决脏读的问题,因此仍需要数据库至少启用“读已提交”的事务隔离级别。 +## 说一下乐观锁和悲观锁?⭐ + +乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。 + +数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加 1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致,如果不一致就不修改,这样就实现了乐观锁。 + +(如SVN、GIT提交代码就是这样的) + +悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。 + +一般是 where id=XX for update 来实现 (一般银行转账、工单审批) + +**优缺点:** + +乐观锁:性能高、重试失败成本不高建议乐观 + +悲观锁:性能低,但安全,失败成功高建议悲观,使用不当有死锁风险 + ## 多版本并发控制(MVCC)⭐ (Multi-Version Concurrency Control) @@ -128,11 +150,15 @@ UPDATE table SET status = 1 WHERE id=1 AND status = 0; ![1](C:\Users\吕明辉\Desktop\github笔记\MySQL\1.png) -## 说一下 mysql 常用的引擎?⭐ +- 最上层的服务类似其他CS结构,比如连接处理,授权处理。 +- 第二层是Mysql的服务层,包括SQL的解析分析优化,存储过程触发器视图等也在这一层实现。 +- 最后一层是存储引擎的实现。 + +## 说一下 mysql 常用的引擎? -**InnoDB 引擎**:InnoDB 引擎提供了对数据库 acid 事务的支持,并且还提供了行级锁和外键的约束,它的设计的目标就是处理大数据容量的数据库系统。MySQL 运行的时候,InnoDB 会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎是不支持全文搜索,同时启动也比较的慢,它是不会保存表的行数的,所以当进行 select count(*) from table 指令的时候,需要进行扫描全表。由于锁的粒度小,写操作是不会锁定全表的,所以在并发度较高的场景下使用会提升效率的。 +**InnoDB 引擎**:MySQL 的5.5之后的默认引擎,InnoDB 引擎提供了对数据库事务的支持,并且还提供了行级锁和外键的约束,它的设计的目标就是处理大数据容量的数据库系统。MySQL 运行的时候,InnoDB 会在内存中建立缓冲池,用于缓冲数据和索引。由于锁的粒度小,写操作是不会锁定全表的,所以在并发度较高的场景下使用会提升效率的。 -**MyISAM 引擎**:MySQL 的默认引擎,但不提供事务的支持,也不支持行级锁和外键。因此当执行插入和更新语句时,即执行写操作的时候需要锁定这个表,所以会导致效率会降低。不过和 InnoDB 不同的是,MyIASM 引擎是保存了表的行数,于是当进行 select count(*) from table 语句时,可以直接的读取已经保存的值而不需要进行扫描全表。所以,如果表的读操作远远多于写操作时,并且不需要事务的支持的,可以将 MyIASM 作为数据库引擎的首选。 +**MyISAM 引擎**:不提供事务的支持,也不支持行级锁和外键。因此当执行插入和更新语句时,即执行写操作的时候需要锁定这个表,所以会导致效率会降低。不过和 InnoDB 不同的是,MyIASM 引擎是保存了表的行数,于是当进行 select count(*) from table 语句时,可以直接的读取已经保存的值而不需要进行扫描全表。所以,如果表的读操作远远多于写操作时,并且不需要事务的支持的,可以将 MyIASM 作为数据库引擎的首选。 ## Myisam和InnoDB的区别⭐ @@ -144,22 +170,24 @@ UPDATE table SET status = 1 WHERE id=1 AND status = 0; ## mysql 索引是怎么实现的? - 索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找数据。 MySQL 中的索引,不同的数据引擎实现有所不同,但目前主流的数据库引擎的索引都是 B+ 树实现的 。 +索引是满足某种特定查找算法的数据结构,而这些数据结构会以某种方式指向数据,从而实现高效查找数据。 MySQL 中的索引,不同的数据引擎实现有所不同,但目前主流的数据库引擎的索引都是 B+ 树实现的 。 ## B树和B+树的概念和区别 -1)B-树的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。 +1)先说一下B-树是一种多路搜索树,关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+树的非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。 2)在B-树中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而B+树中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中还要再比较关键字。从这个角度看B-树的性能好像要比B+树好,而**在实际应用中却是B+树的性能要好些**。 ## 为什么选择B+树作为索引结构⭐ * 因为B+树的非叶子节点不存放实际的数据,这样每个节点可容纳的元素个数比B-树多,树高比B-树小,这样带来的好处是减少磁盘访问次数。尽管B+树找到一个记录所需的比较次数要比B-树多,但是一次磁盘访问的时间相当于成百上千次内存比较的时间,因此实际中B+树的性能可能还会好些。 -* B+树的叶子节点使用指针连接在一起,方便顺序遍历(例如查看一个目录下的所有文件,一个表中的所有记录等)。 +* B+树的叶子节点使用指针连接在一起,方便顺序遍历和范围查询,这也是优于hash索引的地方。 * B+树的查询效率更加稳定,每次查询的效率一样。 -### Hash索引 +**Hash索引底层是哈希表**,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,**哈希索引只适用于等值查询的场景**。而B+ 树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描 -Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ 树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描 +- 二叉查找树:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表。 +- 平衡二叉树:通过旋转解决了平衡的问题,但是旋转操作效率太低。 +- 红黑树:通过舍弃严格的平衡和引入红黑节点,解决了 AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多。 ## B+树的叶子节点都可以存哪些东西⭐ @@ -173,11 +201,11 @@ Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据 -非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行 +非聚簇索引:将数据存储金和索引分开的结构,索引结构的叶子节点指向了数据的对应行 -**聚簇索引具有唯一性**, 聚簇索引是将数据跟索引结构放到一块,因此一个表仅有一个聚簇索引 。 +**聚簇索引具有唯一性**, 一个表仅有一个聚簇索引 。 - **聚簇索引默认是主键**,如果表中没有定义主键,InnoDB 会选择一个**唯一的非空索引**代替。如果没有这样的索引,InnoDB 会**隐式定义一个主键**来作为聚簇索引。 +**聚簇索引默认是主键**,如果表中没有定义主键,InnoDB 会选择一个**唯一的非空索引**代替。如果没有这样的索引,InnoDB 会**隐式定义一个主键**来作为聚簇索引。 聚簇索引和非聚簇索引类似查字典时直接根据经验查字的大概位置和先去查偏旁部首再去翻页查询类似。 @@ -192,23 +220,10 @@ MyISAM没有聚簇索引,都是二级索引。 指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。 -## 说一下乐观锁和悲观锁?⭐ +## 最佳左前缀法则⭐ -乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。 - -数据库的乐观锁需要自己实现,在表里面添加一个 version 字段,每次修改成功值加 1,这样每次修改的时候先对比一下,自己拥有的 version 和数据库现在的 version 是否一致,如果不一致就不修改,这样就实现了乐观锁。 - -(如SVN、GIT提交代码就是这样的) - -悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放。 - -一般是 where id=XX for update 来实现 (一般银行转账、工单审批) - -**优缺点:** - -乐观锁:性能高、重试失败成本不高建议乐观 - -悲观锁:性能低,但安全,失败成功高建议悲观,使用不当有死锁风险 +指的是查询从索引的最左前列开始并且不跳过索引中的列。 +在创建索引的字段中第一个就是最左,每个左边的字段都是后面一个字段的一整个树,过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。**要按照顺序命中索引** ## mysql 问题排查都有哪些手段? @@ -249,7 +264,7 @@ order by,group by或者关联查询是否使用了索引。 如果同时出现using where,表明索引被用来执行索引键值的查找; 如果没有同时出现using where,表明索引只是用来读取数据而非利用索引执行查找。 -## 查询在什么时候不走(预期中的)索引⭐ +## 1. 模糊查询 %like 2. 索引列参与计算,使用了函数 @@ -259,24 +274,39 @@ order by,group by或者关联查询是否使用了索引。 6. or操作有至少一个字段没有索引 7. 需要回表的查询结果集过大(超过配置的范围) -## sql 优化可以从哪些方面考虑? +## sql 优化可以从哪些方面考虑?⭐ + +主要是从怎么**合理创建索引 合理编写 SQL 语句和防止索引失效 合理创建表字段**这3个方面入手 -主要是从怎么**合理创建索引 合理使用索引以防止索引失效 合理创建表字段**这3个方面入手 +* **合理创建索引:** -* **合理创建索引:** - * 对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。 +* **合理编写 SQL 语句:** + + 不使用 select *,使用 LIMIT 语句来限制返回的数据,IN包含的值不应过多等 + +* **防止索引失效:**保证最左前缀法则,尽量不适用前缀模糊查询 %like,避免索引列参与计算或使用了函数,避免在where子句中对字段进行null值判断,看看表编码,表字段是否一样,联合索引中范围查询会让后面的索引字段失效,join查询时要用小表驱动大表 -* **防止索引失效:** - * 创建多列索引时遵循**最佳左前缀法则** ; - * 避免使用 select *,列出需要查询的字段; - * 不在索引列上和where后做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描; - * mysql 在使用不等于(!= 或者<>)的时候无法使用索引会导致全表扫描; - * like以通配符开头(’%abc…’) ,索引失效会变成全表扫描的操作 ; - * is not null 也无法使用索引 * **合理创建表字段:** 最好不要给数据库留NULL,尽可能的使用 NOT NULL填充数据库 -* 使用 LIMIT 语句来限制返回的数据或者垂直分割分表; -## **批量往mysql导入1000万数据有什么方法?** +## 索引的使用经验 + +创建索引考虑几个因素: + +覆盖索引:因为覆盖索引可以减少回表的次数,而且在MySQL5.6后增加了一个索引下推的功能,可以在让覆盖索引配合索引下推,尽量减少回表的次数。 + +可以explain命令查看执行计划时看到 extra 列的 using index condition 是说明用到了索引, Using filesort,Using temporary 都是不好的,看rows 列可以知道扫描的行数,可以根据这个判断是否需要优化。 + +我们可以考虑在读少写多的场景下(日志,账单),我们可以使用普通索引,因为innodb对普通索引做了优化,使用了 **Change buffer**,它可以把写操作缓存下来,在读的时候再去merge,这样可以减少io次数,提高语句执行速度,提高内存利用率。 + +还可以考虑索引统计信息是否有问题,analyze table重新统计信息,因为索引信息并不是一个准确值,是一个随机采样的过程。如果发现执行计划中的key列使用的索引不好时,应急预案可以考虑使用 force index 强制索引 + +## 数据库调优经验 + +使用了索引却仍然不是很快,就使用 explain 分析了一下发现表中有多个索引,因为可能涉及回表,排序的操作,MySQL 优化器选用了错误的索引导致查询效率偏低,然后通过 SQL 语句中使用 useindex 来指定索引解决。 + + + +## 批量往mysql导入1000万数据有什么方法? * 减少IO次数 @@ -285,3 +315,17 @@ order by,group by或者关联查询是否使用了索引。 * 合理设置批量大小 * 尽量顺序插入, 减少索引的维护压力 + +### redolog,undolog,binlog + +- undoLog 也就是我们常说的回滚日志文件 主要用于事务中执行失败,进行回滚,以及MVCC中对于数据历史版本的查看。由引擎层的InnoDB引擎实现,是逻辑日志,记录数据修改被修改前的值,比如"把id='B' 修改为id = 'B2' ,那么undo日志就会用来存放id ='B'的记录”。当一条数据需要更新前,会先把修改前的记录存储在undolog中,如果这个修改出现异常,,则会使用undo日志来实现回滚操作,保证事务的一致性。当事务提交之后,undo log并不能立马被删除,而是会被放到待清理链表中,待判断没有事物用到该版本的信息时才可以清理相应undolog。它保存了事务发生之前的数据的一个版本,用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 +- redoLog 是重做日志文件是记录数据修改之后的值,用于持久化到磁盘中。redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。由引擎层的InnoDB引擎实现,是物理日志,记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动”。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redoLog 在内存中,然后找个时间将redoLog的操作执行到磁盘上的文件上。不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性。 +- binlog由Mysql的Server层实现,是逻辑日志,记录的是sql语句的原始逻辑,比如"把id='B' 修改为id = ‘B2’。binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。 产生:事务提交的时候,一次性将事务中的sql语句,按照一定的格式记录到binlog中。用于复制和恢复在主从复制中,从库利用主库上的binlog进行重播(执行日志中记录的修改逻辑),实现主从同步。业务数据不一致或者错了,用binlog恢复。 + +### binlog和redolog的区别 + +1. redolog是在InnoDB存储引擎层产生,而binlog是MySQL数据库的上层服务层产生的。 +2. 两种日志记录的内容形式不同。MySQL的binlog是逻辑日志,其记录是对应的SQL语句。而innodb存储引擎层面的重做日志是物理日志。 +3. 两种日志与记录写入磁盘的时间点不同,binlog日志只在事务提交完成后进行一次写入。而innodb存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的。 +4. binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用。 +5. binlog可以作为恢复数据使用,主从复制搭建,redolog作为异常宕机或者介质故障后的数据恢复使用。 diff --git "a/docs/Redis \351\235\242\350\257\225\351\242\230.md" "b/docs/Redis \351\235\242\350\257\225\351\242\230.md" index 4f3af04..d3e61b0 100644 --- "a/docs/Redis \351\235\242\350\257\225\351\242\230.md" +++ "b/docs/Redis \351\235\242\350\257\225\351\242\230.md" @@ -1,6 +1,6 @@ # Redis面试题 -### redis是什么 +### Redis是什么 Redis是C语言开发的一个开源的(遵从BSD协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。它是一种NoSQL(not-only sql,泛指非关系型数据库)的数据库。 @@ -14,7 +14,14 @@ Redis作为一个内存数据库。 性能优秀,数据在内存中,读写 可以用作分布式锁; 可以作为消息中间件使用,支持发布订阅 -### redis 和 memcached 的区别 +### 应用场景⭐ + +1. 缓存 +2. 共享Session +3. 消息队列系统 +4. 分布式锁 + +### Redis 和 memcached 的区别 1. **redis支持更丰富的数据类型(支持更复杂的应用场景)**:Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。 2. **Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。** @@ -23,11 +30,11 @@ Redis作为一个内存数据库。 性能优秀,数据在内存中,读写 ### redis 为什么是单线程的? - 因为 cpu 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。 可以避免多线程上下文切换。 +因为 cpu 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。 可以避免多线程上下文切换。 -### 为什么redis这么快? +### 为什么Redis这么快?⭐ 完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高 采用单线程,单线程也能处理高并发请求,想多核也可启动多实例 @@ -36,9 +43,13 @@ Redis作为一个内存数据库。 性能优秀,数据在内存中,读写 核心是基于非阻塞的 IO 多路复用机制。 -### redis 支持的数据类型有哪些?应用? +### Redis 支持的数据类型有哪些?应用?⭐ -string、list、hash、set、zset。 +1. String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型, Value 不仅是 String,也可以是数字。常用在缓存、计数、共享Session、限速等。 +2. Hash哈希:在Redis中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车。 +3. List列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。 数据结构:List 就是链表,可以用来当消息队列用。Redis 提供了 List 的 Push 和 Pop 操作,还提供了操作某一段的 API,可以直接查询或者删除某一段的元素。 实现方式:Redis List 的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销。 +4. Set集合:集合(set)类型也是用来保存多个的字符串元素,集合是通过 hashtable 实现的。 但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。 +5. Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。实现方式:Redis Sorted Set 的内部使用 HashMap 和跳跃表(skipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射。 **String 在你们项目怎么用的?** @@ -92,6 +103,21 @@ eg: user:id:3506728370 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。 +### zset跳表的数据结构⭐ + +增加了向前指针的链表叫作跳表跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。 + +原理: + +跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。 + +**为什么使用跳跃表** + +首先,因为 zset 要支持随机的插入和删除,所以它 **不宜使用数组来实现**,关于排序问题,我们也很容易就想到 **红黑树/ 平衡树** 这样的树形结构,为什么 Redis 不使用这样一些结构呢? + +1. **性能考虑:** 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 *(下面详细说)*; +2. **实现考虑:** 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观; + ### redis 设置过期时间 Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是**短信验证码都是有时间限制的**,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。 @@ -100,6 +126,8 @@ Redis中有个设置时间过期的功能,即对存储在 redis 数据库中 如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的? +### 数据过期策略⭐ + **定期删除+惰性删除。** 通过名字大概就能猜出这两个删除方式的意思了。 @@ -109,30 +137,16 @@ Redis中有个设置时间过期的功能,即对存储在 redis 数据库中 但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? **redis 内存淘汰机制。** -### 数据淘汰机制 +### 数据淘汰机制⭐ 当内存到达最大内存限制时进行的数据淘汰策略 -**配置** - -最大可用内存:maxmemory //默认为0,一般设置全部内存50%以上 -每次选取带删除数据个数:maxmemory-samples //采用随机获取方式 -删除策略:maxmemory-policy //达到最大内存后,对被选取带数据进行的删除策略 - -**检测易失数据集( **设置了过期时间的键空间) -volatile-lru:挑选最近最少使用的数据淘汰(最近数据中使用时间离当前最远的数据)。**常用** -volatile-lfu:挑选最近使用次数最少的数据淘汰(最近数据中使用次数最少的数据) -volatile-ttl:挑选将要过期数据淘汰 -volatile-random:任意挑选数据淘汰 - -**检测全库数据(所有数据集)** -allkeys-lru:挑选最近最少使用的数据淘汰 -allkeys-lfu:挑选最近使用次数最少的数据淘汰 -allkeys-random:任意挑选数据淘汰 - -**放弃数据驱逐** - -no-enviction //禁止驱逐数据 4.0中默认策略,会引发OOM +1. 新写入操作会报错。(Redis 默认策略) +2. 在键空间中,移除最近最少使用的 Key。(LRU推荐使用) +3. 在键空间中,随机移除某个 Key。 +4. 在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。 +5. 在设置了过期时间的键空间中,随机移除某个 Key。 +6. 在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。 **LRU 算法实现**:1.通过双向链表来实现,新数据插入到链表头部;2.每当缓存命中(即缓存 数据被访问),则将数据移到链表头部;3.当链表满的时候,将链表尾部的数据丢弃。 @@ -140,16 +154,14 @@ LinkedHashMap:HashMap 和双向链表合二为一即是 LinkedHashMap。HashMa 的,LinkedHashMap 通过维护一个额外的双向链表保证了迭代顺序。该迭代顺序可以是插 入顺序(默认),也可以是访问顺序。 +### Redis的LRU具体实现: +传统的LRU是使用栈的形式,每次都将最新使用的移入栈顶,但是用栈的形式会导致执行select *的时候大量非热点数据占领头部数据,所以需要改进。Redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳。Redis初始的实现算法很简单,随机从dict中取出五个key,淘汰一个lru字段值最小的。在3.0的时候,又改进了一版算法,首先第一次随机选取的key都会放入一个pool中(pool的大小为16),pool中的key是按lru大小顺序排列的。接下来每次随机选取的keylru值必须小于pool中最小的lru才会继续放入,直到将pool放满。放满之后,每次如果有新的key需要放入,需要将pool中lru最大的一个key取出。淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰。 -### redis 持久化的两种方式 +### Redis 持久化的两种方式⭐ -- RDB:RDB 持久化机制,是对 redis 中的数据执行**周期性**的持久化。 -- AOF:AOF 机制对每条写入命令作为日志,以 `append-only` 的模式写入一个日志文件中,在 redis 重启的时候,可以通过**回放** AOF 日志中的写入指令来重新构建整个数据集。 - -通过 RDB 或 AOF,都可以将 redis 内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云等云服务。 - -如果 redis 挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动 redis,redis 就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。 +- RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。 当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉。 +- AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。 使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。 如果同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 **AOF** 来重新构建数据,因为 AOF 中的**数据更加完整**。 @@ -165,9 +177,9 @@ RDB恢复数据速度比AOF快 **AOF 优点** -- AOF 可以更好的保护 数据不丢失,一般 AOF 会每隔 1 秒, 最多丢失 1 秒钟的数据。 -- 写入性能非常高,而且文件不容易破损 -- **适合做灾难性的误删除的紧急恢复**。 +- AOF 可以更好的保护 数据不丢失,一般 AOF 会每隔 1 秒,最多丢失 1 秒钟的数据。 +- 写入性能非常高,而且文件不容易破损 +- **适合做灾难性的误删除的紧急恢复**。 **AOF 缺点** @@ -179,36 +191,28 @@ RDB恢复数据速度比AOF快 对数据非常敏感,建议使用默认的AOF持久化方案 AOF策略使用everysec,每秒fsync一次,该策略仍可保持很好性能,出现问题最多丢失一秒内的数据 -数据呈现阶段有效性,建议使用RDB持久化方案 数据可以做到阶段内无丢失,且恢复较快,阶段点数据恢复通常使用RDB方案 -注意: -AOF文件存储体积较大,恢复速度较慢 -利用RDB使用线紧凑的数据持久化会使Redis性能降低 - 综合: -RDB与AOF选择实际上是在一种权衡,每种都有利有弊 如果不能承受分钟内的数据丢失,对业务数据非常敏感,选用AOF -如果能承受分钟内的数据丢失,且追求大数据集的恢复速度选用RDB -灾难恢复选用RDB +如果能承受分钟内的数据丢失,且追求大数据集的恢复速度选用RDB,RDB 非常适合灾难恢复。 双保险策略,同时开启RDB和AOF,重启后Redis优先使用AOF来恢复数据,降低丢失数据量 - - -### 项目中缓存是如何使用的? - -这个,需要结合自己项目的业务来。 - ### 为什么要用缓存? 用缓存,主要有两个用途:**高性能**、**高并发**。 -### 怎么保证缓存和数据库数据的一致性? +### 怎么保证缓存和数据库数据的一致性?⭐ + +分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。 + +我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。 - 合理设置缓存的过期时间。 - 新增、更改、删除数据库操作时同步更新 Redis,可以使用事务机制来保证数据的一致性。 +- 缓存失败时增加重试机制。 ### redis 怎么实现分布式锁? @@ -223,12 +227,11 @@ Redis 分布式锁其实就是在系统里面占一个“坑”,其他程序 ### 缓存雪崩 - **在一个较短的时间内,缓存中较多的key集中过期或者缓存挂了**,导致了**数据库服务器崩溃** 缓存雪崩的事前事中事后的解决方案如下: -在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。 +在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能避免全部失效。或者设置热点数据永不过期,有更新操作就更新缓存就好了 ### 缓存穿透 @@ -239,13 +242,13 @@ Redis 分布式锁其实就是在系统里面占一个“坑”,其他程序 解决方案:最简单粗暴的方法如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。 - **布隆过滤器(Bloom Filter)**这个也能很好的预防缓存穿透的发生,他的原理也很简单,就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查DB刷新KV再return + **布隆过滤器(Bloom Filter)**这个也能很好的预防缓存穿透的发生,就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查DB刷新KV再return ### 缓存击穿 缓存击穿是指一个Key非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发直接落到了数据库上,就在这个Key的点上击穿了缓存。 -解决:设置热点数据永不过期,或者加上互斥锁就搞定了。 +解决:设置热点数据永不过期,或者加上个锁就搞定了。 **假如 Redis 里面有 1 亿个 key ,其中有 10w 个 个 key 是以某个固定的已知的前缀开头的,如** **果将它们全部找出来?** @@ -257,4 +260,53 @@ Redis 分布式锁其实就是在系统里面占一个“坑”,其他程序 令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客 户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。 - +### 实际项目中使用缓存有遇到什么问题或者会遇到什么问题你知道吗? + + 缓存和数据库数据一致性问题 + +### 主从复制 + +**作用:** +读写分离:master写、slave读,提高服务器的读写负载能力 +负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量 +故障恢复:当master出现问题时,由slave提供服务,实现快速的故障恢复 +数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式 +高可用基石:基于主从复制,构建哨兵模式与集群,实现Redis的高可用方案 + +**过程:** + +- 从节点执行 **slaveof IP,port** 发送指令 +- 主节点响应 +- 从节点保存主节点信息(IP,port),建立和主节点的 Socket 连接。 +- 从节点发送 Ping 信号,主节点返回 Pong,确定两边能互相通信。 +- 连接建立后,主节点将所有数据发送给从节点(数据同步)。 +- 主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。 + +**复制/数据同步过程分为两个阶段** + +1. 全量复制: + slave接收到master生成的RDB文件,先清空自身的旧数据,然后执行RDB恢复过程,然后告知master已经恢复完毕。 +2. 部分复制(增量复制) + 主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。master把自己之前创建的复制缓冲区的数据发送到slave,slave接收到aof指令后执行重写操作,恢复数据。 + +**主从复制会存在以下问题:** + +- 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。 +- 主节点的写能力受到单机的限制。 +- 主节点的存储能力受到单机的限制。 + +**哨兵:** + +哨兵(sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。 + +**作用:** + +**监控** + 不断的检查master和slave是否正常运行。 + master存活检测、master与slave运行情况检测 + +**通知(提醒)** + 当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知。 + +**自动故障转移** +断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址 diff --git a/docs/Spring Cloud.md b/docs/Spring Cloud.md index 6a342eb..b5851ff 100644 --- a/docs/Spring Cloud.md +++ b/docs/Spring Cloud.md @@ -6,7 +6,7 @@ Spring Cloud就是微服务系统架构的一站式解决方案,在平时我 **服务注册 Register** : -当 Eureka 客户端(服务提供者)向 Eureka Server 注册时,它提供自身的**元数据** ,比如IP地址、端口,运行状况指示符URL,主页等。 +当 Eureka 客户端(服务提供者)向 Eureka Server 注册时,它存储该服务的信息 ,比如IP地址、端口,运行状况指示符URL,主页等。 **服务续约 Renew** : @@ -14,7 +14,7 @@ Spring Cloud就是微服务系统架构的一站式解决方案,在平时我 **获取注册列表信息 Fetch Registries** : -Eureka 客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息如果与 Eureka 客户端的缓存信息不同,Eureka 客户端自动处理。Eureka 客户端和 Eureka 服务器可以使用JSON / XML格式进行通讯。 +Eureka 客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。Eureka 客户端和 Eureka 服务器可以使用JSON / XML格式进行通讯。 **服务下线 Cancel** : @@ -26,10 +26,50 @@ Eureka 客户端在程序关闭时向 Eureka 服务器发送取消请求。发 **架构图**: -![image-20200229172633604](https://github.com/lvminghui/Java-Notes/blob/master/docs/imgs/Eureka.png) +![image-20200229172633604](C:\Users\吕明辉\AppData\Roaming\Typora\typora-user-images\image-20200229172633604.png) 可以充当服务发现的组件有很多:Zookeeper ,Consul , Eureka 等。 +## Eureka 原理⭐ + +Eureka 主要包括两块: Eureka Server 和 Eureka Client。 + +**Eureka Server,服务端**,有三个功能: **服务注册** **提供注册表** **同步状态** + +**Eureka Client,客户端**,是一个 Java 客户端,用于简化与 Eureka Server 的交互。它会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。**服务续约**, **服务剔除**, **服务下线** 的功能。 + +Eurka 工作流程是这样的: + +1、Eureka Server 启动成功,等待服务端注册。 + +2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务 + +3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常 + +4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例 + +5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端 + +6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式 + +7、Eureka Client 定时从注册中心获取服务注册表,并且将获取到的信息缓存到本地 + +8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存 + +9、Eureka Client 获取到目标服务器信息,发起服务调用 + +10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除 + +## Eureka 和 ZooKeeper 的区别 ⭐ + +* C (Consistency) 强一致性 +* A(Availability) 可用性 +* P (Partition tolerance) 分区容错性 + + Zookeeper保证的是CP,Eureka保证的是AP。 + +**Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整 个注册服务瘫痪** + ### 负载均衡之 Ribbon Ribbon 是一个客户端/进程内负载均衡器,**运行在消费者端** 。 @@ -89,6 +129,10 @@ public News getHystrixNews(@PathVariable("id") int id) { } ``` +## 服务熔断原理 + +hystrix会监控微服务之间调用的状况,当失败的调用到一定阀值,缺省是5秒内20次调用失败就会启动熔断机制。熔断机制的注解是@HystrixCommand。 是通过spring 的 AOP 功能实现的 HystrixCommand 注解的方法是一个切点,有一个对方法增强的类,对他增强,如果出现失败就中断这个方法的调用,返回失败。 + ## 微服务网关——Zuul ZUUL 是为了实现动态路由、监视、弹性和安全性而构建的。**就是这样的一个对于消费者的统一入口。** @@ -180,3 +224,4 @@ zuul: ignore-patterns:**/auto/** 你可以简单理解为 `Spring Cloud Bus` 的作用就是**管理和广播分布式系统中的消息** ,也就是消息引擎系统中的广播模式。当然作为 **消息总线** 的 `Spring Cloud Bus` 可以做很多事而不仅仅是客户端的配置刷新功能。 而拥有了 `Spring Cloud Bus` 之后,我们只需要创建一个简单的请求,并且加上 `@ResfreshScope` 注解就能进行配置的动态修改了 。 + diff --git "a/docs/Spring \351\235\242\350\257\225\351\242\230.md" "b/docs/Spring \351\235\242\350\257\225\351\242\230.md" index 5bea5a8..36d40fe 100644 --- "a/docs/Spring \351\235\242\350\257\225\351\242\230.md" +++ "b/docs/Spring \351\235\242\350\257\225\351\242\230.md" @@ -2,24 +2,29 @@ 让 java 开发模块化,并且全面。Spring 通过控制反转降低耦合性,一个对象的依赖通过被动注入的方式而非主动 new,还通过代理模式实现了面向切面编程。 -## IOC 是什么,什么是 Spring IOC 容器? +## IOC 是什么,什么是 Spring IOC 容器?⭐ -IOC 是一种设计思想。 **IOC 容器是 Spring 用来实现 IOC 的载体, IOC 容器在某种程度上就是个Map(key,value),key是 name 属性,Map 是对应的对象。**容器创建 Bean 对象, 使用依赖注入来管理对象之间的相互依赖关系,配置它们并管理它们的完整生命周期,很大程度上简化应用的开发,降低了耦合度。 +IOC 是一种设计思想。 **IOC 容器是 Spring 用来实现 IOC 的载体, IOC 容器在某种程度上就是个Map(key,value),key是 name 属性,value 是对应的对象。**容器创建 Bean 对象, 使用依赖注入来管理对象之间的相互依赖关系,配置它们并管理它们的完整生命周期,很大程度上简化应用的开发,降低了耦合度。 容器通过读取提供的配置,比如 XML,注解或 Java 代码来接收对象信息进行实例化,配置和组装。 -### IoC 的实现机制⭐ +Spring在创建容器时有一个点就是利用了模板方法设计模式设计了 refresh 方法,这个方法是模板方法,低级容器实现了 obtainFreshBeanFactory 的抽象方法,调用 refreshBeanFactory 加载了所有 BeanDefinition 和 Properties 到 **DefaultListableBeanFactory** 容器中。发送了注册事件后高级容器启动功能,比如接口回调,监听器,创建单例bean,发布事件等功能。 -实现原理就是工厂模式加反射机制。 +### IoC 的实现机制/初始化流程⭐ -* Spring 容器在启动的时候,先会保存所有注册进来的 Bean 的定义信息, 注册到 BeanFactory 中。 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-> beanDefinition 的 map) -* 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 bean,如environment、systemProperties -* 如果有 Bean 实现了 BeanFactoryPostProcessor 接口,Spring 会负责调用里面的 postProcessBeanFactory 方法,这是一个扩展方法 -* 注册 BeanPostProcessor 的实现类,这是在 Bean 初始化前后执行的方法 +主要实现原理就是工厂模式加反射机制。 + +调用 refresh() 方法: + +* 刷新准备,设置开始时间,状态, 初始化占位符等操作 + +* 获取内部的 BeanFactory,Spring 容器在启动的时候,先会保存所有注册进来的 Bean 的定义信息, 注册到 BeanFactory 中。 +* 设置 BeanFactory 的类加载器和后置处理器,添加几个 BeanPostProcessor,手动注册默认的环境 bean +* 为子类提供后置处理 BeanFactory 的扩展能力,初始化上下文之前,可以复写 postProcessBeanFactory这个方法 +* 执行 Context 中注册的 BeanFactory 后置处理器,对 SpringBoot 来说,这一步会进行 BeanDefintion 的解析 +* 按优先级在 BeanFactory 注册 Bean 的后置处理器,这是在 Bean 初始化前后执行的方法 * 初始化国际化,事件广播器的模块,注册事件监听器 -* 然后 **Spring容器就会创建这些单例 Bean** - 1. 用到这个 Bean 的时候;利用 getBean 创建 Bean,创建好以后保存在容器中; - 2. 统一创建剩下所有的 Bean 的时候;调用 finishBeanFactoryInitialization() 初始化所有剩下的单例 Bean。 +* 然后 **Spring容器就会创建这些非延迟加载的单例 Bean** * 最后广播事件,ApplicationContext 初始化/刷新完成 具体源码实现分析请看我的另一篇文章 。 @@ -36,24 +41,68 @@ IOC 是一种设计思想。 **IOC 容器是 Spring 用来实现 IOC 的载体 - **Singleton** - 每个 Spring IoC 容器仅有一个单实例。 - **Prototype** - 每次请求都会产生一个新的实例。 -- **Request** - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。 -- **Session** - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。 -- **Global-session** - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。 +- **Request** - 每次请求都会创建一个实例 +- **Session** - 在一个会话周期内只有一个实例 +- Global-session - 类似于标准的 HTTP Session 作用域,5.0版本后已不再使用 +- **Appilcation** - 在一个 ServletContext 中只有一个实例 +- **Websocket** - 在一个 Websocket 只有一个实例 -仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。 +仅当用户使用支持 Web 的 ApplicationContext 时,最后几个才可用。 #### Bean 的生命周期⭐ -防止篇幅过长和内容重复,请看 SpringIOC 源码分析 +* Bean容器/BeanFactory 通过对象的构造器或工厂方法先实例化 Bean; + +* 再根据 Resource 中的信息再通过设定好的方法(典型的有setter,统称为BeanWrapper)对 Bean 设置属性值,得到 BeanDefintion 对象,然后 put 到 beanDefinitionMap 中,调用 getBean 的时候,从 beanDefinitionMap 里拿出 Class 对象进行注入(**使用了反射**),同时如果有依赖关系,将递归调用 getBean 方法,即依赖注入的过程。 + +* 检查 xxxAware 相关接口,比如 BeanNameAware,BeanClassLoaderAware,ApplicationContextAware( BeanFactoryAware)等等,如果有就调用相应的 setxxx 方法把所需要的xxx传入到 Bean 中。 + + **补充**:关于 Aware ,Aware 就是感知的意思, Aware 的目的是为了让Bean获得Spring容器的服务。 实现了这类接口的 bean 会存在“意识感”,从而让容器调用 setxxx 方法把所需要的 xxx 传到 Bean 中。 + +* 此时检查是否存在有于 Bean 关联的任何 BeanPostProcessors, 执行 postProcessBeforeInitialization() 方法(前置处理器)。 + +* 如果 Bean 实现了InitializingBean接口(正在初始化的 Bean),执行 afterPropertiesSet() 方法。 + +* 检查是否配置了自定义的 init-method 方法,如果有就调用。 + +* 此时检查是否存在有于 Bean 关联的任何 BeanPostProcessors, 执行 postProcessAfterInitialization() 方法(后置处理器)。返回 wrapperBean(包装后的 Bean)。 + +* 这时就可以开始使用 Bean 了,当容器关闭时,会检查 Bean 是否实现了 DisposableBean 接口,如果有就调用 destory() 方法。 + +* 如果 Bean 配置文件中的定义包含 destroy-method 属性,执行指定的方法。 + +上面整个过程就是 Bean 的整个生命周期了。 + +**Bean 单例和多例的情况:** -#### Spring 中的单例 bean 的线程安全问题 +在实际情况中一般并不会实现很多扩展接口,我们知道,Bean 的基本类型分为 singleton(单例) 和 prototype(原型/多例) 两种,在容器创建过程中,单例 Bean 默认跟随容器一起实例化,而当我们指定 Bean节点的 lazy-init=”true” 时,只有在第一次获取 Bean 的时候才会初始化 Bean。当然,如果想让所有单例 Bean 都延迟加载,可以在根节点设置此属性。 -Spring容器中的Bean是否线程安全,容器本身并没有提供 Bean 的线程安全策略,因此可以说 Spring 容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体 scope 的 Bean 去研究。 +当 scope="prototype" 时,容器也会延迟初始化 Bean,并不会立刻创建对象,而是在第一次请求该 bean 时才初始化(如调用 getBean 方法时)。和单例不同的情况是:在对象销毁时,容器不会帮我们调用任何方法。 -常见的有两种解决办法: +Spring不能对一个 prototype bean 的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个 prototype 实例后,将它交给客户端,随后就对该prototype 实例不闻不问了。 -1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 -2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 +也许你会问,那么怎么释放被 prototype 作用域 bean 占用的资源? + +我们可以通过 Bean 的后置处理器, 该处理器持有要被清除的bean的引用。 + +Spring 被设计成**一个管理应用程序模块定义的容器以及工厂,而不是管理模块自身的容器**,让模块可以分别独立开发,实现了模块之间的解耦。 + +为什么要使用Spring: + +1. Spring提供一个容器/工厂,统一管理模块的定义,根据需要创建。 +2. 把模块的配置参数统一管理,模块不需要自行读取配置。 +3. Spring提供依赖注入,把依赖的模块自动推送进来,不需要模块自己拉取。 +4. 此外,Spring提供了对很多其他第三方框架的集成功能,减少了样板代码(boilerplate)。 + +## 常见扩展接口 + +BeanFactoryPostProcessor:处理所有bean前,对bean factory进行预处理 +BeanDefinitionRegistryPostProcessor:可以添加自定义的bean +BeanPostProcessor:支持在Bean初始化前、后对bean进行处理 +ApplicationContextAware:可以获得ApplicationContext及其中的bean +InitializingBean:在bean创建完成,所有属性注入完成后执行 +DisposableBean:在bean销毁前执行 +ApplicationListener:用来监听产生的应用事件 ### Spring的后置处理器 @@ -118,9 +167,9 @@ Spring使用了三级缓存解决了循环依赖的问题。在populateBean()给 **现在再来了解一下三级缓存:** -1. `singletonObjects`:第一级单例缓存池。用于存放完全初始化好的 bean,**从该缓存中取出的 bean 可以直接使用** +1. `singletonObjects`:第一级,单例缓存池。用于存放完全初始化好的 bean,**从该缓存中取出的 bean 可以直接使用** 2. `earlySingletonObjects`:第二级。提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性的 bean) -3. `singletonFactories`:第三纪单例对象工厂缓存 。单例对象工厂的cache,存放 bean 工厂对象 +3. `singletonFactories`:第三级,单例对象工厂缓存 。单例对象工厂的cache,存放 bean 工厂对象 **了解完缓存就可以开始了解单例 Bean 的创建过程:** @@ -161,56 +210,14 @@ JDK 动态代理基于接口,所以只有接口中的方法会被增强,而 ### 实现原理 -JDK动态代理:基于反射,生成实现代理对象接口的匿名类,通过生成代理实例时传递的InvocationHandler处理程序实现方法增强。 +JDK动态代理:基于反射,利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。 CGLIB动态代理:基于操作字节码,通过加载代理对象的类字节码,为代理对象创建一个子类,并在子类中拦截父类方法并织入方法增强逻辑。底层是依靠ASM(开源的java字节码编辑类库)操作字节码实现的。 -## AspectJ 和 Spring AOP 的对比: - -**Spring AOP:** - -- 它基于动态代理来实现。默认地,如果使用接口的,用 JDK 提供的动态代理实现,如果没有接口,使用 CGLIB 实现。大家一定要明白背后的意思,包括什么时候会不用 JDK 提供的动态代理,而用 CGLIB 实现。 -- Spring 的 IOC 容器和 AOP 都很重要,Spring AOP 需要依赖于 IOC 容器来管理。 -- Spring AOP 只能作用于 Spring 容器中的 Bean,它是使用纯粹的 Java 代码实现的,只能作用于 bean 的方法。 -- Spring 提供了 AspectJ 的支持,一般来说我们用**纯的** Spring AOP 就够了。 -- 很多人会对比 Spring AOP 和 AspectJ 的性能,Spring AOP 是基于代理实现的,在容器启动的时候需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能不如 AspectJ 那么好。 - -**AspectJ:** - -- 属于静态织入,它是通过修改代码来实现的,它的织入时机可以是: - - Compile-time weaving:编译期织入,如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。 - - Post-compile weaving:也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。 - - **Load-time weaving**:指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法。1、自定义类加载器来干这个,这个应该是最容易想到的办法,在被织入类加载到 JVM 前去对它进行加载,这样就可以在加载的时候定义行为了。2、在 JVM 启动的时候指定 AspectJ 提供的 agent:`-javaagent:xxx/xxx/aspectjweaver.jar`。 - -- AspectJ 能干很多 Spring AOP 干不了的事情,它是 **AOP 编程的完全解决方案**。Spring AOP 致力于解决的是企业级开发中最普遍的 AOP 需求(方法织入),而不是力求成为一个像 AspectJ 一样的 AOP 编程完全解决方案。 -- 因为 AspectJ 在实际代码运行前完成了织入,所以大家会说它生成的类是没有额外运行时开销的。 +## AspectJ 和 Spring AOP ### 区别 -**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。 - -Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单, - -如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。 - -### **什么是切点(JoinPoint)** - -程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理. - -在 Spring AOP 中, join point 总是方法的执行点。 - -### **什么是通知(Advice)?** - -特定 JoinPoint 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。 - -### **有哪些类型的通知(Advice)?** - -- **Before** - 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。 -- **After Returning** - 这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。 -- **After Throwing** - 这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。 -- **After (finally)** - 这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。 -- **Around** - 这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。 - - +**Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** ## springAOP 项目中的实际应用 @@ -274,25 +281,64 @@ View是一个接口,实现类支持不同的View类型(jsp、freemarker、pd ## 常用注解 -* @Controller 负责注册一个bean 到spring 上下文中. +**类型类** -* @ResponseBody 将java对象转换成json格式的字符串,返回给浏览器 +- @Controller:负责注册一个bean 到spring 上下文中 - 该注解用于将 Controller 的方法返回的对象,通过适当的 **HttpMessageConverter** 转换为指定格式后,写入到 Response 对象的 body 数据区。 +- @Service -**HttpMessageConverter是处理器适配器创建的,用于数据转换**。 +- @Repository -* @RequestBody 接收JSON数据 +- @Component - 该注解用于读取 Request 请求的 body 部分数据,使用系统默认配置的 HttpMessageConverter 进行解析,然后把相应的数据绑定到要返回的对象上 +- @Configuration:声明当前类为配置类,相当于xml形式的Spring配置 -* @RequestController **@RestController=@ResponseBody+@Controller** +- @Bean:注解在方法上,声明当前方法的返回值为一个 bean -* @PathVariable URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中。 + **@Bean和@Component的区别** -* @ControllerAdvice 使一个Contoller成为全局的异常处理类,类中用@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常。 + * @Component 在类上使用,表示这是一个组件类,需要 Spring 为这个类创建 Bean + + * @Bean 在方法上使用,告诉 Spring 这个方法将返回一个 Bean 对象,需要把返回的对象注册到应用上下文中 + +**设置类** + +- @Required:确保值一定被设置 +- @Autowired && @Qualifier + - @Qualifier:当一个接口有多个实现的时候,为了指名具体调用哪个类的实现。 +- @Scope:生命周期 + +**Web类** -* RequestMapping 是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。 +- @RequestMapping && @GetMapping @ PostMapping + + - RequestMapping:用于映射Web请求,包括访问路径和参数(类或方法上) + +- @PathVariable && @RequestParam + + - @PathVariable:用于接收路径参数,比如@RequestMapping(“/hello/{name}”)申明的路径,将注解放在参数中前,即可获取该值,通常作为Restful的接口实现方法。 + +- @RequestBody && @ResponseBody + + - @RequestBody 接收JSON数据 + + 该注解用于读取 Request 请求的 body 部分数据。允许request的参数在request体中,而不是在直接连接在地址后面。(放在参数前) + + - @ResponseBody 将java对象转换成json格式的字符串,返回给浏览器。该注解用于将 Controller 的方法返回的对象,写入到 Response 对象的 body 数据区。 (返回值旁或方法上) + + +**功能类** + +- @ImportResource:引用类 +- @ComponentScan:自动扫描 +- @EnableCaching && Cacheable: 开启注解式的缓存支持/缓存 +- @Transactional:开启事务 +- @Aspect && Poincut:切面和切点 +- @Scheduled:来申明这是一个任务 + +* @RequestController **@RestController=@ResponseBody+@Controller** 意味着,该Controller的所有方法都默认加上了@ResponseBody。 + +* @ControllerAdvice 使一个Contoller成为全局的异常处理类,类中用@ExceptionHandler方法注解的方法可以处理所有Controller发生的异常。 * @ModelAttribute最主要的作用是将数据添加到模型对象中,用于视图页面展示时使用。 等价于 model.addAttribute("attributeName", abc)。 @@ -335,7 +381,7 @@ View是一个接口,实现类支持不同的View类型(jsp、freemarker、pd 在`@Transactional`注解中如果不配置`rollbackFor`属性,那么事物只会在遇到`RuntimeException`的时候才会回滚,加上`rollbackFor=Exception.class`,可以让事物在遇到非运行时异常时也回滚。 -## 事务传播行为⭐ +## 事务传播行为/机制⭐ 事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。 diff --git a/docs/SpringBoot.md b/docs/SpringBoot.md index a267cdc..6f0723e 100644 --- a/docs/SpringBoot.md +++ b/docs/SpringBoot.md @@ -12,11 +12,45 @@ 部署:不需要单独的 Web 服务器。这意味着您不再需要启动 Tomcat 或其他任何东西。 +## Spring Boot 启动流程⭐ + +首先 prepareEnvironment 配置环境,然后准备 Context 上下文,ApplicationContext 的后置处理器,初始化 lnitializers,通知处理上下文准备和加载时间,然后开始refresh。 + +prepareEnvironment + +createApplicationContext +postProcessApplicationContext +applylnitializers +listeners.contextPrepared +listeners.contextLoaded +refreshContext + ### Spring Boot提供了两种常用的配置文件: - properties文件 - yml文件 +## Spring Boot 的核心注解是哪个?⭐ + +启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: + +@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 + +@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 + +@ComponentScan:Spring组件扫描。 + +## Spring Boot 自动配置原理是什么⭐ + +@EnableAutoConfiguration这个注解开启自动配置,它的作用: + +* 利用EnableAutoConfigurationImportSelector给容器中导入一些组件 +* 这个类父类有一个方法:selectImports(),这个方法返回 **configurations** : +* List configurations = getCandidateConfigurations(annotationMetadata, attributes);获取候选的配置 + * 将 类路径下 META-INF/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中 +* 加载某个组件的时候根据注解的条件判断每个加入的组件是否生效,如果生效就把类的属性和配置文件绑定起来 +* 这时就读取配置文件的值加载组件 + ### SpringBoot、SpringMVC和Spring区别 spring boot只是一个配置工具,整合工具,辅助工具. @@ -29,4 +63,105 @@ Spring 框架就像一个家族,有众多衍生产品例如 boot、security、 - Spring 是一个“引擎”; - Spring MVC 是基于Spring的一个 MVC 框架; -- Spring Boot 是基于Spring的条件注册的一套快速开发整合包。 +- Spring Boot 是基于Spring的条件注册的一套快速开发整合包 + +## SpringBoot 拦截器和过滤器 + +​ 1、Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。 + +  2、Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理的方式来执行。 + +  3、Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便。 + + + +```java +@WebFilter(urlPatterns = "/*", filterName = "logFilter") +public class LogCostFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + long start = System.currentTimeMillis(); + filterChain.doFilter(servletRequest, servletResponse); + System.out.println("LogFilter Execute cost=" + (System.currentTimeMillis() - start)); + } + + @Override + public void destroy() { + + } +} +``` + + + +这段代码的逻辑比较简单,就是在方法执行前先记录时间戳,然后通过过滤器链完成请求的执行,在返回结果之间计算执行的时间。这里需要主要,这个类必须继承Filter类,这个是Servlet的规范,这个跟以前的Web项目没区别。 这里直接用@WebFilter就可以进行配置,同样,可以设置url匹配模式,过滤器名称等。这里需要注意一点的是@WebFilter这个注解是Servlet3.0的规范,并不是Spring boot提供的。除了这个注解以外,我们还需在配置类中加另外一个注解:@ServletComponetScan,指定扫描的包。 + +```java +@SpringBootApplication +@MapperScan("com.pandy.blog.dao") +@ServletComponentScan("com.pandy.blog.filters") +public class Application { + public static void main(String[] args) throws Exception { + SpringApplication.run(Application.class, args); + } +} +``` + +上面我们已经介绍了过滤器的配置方法,接下来我们再来看看如何配置一个拦截器。我们使用拦截器来实现上面同样的功能,记录请求的执行时间。首先我们实现拦截器类: + +```java +public class LogCostInterceptor implements HandlerInterceptor { + long start = System.currentTimeMillis(); + @Override + public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { + start = System.currentTimeMillis(); + return true; + } + + @Override + public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { + System.out.println("Interceptor cost="+(System.currentTimeMillis()-start)); + } + + @Override + public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { + } +} +``` + +  这里我们需要实现HandlerInterceptor这个接口,这个接口包括三个方法,preHandle是请求执行前执行的,postHandler是请求结束执行的,但只有preHandle方法返回true的时候才会执行,afterCompletion是视图渲染完成后才执行,同样需要preHandle返回true,该方法通常用于清理资源等工作。除了实现上面的接口外,我们还需对其进行配置: + +```java +@Configuration +public class InterceptorConfig extends WebMvcConfigurerAdapter { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new LogCostInterceptor()).addPathPatterns("/**"); + super.addInterceptors(registry); + } +} +``` + + 这里我们继承了WebMVCConfigurerAdapter,看过前面的文章的朋友应该已经见过这个类了,在进行静态资源目录配置的时候我们用到过这个类。这里我们重写了addInterceptors这个方法,进行拦截器的配置,主要配置项就两个,一个是指定拦截器,第二个是指定拦截的URL。 + + + +## spring boot处理一个http请求的全过程 + +* 由前端发起请求 +* 根据路径,Springboot会加载相应的Controller进行拦截 +* 拦截处理后,跳转到相应的Service处理层 +* 跳转到ServiceImplement(service实现类) +* 在执行serviceimplement时会加载Dao层,操作数据库 +* 再跳到Dao层实现类 +* 执行会跳转到mapper层 +* 然后MallMapper会继续找对应的mapper.xml配置文件 +* 之后便会跳转到第4步继续执行,执行完毕后会将结果返回到第1步,然后 +* 便会将数据以JSON的形式返回到页面,同时返回状态码,正常则会返回200,便会回到步骤1中查询判断。 + diff --git "a/docs/typora-user-images/B+\346\240\221.png" "b/docs/typora-user-images/B+\346\240\221.png" new file mode 100644 index 0000000..4823b10 Binary files /dev/null and "b/docs/typora-user-images/B+\346\240\221.png" differ diff --git "a/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" "b/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" new file mode 100644 index 0000000..7c4b17d Binary files /dev/null and "b/docs/typora-user-images/InnoDB \351\224\201\345\205\263\347\263\273\347\237\251\351\230\265.png" differ diff --git "a/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" "b/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" new file mode 100644 index 0000000..5a9e407 Binary files /dev/null and "b/docs/typora-user-images/InnoDB\346\236\266\346\236\204.png" differ diff --git "a/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" "b/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" new file mode 100644 index 0000000..1664ddc Binary files /dev/null and "b/docs/typora-user-images/InnoDB\347\237\245\350\257\206\344\275\223\347\263\273.png" differ diff --git a/docs/typora-user-images/MVCC3.png b/docs/typora-user-images/MVCC3.png new file mode 100644 index 0000000..8e7eb5e Binary files /dev/null and b/docs/typora-user-images/MVCC3.png differ diff --git "a/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" "b/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" new file mode 100644 index 0000000..b62bf71 Binary files /dev/null and "b/docs/typora-user-images/MVCC\345\256\236\347\216\260.png" differ diff --git "a/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" "b/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" new file mode 100644 index 0000000..b64a96a Binary files /dev/null and "b/docs/typora-user-images/MVCC\345\256\236\347\216\2602.png" differ diff --git "a/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" "b/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" new file mode 100644 index 0000000..e1f9661 Binary files /dev/null and "b/docs/typora-user-images/MySQL\346\236\266\346\236\204.png" differ diff --git "a/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" "b/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" new file mode 100644 index 0000000..9a070ee Binary files /dev/null and "b/docs/typora-user-images/SQL\350\257\255\345\217\245\347\232\204\346\211\247\350\241\214\346\265\201\347\250\213.png" differ diff --git "a/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" "b/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" new file mode 100644 index 0000000..5632da0 Binary files /dev/null and "b/docs/typora-user-images/\345\206\205\345\255\230\345\222\214\347\211\251\347\220\206\347\273\223\346\236\204.png" differ diff --git "a/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" "b/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" new file mode 100644 index 0000000..4ab8906 Binary files /dev/null and "b/docs/typora-user-images/\345\220\204\345\255\227\350\212\202\347\261\273\345\236\213\345\215\240\347\224\250\347\232\204\347\251\272\351\227\264.png" differ diff --git "a/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" "b/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" new file mode 100644 index 0000000..6393995 Binary files /dev/null and "b/docs/typora-user-images/\346\205\242\346\237\245\350\257\242\344\276\213\345\255\220.png" differ diff --git "a/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" "b/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" new file mode 100644 index 0000000..e46c780 Binary files /dev/null and "b/docs/typora-user-images/\346\237\245\350\257\242\344\274\230\345\214\226.png" differ diff --git "a/docs/\345\244\232\347\272\277\347\250\213\351\235\242\350\257\225\351\242\230.md" "b/docs/\345\244\232\347\272\277\347\250\213\351\235\242\350\257\225\351\242\230.md" index 6850337..0d6aef2 100644 --- "a/docs/\345\244\232\347\272\277\347\250\213\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/\345\244\232\347\272\277\347\250\213\351\235\242\350\257\225\351\242\230.md" @@ -1,10 +1,58 @@ # 多线程面试题 -## 线程和进程的区别? +**临界资源** -* 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。 -* 进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。 -* 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。 + 临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。 + +**临界区:** + + 每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。 + +## 线程和进程的区别?⭐ + +* 进程是系统资源分配的最小单位,线程是程序执行的最小单位 +* 进程使用独立的数据空间,而线程共享线程的数据空间 +* 进程的切换效率比线程低 +* 通信方式不同 + +## 进程间通信方式 + +1. 无名管道:半双工的,即数据只能在一个方向上流动,只能用于具有亲缘关系的进程之间的通信,可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。 +2. FIFO命名管道:FIFO是一种文件类型,可以在无关的进程之间交换数据,与无名管道不同,FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。 +3. 消息队列:消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。 +4. 信号量:信号量是一个计数器,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。 +5. 共享内存:共享内存指两个或多个进程共享一个给定的存储区,一般配合信号量使用。 + +**TODO:简单介绍进程的切换过程** + +主要考察线程上下文的切换代价,要回答切换会保持寄存器、栈等线程相关的现场,需要由用户态切换到内核态,最后知道可以通过vmstate命令查看上下文的切换状况 + +## 线程通信方式 + +* volatile 关键字: 使用共享内存的思想,多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。 +* 使用 Object 类的wait() 和 notify() 方法 +* 使用 ReentrantLock 结合 Condition的 await() 和 signal() 方法 +* 信号量 **Semaphore**,可以控制对共享资源的并发访问度,有 accquire() 和 release() 方法 +* **CountDownLatch**:控制线程等待,计数器功能,可以用来等待多个线程执行任务后进行汇总 +* **CyclicBarrier**:类似CountDownLatch但更强大,可以重复使用,控制多个线程,一般测试使用 +* 基本LockSupport实现线程间的阻塞和唤醒 + +### 死锁的4个必要条件⭐ + +1. 互斥条件:一个资源每次只能被一个线程使用; +2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放; +3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺; +4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 + +## 如何分析是否有线程死锁? ⭐ + +使用jconsole图形化工具直接检查死锁 + +使用jstack命令行分析线程Dump信息 + +## 理解线程的同步与异步、阻塞与非阻塞 + +同步与异步的区别是任务是否在同一个线程中执行的 ,阻塞与非阻塞的区别是异步执行任务时线程是不是会阻塞等待结果还是会继续等待后面的逻辑 ## 创建线程有哪几种方式,如何实现? @@ -38,19 +86,16 @@ - Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已; - Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。 -## sleep和wait的区别 - -**基本区别** -sleep是Thread类的方法, wait是Object类中定义的方法 -sleep()方法可以在任何地方使用 -wait()方法只能在synchronized方法或synchronized块中使用(原因:wait方法会释放锁,只有在syn中才有所) +## sleep和wait的区别⭐ **本质区别** Thread.sleep只会让出CPU ,不会导致锁行为的改变 Object.wait不仅让出CPU , 还会释放已经占有的同步资源锁 -sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。 -当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问, 可以通过notify,notifyAll方法来唤醒等待的线程 。 +- wait属于Object类,sleep属于Thread类 +- wait会释放对象锁,而sleep不会 +- wait需要在同步块中使用,sleep可以在任何地方使用 +- sleep需要捕获异常、wait不需要 ## notify()和 notifyAll()有什么区别 @@ -99,7 +144,7 @@ start()方法来启动一个线程,这时无需等待run方法体代码执行 * 结束(Terminated):已终止线程的状态,线程已经结束执行 -## 线程的各种状态的切换(重要) +## 线程的各种状态的切换(重要)⭐

@@ -107,16 +152,16 @@ start()方法来启动一个线程,这时无需等待run方法体代码执行 2. 调用start方法就进入Runnable(可运行状态) 3. 如果此状态被操作系统选中并获得时间片就进入Running状态 4. 如果Running状态的线程的时间片用完或者调用yield方法就**可能**回到Runnable状态 -5. 处于Running状态的线程如果在等待用户输入或者调用了sleep方法就会进入Blocked状态(阻塞状态),会让出CPU,但如果有锁不会释放锁。 -6. 处于Running状态的线程如果在调用有锁(synchronized)的对象就会进入锁池,在锁池等待的线程如果拿到锁就会回到Runnable状态。 -7. 处于Running状态的线程如果调用了wait就会进入等待池,在等待池的线程如果等待时间到或者调用notify方法就会进入锁池。 +5. 处于Running状态的线程如果在进入同步代码块/方法就会进入Blocked状态(阻塞状态),锁被其它线程占有,这个时候被操作系统挂起。得到锁后会回到Running状态。 +6. 处于Running状态的线程如果调用了wait/join/LockSupport.park()就会进入等待池(无限期等待状态), 如果没有被唤醒或等待的线程没有结束,那么将一直等待。 +7. 处于Running状态的线程如果调用了sleep(睡眠时间)/wait(等待时间)/join(等待时间)/ LockSupport.parkNanos(等待时间)/LockSupport.parkUntil(等待时间)方法之后进入限时等待状态,等待时间结束后自动回到原来的状态。 8. 处于Running状态的线程方法执行完毕或者异常退出就会进入死亡状态。 ## 有哪几种实现生产者消费者模式的方法? +锁、信号量、线程通信、阻塞队列。 - -## 什么是上下文切换? +## 什么是上下文切换?⭐ 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式(程序计数器)。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 @@ -133,10 +178,17 @@ start()方法来启动一个线程,这时无需等待run方法体代码执行 创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。 ③. newSingleThreadExecutor() -这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。 +这是一个单线程的Executor,它的特点是能确保依照任务在队列中的顺序来串行执行,适用于保证异步执行顺序的场景。 ④. newScheduledThreadPool(int corePoolSize)(推荐) -创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。 +创建了一个固定长度的线程池,以定时的方式来执行任务,适用于定期执行任务的场景。 + +⑤.newWorkStealingPool +使用ForkJoinPool ,多任务队列的固定并行度,适合任务执行时长不均匀的场景 + +前四个都是使用 ThreadPoolExecutor() 的不同初始化参数创建的。 + +场景:大量短期的任务场景适合使用 Cached 线程池,系统资源比较紧张时使用固定线程池。慎用无界队列,有OOM风险。**自己项目有一个可能高吞吐量的场景就使用了 Cached 线程池** ## 线程池都有哪些状态 @@ -149,34 +201,39 @@ TERMINATED : 结束方法terminated()执行完后进入该状态 ## 线程池核心参数⭐ * **corePoolSize**:线程池里的线程数量,核心线程池大小 - * **maxPoolSize**:线程池里的最大线程数量 - * **workQueue**: 任务队列,用于存放提交但是尚未被执行的任务。 - * keepAliveTime:当线程池中的线程数量大于 corePoolSize 时,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁; 参数的时间单位为 unit。 +* 阻塞队列种类 * threadFactory:线程工厂,用于创建线程,一般可以用默认的 * handler:拒绝策略 -## 执行execute()方法和submit()方法的区别是什么呢? +### 线程池的拒绝策略⭐ -1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** -2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 +1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 +2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 +3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 +4. ThreadPoolExecutor.CallerRunsPolicy:由提交任务的线程直接处理该任务 -## 使用线程池比手动创建线程好在哪里? +## 如何向线程池提交任务 + **有2种**:分别使用execute 方法和 submit 方法 +## 执行execute()方法和submit()方法的区别是什么呢? + +1. **execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** +2. **submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以了解任务执行情况**,并且可以通过 Future 的 get() 方法来获取返回值,还可以取消任务执行。底层也是通过 execute() 执行的。 ## 线程池常用的阻塞队列? -* ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。 -* LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。 +* **ArrayBlockingQueue**:基于数组实现的一个单端阻塞队列,只能从队尾出队。 +* **LinkedBlockingQueue**:基于链表实现的一个双端阻塞队列,分别从队头队尾操作入队出队。 * PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。 * DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。 -## 在 java 程序中怎么保证多线程的运行安全? +## 怎么保证多线程的运行安全/保证线程安全的方法⭐ -线程安全在三个方面体现: +前提是保证下面三个方面: 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized); @@ -184,7 +241,11 @@ TERMINATED : 结束方法terminated()执行完后进入该状态 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。 -synchronized锁的不是代码是对象。 +**可以使用 CAS、Synchronized、Lock、ThreadLocal 来实现。** + +## 如何尽可能提高多线程并发性能? + +尽量减少临界区范围、使用ThreadLocal、减少线程切换、使用读写锁或CopyOnWrite机制 ## 为什么要使用线程池⭐ @@ -208,18 +269,10 @@ synchronized锁的不是代码是对象。 * 没有满就提交给线程池 * 满了就执行拒绝策略 -## 如何向线程池提交任务,提交任务有几种方式有什么区别 - - **有2种**:分别为Runnable和Callable。 +### 如何指定多个线程的执行顺序/ 如何控制线程池线程的优先级 ⭐ -分别使用execute 方法和 submit 方法 - -### 线程池的拒绝策略⭐ - -1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 -2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 -3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务 -4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 +1. 设定一个 orderNum,每个线程执行结束之后,更新 orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程。 +2. 在每一个线程的开始,要 while 判断 orderNum 是否等于自己的要求值,不是,则 wait,是则执行本线程。 ### 线程池的线程数量怎么确定 @@ -227,9 +280,19 @@ synchronized锁的不是代码是对象。 2. 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。 3. 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 -## volatile⭐ +## 常用的线程分析工具与方法 + +jstack 分析线程的运行状态,查看锁对象的持有状况。 + +jconsole:JDK自带的图形化界面工具 + +## volatile作用⭐ + +在多线程开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。 + +**volatile 告诉编译器它修饰的变量会不断的被修改,编译器就会通过强制主内存读写同步,防止指令重排序来保证原子性,可见性和有序性。但不能代替锁,不能保证i++这种复合操作的原子性。** + -volatile在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。 ## Java 中是如何实现线程同步的? @@ -257,20 +320,30 @@ volatile在多处理器开发中保证了共享变量的“ 可见性”。可 ​ +## i++是线程安全的吗? +分2种情况 -### Atomic类的CAS操作⭐ +1. 局部变量肯定是线程安全的(原因:方法内局部变量是线程私有的) +2. 成员变量多个线程共享时,就不是线程安全的(原因:成员变量是线程共享的,因为 i++ 是三步操作。 -​ CAS,Compare and Swap即比较并交换。java.util.concurrent 包借助 CAS 实现了区别于 synchronized 同步锁的一种乐观锁。乐观锁就是每次去取数据的时候都乐观的认为数据不会被修改,所以不会上锁,但是在更新的时候会判断一下在此期间数据有没有更新。CAS 有3个操作数:内存值 V,旧的预期值 A,要修改的新值 B +## 介绍 Synchronized -## i++是线程安全的吗? +保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 -分2种情况 +**三种使用方式:** -1. 局部变量肯定是线程安全的(原因:方法内局部变量是线程私有的) -2. 成员变量多个线程共享时,就不是线程安全的(原因:成员变量是线程共享的,因为 i++ 是三步操作。 +synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。 + +## Synchronized 的实现原理 + +对象在内存中分为对象头,实例数据和对齐填充三个区域。在对象头中保存了锁标志位和指向 Monitor 对象的起始地址。当 Monitor 被某个线程占用后就会处于锁定状态。 **synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** synchronized 修饰的方法使用是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 + +### JDK1.6 之后的synchronized 关键字底层做了一些优化 + +偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化,详见 JVM 篇。 ## Synchronized与Lock的区别⭐ @@ -302,15 +375,58 @@ volatile在多处理器开发中保证了共享变量的“ 可见性”。可 进入阻塞队列的线程,竞争锁时都是公平的,因为队列为先进先出(FIFO)。 +# JUC工具类 + +## Atomic类 + +所谓原子类说简单点就是具有原子/原子操作特征的类。 + +### 基本数据类型的原子类 + +* AtomicLong/AtomicInteger/AtomicBoolean:通过底层工具类 unsafe 类实现,基于 CAS。unsafe 类提供了类似 C 的指针操作,都是本地方法。 +* LongAdder/LongAccumulator:基于 Cell 实现,基于分段锁思想,是一种以空间换时间的策略,适合高并发场景。 +* AtomicReference:引用类型原子类,用于原子性对象的读写。 +* AtomicStampedReference/AtomicMarkableReference:解决 ABA 问题的类 + +### Atomic类如何保证原子性⭐ + +​ CAS,Compare and Swap即比较并交换。主要利用 CAS (compare and swap) + volatile 和 unsafe 类的 底层 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 + +## CAS 可能会导致什么问题? + +ABA问题,就是在写入时读取到的数据是和预期的一样的 A,但是这个 A 可能已经被其他线程修改成 B 再修改回来了。解决办法:增加额外的标志位或时间戳。 + + + +# AQS + +AQS的全称为(AbstractQueuedSynchronizer),在java.util.concurrent.locks包下面。 + +**AQS定义两种资源共享方式** + +- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: + +- - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 + - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 + +- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 + +**异步工具类:** + +Executors +CompletableFuture:支持流式调用,多future组合,可以设置完成时间 +FutureTask +ForkJoinPool :分治思想+工作窃取 +### ThreadLocal 实现原理 +每个线程独享的局部变量,ThreadLocal使用弱引用 ThreadLocalMap 保存弱引用的局部变量。使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。 +## ThreadLocal用来解决什么问题? +ThreadLocak不是用来解决多线程共享变量的问题,而是线程数据隔离的问题 -**TODO:** -多线程的线程池是如何工作的?底层代码和原理? -Thredlocal 实现原理? diff --git "a/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" "b/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" new file mode 100644 index 0000000..0d97774 --- /dev/null +++ "b/docs/\346\267\261\345\205\245\347\220\206\350\247\243MySQL\345\237\272\347\241\200\347\257\207.md" @@ -0,0 +1,343 @@ +# MySQL体系结构与存储引擎 + +## MySQL体系结构 + +先看 MySQL 数据库的体系结构,如下图所示。 + + + + + + + +MySQL体系结构由ClientConnectors层、MySQLServer层及存储引擎层组成。 + +**ClientConnectors层** + +负责处理客户端的连接请求,与客户端创建连接。目前 MySQL 几乎支持所有的连接类型,例如常见的 JDBC、Python、Go 等。 + +**MySQL Server 层** + +MySQLServer层主要包括ConnectionPool、Service&utilities、SQLinterface、Parser解析器、Optimizer查询优化器、Caches缓存等模块。 + +1. ConnectionPool,负责处理和存储数据库与客户端创建的连接,线程池资源管理,一个线程负责管理一个连接。还包括了用户认证模块,就是用户登录身份的认证和鉴权及安全管理,也就是用户执行操作权限校验。 + +2. Service & utilities 是管理服务&工具集,包括备份恢复、安全管理、集群管理服务和工具。 +3. SQL interface,负责接收客户端发送的各种 SQL 语句,比如 DML、DDL 和存储过程等。 +4. Parser 解析器会对 SQL 语句进行语法解析生成解析树。 +5. Optimizer 查询优化器会根据解析树生成执行计划,并选择合适的索引,然后按照执行计划执行 SQL 语言并与各个存储引擎交互。 +6. Caches缓存包括各个存储引擎的缓存部分,比如:InnoDB存储的BufferPool,Caches中也会缓存一些权限,也包括一些 Session 级别的缓存。 + +**存储引擎层** + +存储引擎包括MyISAM、InnoDB,以及支持归档的Archive和内存的Memory等。MySQL是插件式的存储引擎,只要正确定义与MySQLServer交互的接口,任何引擎都可以访问MySQL。 + +**物理存储层** + +存储引擎底部是物理存储层,是文件的物理存储层,包括二进制日志、数据文件、错误日志、慢查询日志、全日志、redo/undo 日志等。 + + + +下面是一条SQL SELECT语句的执行过程: + + + +## 存储引擎 + +存储引擎是 MySQL 中具体与文件打交道的子系统,它是根据 MySQL AB 公司提供的文件访问层的抽象接口,定制的一种文件访问机制,这种机制就叫作存储引擎。 + +InnoDB 存储引擎的具体架构如下图所示。上半部分是实例层(计算层),位于内存中,下半部分是物理层,位于文件系统中。 + + + + + +### 实例层 + +实例层分为线程和内存。 + +InnoDB 重要的线程有 Master Thread,Master Thread 是 InnoDB 的主线程,负责调度其他各线程。 + +* MasterThread的优先级最高,其内部包含几个循环:主循环(loop)、后台循环(backgroundloop)、刷新循环(flushloop)、暂停循环(suspendloop)。Master Thread 会根据其内部运行的相关状态在各循环间进行切换。 + + 大部分操作在主循环(loop)中完成,其包含 1s 和 10s 两种操作。 + +* buf_dump_thread 负责将 buffer pool 中的内容 dump 到物理文件中,以便再次启动 MySQL 时,可以快速加热数据。 + +* page_cleaner_thread负责将bufferpool中的脏页刷新到磁盘,在5.6版本之前没有这个线程,刷新操作都是由主线程完成的,所以在刷新脏页时会非常影响MySQL的处理能力,在5.7 版本之后可以通过参数设置开启多个 page_cleaner_thread。 + +* purge_thread 负责将不再使用的 Undo 日志进行回收。 + +* read_thread 处理用户的读请求,并负责将数据页从磁盘上读取出来,可以通过参数设置线程数量。 + +* write_thread 负责将数据页从缓冲区写入磁盘,也可以通过参数设置线程数量,page_cleaner 线程发起刷脏页操作后 write_thread 就开始工作了。 + +* redo_log_thread 负责把日志缓冲中的内容刷新到 Redo log 文件中。 + +* insert_buffer_thread 负责把 Insert Buffer 中的内容刷新到磁盘。 + +实例层的内存部分主要包含InnoDBBufferPool,这里包含InnoDB最重要的缓存内容。数据和索引页、undo页、insertbuffer页、自适应Hash索引页、数据字典页和锁信息等。additionalmemorypool后续已不再使用。Redobuffer里存储数据修改所产生的Redolog。doublewritebuffer是 double write 所需的 buffer,主要解决由于宕机引起的物理写入操作中断,数据页不完整的问题。 + +### 物理层 + +物理层在逻辑上分为系统表空间、用户表空间和 Redo日志。 + +系统表空间里有 ibdata 文件和一些 Undo,ibdata 文件里有 insert buffer 段、double write段、回滚段、索引段、数据字典段和 Undo 信息段。 + +用户表空间是指以 .ibd 为后缀的文件,文件中包含 insert buffer 的 bitmap 页、叶子页(这里存储真正的用户数据)、非叶子页。 + +Redo日志中包括多个Redo文件,这些文件循环使用,当达到一定存储阈值时会触发checkpoint刷脏页操作,同时也会在MySQL实例异常宕机后重启,InnoDB表数据自动还原恢复过程中使用。 + +### 内存和物理结构 + +内存和物理结构,如下图所示。 + + + +**BufferPool** + +用户读取或者写入的最新数据都存储在BufferPool中,如果BufferPool中没有找到则会读取物理文件进行查找,之后存储到BufferPool中并返回给MySQLServe。Buffer Pool 采用LRU 机制。 + +BufferPool决定了一个SQL执行的速度快慢,如果查询结果页都在内存中则返回结果速度很快,否则会产生物理读(磁盘读),返回结果时间变长。但我们又不能将所有数据页都存储到BufferPool中。在单机单实例情况下,我们可以配置BufferPool为物理内存的60%~80%,剩余内存用于session产生的sort和join等,以及运维管理使用。如果是单机多实例,所有实例的bufferpool总量也不要超过物理内存的80%。开始时我们可以根据经验设置一个BufferPool的经验值,比如16GB,之后业务在MySQL运行一段时间后可以根据show global status like'%buffer_pool_wait%' 的值来看是否需要调整 Buffer Pool 的大小。 + +**Redolog** + +Redolog是一个循环复用的文件集,**负责记录InnoDB中所有对BufferPool的物理修改日志**,当Redolog文件空间中,检查点位置的LSN和最新写入的LSN差值(checkpoint_age)达到Redolog文件总空间的75%后,InnoDB会进行异步刷新操作,直到降至75%以下,并释放Redolog的空间;当checkpoint_age达到文件总量大小的 90% 后,会触发同步刷新,此时 InnoDB 处于挂起状态无法操作。 + +补充: + +* 日志序号 (LSN:Log sequence number) 标识特定日志文件记录在日志文件中的位置。 +* checkpoint_age:检查点,将缓冲池中的脏页刷回到磁盘。当缓冲池不够用时,根据LRU算法溢出的页,若此页为脏页,那么需要强制执行Checkpoint,将脏页也就是页的新版本刷回磁盘。 + +每个页有LSN,重做日志中也有LSN,Checkpoint也有LSN。可以通过命令SHOW ENGINE INNODB STATUS来观察 。 + +这样我们就看到**Redolog的大小直接影响了数据库的处理能力**,如果设置太小会导致强行checkpoint操作频繁刷新脏页,那我们就需要将Redolog设置的大一些,5.6版本之前Redo log 设置的大一些,5.6 版本之前 Redo log 总大小不能超过 3.8GB,5.7 版本之后放开了这个限制。 + +事务提交时 log buffer 会刷新到 Redo log 文件中,具体刷新机制由参数控制。 + +### Myisam和InnoDB的区别 + +* **是否支持行级锁** : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁,适合高并发操作。 +* **是否支持外键**: MyISAM不支持,而InnoDB支持 +* **是否支持事务**:MyISAM不支持,而InnoDB支持 +* **缓存**:MyISAM只缓存索引,InnoDB缓存索引和真实数据,所以对内存要求高 +* **崩溃恢复**:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 +* InnoDB 支持 MVCC,MyISAM 不支持; + + +InnoDB 表最大还可以支持 64TB,支持聚簇索引、支持压缩数据存储,支持数据加密,支持查询/索引/数据高速缓存,支持自适应hash索引、空间索引,支持热备份和恢复等 + +## InnoDB 核心要点 + + + + + + +ARIES 三原则 + +WriteAheadLogging(WAL)。 + +* 先写日志后写磁盘,日志成功写入后事务就不会丢失,后续由checkpoint机制来保证磁盘物理文件与Redo日志达到一致性; +* 利用Redo 记录变更后的数据,即 Redo 记录事务数据变更后的值; +* 利用 Undo 记录变更前的数据,即 Undo 记录事务数据变更前的值,用于回滚和其他事务多版本读。 + +show engine innodb status\G 的结果里面有详细的 InnoDB 运行态信息,分段记录的,包括内存、线程、信号、锁、事务。 + +# 深入理解事务与锁机制 + +## 事务及其特性 + +一个逻辑工作单元要成为事务,在关系型数据库管理系统中,必须满足 4 个特性,即所谓的 ACID:原子性、一致性、隔离性和持久性。 + +Atomicity(原子性):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行 + +Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。 + +Isolation(隔离性):同一时间,只允许一个事务操作同一数据,不同的事务之间彼此没有任何干扰。 + +Durability(持久性):事务处理结束后,对数据的修改是永久的。 + +### 一致性 + +一致性其实包括两部分内容,分别是约束一致性和数据一致性。 + +* 约束一致性:数据库创建表结构时所制定的外键,唯一索引等约束。 +* 数据一致性:是一个综合性的规定,或者说是一个把握全局的规定。因为它是由原子性、持久性、隔离性共同保证的结果,而不是单单依赖于某一种技术。 + +### 原子性 + +原子性就是前面提到的两个“要么”,即要么改了,要么没改。也就是说用户感受不到一个正在改的状态。MySQL 是通过 WAL(Write Ahead Log)技术来实现这种效果的。 + +举例来讲,如果事务提交了,那改了的数据就生效了,如果此时BufferPool的脏页没有刷盘,如何来保证改了的数据生效呢?就需要使用Redo日志恢复出来的数据就需要使用Redo日志恢复出来的数据。而如果事务没有提交,且BufferPool的脏页被刷盘了,那这个本不应该存在的数据如何消失呢?就需要通过 Undo 来实现了,Undo 又是通过 Redo 来保证的,所以最终原子性的保证还是靠 Redo 的 WAL 机制实现的。 + +### 持久性 + +所谓持久性,就是指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,接下来的操作或故障不应该对其有任何影响。持久性是如何保证的呢?一旦事务提交,通过原子性,即便是遇到宕机,也可以从逻辑上将数据找回来后再次写入物理存储空间,这样就从逻辑和物理两个方面保证了数据不会丢失,即保证了数据不会丢失,即保证了数据库的持久性。 + +### 隔离性 + +所谓隔离性,指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。锁和多版本控制就符合隔离性。 + +## 并发事务控制 + +### 单版本控制-锁 + +锁用独占的方式来保证在只有一个版本的情况下事务之间相互隔离,所以锁可以理解为单版本控制。在MySQL事务中,锁的实现与隔离级别有关系,在RR(RepeatableRead)隔离级别下,MySQL为了解决幻读的问题,以牺牲并行度为代价,通过Gap锁来防止数据的写入,而这种锁,因为其并行度不够,冲突很多,经常会引起死锁。现在流行的Row模式可以避免很多冲突甚至死锁问题,所以推荐默认使用 Row + RC(Read Committed)模式的隔离级别,可以很大程度上提高数据库的读写并行度。 + +补充: 在row level模式下,bin-log中可以不记录执行的sql语句的上下文相关的信息,仅仅只需要记录那一条被修改。 + +### 多版本控制-MVCC + + MVCC,是指在数据库中,为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本。 + +那个多版本是如何生成的呢?每一次对数据库的修改,都会在Undo日志中记录当前修改记录的事务号及修改前数据状态的存储地址(即ROLL_PTR),以便在必要的时候可以回滚到老的数据版本。例如,一个读事务查询到当前记录,而最新的事务还未提交,根据原子性,读事务看不到最新数据,但可以去回滚段中找到老版本的数据,这样就生成了多个版本。多版本控制很巧妙地将稀缺资源的独占互斥转换为并发,大大提高了数据库的吞吐量及读写性能。 + +## 技术原理 + +### 原子性技术原理 + +每一个写事务,都会修改BufferPool,从而产生相应的Redo日志,这些日志信息会被记录到ib_logfiles文件中。因为Redo日志是遵循WriteAheadLog的方式写的,所以事务是顺序被记录的。在MySQL中,任何BufferPool中的页被刷到磁盘之前,都会先写入到日志文件中,这样做有两方面的保证。如果BufferPool中的这个页没有刷成功,此时数据库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写下去的数据不会丢失,所以必须要保证 Redo 先写。 + +为 Buffer Pool 的空间是有限的,要载入新页时,需要从 LRU 链表中淘汰一些页,而这些页必须要刷盘之后,才可以重新使用,那这时的刷盘,就需要保证对应的 LSN 的日志也要提前写到 ib_logfiles 中,如果没有写的话,恰巧这个事务又没有提交,数据库挂了,在数据库启动之后,这个事务就没法回滚了。所以如果不写日志的话,这些数据对应的回滚日志可能就不存在,导致未提交的事务回滚不了,从而不能保证原子性,所以原子性就是通过 WAL 来保证的。 + +### 持久性技术原理 + +通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持久性。这个过程与前面提到的Redo日志、事务状态、数据库恢复、参数innodb_flush_log_at_trx_commit 有关,还与 binlog 有关。 + +### 隔离性技术原理 + +InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。 + +具体说到隔离性的实现方式,我们通常用ReadView表示一个事务的可见性。RC级别的事务可见性比较高,它可以看到已提交的事务的所有修改。而RR级别的事务,则没有这个功能,一个读事务中,不管其他事务对这些数据做了什么修改,以及是否提交,只要自己不提交,查询的数据结果就不会变。 + +随着时间的推移,读提交每一条读操作语句都会获取一次 Read View,每次更新之后,都会获取数据库中最新的事务提交状态,也就可以看到最新提交的事务了,即每条语句执行都会更新其可见性视图。 + +而反观可重复读,这个可见性视图,只有在自己当前事务提交之后,才去更新,所以与其他事务是没有关系的。 + +**在 RR 级别下,长时间未提交的事务会影响数据库的 PURGE 操作,从而影响数据库的性能,所以可以对这样的事务添加一个监控**。 + +### 一致性技术原理 + +数据的完整性是通过其他三个特性来保证的,为了保证数据的完整性,提出来三个特性,这三个特性又是由同一个技术来实现的,所以理解 Redo/Undo 才能理解数据库的本质。 + +### MVCC 实现原理 + +MVCC最大的好处是读不加锁,读写不冲突。在读多写少的OLTP(On-LineTransactionProcessing)应用中,读写不冲突是非常重要的,极大的提高了系统的并发性能。 + +在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read)与当前读 (Current Read)。 + +* 快照读:读取的是记录的可见版本(有可能是历史版本),不用加锁。 +* 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。 + +如何区分快照读和当前读呢? 可以简单的理解为: + +* 快照读:简单的 select 操作,属于快照读,不需要加锁。 +* 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。 + +下面用一个事务对某行记录更新的过程来说明MVCC中多版本的实现。 + +假设 F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针,如下图所示。 + +image-20200412113401177 + +* 隐含ID(DB_ROW_ID),6个字节,当由InnoDB自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值。 +* 事务号(DB_TRX_ID),6个字节,标记了最新更新这条行记录的TransactionID,每处理一个事务,其值自动+1。 +* 回滚指针(DB_ROLL_PT),7个字节,指向当前记录项的RollbackSegment的Undolog记录,通过这个指针才能查找之前版本的数据。 + +首先,假如这条数据是刚 INSERT 的,可以认为 ID 为 1,其他两个字段为空。 + +然后,当事务 1 更改该行的数据值时,会进行如下操作,如下图所示。 + + + +* 用排他锁锁定该行;记录 Redo log +* 把该行修改前的值复制到 Undo log,即图中下面的行 +* 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行 + +如果数据继续执行,此时Undolog中有两行记录,并且通过回滚指针连在一起。因此,如果Undolog一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的是在InnoDB中存在purge 线程,它会查询那些比现在最老的活动事务还早的 Undo log,并删除它们,从而保证 Undo log 文件不会无限增长,如下图所示。 + + + +## 并发事务问题及解决方案 + +脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。 + +不可重复读 :是指在一个事务内,多次读同一数据数据发生了变化。 + +幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。 + +产生的这些问题,MySQL 数据库是通过事务隔离级别来解决的。值得一提的是,InnoDB通过Gap锁解决了幻读的问题。 + +不可重复读重点在于 UPDATA 和 DELETE,而幻读的重点在于 INSERT。它们之间最大的区别是如何通过锁机制来解决它们产生的问题。 + + + +## InnoDB 的锁 + +InnoDB 的锁分为行锁和表锁。 + +其中行锁包括两种: + +* 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。 +* 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。 + +为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。表锁又分为三种。 + +* 意向共享锁(IS):事务计划给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁 +* 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的的 IX 锁。 +* 自增锁(AUTO-INC Locks):特殊表锁,自增长计数器通过该“锁”来获得子增长计数器最大的计数值。 + +在加行锁之前必须先获得表级意向锁,否则等待 innodb_lock_wait_timeout 超时后根据innodb_rollback_on_timeout 决定是否回滚事务。 + +### InnoDB自增锁 + +在MySQLInnoDB存储引擎中,我们在设计表结构的时候,通常会建议添加一列作为自增主键。这里就会涉及一个特殊的锁:自增锁(即:AUTO-INCLocks),它属于表锁的一种,在 INSERT 结束后立即释放。我们可以执行 **show engine innodb status\G** 来查看自增锁的状态信息。 + +InnoDB 锁关系矩阵如下图,其中:+ 表示兼容,- 表示不兼容。 + + + + + +### InnoDB 行锁 + +InnoDB 行锁是通过对索引数据页上的记录(record)加锁实现的。主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock。 + +* RecordLock锁:单个行记录的锁(锁数据,不锁Gap)。 +* GapLock锁:间隙锁,锁定一个范围,不包括记录本身(不锁数据,仅仅锁数据前面的Gap)。 +* Next-keyLock 锁:同时锁住数据,并且锁住数据前面的 Gap。 + +**排查 InnoDB 锁问题** + +排查InnoDB锁问题通常有2种方法。打开innodb_lock_monitor表,注意使用后记得关闭,否则会影响性能。在MySQL5.5版本之后,可以通过查看information_schema 库下面的 innodb_locks、innodb_lock_waits、innodb_trx 三个视图排查 InnoDB 的锁问题。 + +### InnoDB死锁 + +在MySQL中死锁不会发生在MyISAM存储引擎中,但会发生在InnoDB存储引擎中,因为InnoDB是逐行加锁的,极容易产生死锁。那么死锁产生的四个条件是什么呢? + +* 互斥条件:一个资源每次只能被一个进程使用; +* 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放; +* 不剥夺条件:进程已获得的资源,在没使用完之前,不能强行剥夺; +* 循环等待条件:多个进程之间形成的一种互相循环等待资源的关系。 + +在发生死锁时,InnoDB存储引擎会自动检测,并且会自动回滚代价较小的事务来解决死锁问题。但很多时候一旦发生死锁,InnoDB存储引擎的处理的效率是很低下的或者有时候根本解决不了问题,需要人为手动去解决。 + +既然死锁问题会导致严重的后果,那么在开发或者使用数据库的过程中,如何避免死锁的产生呢?这里给出一些建议: + +* 加锁顺序一致; +* 尽量基于primary或uniquekey更新数据。 +* 单次操作数据量不宜过多,涉及表尽量少。 +* 减少表上索引,减少锁定资源。 +* 相关工具:pt-deadlock-logger + +查看MySQL数据库中死锁的相关信息,可以执行showengineinnodbstatus\G来进行查看,重点关注“LATESTDETECTEDDEADLOCK”部分。 + +一些开发建议来避免线上业务因死锁造成的不必要的影响。 + +* 更新SQL的where条件时尽量用索引; +* 加锁索引准确,缩小锁定范围; +* 减少范围更新,尤其非主键/非唯一索引上的范围更新。 +* 控制事务大小,减少锁定数据量和锁定时间长度 (innodb_row_lock_time_avg)。 +* 加锁顺序一致,尽可能一次性锁定所有所需的数据行。 + diff --git "a/docs/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225\351\242\230.md" "b/docs/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225\351\242\230.md" index 2566bae..42bc580 100644 --- "a/docs/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225\351\242\230.md" +++ "b/docs/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225\351\242\230.md" @@ -1,23 +1,84 @@ -## OSI与TCP/IP都有哪些协议(五层协议)? +## TCP/IP四层网络模型⭐ -- **应用层** :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等协议。数据单位为报文。 -- **传输层** :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 -- **网络层** :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。 -- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。 -- **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 +**第一层 网络接口层** -## 简述 tcp 和 udp的区别 +网络接口层包括用于协作IP数据在已有网络介质上传输的协议。 + +协议:ARP,RARP +**第二层 网间层** + +网间层对应于OSI七层参考模型的网络层。负责数据的包装、寻址和路由。同时还包含网间控制报文协议(Internet Control Message Protocol,ICMP)用来提供网络诊断信息。 + +协议:本层包含IP协议、RIP协议(Routing Information Protocol,路由信息协议),ICMP协议。 +**第三层 传输层** + +传输层对应于OSI七层参考模型的传输层,它提供两种端到端的通信服务。 + +其中TCP协议(Transmission Control Protocol)提供可靠的数据流运输服务,UDP协议(Use Datagram Protocol)提供不可靠的用户数据报服务。 +**第四层 应用层** + +应用层对应于OSI七层参考模型的应用层和表达层。 + +因特网的应用层协议包括Finger、Whois、FTP(文件传输协议)、Gopher、HTTP(超文本传输协议)、Telent(远程终端协议)、SMTP(简单邮件传送协议)、IRC(因特网中继会话)、NNTP(网络新闻传输协议)等。 + +## OSI七层网络模型⭐ + +**第一层 物理层** + +作用:负责最后将信息编码成电流脉冲或其它信号用于网上传输。它由计算机和网络介质之间的实际界面组成,可定义电气信号、符号、线的状态和时钟要求、数据编码和数据传输用的连接器。所有比物理层高的层都通过事先定义好的接口而与它通话。 + +协议:如最常用的RS-232规范、10BASE-T的曼彻斯特编码以及RJ-45就属于第一层。 +**第二层 数据链路层** + +作用:数据链路层通过物理网络链路提供可靠的数据传输。 + +协议:ATM,FDDI等。 +**第三层 网络层** + +作用:这层对端到端的包传输进行定义,他定义了能够标识所有结点的逻辑地址,还定义了路由实现的方式和学习的方式。为了适应最大传输单元长度小于包长度的传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法。 + +协议:IP,IPX等 +**第四层 传输层** + +作用:传输层向高层提供可靠的端到端的网络数据流服务。传输层的功能一般包括流控、多路传输、虚电路管理及差错校验和恢复。流控管理设备之间的数据传输,确保传输设备不发送比接收设备处理能力大的数据;多路传输使得多个应用程序的数据可以传输到一个物理链路上;虚电路由传输层建立、维护和终止;差错校验包括为检测传输错误而建立的各种不同结构;而差错恢复包括所采取的行动(如请求数据重发),以便解决发生的任何错误。 + +协议:TCP,UDP,SPX。 +**第五层 会话层** + +作用:会话层建立、管理和终止表示层与实体之间的通信会话。通信会话包括发生在不同网络应用层之间的服务请求和服务应答,这些请求与应答通过会话层的协议实现。它还包括创建检查点,使通信发生中断的时候可以返回到以前的一个状态。 + +协议:RPC,SQL等 +**第六层 表示层** + +作用:这一层的主要功能是定义数据格式及加密。 + +协议:FTP,加密 +**第七层 应用层** + +作用:应用层是最接近终端用户的OSI层,这就意味着OSI应用层与用户之间是通过应用软件直接相互作用的。应用层的功能一般包括标识通信伙伴、定义资源的可用性和同步通信。 +协议:telnet,HTTP,FTP,WWW,NFS,SMTP等。 + +## 简述 TCP 和 UDP 的区别⭐ - TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。 - TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。 -- Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。 - UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。 - 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。 - TCP对系统资源要求较多,UDP对系统资源要求较少。 +### TCP 特点⭐ + +- 基于链接,点对点传输,在传输数据前要先建立好连接,再进行传输。 +- 一旦建立连接就可以是双向通信 +- 传输是基于字节流而不是报文,将数据根据大小进行变好,接收端通过 ack 大小进行编号,从而保证接收数据的有序性和完整性。 +- TCP 还可提供流量控制能力,通过滑动窗口来控制数据的发送速率,滑动窗口的本质就是动态缓冲区 +- 通过慢启动,拥塞避免,拥塞发生,快速恢复四个算法实现了拥塞控制 +**选择** -## TCP 三次握手和四次挥手(重点) +什么时候应该使用TCP: 当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。 在日常生活中,常见使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输。什么时候应该使用UDP: 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。 比如,日常生活中,常见使用UDP协议的应用如下: QQ语音 QQ视频 TFTP。 + +## TCP 三次握手和四次挥手(重点)⭐ 先介绍各单词的含义: @@ -27,7 +88,7 @@ - **ACK:确认序号标志** - PSH:push标志 - RST:重置连接标志 -- SYN:同步序列号,用于建立连接过程 +- SYN (Synchronize Sequence Numbers) :同步序列号,用于建立连接过程 - FIN:finish标志,用于释放链接 握手是为了建立连接,流程如下图 : @@ -38,68 +99,68 @@ seq:为自己的标记缓存的初始序号 ack:确认号 **第一次握手**: -一开始都是close状态,假设主动客户端主动打开,服务端进入listen监听状态,等待请求,客户端发出连接请求报文(SYN包),报文头为SYN=1,seq为任意正整数,此时进入同步发送状态(SYN_SEND),等待服务器确认。 +一开始都是close状态,客户端主动打开,服务端进入listen监听状态,等待请求,客户端发出连接请求报文(SYN包),报文头为SYN,seq为任意正整数x,此时进入同步发送状态(SYN_SEND),等待服务器确认。 **第二次握手:** -如果服务端同意接收信息,会发出确认报文(SYN+ACK包 ),报文头seq为另外一个正整数,ack为x+1,服务端进入同步收到的状态(SYN_RCVD); +如果服务端同意接收信息,会发出确认报文(SYN+ACK包 ),报文头seq为另外一个正整数y,ack为x+1,服务端进入同步收到的状态(SYN_RCVD); 前两步都不携带数据,都需要消耗一个序列号。 **第三次握手:** -客户端接收到确认报文(SYN+ACK包 ),还要向服务端给出确认,发出确认报文(ACK包),两端都进入ESTABLISHED状态,完成三次握手。 +客户端接收到确认报文(SYN+ACK包 ),进入ESTABLISHED状态,还要向服务端给出确认,发出确认报文(ACK包 ack=y+1 ),两端都进入ESTABLISHED状态,完成三次握手。 **此后,双方就建立了链接,可以开始通信了。** -## 为什么需要三次握手? +## 为什么需要三次握手?⭐ -简单说就是: **为了双方确认自己与对方的发送与接收是正常的。** +简单说就是: 为了双方确认自己与对方的发送与接收是正常的。两次握手的话至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认,不能建立起双向通信。 **为了初始化Sequence Number 的初始值**: 为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤。 ## 首次握手的隐患–SYN超时 **原因:** -Server收到Client的SYN ,回复SYN- ACK的时候未收到ACK确认 -Server不断重试直至超时, Linux默认等待63秒才断开连接 -黑客可能利用这个漏洞进行恶意攻击。 +Server收到Client的SYN ,回复SYN- ACK的时候未收到ACK确认,导致 Server 一直在 SYN_RCVD 状态, +Server不断重试直至超时, 影响正常数据发送,但 Linux 默认等待63秒才断开连接 +所以黑客可能利用这个漏洞进行恶意攻击。 **防护措施:** SYN队列满后,通过tcp_ syncookies参数回发SYN Cookie -若为正常连接则客户端会回发SYN Cookie 告诉服务端已经接收到请稍等,直接建立连接。 +若为正常连接则客户端会回发 SYN Cookie 告诉服务端已经接收到请稍等,直接建立连接。 ## 建立连接后,Client出现故障怎么办 保活机制 一直发送探测报文,直到达到设定次数还无响应就中断连接 -## TCP四次挥手(重点) +## TCP四次挥手(重点)⭐ 目的:为了中止连接 流程图: -

- +

**第一次挥手:** -最开始两方都处于ESTABLISHED状态,客户端主动关闭,发出连接释放报文(FIN)并且停止发送数据,报文头:FIN和seq,然后进入**FINWAIT1**状态。 +最开始两方都处于ESTABLISHED状态,客户端主动关闭,发出连接释放报文(FIN)并且停止发送数据,报文头:FIN和seq =u(等于前面已经传送过来的数据的最后一个字节的序号加1),然后进入**FINWAIT1**状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。 **第二次挥手:** -服务器收到报文,发出确认报文(ACK),进入**CLOSEWAIT**状态。 -CLOSEWAIT状态:半关闭状态,客户端没有数据需要发送,服务器如果要发送数据客户端也可以接收。 +服务器收到报文,发出确认报文(ACK,ack=u+1,序列号seq=v),进入**CLOSEWAIT**状态。 +CLOSEWAIT状态:半关闭状态,服务器端不接收数据但可能还要发送数据。 **第三次挥手:** -客户端收到报文进入**FINWAIT2**状态,等待服务器发送第三次挥手,这段时间可以接收数据。服务端数据发送完后,会发送释放报文,然后进入**LASTACK**状态。 +客户端收到报文进入**FINWAIT2**状态,等待服务器发送第三次挥手,这段时间可以接收数据。服务端数据发送完后,会发送释放报文(FIN),但服务器很可能又发送了一些数据,序列号会变化,假定此时的序列号为seq=w ,此时进入**LASTACK**状态。 **第四次挥手:** -客户端收到报文后必须发送确认报文,进入**TIMEWAIT**,但连接没有释放。 -服务端收到确认直接进入**CLOSED**状态 +客户端收到报文后必须发送确认报文(ACK,ack=w+1,seq=u+1),进入**TIMEWAIT**,但连接没有释放,等待2MSL(2倍最大报文段寿命)来保证连接的可靠关闭才进入**CLOSED**状态。服务端收到确认直接进入**CLOSED**状态。 **为什么什么有TIME_WAIT状态** -确保有足够的时间让对方收到ACK包,所以才设置为2MS -避免新旧连接混淆 +* 保证全双工链接可靠关闭 +* 保证重复的数据段消失,方式端口被重用时产生数据混淆 -#### 为什么需要四次握手才能断开连接 +### 为什么连接的时候是三次握手,关闭的时候却是四次握手⭐ -因为全双工,发送方和接收方都需要FIN报文和ACK报文 +因为在连接时当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,因为可能还有数据需要传输回去,并不能立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了,但我还有一些数据没有发完"。只有等到我Server端所有的报文都发送完了,才会发送FIN报文,因此不能一起发送。所以需要四步握手。 tip: 全双工(Full Duplex)是通讯传输的一个术语。通信允许数据在两个方向上同时传输 #### 服务器出现大量CLOSE_ WAIT状态的原因 +被动关闭的一方可能存在代码问题没有正确关闭链接导致的 + 对方关闭socket连接,我方忙于读或写,没有及时关闭连接 **解决:** 检查代码,特别是释放资源的代码 @@ -114,7 +175,7 @@ tip: 全双工(Full Duplex)是通讯传输的一个术语。通信允许数 尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表 面向报文,不对应用程序提交的报文信息进行拆分或者合并 -## 在浏览器中输入url地址 ->> 显示主页的过程 +## 在浏览器中输入url地址 ->> 显示主页的过程⭐ 总体来说分为以下几个过程: @@ -125,7 +186,7 @@ tip: 全双工(Full Duplex)是通讯传输的一个术语。通信允许数 5. 浏览器解析渲染页面 6. 连接结束,四次挥手 -## HTTP状态码 +## HTTP状态码⭐ 1xx :指示信息–表示请求已接收,继续处理 2xx :成功–表示请求已被成功接收、理解、接受 @@ -133,7 +194,7 @@ tip: 全双工(Full Duplex)是通讯传输的一个术语。通信允许数 4xx :**客户端错误**–请求有语法错误或请求无法实现 5xx :**服务器端错误**–服务器未能实现合法的请求 -## **get 和 post 请求有哪些区别?** +## **get 和 post 请求有哪些区别?** ⭐ - GET在浏览器回退时是无害的,而POST会再次提交请求。 - GET请求会被浏览器主动缓存,而POST不会,除非手动设置。 @@ -144,7 +205,7 @@ tip: 全双工(Full Duplex)是通讯传输的一个术语。通信允许数 - GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。 - GET参数通过URL传递,POST放在Request body中。 -## Cookie ,Session区别 +## Cookie ,Session区别⭐ Cookie数据存放在客户的浏览器上, Session数据放在服务器上 Session相对于Cookie更安全 diff --git "a/docs/\351\233\206\345\220\210\346\241\206\346\236\266\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271.md" "b/docs/\351\233\206\345\220\210\346\241\206\346\236\266\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271.md" index c809410..c0d599f 100644 --- "a/docs/\351\233\206\345\220\210\346\241\206\346\236\266\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271.md" +++ "b/docs/\351\233\206\345\220\210\346\241\206\346\236\266\351\235\242\350\257\225\347\237\245\350\257\206\347\202\271.md" @@ -85,12 +85,70 @@ 5.hashcode:计算键的hashcode作为存储键信息的数组下标用于查找键对象的存储位置。equals:HashMap 使用 equals() 判断当前的键是否与表中存在的键相同。 +## HashMap 的结构 + +在 JDK 1.7 中 HashMap 是以数组加链表的形式组成的,JDK 1.8 之后新增了红黑树的组成结构,当链表大于 8 并且容量大于 64 时,链表结构会转换成红黑树结构,添加红黑树是因为一旦链表过长,会严重影响 HashMap 的性能,而红黑树具有快速增删改查的特点,这样就可以有效的解决链表过长时操作比较慢的问题。 + ## HashMap 的长度为什么是2的幂次方 HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1), **hash%length==hash&(length-1)的前提是 length 是2的 n 次方;** +## 什么是 HashMap 的加载因子?加载因子为什么是 0.75? + +判断什么时候进行扩容的,假如加载因子是 0.5,HashMap 的初始化容量是 16,那么当 HashMap 中有 16*0.5=8 个元素时,HashMap 就会进行扩容。 + +这其实是出于容量和性能之间平衡的结果: + +* 当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低; +* 而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,多次扩容也会影响性能。 +* HashMap的容量有一个固定的要求就是一定是2的幂次方。所以,如果负载因子是3/4的话,那么和capacity的乘积结果就可以是一个整数。 + + +所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。 + +## put 方法流程 + +map.put("a","b")的整个流程: + + 1. 先判断散列表是否没有初始化或者为空,如果是就扩容 + 2. 根据键值 key 计算 hash 值,得到要插入的数组索引 + 3. 判断要插入的那个数组是否为空: + 1. 如果为空直接插入。 + 2. 如果不为空,判断 key 的值是否是重复(用 equals 方法): + 1. 如果是就直接覆盖 + 2. 如果不重复就再判断此节点是否已经是红黑树节点: + 1. 如果是红黑树节点就把新增节点放入树中 + 2. 如果不是,就开始遍历链表: + 1. 循环判断直到链表最底部,到达底部就插入节点,然后判断是否大于链表长度是否大于8: + 1. 如果大于8就转换为红黑树 + 2. 如果不大于8就继续下一步 + 2. 到底部之前发现有重复的值,就覆盖。 + 4. 判断是否需要扩容,如果需要就扩容。 + +## HashMap 的扩容机制 + +扩容时机:当`size`大于`threshold`的时候,并不一定会触发扩容机制,只要有一个新建的节点出现哈希冲突,则立刻`resize`。 + +- size记录的是map中包含的Entry的数量 +- 而threshold记录的是需要resize的阈值 且 `threshold = loadFactor * capacity` +- capacity 其实就是桶的长度 + +步骤: + +* 数组,阈值都扩大一倍 +* 如果旧数组不为空,开始遍历旧数组 +* 遍历到的数组上只有一个元素,就直接迁移 +* 如果是红黑树就使用 split 方法 +* 如果是链表就把链表拆成两个,按照高位运算的结果放到新数组中并且保留顺序 + +#### JDK 1.8 在扩容方面对 HashMap 做了哪些优化? + +1.7创建一个容量的新数组,重新计算每个元素在数组中的位置并且进行迁移。 + +1.8中在扩容HashMap的时候,不需要像1.7中去重新计算元素的hash,只需要看看原来的hash值新增的哪个二进制数是1还是0就好了,如果是0的话表示索引没有变,是1的话表示索引变成“oldCap+原索引”,这样即省去了重新计算hash值的时间,并且扩容后链表元素位置不会倒置。 + ## HashMap 1.7和1.8版本区别 * **数据结构:**1.7:数组+链表,1.8:数组+链表+红黑树 @@ -99,12 +157,6 @@ HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分 * **插入和扩容的判断:**1.7:先扩容后插入,1.8:先插入后扩容 * 为什么?1.8增加了判断是否为红黑树节点,先扩容的话不知道到底扩链表节点还是红黑树。 -## ConcurrentHashMap线程安全的实现方式/数据结构⭐ - -在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,在该类里面维护了一个 HashEntry[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。 - -详见java容器总结篇。 - ## HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标? `hashCode()`方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1), 而HashMap的容量范围是在16(初始化默认值)~2 ^ 30, HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过`hashCode()`计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; @@ -119,6 +171,14 @@ HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分 这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的; +## HashMap1.7为什么不安全?⭐ + +HashMap在rehash的时候,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他put操作,如果hash值相同,把值插入同一个链表,会因为头插法的特性造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。 + +#### 高并发下HashMap1.7的环是如何产生的 + +若当前线程一此时获得ertry节点,但是被线程中断无法继续执行,此时线程二进入transfer函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程一获得执行权继续执行,在tranfer方法中会把next指向自己造成闭环,然后在get时会出现死循环。 + ## 为什么HashMap中String、Integer这样的包装类适合作为Key?⭐ String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 @@ -130,35 +190,13 @@ String、Integer等包装类的特性能够保证Hash值的不可更改性和计 重写`hashCode()`和`equals()`方法 。 **重写`hashCode()`是因为需要计算存储数据的存储位置**, **重写`equals()`方法** **目的是为了保证key在哈希表中的唯一性**; -## HashMap 的扩容机制 - -扩容时机: **当`map`中包含的`Entry`的数量大于等于`threshold = loadFactor \* capacity`的时候,且新建的`Entry`刚好落在一个非空的桶上,此刻触发扩容机制,将其容量扩大为2倍。** - -当`size`大于等于`threshold`的时候,并不一定会触发扩容机制,但是会很可能就触发扩容机制,只要有一个新建的节点出现哈希冲突,则立刻`resize`。 - -- size记录的是map中包含的Entry的数量 -- 而threshold记录的是需要resize的阈值 且 `threshold = loadFactor * capacity` -- capacity 其实就是桶的长度 - -1.7创建一个容量的新数组,重新计算每个元素在数组中的位置并且进行迁移。 - -1.8中在扩容HashMap的时候,不需要像1.7中去重新计算元素的hash,只需要看看原来的hash值新增的哪个二进制数是1还是0就好了,如果是0的话表示索引没有变,是1的话表示索引变成“oldCap+原索引”,这样即省去了重新计算hash值的时间,并且扩容后链表元素位置不会倒置。 -步骤: -* 数组,阈值都扩大一倍 -* 如果旧数组不为空,开始遍历旧数组 -* 遍历到的数组上只有一个元素,就直接迁移 -* 如果是红黑树就使用 split 方法 -* 如果是链表就把链表拆成两个,放到新数组中并且保留顺序 - -## HashMap为什么不安全?⭐ - -HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。 +## ConcurrentHashMap线程安全的实现方式/数据结构⭐ -## 高并发下HashMap的环是如何产生的 +在JDK1.7版本中,ConcurrentHashMap维护了一个Segment数组,Segment这个类继承了重入锁ReentrantLock,在该类里面维护了一个 HashEntry[] table数组,在写操作put,remove,扩容的时候,会对Segment加锁,所以仅仅影响这个Segment,不同的Segment还是可以并发的,所以解决了线程的安全问题,同时又采用了分段锁也提升了并发的效率。在JDK1.8版本中,ConcurrentHashMap摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。 -若当前线程此时获得ertry节点,但是被线程中断无法继续执行,此时线程二进入transfer函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程一获得执行权继续执行,因为并发transfer,所以两者都是扩容的同一个链表,当线程一执行到e.next = new table[i] 的时候,由于线程二之前数据迁移的原因导致此时new table[i] 上就有ertry存在,所以线程一执行的时候,会将next节点,设置为自己,导致自己互相使用next引用对方,因此产生链表,导致死循环。 +详见java容器总结篇。 ## BlockingQueue是什么? diff --git "a/docs/\351\241\271\347\233\256\345\270\270\347\224\250\345\267\245\345\205\267.md" "b/docs/\351\241\271\347\233\256\345\270\270\347\224\250\345\267\245\345\205\267.md" new file mode 100644 index 0000000..e65cfa0 --- /dev/null +++ "b/docs/\351\241\271\347\233\256\345\270\270\347\224\250\345\267\245\345\205\267.md" @@ -0,0 +1,113 @@ +## 整体介绍 + +**团队协作** + +- Ant:较少使用 +- Maven +- Gradle +- Git +- SVN:较少使用 + +**质量保证** + +- Checkstyle +- FindBugs:代码检测工具 +- SonarQube:平台,集成了上面两种工具 + +**压测** + +- JMeter +- JMH +- AB +- LoadRunner + +**容器与代理(随着微服务的盛行,Envoy、OpenResty、Kong等API网关的使用也越来越普遍)** + +- Tomcat +- Jetty +- Nginx +- Envoy +- OpenResty +- Kong + +**CI/CD** + +- Gitlab-CI +- Jenkins +- Travis + +**JVM相关** + +- JMC(JFR) +- jstack、jmap、jstat + +**系统分析** + +- vmstat +- iostat & iotop +- ifstat & iftop +- netstat +- dstat +- strace +- GDB +- lsof +- tcpdump +- traceroute + +**文档管理** + +- JavaDoc +- Swagger + +**网络工具** + +- PostMan +- WireShark(网络包分析工具) +- Fiddler(只针对HTTP进行抓捕) +- Charies + +## JVM相关工具 + +### JMC(Java Mission Control) + +1.7之后提供的 JVM 图形化监控工具,包括 JVM 浏览器,飞行控制器 JFR(Java Filght Recorder),JMX(Java Management Extensions)控制台。 + +jps:查看java进程信息 +jmap:查看JVM中对象的统计信息,**可以在内存溢出时查看堆中最大的对象类型** +jstat:对JVM的资源和性能进行实时监控 +jstack:查看JVM线程栈信息,**可以检查线程死锁问题** +jinfo:动态查看、调整jvm参数 +jcmd:1.7提供的综合工具,使用飞行计数器分析性能。 + + + +## Git + +## ![git命令](C:\Users\吕明辉\Desktop\github笔记\git命令.png) + +分布式管理,有四个保存数据区域,如图中的工作区,暂存区,本地仓库,远程仓库。 + +开发时线程远程仓库拉取代码到工作区,可以使用clone,pull,fetch,checkout。pull=fetch+merge + +提交代码时先使用add,再commit,再push。 + +### Git 工作流 + +![git工作流](C:\Users\吕明辉\Desktop\github笔记\git工作流.png) + +github 工作流:master 分支的代码一直是可发布状态,需要新开发就直接开一个feature分支,完成后pullrequest(PR),当Review通过后,合并到mater分支。 + + + +## Linux 分析工具 + +* **vmstat**:进程、虚拟内存、页面交换、IO读写、CPU活动等 +* iostat & iotop:系统IO状态信息 +* ifstat & iftop:实时网络流量监控 +* **netstat**:查看网络相关信息,各种网络协议套接字状态 +* dstat:全能型实时系统信息统计 +* strace:诊断、调试程序的系统调用 +* GDB:程序调试、coredump分析 +* Isof:查看系统当前打开的文件信息 +* tcpdump:网络抓包工具 +* traceroute:网络路由分析工具 diff --git "a/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" "b/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" new file mode 100644 index 0000000..7909d36 --- /dev/null +++ "b/docs/\351\253\230\346\200\247\350\203\275\345\256\236\350\267\265\347\257\207.md" @@ -0,0 +1,470 @@ +# 高性能库表设计 + +### 范式和反范式 + +#### 范式 + +范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则和指导方法。数据库的设计范式是数据库设计所需要满足的规范。只有理解数据库的设计范式,才能设计出高效率、优雅的数据库,否则可能会设计出低效的库表结构。目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)、第四范式(4NF)和第五范式(5NF,还又称完美范式。 + +满足最低要求的叫第一范式,简称1NF。在第一范式基础上进一步满足一些要求的为第二范式,简称2NF。其余依此类推。各种范式呈递次规范,越高的范式数据库冗余越小。通常所用到的只是前三个范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)。 + +**第一范式** + +第一范式无重复的列,表中的每一列都是拆分的基本数据项,即列不能够再拆分成其他几列,强调的是列的原子性。 + +如果在实际场景中,一个联系人有家庭电话和公司电话,那么以“姓名、性别、电话”为表头的表结构就没有达到1NF。要符合1NF我们只需把电话列拆分,让表头变为姓名、性别、家庭电话、公司电话即可。 + +**第二范式** + +属性完全依赖于主键,首先要满足它符合1NF,另外还需要包含两部分内容: + +* 表必须有一个主键; +* 没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。即要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。 + +**第三范式** + +第三范式属性不传递依赖于其他非主属性,首先需要满足 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。 + +**第二范式和第三范式的区别第二范式:** + +* 非主键列是否依赖主键(包括一列通过某一列间接依赖主键),要是有依赖关系就是第二范式; +* 第三范式:非主键列是否直接依赖主键,不能是那种通过传递关系的依赖。要是符合这种依赖关系就是第三范式。通过对前三个范式的了解,我们知道 3NF 是 2NF 的子集,2NF 是 1NF 的子集。 + +**范式优缺点** + +优点: + +* 避免数据冗余,减少维护数据完整性的麻烦; +* 减少数据库的空间; +* 数据变更速度快。 + +缺点: + +* 按照范式的规范设计的表,等级越高的范式设计出来的表数量越多。 +* 获取数据时,表关联过多,性能较差。 + +表的数量越多,查询所需要的时间越多。也就是说所用的范式越高,对数据操作的性能越低。 + +#### 反范式 + +范式是普适的规则,满足大多数的业务场景的需求。对于一些特殊的业务场景,范式设计的表,无法满足性能的需求。此时,就需要根据业务场景,在范式的基础之上进行灵活设计,也就是反范式设计。 + +反范式设计主要从三方面考虑: + +**业务场景,相应时间,字段冗余。** + +反范式设计就是用空间来换取时间,提高业务场景的响应时间,减少多表关联。主要的优点如下: + +允许适当的数据冗余,业务场景中需要的数据几乎都可以在一张表上显示,避免关联; + +可以设计有效的索引。 + +#### 范式与反范式异同 + +**范式化模型**:数据没有冗余,更新容易;当表的数量比较多,查询数据需要多表关联时,会导致查询性能低下。 + +**反范式化模型**:冗余将带来很好的读取性能,因为不需要join很多很多表;虽然需要维护冗余数据,但是对磁盘空间的消耗是可以接受的。 + +## MySQL 使用原则和设计规范 + +### 概述 + +MySQL虽然具有很多特性并提供了很多功能,但是有些特性会严重影响它的性能,比如,在数据库里进行计算,写大事务、大SQL、存储大字段等。想要发挥MySQL的最佳性能,需要遵循3个基本使用原则。 + +* 首先是需要让MySQL回归存储的基本职能:MySQL数据库只用于数据的存储,不进行数据的复杂计算,不承载业务逻辑,确保存储和计算分离; +* 其次是查询数据时,尽量单表查询,减少跨库查询和多表关联; +* 还有就是要杜绝大事务、大 SQL、大批量、大字段等一系列性能杀手。 + +补充: + +大事务,运行步骤较多,涉及的表和字段较多,容易造成资源的争抢,甚至形成死锁。一旦事务回滚,会导致资源占用时间过长。 + +大SQL,复杂的SQL意味着过多的表的关联,MySQL数据库处理关联超过3张表以上的SQL时,占用资源多,性能低下。 + +大批量,意味着多条SQL一次性执行完成,必须确保进行充分的测试,并且在业务低峰时段或者非业务时段执行。大字段,blob、text等大字段,尽量少用。必须要用时,尽量与主业务表分离,减少对这类字段的检索和更新。 + +下面具体讲解数据库的基本设置规则: + +1. 必须指定默认存储引擎为InnoDB,并且禁用MyISAM存储引擎,随着MySQL8.0版本的发布,所有的数据字典表都已经转换成了InnoDB,MyISAM存储引擎已成为了历史。 +2. 默认字符集UTF8mb4,以前版本的UTF8是UTF8mb3,未包含个别特殊字符,新版本的UTF8mb4包含所有字符,官方强烈建议使用此字符集。 +3. 关闭区分大小写功能。设置lower_case_tables_name=1,即可关闭区分大小写功能,即大写字母 T 和小写字母 t 一样。 + +MySQL数据库提供的功能很全面,但并不是所有的功能性能都高效。 + +1. 存储过程、触发器、视图、event。为了存储计算分离,这类功能尽量在程序中实现。这些功能非常不完整,调试、排错、监控都非常困难,相关数据字典也不完善,存在潜在的风险。一般在生产数据库中,禁止使用。 +2. lob、text、enum、set。这些字段类型,在MySQL数据库的检索性能不高,很难使用索引进行优化。如果必须使用这些功能,一般采取特殊的结构设计,或者与程序结合使用其他的字段类型替代。比如:set 可以使用整型(0,1,2,3)、注释功能和程序的检查功能集合替代。 + +### 规范命名 + +统一的规范命名,可以增加可读性,减少隐式转换。 + +命名时的字符取值范围为:a~z,0~9 和 _(下画线)。 + +1. 所有表名小写,不允许驼峰式命名; + +2. 允许使用 -(横线)和 (空格);如下图所示,当使用 -(横线),后台默认会转化成 @002d; + +3. 不允许使用其他特殊字符作为名称,减少潜在风险。 + +数据库库名的命名规则必须遵循“见名知意”的原则,即库名规则为“数据库类型代码 + 项目简称 + 识别代码 + 序号”。 + +**表名的命名规则分为:** + +单表仅使用 a~z、_; + +分表名称为“表名_编号”; + +业务表名代表用途、内容:子系统简称\_业务含义_后缀。 + +常见业务表类型有:临时表,tmp;备份表,bak;字典表,dic;日志表,log。 + +**字段名精确**,遵循“见名知意”的原则,格式:名称_后缀。 + +* 避免普遍简单、有歧义的名称。用户表中,用户名的字段为 UserName 比 Name 更好。 +* 布尔型的字段,以助动词(has/is)开头。用户是否有留言 hasmessage,用户是否通过检查 ischecked 等。 + +**索引命名格式**,主要为了区分哪些对象是索引: + +前缀\_表名(或缩写)_字段名(或缩写);主键必须使用前缀“pk\_”;UNIQUE约束必须使用前缀“uk\_”;普通索引必须使用前缀“idx\_”。 + +### 表创建的注意事项 + +1. 主键列,UNSIGNED整数,使用auto_increment;禁止手动更新auto_increment,可以删除。 +2. 必须添加comment注释。 +3. 必须显示指定的engine。 +4. 表必备三字段:id、xxx_create、xxx_modified。 + * id为主键,类型为unsignedbigint等数字类型; + * xxx_create、xxx_modified的类型均为 datetime 类型,分别记录该条数据的创建时间、修改时间。 + +#### 备份表/临时表等常见表的设计规范 + +1. 备份表,表名必须添加bak和日期,主要用于系统版本上线时,存储原始数据,上线完成后,必须及时删除。 +2. 临时表,用于存储中间业务数据,定期优化,及时降低表碎片。 +3. 日志类表,首先考虑不入库,保存成文件,其次如果入库,明确其生命周期,保留业务需求的数据,定期清理。 +4. 大字段表,把主键字段和大字段,单独拆分成表,并且保持与主表主键同步,尽量减少大字段的检索和更新。 +5. 大表,根据业务需求,从垂直和水平两个维度进行拆分。 + +### 字段设计要求 + +1. 根据业务场景需求,选择合适的类型,最短的长度;确保字段的宽度足够用,但也不要过宽。所有字段必须为NOTNULL,空值则指定default值,空值难以优化,查询效率低。比如:人的年龄用unsignedtinyint(范围0~255,人的寿命不会超过255岁);海龟就必须是smallint,但如果是太阳的年龄,就必须是int;如果是所有恒星的年龄都加起来,那么就必须使用bigint。 +2. 表字段数少而精,尽量不加冗余列。 +3. 单实例表个数必须控制在 2000 个以内。 +4. 单表分表个数必须控制在 1024 个以内。 +5. 单表字段数上限控制在 20~50 个。 + +**禁用ENUM、SET类型。**兼容性不好,性能差。 + +解决方案:使用TINYINT,在COMMENT信息中标明被枚举的含义。\`is_disable` TINYINT UNSIGNED DEFAULT'0'COMMENT '0:启用1:禁用2:异常’。 + +**禁用列为NULL。**MySQL难以优化NULL列;NULL列加索引,需要额外空间;含NULL复合索引无效。 + +解决方案:在列上添加 NOT NULL DEFAULT 缺省值。 + +**禁止 VARBINARY、BLOB 存储图片、文件等。**禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统中,数据库中存储路径。 + +不建议使用TEXT/BLOB:处理性能差;行长度变长;全表扫描代价大。 + +解决方案:拆分成单独的表。 + +存储字节越小,占用空间越小。尽量选择合适的整型,如下图所示。 + + +建议: + +* 主键列,无负数,建议使用INTUNSIGNED或者BIGINTUNSIGNED;预估字段数字取值会超过42亿,使用BIGINT类型。 +* 短数据使用TINYINT或SMALLINT,比如:人类年龄,城市代码。 +* 使用 UNSIGNED 存储非负数值,扩大正数的范围。 + +推荐字符集都为 UTF8mb4,中文存储占三个字节,而数据或字母,则只占一个字节。 + +CHAR(N) 和 VARCHAR(N) 的长度 N,不是字节数,是**字符数**。 + +例:username VARCHAR(40):username 最多能存储 40 个字符,占用 120 个字节。 + +**Char 与 Varchar 类型** + +存储字符串长度相同的全部使用 Char 类型;字符长度不相同的使用 Varchar 类型,不预先分配存储空间,长度不要超过 255。 + +## 案例处理 + +### IP处理 + +一般使用Char(15)进行存储,但是当进行查找和统计时,字符类型不是很高效。MySQL数据库内置了两个IP相关的函数INET_ATON()、INET_NTOA(),可以以实现 IP 地址和整数的项目转换。 + +因此,我们使用 INT UNSIGNED(占用 4 个字节)存储 IP。 + +将 IP 的存储从字符型转换成整形,转化后数字是连续的,提高了查询性能,使查询更快,占用空间更小。 + +### TIMESTAMP处理 + +同样的方法,我们使用MySQL内置的函数(FROM_UNIXTIME(),UNIX_TIMESTAMP()),可以将日期转化为数字,用INTUNSIGNED存储日期和时间。 + +# 高性能索引设计 + +## 索引概述 + +数据库索引是一种数据结构,它以额外的写入和存储空间为代价来提高数据库表上数据检索操作的速度。通俗来说,索引类似于书的目录,根据其中记录的页码可以快速找到所需的内容。 + +MySQL官方对索引(Index)的定义是存储引擎用于快速查找记录的一种数据结构。 + +* 索引是物理数据页,数据库页大小(PageSize)决定了一个页可以存储多少个索引行,以及需要多少页来存储指定大小的索引。 +* 索引可以加快检索速度,但同时也降低索引列插入、删除、更新的速度,索引维护需要代价。 + +## 索引原理 + +二分查找是索引实现的理论基础。在数据库中大部分索引都是通过 B+Tree 来实现的。当然也涉及其他数据结构,在 MySQL 中除了 B+Tree 索引外我们还需要关注下 Hash 索引。 + +### Hash 索引 + +哈希表是数据库中哈希索引的基础,是根据键值存储数据的结构。简单说,哈希表是使用哈希函数将索引列计算到桶或槽的数组,实际存储是根据哈希函数将key换算成确定的存储位置,并将 value 存放到该数组位置上。访问时,只需要输入待查找的 key,即可通过哈希函数计算得出确定的存储位置并读取数据。 + +#### Hash 索引的实现 + +数据库中哈希索引是基于哈希表实现的,对于哈希索引列的数据通过Hash算法计算,得到对应索引列的哈希码形成哈希表,由哈希码及哈希码指向的真实数据行的指针组成了哈希索引。哈希索引的应用场景是只在对哈希索引列的等值查询才有效。 + +因为哈希索引只存储哈希值和行指针,不存储实际字段值,所以其结构紧凑,查询速度也非常快,在无哈希冲突的场景下访问哈希索引一次即可命中。但是哈希索引只适用于等值查询,包括=、IN()、<=>(安全等于,selectnull<=>null和selectnull=null是不一样的结果),不支持范围查询。 + +#### Hash碰撞的处理 + +Hash碰撞是指不同索引列值计算出相同的哈希码,表中字段为A和B两个不同值根据Hash算法计算出来的哈希码都一样就是出现了哈希碰撞。对于 Hash 碰撞通用的处理方法是使用链表,将 Hash 冲突碰撞的元素形成一个链表,发生冲突时在链表上进行二次遍历找到数据。 + +#### MySQL 中如何使用 Hash 索引? + +在 MySQL 中主要是分为 Memory 存储引擎原生支持的 Hash 索引 、InnoDB 自适应哈希索引及 NDB 集群的哈希索引3类。 + +InnoDB自适应哈希索引是为了提升查询效率,InnoDB存储引擎会监控表上各个索引页的查询,当InnoDB注意到某些索引值访问非常频繁时,会在内存中基于B+Tree索引再创建一个哈希索引,使得内存中的 B+Tree 索引具备哈希索引的功能,即能够快速定值访问频繁访问的索引页。 + +为什么要为B+Tree索引页二次创建自适应哈希索引呢?这是因为B+Tree索引的查询效率取决于B+Tree的高度,在数据库系统中通常B+Tree的高度为3~4层,所以访问数据需要做 3~4 次的查询。而 Hash 索引访问通常一次查找就能定位数据(无 Hash 碰撞的情况),其等值查询场景 Hash 索引的查询效率要优于 B+Tree。 + +自适应哈希索引的建立使得InnoDB存储引擎能自动根据索引页访问的频率和模式自动地为某些热点页建立哈希索引来加速访问。另外InnoDB自适应哈希索引的功能,用户只能选择开启或关闭功能,无法进行人工干涉。功能开启后可以通过 Show Engine Innodb Status 看到当前自适应哈希索引的使用情况。 + +### B+Tree 索引 + +如下图所示为一个简单的、标准的 B+tree,每个节点有 K 个键值和 K+1 个指针。 + + +B+Tree索引能够快速访问数据,就是因为存储引擎可以不再需要通过全表扫描来获取数据,而是从索引的根结点(通常在内存中)开始进行二分查找,根节点的槽中都存放了指向子节点的指针,存储引擎根据这些指针能快速遍历数据。 + +叶子节点存放的 ,对于真正要存放哪些数据还得取决于该 B+Tree 是聚簇索引(Clustered Index)还是辅助索引(Secondary Index)。 + +#### 聚簇索引和辅助索引 + +聚簇索引是一种数据存储方式,它表示表中的数据按照主键顺序存储,是索引组织表。InnoDB的聚簇索引就是按照主键顺序构建B+Tree,B+Tree的叶子节点就是行记录,数据行和主键值紧凑地存储在一起。这也意味着InnoDB的主键索引就是数据表本身,它按主键顺序存放了整张表的数据。而InnoDB辅助索引(也叫作二级索引)只是根据索引列构建B+Tree,但在B+Tree的每一行都存了主键信息,加速回表操作。 + +聚簇索引占用的空间就是整个表数据量的大小,而二级索引会比聚簇索引小很多, 通常创建辅助索引就是为了提升查询效率。 + +在MySQLInnoDB中索引通常可以分为两大类:主键索引(即聚簇索引)和辅助索引(非聚簇索引)。对于没有指定主键的表,InnoDB会自己选择合适字段为主键,其选择顺序如下: + +* 显式主键; +* 第一个唯一索引(要求唯一索引所有列都非 NULL); +* 内置的 6 字节 ROWID。 + +建议使⽤ UNSIGNED 自增列显示创建主键。 + +#### 联合索引和覆盖索引 + +根据索引列个数和功能描述不同索引也可以分为:联合索引和覆盖索引。 + +* 联合索引是指在多个字段联合组建索引的。 +* 当通过索引即可查询到所有记录,不需要回表到聚簇索引时,这类索引也叫作覆盖索引。 +* 主键查询是天然的覆盖索引,联合索引可以是覆盖索引。 + +那么如何看SQL语句是否使用到覆盖索引了呢?通常在查看执行计划时,Extra列为Usingindex则表示优化器使用了覆盖索引。 + +我们通常建议优先考虑使用覆盖索引,这是因为如果SQL需要查询辅助索引中不包含的数据列时,就需要先通过辅助索引查找到主键值,然后再回表通过主键查询到其他数据列(即回表查询),需要查询两次。而覆盖索引能从索引中直接获取查询需要的所有数据,从⽽避免回表进行二次查找,节省IO,效率较⾼。 + +## 索引使用技巧 + +接下来聊一聊索引使用技巧的基础知识,这些知识可以帮助你建立高效索引,主要有谓词、过滤因子、基数(Cardinality)、选择率和回表。 + +#### 谓词和过滤因子 + +谓词本身就是条件表达式,通俗讲就是过滤字段。如下图中这句SQL语句,可以拆解为下面所示: + +`select * from city where city ='BeiJing' and last_updata = '2019-09-01'` + +简单谓词:city和last_updata。组合谓词:cityandlast_updata。 + +知道谓词后就可以计算谓词的过滤因子了,过滤因子直接描述了谓词的选择性,表示满足谓词条件的记录行数所占比例,过滤因子越小意味着能过滤越多数据,你需要在这类谓词字段上创建索引。 + +过滤因子的计算算法,就是满足谓词条件的记录行数除以表总行数。 + +* 简单谓词的过滤因子 = 谓词结果集的数量 / 表总行数 +* 组合谓词的过滤因子 = 谓词 1 的过滤因子 × 谓词 2 的过滤因子 + +下面用一个例子来看下,如何快速根据SQL语句计算谓词、过滤因子。 + +1. 根据SQL语句可以快速得到谓词信息:简单谓词 city 和 last_update,组合谓词 city and last_update。 +2. 计算每个谓词信息的过滤因子,过滤因子越小表示选择性越强,字段越适合创建索引。例如: + * city 的过滤因子=谓词 city 结果集的数量/表总行数=select count(\*) from city where city = ‘BeiJing’ / select countt(\*) from city = 20%; + * \*last_update 的过滤因子 = 谓词 last_update 结果集的数量 / 表总行数 = select count(\*) from city where last_update = ‘2019-08-01’ / select count(\*) from city = 10%; + * 组合谓词 = city 过滤因子 * last_update 过滤因子 = 20% × 10% = 2%,组合谓词的过滤因子为 2%,即只有表总行数的 2% 匹配过滤条件,可以考虑创建组合索引 (city,last_update)。 + +#### 基数和选择率 + +基数(Cardinality )是某个键值去重后的行数,索引列不重复记录数量的预估值,MySQL优化器会依赖于它。选择率是count (distinct city) / count(*),选择率越接近1则越适合创建索引,例如主键和唯一键的选择率都是 1。回表是指无法通过索引扫描访问所有数据,需要回到主表进行数据扫描并返回。 + +Cardinality 能快速告知字段的选择性,高选择性字段有利于创建索引。优化器在选择执行计划时会依赖该信息,通常这类信息也叫作统计信息,数据库中对于统计信息的采集是在存储引擎层进行的。 + +执行 show index from table_name会看到 Cardinality,同时也会触发 MySQL 数据库对 Cardinaltiy 值的统计。除此之外,还有三种更新策略。 + +* 触发统计:Cardinality 统计信息更新发生在 INSERT 和 UPDATE 时,InnoDB 存储引擎内部更新的 Cardinality 信息的策略为:1.表中超过1/16的数据发生变化;2.stat_modified_counter > 2000 000 000 (20亿)。 +* 采样统计(sample):为了减少统计信息更新造成的资源消耗,数据库对 Cardinality 通过采样来完成统计信息更新,每次随机获取 innodb_stats_persistent_sample_pages 页的数量进行 Cardinality 统计。 +* 手动统计:alter table table_name engine=innodb或 analyze table table_name,当发现优化器选择错误的执行计划或没有走理想的索引时,执行 SQL 语句来手动统计信息有时是一种有效的方法。 + +由于采样统计的信息是随机获取8个(8是由innodb_stats_transient_sample_pages参数指定)页面数进行分析,这就意味着下一次随机的8个页面可能是其他页面,其采集页面的 Carinality 也不同。因此当表数据无变化时也会出现 Cardinality 发生变化的情况。 + +## 索引使用细节 + +创建索引后如何确认SQL语句是否走索引了呢?创建索引后通过查看执行SQL语句的执行计划即可知道SQL语句是否走索引。执行计划重点关注跟索引相关的关键项,有type、possible_keys、key、key_len、ref、Extra 等。 + +其中,**possible_keys** 表示查询可能使用的索引,**key**表示真正实际使用的索引,**key_len** 表示使用索引字段的长度。另外执行计划中Extra选项也值得关注,例如**Extra**显示use index时就表示该索引是覆盖索引,通常性能排序的结果是use index>use where>use filsort + +当索引选择组合索引时,通过计算key_len来了解有效索引长度对索引优化也是非常重要的,接下来重点讲解key_len计算规则。key_len表示得到结果集所使用的选择索引的长度[字节数],不包括orderby,也就是说如果orderby也使用了索引则key_len不计算在内。key_len计算规则从两个方面考虑,一方面是索引字段的数据类型,另一方面是表、字段所使用的字符集。 + +索引字段的数据类型,根据索引字段的定义可以分为变长和定长两种数据类型: + +1. 索引字段为定长数据类型,比如char、int、datetime,需要有是否为空的标记,这个标记需要占用1个字节; +2. 对于变长数据类型,比如Varchar,除了是否为空的标记外,还需要有长度信息,需要占用两个字节。 + +表所使用的字符集,不同的字符集计算的 key_len 不一样,例如,GBK 编码的是一个占用 2 个字节大小的字符,UTF8 编码的是一个占用 3 个字节大小的字符。 + +通过key_len计算也帮助我们了解索引的**最左前缀匹配原则**。最左前缀匹配原则是指在使用B+Tree联合索引进行数据检索时,MySQL优化器会读取谓词(过滤条件)并按照联合索引字段创建顺序一直向右匹配直到遇到范围查询或非等值查询后停止匹配,此字段之后的索引列不会被使用,这时计算 key_len 可以分析出联合索引实际使用了哪些索引列。 + +## 设计高性能索引 + +1. **定位由于索引不合适或缺少索引而导致的慢查询。**通常在业务建库建表时就需要提交业务运行相关的SQL给DBA审核,也可以借助ArkcontrolArkit来自动化审核。比如,慢查询日志分析,抓出运行慢的SQL进行分析,也可以借助第三方工具例如Arkcontrol慢查询分析系统进行慢查询采集和分析。在分析慢查询时进行参数最差输入,同时,对SQL语句的谓词进行过滤因子、基数、选择率和SQL查询回表情况的分析。 +2. **设计索引**的目标是让查询语句运行得足够快,同时让表、索引维护也足够快,例如,使用业务不相关自增字段为主键,减缓页分裂、页合并等索引维护成本,加速性能。也可以使用第三方工具进行索引设计,例如Arkcontrol SQL 优化助手,会给出设计索引的建议。 +3. **创建索引策略:**优先为搜索列、排序列、分组列创建索引,必要时加入查询列创建覆盖索引;计算字段列基数和选择率,选择率越接近于1越适合创建索引;索引选用较小的数据类型(整型优于字符型),字符串可以考虑前缀索引;不要建立过多索引,优先基于现有索引调整顺序;参与比较的字段类型保持匹配并创建索引。 +4. **调优索引:**分析执行计划;更新统计信息(AnalyzeTable);Hint优化,方便调优(FORCEINDEX、USEINDEX、IGNOREINDEX、STRAIGHT_JOIN);检查连接字段数据类型、字符集;避免使用类型转换;关注 optimizer_switch,重点关注索引优化特性 MRR(Multi-Range Read)和 ICP(Index Condition Pushdown)。 + * MRR优化是为了减少磁盘随机访问,将随机IO转化为顺序IO的数据访问,其方式是将查询得到辅助索引的键值放到内存中进行排序,通常是按照主键或RowID进行排序,当需要回表时直接根据主键或 RowID 排序顺序访问实际的数据文件,加速 SQL 查询。 + * ICP优化同样也是对索引查询的优化特性,MySQL根据索引查询到数据后会优先应用where条件进行数据过滤,即无法使用索引过滤的where子句,其过滤由之前Server层的数据过滤下推到了存储引擎层,可以减少上层对记录的检索,提高数据库的整体性能。 + +## 创建索引规范 + +* 命名规范,各个公司内部统一。 +* 考虑到索引维护的成本,单张表的索引数量不超过5个,单个索引中的字段数不超过5个。 +* 表必需有主键,推荐使⽤UNSIGNED自增列作为主键。表不设置主键时InnoDB会默认设置隐藏的主键列,不便于表定位数据同时也会增大MySQL运维成本(例如主从复制效率严重受损、pt工具无法使用或正确使用)。 +* 唯一键由3个以下字段组成,并且在字段都是整形时,可使用唯一键作为主键。其他情况下,建议使用自增列或发号器作主键。 +* 禁止冗余索引、禁止重复索引,索引维护需要成本,新增索引时优先考虑基于现有索引进行rebuild,例如(a,b,c)和(a,b),后者为冗余索引可以考虑删除。重复索引也是如此,例如索引(a)和索引(a,主键ID) 两者重复,增加运维成本并占用磁盘空间,按需删除冗余索引。 +* 联表查询时,JOIN 列的数据类型必须相同,并且要建⽴索引。 +* 不在低基数列上建⽴索引,例如“性别”。 在低基数列上创建的索引查询相比全表扫描不一定有性能优势,特别是当存在回表成本时。 +* 选择区分度(选择率)大的列建立索引。组合索引中,区分度(选择率)大的字段放在最前面。 +* 对过长的Varchar段建立索引。建议优先考虑前缀索引,或添加CRC32或MD5伪列并建⽴索引。 +* 合理创建联合索引,(a,b,c) 相当于 (a) 、(a,b) 、(a,b,c)。 +* 合理使用覆盖索引减少IO,避免排序。 + +# 查询优化 + +MySQL采用基于开销的优化器,以确定处理查询的最佳方式,也就是说执行查询之前,都会先选择一条自以为最优的方案。在很多情况下,MySQL能够计算最佳的可能查询计划,但在某些情况下,MySQL没有关于数据的足够信息,或者是提供太多的相关数据信息,它所采用的可能并非就是事实上的最优方案。这里举了两个例子来说明。 + +### 案例一 + +**Range Query Optimizer的流程** + +1. 根据查询条件计算所有的 possible keys。 +2. 计算全表扫描代价(cost_all)。 +3. 计算最小的索引范围访问代价(这一步很关键,直接决定了 Range 的查询效率),它有三步: + 1. 对于每一个possible keys(可选索引),调用records_in_ranges函数计算范围中的rows; + 2. 根据rows,计算二级索引访问代价; + 3. 获取cost最小的二级索引访问(cost_range)。 +4. 选择执行最小化访问代价的执行计划。如果 cost_all <= cost_range,则全表扫描,否则索引范围扫描。 + +Range使用了records_in_range函数估算每个值范围的rows,结果依赖于possible_keys;possible_keys越多,随机IO代价越大,Range查询效率。所以,索引不是越多越好,相反,我们应该尽量减少possible_keys,减少records_in_range从而减少IO的消耗。这里给大家推荐两个工具,用pt-index-usage工具来删除冗余索引,用 pt-duplicate-key-checker 工具来删除重复索引。 + +### 案例二 + +优化前有一个索引idx_global_id。图中的这条SQL语句的where条件包括一个sub_id的等值查询和一个global_id的范围查询。执行一次需要2.37秒。从下一页的执行计划中,我们可以看到虽然查询优化器使用了唯一索引uniq_subid_globalid,但是由于idx_global_id的干扰,实际只使用了前面的4个长度就access,剩余8个长度都被filter了。 + +从优化后的执行计划中可以看到,使用了forceindex来强制使用唯一索引。正如上文列举的,相似的命令还有ignoreindex忽略索引,straght_join强制优化器按特定的顺序使强制优化器按特定的顺序使用数据表,high_priority 或 low_priority 两个命令来控制 SQL 的执行优先权。 + +### ICP,MRR,BKA + +1.ICPICP是IndexConditionPushdown的简称,是MySQL使用索引从表中检索行数据的一种优化方式。目的是减少从基表中全记录读取操作的数量,从而降低IO操作。 + +在没有开启ICP之前,存储引擎会通过遍历索引查找基表中的行,然后返回给MySQLServer层,再去为这些数据行进行where后的条件过滤。开启ICP之后,如果部分where条件能使用索引中的字段,MySQLServer会把这部分下推到存储引擎层,存储引擎通过索引过滤,把满足的行从表中读取出来。ICP能减少引擎层访问基表的次数和MySQLServer访问存储引擎的次数。对于 InnoDB 表来说,ICP 只适用于辅助索引. + +2.MRR + +MRR 是 Multi-Range Read 的简称,是 MySQL 优化器将随机 IO转化为顺序IO以降低查询过程中IO开销的一种手段。MRR的适用场景是辅助索引,如INDEX(key1),查询key1在n到m范围内的数据。使用限制就是MRR,MR适用于range、ref、eq_ref的查询。 + +3.BKA和BNLBKA是BatchedKeyAccess的简称,是MySQL优化器提高表join性能的一种手段,它是一种算法。而BNL是BlockNestedLoop的简称,它是默认的处理表join的方式和算法。那么二者有什么区别呢? + +•BNL比BKA出现的早,BKA直到MySQL 5.6 版本才出现,而 BNL 至少在 MySQL 5.1 版本中就存在了; + +• BNL 主要用于被 join 的表上无索引时; + +• BKA 只在被 join 表上有索引时可以使用,那么就在行提交给被 join 的表之前,对这些行按照索引字段进行排序,因此减少了随机 IO,排序才是两者最大的区别,但如果被 join 的表没有索引呢?那么就只能使用 BNL 了。 + +使用BKA需要注意一些问题,比如:BKA的适用场景支持inner join、outer join、semi-join operations、including nested outer joins等; + +BKA 有两个使用限制,一个是使用 BKA 特性,必须启用 MRR 特性;二是 BKA 主要适用于 join 的表上有索引可使用的情况,否则只能使用 BNL。 + +## MySQL执行计划分析三部曲 + +当有慢查询或者执行 SQL 遇到瓶颈时,我们分析这类问题时可以参考 MySQL 执行计划分析“三步曲”。 + +1. 查看 SQL 执行计划: + * explain SQL; + * desc 表名; + * show create table 表名。 +2. 通过 Profile 定位 QUERY 代价消耗: + * setprofiling=1; + * 执行SQL; + * show profiles;获取Query_ID。 + * show profile for query Query_ID;查看详细的profile信息 +3. 通过Optimizer Trace表查看SQL执行计划树: + * set session optimizer_trace = 'enabled = on'; + * 执行SQL; + * 查询information_schema.optimizer_trace 表,获取 SQL 查询计划树; + * set session optimizer_trace=‘enabled=off';开启此项影响性能,记得用后关闭。 + +## 查询相关参数和分析工具 + +MySQL 可以通过设置一些参数,将运行时间长或者非索引查找的 SQL 记录到慢查询文件中。可以分析慢查询文件中的 SQL,有针对性的进行优化。 + +* 参数slow_query_log,表示是否开启慢查询日志,ON或者1表示开启,OFF或者0表示关闭。 +* 参数long_query_time,设置慢查询的阈值,MySQL5.7版本支持微秒级。 +* 参数slow_query_log_file,慢查询文件的存放路径。 +* 参数log_queries_not_using_indexes,表示是否将非索引查找的SQL也记录到慢查询文件中未使用索引的 SQL 语句上限,0 表示没限制。 +* 参数 log_throttle_queries_not_using_indexes,表示每分钟记录到慢查询文件中未使用索引的 SQL 语句上限,0 表示没限制。 +* 参数 max_execution_time,用来控制 SELECT 语句的最大执行时间,单位毫秒,超过此值MySQL 自动 kill 掉该查询。 + + + + +如上图所示是一个慢查询的例子,通过这个例子你可以看到慢查询文件中记录了哪些信息。包括了慢SQL产生的时间,SQL源自的IP和对应的数据库用户名,以及访问的数据库名称;查询的总耗时,被lock 的时间,结果集行数,扫描的行数,以及字节数等。当然还有具体的 SQL 语句。 + +分析慢查询常用的工具有: + +explain; + +Mysql dump slow,官方慢查询分析工具; + +pt-query-digest,Percona公司开源的慢查询分析工具; + +vc-mysql-sniffer,第三方的慢查询抓取工具; + +pt-kill,Percona公司开源的慢查询kill工具,常用于生产环境的过载保护。 + +这里重点介绍pt-query-digest,它是用于分析MySQL慢查询的一个常用工具,先对查询语句的条件进行参数化,然后对参数化以后的查询进行分组统计,统计出各查询的执行时间、次数、占比等,同时把分析结果输出到文件中。也可以结合 Anemometer 工具将慢查询平台化展示。 +## 如何优化SQL + +1. 全表扫描还是索引扫描。对于小表来说,二者IO调用次数和返回时间相差不大;但对于大表,如果全表扫描,那么查询返回的时间就会很长,就需要使用索引扫描加快查询速度。但并不是要求DBA根据每一种查询条件组合都要创建索引,索引过多也会降低写入和修改的速度,而且如果导致表数据和索引数据比例失调,也不利于后期的正常维护。 + + + +2. 如何创建索引,在哪些列上建立索引适合业务需求?一般情况下,你可以在选择度高的列上创建索引,也可以在status列上创建索引。创建索引时,要注意避免冗余索引,除非一些特殊情况外。如index(a,b,c)和index(a),其中a的单列索引就是冗余索引。 +3. 创建索引以后,尽量不要过频修改。业务可以根据现有的索引情况合理使用索引,而不是每次都去修改索引。能在索引中完成的查找,就不要回表查询。比如SELECT某个具体字段,就有助于实现覆盖索引从而降低IO次数,达到优化SQL的目的。 +4. 多表关联的SQL,在关联列上要有索引且字段类型一致,这样MySQL在进行嵌套循环连接查找时可以使用索引,且不会因为字段类型不一致或者传入的参数类型与字段类型不匹配的情况,这样就会导致无法使用索引,在优化SQL时需要重点排查这种情况。另外索引列上使用函数也不会涉及索引。多表关联时,尽量让结果集小的表作为驱动表,注意是结果集小的表,不是小表。 +5. 在日常中你会发现全模糊匹配的查询,由于MySQL的索引是B+树结构,所以当查询条件为全模糊时,例如‘%**%’,索引无法使用,这时需要通过添加其他选择度高的列或者条件作为一种补充,从而加快查询速度。当然也可以通过强制SQL进行全索引扫描,但这种方式不好,尽量不要在SQL中添加hints。对于这种全模糊匹配的场景,可以放到ES或者solr中解决。尽量不要使用子查询,对子查询产生的临时表再扫描时将无索引可查询,只能进行全表扫描,并且MySQL对于出现在from中的表无所谓顺序,对于where中也无所谓顺序,这也是可以优化SQL的地方。 +6. 另外orderby/groupby的SQL涉及排序,尽量在索引中包含排序字段,并让排序字段的排序顺序与索引列中的顺序相同,这样可以避免排序或减少排序次数。 +7. 除此之外,复杂查询还是简单查询?貌似总会面临这样的疑问和选择。不要总想着用一个SQL解决所有事情,可以分步骤来进行,MySQL也十分擅长处理短而简单的SQL,总体耗时会更短,而且也不会产生臃肿的 SQL,让人难以理解和优化。 + +常用的SQL编写规范如下所示。 + +* SELECT只获取必要的字段,禁止使用SELECT*。这样能减少网络带宽消耗,有效利用覆盖索引,表结构变更对程序基本无影响。 +* 用IN代替OR。SQL语句中IN包含的值不宜过多,应少于1000个。过多会使随机IO增大,影响性能。 +* 禁止使用orderbyrand()。orderbyrand()会为表增加几个伪列,然后用rand() 函数为每一行数据计算 rand() 值,最后基于该行排序,这通常都会生成磁盘上的临时表,因此效率非常低。建议先使用 rand() 函数获得随机的主键值,然后通过主键获取数据。 +* SQL中避免出现now()、rand()、sysdate()、current_user()等不确定结果的函数。在语句级复制场景下,引起主从数据不一致;不确定值的函数,产生的SQL语句无法使用QUERY CACHE。 +