JavaGuide
2024-03-24 22:02:42 0 举报
AI智能生成
JavaGuide
作者其他创作
大纲/内容
Java
基础
特点
OOP(封装,继承,多态)。
平台无关性( Java 虚拟机实现平台无关性)。
可靠性(具备异常处理和自动内存管理机制)。
安全性(访问权限修饰符、限制程序直接访问操作系统资源)。
高效性(通过 Just In Time 编译器等技术的优化,Java 语言的运行效率还是非常不错的)。
多线程、支持网络编程且很方便、编译与解释并存。
大版本
Java SE(Java Platform,Standard Edition)
适合开发桌面应用程序或简单的服务器应用程序。
Java EE(Java Platform,Enterprise Edition )
适合开发复杂的企业级应用程序或 Web 应用程序。
Java ME(Java Platform,Micro Edition)
主要用于开发嵌入式消费电子设备的应用程序。(淘汰了)
JVM vs JDK vs JRE
JVM(Java Virtual Machine)
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。
JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,都会给出相同的结果。
JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。
JDK(Java Development Kit)
功能齐全的 Java SDK,是提供给开发者使用,能够创建和编译 Java 程序的开发套件。
包含 JRE、码编译器 javac 以及一些其他工具,如 javadoc(文档注释工具)、jdb(调试器)、jconsole(可视化监控⼯具)、javap(反编译工具)等。
JRE(Java Runtime Environment)
Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。
从 JDK 9 开始就不需要区分 JDK 和 JRE 的关系了,取而代之的是模块系统(JDK 被重新组织成 94 个模块)+ jlink 工具。JDK 11 不单独提供 JRE 下载。
可以用 jlink 根据自己的需求,创建一个更小的 runtime(运行时),而不是不管什么应用,都是同样的 JRE。
源码编译过程
字节码
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
.class->机器码
JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。
编译模式
JIT
JIT(Just in Time Compilation)属于运行时编译,解决热点代码(经常使用的代码)频繁重复编译问题,从而提高性能。
原理
当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
优点
具备更高的极限处理能力,可以降低请求的最大延迟。
AOT
AOT(Ahead of Time Compilation) 在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。
AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。
AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。
优点
优势在于启动时间、内存占用和打包体积。
缺点
无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。
应用
GraalVM
一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。
GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。
编译与解释并存
编译型
通过编译器将源代码一次性翻译成可被该平台执行的机器码。
编译语言的执行速度比较快,开发效率比较低。
常见的编译性语言有 C、C++、Go、Rust 等等。
解释型
通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。
解释型语言开发效率比较快,执行速度比较慢。
常见的解释性语言有 Python、JavaScript、PHP 等等。
即时编译
为了改善编译语言的效率而发展出的即时编译技术,已经缩小了编译型和解释型语言间的差距。
这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。
Java 与 LLVM 是这种技术的代表产物。
Oracle JDK vs OpenJDK
2006 年 SUN 公司将 Java 开源,也就有了 OpenJDK(完全免费,GPL v2 许可协议)。
2009 年 Oracle 收购了 Sun 公司,在 OpenJDK 的基础上搞了一个 Oracle JDK(部分(短期)免费,BCL/OTN 许可协议)。
建议选择 OpenJDK 或者基于 OpenJDK 的发行版,比如 AWS 的 Amazon Corretto,阿里巴巴的 Alibaba Dragonwell。
基本语法
注释
单行注释、多行注释、文档注释。
用的比较多的还是单行注释和文档注释,多行注释在实际开发中使用的相对较少。
注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释)。
代码的注释不是越详细越好。实际上好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。
若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。
标识符
标识符就是为程序、类、变量、方法等取的名字 。
关键字
被赋予特殊含义的标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方。
continue、break 和 return
continue:指跳出当前的这一次循环,继续下一次循环。
break:指跳出整个循环体,继续执行循环下面的语句。
return
用于跳出所在方法,结束该方法的运行。
return;:直接使用 return 结束方法执行,用于没有返回值函数的方法。
return value;:return 一个特定值,用于有返回值函数的方法。
注意
default 这个关键字很特殊,既属于程序控制,也属于类,方法和变量修饰符,还属于访问控制。
虽然 true, false, 和 null 看起来像关键字但实际上他们是字面值,同时你也不可以作为标识符来使用。
运算符
自增自减运算符
自增运算符(++)、自减运算符(--)
++ 和 -- 运算符可以放在变量之前或变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。
移位运算符
移位运算符是最基本的运算符之一,几乎每种编程语言都包含这一运算符。
移位操作中,被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。
在 Java 代码里使用 <<、 >> 和 >>> 转换成的指令码运行起来会更高效些。
分类
<< :左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
>> :带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
>>> :无符号右移,忽略符号位,空位都以 0 补齐。
注意
由于 double,float 在二进制中的表现比较特殊,因此不能来进行移位操作。
移位操作符实际上支持的类型只有 int 和 long,编译器在对short、byte、char类型进行移位前,都会将其转换为int类型再操作。
当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。即 x<<42 等同于 x<<10。
new 运算符
new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
基本数据类型
基本类型
数字类型
整数型
byte、short、int、long
Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析。
BigInteger
用于超过 long 整型数据的数据类型。
原理
内部使用 int[] 数组来存储任意大小的整形数据。
缺点
相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。
浮点型
float、double
精度丢失
无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
解决
BigDecimal
可以实现对浮点数的运算,不会造成精度丢失。
防止精度丢失
BigDecimal(String val)
BigDecimal.valueOf(double val)
equals(),比较精度
compareTo(),忽略精度
字符类型
char
char a = 'h'char :单引号,String a = "hello" :双引号。
布尔型
boolean
依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。
默认值及所占空间
字节 vs 位数
位数(bit):最小存储单位。
字节(byte):由8个bit组成。
Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。
包装类型
包装类
Byte、Short、Integer、Long、Float、Double、Character、Boolean 。
缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
缓存范围
Byte,Short,Integer,Long 默认创建了数值 [-128,127] 的相应类型的缓存数据。
Character 创建了数值在 [0,127] 范围的缓存数据。
Boolean 直接返回 True or False。
两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。
比较
范围内==比较相等,范围外==比较不等,推荐equals比较。
自动拆装箱
装箱
将基本类型用它们对应的引用类型包装起来。
原理
调用了包装类的 valueOf() 方法。
拆箱
将包装类型转换为基本数据类型。
原理
调用了包装类的 xxxValue() 方法。
注意
如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
基本类型和包装类型
用途:除了定义一些常量和局部变量外,在其他比如方法参数、对象属性中很少会使用基本类型来定义变量。包装类型可用于泛型,而基本类型不可以。
存储方式:基本数据类型的存放在栈中(局部变量)或存放在堆中(成员变量(未被 static 修饰 ))。包装类型属于对象类型,几乎都存在于堆中。
占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
比较方式:对基本数据类型来说,== 比较的是值。对包装数据类型来说,== 比较的是对象的内存地址。包装类对象之间值的比较,全部使用 equals() 方法。
变量
成员变量与局部变量
语法形式:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;
成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但成员变量和局部变量都能被 final 所修饰。
成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但成员变量和局部变量都能被 final 所修饰。
存储方式:如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,
如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
生存时间:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
默认值:成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(被 final 修饰的成员变量必须显式地赋值),而局部变量则不会自动赋值。
静态变量
被 static 关键字修饰的变量,可以被类的所有实例共享。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
字符型常量和字符串常量
形式:字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
含义:字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)。
内存大小:字符常量只占 2 个字节;字符串常量占若干个字节。
方法
方法的返回值
获取到的某个方法体中的代码执行后产生的结果。
静态方法和实例方法
调用方式
在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式(不推荐),而实例方法只有 对象.方法名 这种方式。
访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员,不允许访问实例成员,而实例方法不存在这个限制。
重载和重写
重载
发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
重写
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
规则
方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
构造方法无法被重写。
构造方法
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
一个类即使没有声明构造方法也会有默认的无参构造方法。但若手动添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参构造方法了。
特点
名字与类名相同。
没有返回值,但不能用 void 声明构造函数。
生成类的对象时自动执行,无需调用。
不能被 override(重写),但可以 overload(重载)。
参数
实参&形参
实参(实际参数,Arguments)
用于传递给函数/方法的参数,必须有确定的值。
形参(形式参数,Parameters)
用于定义函数/方法,接收实参,不需要有确定的值。
值传递&引用传递
值传递
方法接收的是实参值的拷贝,会创建副本。
引用传递
方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,在 Java 中只有值传递(创建地址副本)。
可变长参数
从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。
格式
method(String... args)
原理
Java 的可变参数编译后实际会被转换成一个数组。
注意
优先匹配固定参数的方法,因为固定参数的方法匹配度更高。
OOP
面向对象和面向过程
面向对象:会先抽象出对象,然后用对象执行方法的方式解决问题。
面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
面向对象开发的程序一般更易维护、易复用、易扩展。
特征
封装
把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息,但提供一些可以被外界访问的方法来操作属性。
继承
使用已存在的类的定义作为基础建立新类,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
特点
子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
多态
一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
特点
对象类型和引用类型之间具有继承(类)/实现(接口)的关系。
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定。
多态不能调用“只在子类存在但在父类不存在”的方法。
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
接口和抽象类
共同点
都不能被实例化。
都可以包含抽象方法。
都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别
接口主要用于对类的行为进行约束,实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
一个类只能继承一个类,但是可以实现多个接口。
接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义或被重新赋值。
深拷贝和浅拷贝、引用拷贝
浅拷贝
浅拷贝会在堆上创建一个新对象,但原对象内部的属性是引用类型,浅拷贝会直接复制内部对象的引用地址,也就是拷贝对象和原对象共用同一个内部对象。
深拷贝
深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
Object
Object
Object 类是一个特殊的类,是所有类的父类。它主要提供了 11 个方法。
方法
public final native Class<?> getClass();
native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public native int hashCode();
native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
public boolean equals(Object obj);
用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException;
native 方法,用于创建并返回当前对象的一份拷贝。
native 方法,用于创建并返回当前对象的一份拷贝。
public String toString();
返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public final native void notify();
native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll();
native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException;
native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException;
多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。
多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。
public final void wait() throws InterruptedException;
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
protected void finalize() throws Throwable { }
实例被垃圾回收器回收的时候触发的操作。
实例被垃圾回收器回收的时候触发的操作。
== 和 equals()
==
基本数据类型比较的是值。
引用数据类型比较的是对象的内存地址。
equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。
没重写:比较的是对象的内存地址。重写:比较属性或自定义内容。自定义类推荐重写此方法。
hashCode()
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的。
hashCode() 和 equals() 都是用于比较两个对象是否相等。(自定义类推荐重写方法)
特点
如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
如果两个对象的hashCode 值相等并且 equals() 方法也返回 true,我们才认为这两个对象相等。
如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
String
不可变性
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
应用
String:操作少量的数据。
StringBuilder:单线程操作字符串缓冲区下操作大量数据。
StringBuffer:多线程操作字符串缓冲区下操作大量数据。
底层实现
Java 9 以前,底层由 char[] 实现。
Java 9 之后,底层由 byte[] 实现。
原因
Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
String#intern
一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中。
JDK 1.6:拷贝一份对象,放到常量池(永久代)中,返回值是常量池(永久代)中对应字符串实例的引用。
JDK 1.7:拷贝一份引用,放到常量池(堆)中(JDK 1.7 将 String 常量池从 Perm 区移动到了 Java Heap 区)。
JDK 1.7:拷贝一份引用,放到常量池(堆)中(JDK 1.7 将 String 常量池从 Perm 区移动到了 Java Heap 区)。
常量折叠
常量折叠(Constant Folding),把常量表达式的值求出来作为常量嵌在最终生成的代码中的编译优化。
这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以折叠。
可折叠
基本数据类型(byte、boolean、short、char、int、float、long、double)以及字符串常量。
final 修饰的基本数据类型和字符串变量。
字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
注意
在 JDK9 当中,字符串相加 “+” 改为了用动态方法,不再影响性能了。
异常
java.lang.Throwable
Java 异常类顶级父类。
方法
String getMessage():返回异常发生时的简要描述。
String toString():返回异常发生时的详细信息。
String getLocalizedMessage():返回异常对象的本地化信息。
使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同。
使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同。
void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息。
Exception
程序本身可以处理的异常,可以通过 catch 来进行捕获。
分类
Checked Exception
受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch 或者 throws 关键字处理的话,就没办法通过编译。
Unchecked Exception
不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
RuntimeException 及其子类都统称为非受检查异常。
常见
NullPointerException(空指针错误)
IllegalArgumentException(参数错误比如方法入参类型错误)
NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
ArrayIndexOutOfBoundsException(数组越界错误)
ClassCastException(类型转换错误)
ArithmeticException(算术错误)
SecurityException (安全错误比如权限不够)
UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
Error
Error 属于程序无法处理的错误 ,不建议通过catch捕获 。
Error 发生时,Java 虚拟机(JVM)一般会选择线程终止。
异常捕获
try-catch-finally
try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
catch块:用于处理 try 捕获到的异常。
finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
注意
不要在 finally 语句块中使用 return!
当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行 finally 语句中的 return 之后,本地变量的值就变为 finally 语句中的 return 返回值。
当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行 finally 语句中的 return 之后,本地变量的值就变为 finally 语句中的 return 返回值。
finally 不是一定会执行
程序所在的线程死亡。
关闭 CPU。
try-with-resources
适用
任何实现 java.lang.AutoCloseable 或者 java.io.Closeable 的对象。
关闭资源和 finally 块的执行顺序
在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行。
注意
不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
抛出的异常信息一定要有意义。
建议抛出更加具体的异常,比如字符串转换为数字格式错误的时候应该抛出 NumberFormatException 而不是其父类 IllegalArgumentException。
使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。
泛型(Generics)
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。
使用方式
泛型类
public class Generic<T> { private T key; }
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
在实例化泛型类时,必须指定T的具体类型。
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
在实例化泛型类时,必须指定T的具体类型。
泛型接口
public interface Generator<T> { public T method(); }
泛型方法
public static <E> void printArray( E[] inputArray ) { }
作用
使⽤泛型可在编译期间进⾏类型检测。
使⽤ Object 类型需要⼿动添加强制类型转换,降低代码可读性,提⾼出错概率。
泛型可以使⽤⾃限定类型,如 T extends Comparable 。
限制
泛型的限制⼀般是由泛型擦除机制导致的。擦除为 Object 后⽆法进⾏类型判断。
内容
只能声明不能实例化 T 类型变量。
泛型参数不能是基本类型。因为基本类型不是 Object ⼦类,应该⽤基本类型对应的引⽤类型代替。
不能实例化泛型参数的数组。擦除后为 Object 后⽆法进⾏类型判断。
不能实例化泛型数组。
泛型⽆法使⽤ Instance of 和 getClass() 进⾏类型判断。
不能实现两个不同泛型参数的同⼀接⼝,擦除后多个⽗类的桥⽅法将冲突。
不能使⽤ static 修饰泛型变量。
应用
自定义接口通用返回结果 CommonResult<T> 通过参数 T 可根据具体的返回类型动态指定结果的数据类型。
定义 Excel 处理类 ExcelUtil<T> 用于动态指定 Excel 导出的数据类型。
构建集合工具类(参考 Collections 中的 sort, binarySearch 方法)。
擦除机制
类型擦除:编译期间,所有的泛型信息都会被擦掉。
Java 的泛型是伪泛型,因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是类型擦除 。
编译器会在编译期间会动态地将泛型 T 擦除为 Object 或将 T extends xxx 擦除为其限定类型 xxx 。
泛型本质上还是编译器的⾏为,为了保证引⼊泛型机制但不创建新的类型,减少虚拟机的运⾏开销,编译器通过擦除将泛型类转化为⼀般类。
桥⽅法( Bridge Method )
⽤于继承泛型类时保证多态,编译器⾃动⽣成。
通配符
通配符可以允许类型参数变化,⽤来解决泛型⽆法协变的问题。
⽆界通配符
⽆界通配符可以接收任何泛型类型数据,⽤于实现不依赖于具体类型参数的简单⽅法,可以捕获参数类型并交由泛型⽅法进⾏处理。
通配符 ?和通配符 T
T 可以⽤于声明变量或常量⽽ ? 不⾏。
T ⼀般⽤于声明泛型类或⽅法,通配符 ? ⼀般⽤于泛型⽅法的调⽤代码和形参。
T 在编译期会被擦除为限定类型或 Object ,通配符 ? ⽤于捕获具体类型。
边界通配符
上边界通配符 extends 可以实现泛型的向上转型即传⼊的类型实参必须是指定类型的⼦类型。
下边界通配符 super 与上边界通配符 extends 刚好相反,它可以实现泛型的向下转型即传⼊的类型实参必须是指定类型的⽗类型。
? extends xxx 和 ? super xxx
使⽤ ? extends xxx 声明的泛型参数只能调⽤ get() ⽅法返回 xxx 类型,调⽤ set() 报错。
使⽤ ? super xxx 声明的泛型参数只能调⽤ set() ⽅法接收 xxx 类型,调⽤ get() 报错。
T extends xxx 和 ? extends xxx
T extends xxx ⽤于定义泛型类和⽅法,擦除后为 xxx 类型。
? extends xxx ⽤于声明⽅法形参,接收 xxx 和其⼦类型。
Class<?> 和 Class
直接使⽤ Class 的话会有⼀个类型警告,使⽤ Class<?> 则没有,因为 Class 是⼀个泛型类,接收原⽣类型会产⽣警告。
反射(Reflection)
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
获取类模版
.class、Class.forName()、instance.getClass()、xxxClassLoader.loadClass()
优点
可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
缺点
让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。
应用
动态代理的实现依赖反射。
注解(Annotation)
JDK 5 新特性,用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
本质
一个继承了Annotation 的特殊接口。
解析方法
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理。
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value、@Component)都是通过反射来进行处理的。
SPI vs API
SPI
SPI(Service Provider Interface)服务提供者的接口,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或替换服务实现并不需要修改调用方。
实现本质上是通过反射完成的
META-INF/services/
优点
能够大大地提高接口设计的灵活性。
缺点
需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
当多个 ServiceLoader 同时 load 时,会有并发问题。
应用
Spring 框架、数据库加载驱动、日志接口SLF4J (Simple Logging Facade for Java)、以及 Dubbo 的扩展实现等等。
API(Application Programming Interface)
API(Application Programming Interface)应用程序接口,对外提供功能服务的一个接口。
序列化
数据结构或对象和二进制字节流互相转换过程。
序列化
将数据结构或对象转换成二进制字节流的过程。
反序列化
将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
目的
通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
协议
常用协议
Hessian、Kryo、Protobuf、ProtoStuff,不推荐JDK自带(不支持跨语言调用、性能差、存在安全问题)。
像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。
序列化协议对应于 TCP/IP 4 层模型的 表示层。
transient
对于不想进行序列化的变量,使用 transient 关键字修饰。
作用
阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
注意
transient 只能修饰变量,不能修饰类和方法。
transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。
static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
应用
对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化。
将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化。
将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。
将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
语法糖(Syntactic sugar)
编程语言为了方便程序员开发程序而设计的一种特殊语法,compile() 中有一个步骤就是调用 desugar()。
常见语法糖
switch 支持 String 与枚举、泛型、自动拆装箱、变长参数、枚举、内部类。
条件编译、断言、数值字面量、for-each、try-with-resources、lambda表达式等。
代理模式
使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
作用
扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
静态代理
定义
实现和应用角度
对目标对象的每个方法的增强都是手动完成的,非常不灵活且麻烦。
JVM 角度
静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
步骤
定义一个接口及其实现类。
创建一个代理类同样实现这个接口。
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
动态代理
定义
实现和应用角度
不需要针对每个目标类都单独创建一个代理类,并且也不需要必须实现接口,可以直接代理实现类(CGLIB 动态代理机制)。
JVM角度
动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
应用
日常开发使用较少,但在框架中使用非常多。
JDK 动态代理
JDK 动态代理的目标对象必须要有接口实现,也就是说:委托类必须要继承接口。
核心
InvocationHandler 接口
在 invoke() 方法中可以自定义处理逻辑。
Proxy 类
Proxy.newProxyInstance()
主要用来生成一个代理对象。
代理对象在调用方法时,实际会调用实现 InvocationHandler 接口的类的 invoke()方法。
步骤
定义一个接口及其实现类。
自定义 InvocationHandler 并重写 invoke 方法,在 invoke 方法中会调用原生方法(被代理类的方法)并自定义一些处理逻辑。
通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象,再调用接口方法。
CGLIB 动态代理
CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,允许在运行时对字节码进行修改和动态生成。
原理
CGLIB 通过继承方式实现代理。
核心
MethodInterceptor 接口
自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。
Enhancer 类
通过 Enhancer 类来动态获取被代理类,当代理类调用方法时,实际调用的是 MethodInterceptor 中的 intercept 方法。
步骤
手动添加 CGLIB(开源项目)相关依赖。
定义一个类。
自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似。
通过 Enhancer 类的 create() 创建代理类,再调用接口方法。
应用
Spring 的 AOP 模块
如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
区别
JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
效率上,JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
区别
灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。
另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
魔法类 Unsafe
依赖本地方法(Native Method)
native 关键字修饰,Java 中使用其他编程语言编写的方法。
初始化
类加载器、反射、VM参数
内存操作
在 Java 中不允许直接对内存进行操作,对象内存的分配和回收都是由 JVM 实现。但是在 Unsafe 中,提供接口可以直接进行内存操作。
释放
这种方式分配的内存属于堆外内存 ,是无法进行垃圾回收的,需要把这些内存当做一种资源去手动调用 freeMemory 方法进行释放,否则会产生内存泄漏。
通用的操作内存方式是在 try 中执行对内存的操作,最终在 finally 块中进行内存的释放。
内存屏障
CPU 为了防止代码进行重排序而提供的指令。
Java 8 中引入了一种锁的新机制:StampedLock,读写锁的一个改进版本。
loadFence()、storeFence()、fullFence()
对象操作
对象属性
put、get
对象实例化
常规对象实例化方式
new
非常规的实例化方式
allocateInstance
数组操作
arrayBaseOffset、arrayIndexScale
java.util.concurrent.atomic 包下的 AtomicIntegerArray
CAS 操作
比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。
三个操作数——内存位置、预期原值及新值。
JUC 包的并发工具类中大量地使用了 CAS 操作。
线程调度
park、unpark、monitorEnter、monitorExit、tryMonitorEnter
Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer (AQS)
Class 操作
类加载和静态变量的操作方法。
Lambda 表达式实现需要依赖 Unsafe 的 defineAnonymousClass 方法。
系统信息
addressSize、pageSize
集合
定义
Java 集合, 也叫作容器,主要是由两大接口派生而来。
Collection接口
用于存放单一元素。
子接口
List、Set 和 Queue。
Map 接口
用于存放键值对。
List
List(对付顺序的好帮手):存储的元素是有序的、可重复的。
ArrayList:Object[] 数组,性能好,线程不安全。
LinkedList:双向链表,不常用,线程不安全。
CopyOnWriteArrayList:写时复制COW,Object[] 数组,线程安全。
Vector:Object[] 数组,JDK1.5之前使用,线程安全。
Stack:继承自 Vector,是一个后进先出的栈,JDK1.5之前使用,线程安全。
Set
Set(注重独一无二的性质):存储的元素不可重复的。
HashSet(无序,唯一):基于 HashMap,线程不安全 。
LinkedHashSet:HashSet 的子类,基于 LinkedHashMap,线程不安全 。
TreeSet(有序,唯一):红黑树(自平衡的排序二叉树),线程不安全。
Queue
Queue(实现排队功能的叫号机):按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
PriorityQueue:JDK1.5,二叉堆 + Object[] 数组来实现小顶堆,线程不安全。
ArrayDeque:JDK1.6,可扩容动态双向数组,基于可变长的数组和双指针来实现。
阻塞队列
BlockingQueue:阻塞队列,继承自 Queue。
ArrayBlockingQueue:JDK1.5,基于定长的数组,有界队列。创建时需指定容量,支持公平和非公平锁访问机制。
LinkedBlockingQueue:JDK1.5,基于单向链表,无界队列,近似无界。创建时指定容量或默认 Integer.MAX_VALUE,仅支持非公平锁访问机制。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
SynchronousQueue:JDK1.6,同步队列,一个不存储元素的阻塞队列。
DelayQueue:JDK1.8,基于PriorityQueue,一个支持延迟获取元素的阻塞队列。
TransferQueue:JDK1.7,一个支持更多操作的阻塞队列。
阻塞队列就是典型的生产者-消费者模型,调用 put、take、offfer、poll 等 API 即可实现多线程之间的生产和消费。
Map
Map(用 key 来搜索的专家):使用键值对(key-value)存储,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
HashMap:数组 + 链表(红黑树)组成的,线程不安全。
LinkedHashMap
继承自 HashMap,增加了一条双向链表,线程不安全。
accessOrder:访问顺序,属于构造参数。
LRU 缓存
(Least Recently Used,最近最少使用) 缓存,确保当存放的元素超过容器容量时,将最近最少访问的元素移除。
accessOrder 设置为 true 并重写 removeEldestEntry 方法(移除规则)。
Hashtable:数组(Hashtable 的主体)+ 链表组成的,线程安全。
TreeMap:红黑树(自平衡的排序二叉树),线程不安全。
ConcurrentHashMap:分段的数组 + 链表(红黑树),线程安全。
复杂度
O(1) 常数< O(logn) 对数 < O(n) 线性 < O(n2) < O(n3) < O(2n) 指数
ArrayList
头插 O(n);尾插:容量未达极限 O(1),达到极限 O(n);指定插:O(n);
头删 O(n);尾删 O(1);指定删:O(n);
LinkedList
头插/删 O(1);尾插/删:O(1);指定插/删:O(n);
RandomAccess 接口
一个标记接口,用来表明实现该接口的类支持随机访问(即可以通过索引快速访问元素)。
由于 LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,所以不能实现 RandomAccess 接口。
Comparable 和 Comparator
Java 中用于排序的接口,在实现类对象之间比较大小、排序等方面发挥了重要作用。
Comparable
java.lang 包,compareTo(Object obj) 方法用来排序。
方式
实现 Comparable 接口并重写 compareTo(Object obj) 方法。
Comparator
java.util 包,compare(Object obj1, Object obj2) 方法用来排序。
方式
new 一个 Comparator 比较器实例并重写其 compare(Object obj1, Object obj2) 方法。
Queue 与 Deque
Queue
单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。
扩展了 Collection 的接口,因为容量问题而导致操作失败后处理方式的不同:一种在操作失败后会抛出异常,另一种则会返回特殊值。
Deque
双端队列,在队列的两端均可以插入或删除元素。
Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,失败处理方式与 Queue 相似。
Deque 还提供有 push() 和 pop() 等其他方法,可用于模拟栈。
注意事项
集合判空
使用 isEmpty() 方法,而不是 size()==0 的方式。
集合转 Map
toMap(),value为null,抛NPE。
集合遍历
不要在 foreach 循环里进行元素的 remove/add 操作。
remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
集合去重
利用 Set 元素唯一的特性,避免使用 List 的 contains()。
集合转数组
toArray(T[] array)
数组转集合
Arrays.asList()
不能修改
手动实现工具类
new ArrayList<>(Arrays.asList("a", "b", "c"))
Java8 Stream
Arrays.stream(myArray).collect(Collectors.toList())
Guava
不可变
ImmutableList.of("string", "elements")、ImmutableList.copyOf(aStringArray)
可变
Lists.newArrayList("or", "string", "elements")
Apache Commons Collections
CollectionUtils.addAll(list, str)
Java9 List.of()
List<Integer> list = List.of(array)
Synchronized 性能问题
Synchronized 锁升级后,性能不再是问题。
并发编程
进程 vs 线程
线程是进程划分成的更小的运行单位。
线程分类
绿色线程
JDK 1.2 之前,一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。
用户线程
由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
内核线程
由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
区别
用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。
Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程。
线程模型:1:1、1:N、N:N
Java 在 Windows 和 Linux 是 1:N。
程序计数器、虚拟机栈和本地方法栈
程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务。
本地方法栈则为虚拟机使用到的 Native 方法服务。
保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区
堆存放新创建的对象。
方法区存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
并发与并行
并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
同步和异步
同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
异步:调用在发出之后,不用等待返回结果,该调用直接返回。
多线程
为了能提高程序的执行效率提高程序运行速度
问题:内存泄漏、死锁、线程不安全等
线程安全
在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。
线程类型
CPU 密集型和 IO 密集型。
线程生命周期和状态
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start() 等待运行的状态。
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
线程上下文切换
线程切换需要保存以及恢复相应线程的上下文。
切换过程占用CPU。
线程死锁
多线程中一个或多个等待某个资源释放,而被无限期阻塞,导致程序不能正常终止。
条件
互斥条件;请求与保持条件;不剥夺条件;循环等待条件;
sleep() 方法和 wait() 方法
sleep() 方法没有释放锁,wait() 方法释放了锁 。
sleep()方法操作线程,wait()方法操作对象(对象锁)。
start() 方法和 run() 方法
new一个Thread并调用start(),分到时间片后,start()准备后自动调用run()。
真正多线程
直接调用run()就是main线程下的普通方法。
非多线程
JMM(Java 内存模型)
Java Memory Model
Java Memory Model
主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
CPU 缓存模型(CPU Cache):通常分为三层,分别叫 L1,L2,L3 Cache。
CPU 可以通过制定缓存一致协议(比如 MESI 协议)来解决内存缓存不一致性问题。
指令重排序
为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。
编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。
Java源代码重排过程:编译器优化重排 —> 指令并行重排 —> 内存系统重排
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,多线程下可能引发问题。
内存屏障
Memory Barrier,或有时叫做内存栅栏(Memory Fence)
一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。
主内存:所有线程创建的实例对象都存放在主内存中。
本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。
同步操作:锁定(lock);解锁(unlock);read(读取);load(载入);use(使用);assign(赋值);store(存储);write(写入);
同步规则:8种,了解即可。
happens-before 原则
通过逻辑时钟来区分事件发生的前后顺序。
前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
常见规则
程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作。
解锁规则:解锁 happens-before 于加锁。
volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。
传递规则:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
线程启动规则:Thread 对象的 start()方法 happens-before 于此线程的每一个动作。
如果两个操作不满足上述任意一个 happens-before 规则,JVM 就可以对这两个操作进行重排序。
并发编程重要特性
原子性
借助synchronized、各种 Lock 以及各种原子类实现原子性。
可见性
借助synchronized、volatile 以及各种 Lock 实现可见性。
有序性
volatile 关键字可以禁止指令进行重排序优化。
volatile 关键字
保证变量的可见性
标记JVM每次都要到主存中进行读取。
防止 JVM 的指令重排序
插入特定内存屏障
双重检验锁方式实现单例模式
保证变量的可见性,但不能保证对变量的操作是原子性的。
乐观锁和悲观锁
悲观锁
乐观锁
假设最好,提交时验证修改(CAS算法)
java.util.concurrent.atomic包下面的原子变量类
大量失败重试,导致CPU飙升。
多用于写比较少的情况(多读场景,竞争较少)
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。
CAS算法
Compare And Swap(比较与交换)
原子操作,底层依赖于一条 CPU 的原子指令。
三个操作数
V:要更新的变量值(Var)
E:预期值(Expected)
N:拟写入的新值(New)
ABA 问题;循环时间长开销大;只能保证一个共享变量的原子操作(AtomicReference对象原子性);
悲观锁
假设最坏,使用就会上锁
synchronized 和 ReentrantLock 等独占锁
大量阻塞,频繁切换上下文,死锁等
多用于写比较多的情况(多写场景,竞争激烈)
synchronized 关键字
早起属于重量级锁,Java 6 后轻量级锁
自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁
偏向锁增加了 JVM 的复杂性,JDK15默认关闭,JDK18彻底废弃
修饰实例方法;修饰静态方法;修饰代码块;
通过获取对象监视器 monitor 实现。
只能是非公平锁。
ReentrantLock
实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。
更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
内部类 Sync 继承 AQS(AbstractQueuedSynchronizer)
内部类 Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类,默认非公平锁。
公平锁 : 锁被释放之后,先申请的线程先得到锁。
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。
synchronized 和 ReentrantLock 都是可重入锁。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。
ReentrantLock 比 synchronized 多的高级功能
等待可中断;可实现公平锁;可实现选择性通知(锁可以绑定多个条件);
Condition接口:JDK1.5,实现多路通知进行线程调度,可选择性通知。
ReentrantReadWriteLock
使用少,JDK1.8后使用性能更好的读写锁 StampedLock。
锁状态
依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
可中断锁和不可中断锁
可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理,ReentrantLock。
不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理,synchronized。
共享锁和独占锁
共享锁:一把锁可以被多个线程同时获得。
独占锁:一把锁只能被一个线程获得。
写锁可以降级为读锁,但是读锁却不能升级为写锁。
StampedLock
JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Conditon。
提供了三种模式的读写控制模式:读锁、写锁和乐观读。一定条件下进行相互转换。
适合读多写少的业务场景。性能虽好,但使用麻烦,不建议使用高级性能。
基于 CLH 锁 实现的(与AQS一样),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。
ThreadLocal
提供线程局部变量,每个线程 Thread 拥有一份自己的副本变量(ThreadLocalMap),多个线程互不干扰。
ThreadLocalMap
类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
Key
ThreadLocal<?> k ,继承自WeakReference,弱引用
Value
代码中放入的值,强引用
Key被回收,Value仍然存在,容易出现内存泄漏。
Hash 算法
HASH_INCREMENT = 0x61c88647
斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
Hash 冲突
HashMap通过链表结构或红黑树解决。
ThreadLocalMap没有链表结构,采用。。。
过期Key数据清理方式
探测式清理(expungeStaleEntry()):以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理。
启发式清理(cleanSomeSlots()):Heuristically scan some cells looking for stale entries.
扩容机制
先探测式清理再扩容
InheritableThreadLocal
在异步场景下给子线程共享父线程中创建的线程副本数据。
有缺陷,推荐阿里开源的 TransmittableThreadLocal 组件。
内存泄露
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
线程池
为了减少每次获取资源的消耗,提高对资源的利用率。
创建线程池
通过 ThreadPoolExecutor 构造函数来创建(推荐)。
通过 Executor 框架来创建(不推荐)。
FixedThreadPool:返回一个固定线程数量的线程池。
SingleThreadExecutor: 返回一个只有一个线程的线程池。
CachedThreadPool: 返回一个可调整线程数量的线程池。
ScheduledThreadPool:返回一个延迟后运行或者定期执行任务的线程池。
弊端:最大Integer.MAX_VALUE,导致OOM。
线程池的饱和策略
ThreadPoolExecutor.AbortPolicy
抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy
任务回退给调用者,使用调用者的线程来执行任务。
ThreadPoolExecutor.DiscardPolicy
不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy
将丢弃最早的未处理的任务请求。
阻塞队列
LinkedBlockingQueue(无界队列):FixedThreadPool 和 SingleThreadExector 。
SynchronousQueue(同步队列):CachedThreadPool 。
DelayedWorkQueue(延迟阻塞队列):ScheduledThreadPool 和 SingleThreadScheduledExecutor 。
设定线程池的大小
CPU 密集型任务(N+1)
I/O 密集型任务(2N)
最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))
动态修改线程池的参数
美团:自定义队列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
Hippo4j:异步线程池框架
Dynamic TP:轻量级动态线程池
优先级任务线程池
使用 PriorityBlockingQueue (优先级阻塞队列)作为任务队列(workQueue 参数)
监测线程池运行状态
SpringBoot 中的 Actuator 组件。
利用 ThreadPoolExecutor 的相关 API 做监控。
坑儿
重复创建线程池的坑
Spring 内部线程池的坑,一定要手动自定义线程池,配置合理的参数
线程池和 ThreadLocal 共用的坑,可能会导致线程从ThreadLocal获取到的是旧值/脏数据。
解决:TransmittableThreadLocal(TTL)
Executor
Executor 框架是 Java5 之后引进来管理线程的框架,比Thread更好,易管理,效率好,避免this逃逸问题。
构成
任务(Runnable /Callable)
任务的执行(Executor)
异步计算的结果(Future)
对比
Runnable vs Callable
Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。
execute() vs submit()
execute()无返回值,submit()有返回值。
shutdown() vs shutdownNow()
shutdown()关闭线程池,线程池的状态变为 SHUTDOWN。
shutdownNow()关闭线程池,线程池的状态变为 STOP。
isTerminated() vs isShutdown()
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true。
Future
异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。
功能
取消任务;判断任务是否被取消;判断任务是否已经执行完成;获取任务执行结果;
FutureTask
Future 接口的基本实现,用来封装 Callable 和 Runnable。
FutureTask 相当于对 Callable 进行了封装,管理着任务执行的情况,存储了 Callable 的 call 方法的任务执行结果。
CompletableFuture
Future 存在一些缺陷,比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。
Java 8 引入 CompletableFuture 类解决 Future 的缺陷。
CompletableFuture 同时实现了 Future 和 CompletionStage 接口。
CompletionStage 接口描述了一个异步计算的阶段。
常用方法
创建
通过 new 关键字。
基于 CompletableFuture 自带的静态工厂方法:runAsync()、supplyAsync() 。
处理异步结算的结果
thenApply()、thenAccept()、thenRun()、whenComplete()
异常处理
handle()、exceptionally()、completeExceptionally()
组合
thenCompose()、thenCombine()
并行
allOf()、anyOf()
建议
建议使用自定义的线程池,而非默认的 ForkJoinPool.commonPool()
尽量避免使用 get()
如果必须要使用的话,需要添加超时时间
正确进行异常处理
合理组合多个异步任务
AQS
抽象队列同步器(AbstractQueuedSynchronizer):主要用来构建锁和同步器。
原理:用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(即不存在队列实例,仅存在结点之间的关联关系)。
资源共享方式
Exclusive(独占,只有一个线程能执行,如 ReentrantLock)
Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)
自定义同步器
使用了模板方法模式,自定义同步器时需要重写 AQS 提供的钩子方法
钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰。
Semaphore
构造(信号量)可以用来控制同时访问特定资源的线程数量。
原理:共享锁的一种实现,默认构造 AQS 的 state 值为 permits。
场景:限流,仅限单机,实际推荐Redis + Lua 来做。
CountDownLatch
允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
一次性。
原理:共享锁的一种实现,默认构造 AQS 的 state 值为 count。
场景:多个无顺序依赖任务,需要统一返回结果。-- 阻塞,条件,释放
替代:CompletableFuture,Java 8
CyclicBarrier
与 CountDownLatch 非常类似,但更加复杂和强大。
原理:基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的实现。
虚拟线程(Virtual Thread)
Java 21 重量级更新
由 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度。
平台线程(Platform Thread):在引入虚拟线程之前,JVM 调度程序通过平台线程(载体线程)来管理虚拟线程。
主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程。Solaris 系统除外。
优缺点
非常轻量级;简化异步编程;减少资源开销;
不适用计算密集型任务;依赖于语言或库支持;
创建
Thread.startVirtualThread()、Thread.ofVirtual()、ThreadFactory、Executors.newVirtualThreadPerTaskExecutor()
在密集 IO 的场景下,比平台线程性能更好。
并发容器总结
ConcurrentHashMap
线程安全的 HashMap
另一种线程安全的 HashMap实现:Collections.synchronizedMap()
CopyOnWriteArrayList
程安全的 List,在读多写少的场合性能非常好,远远好于 Vector。
在 JDK1.5 之前,如果想要使用并发安全的 List 只能选择 Vector。
ConcurrentLinkedQueue
高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
BlockingQueue
这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
ArrayBlockingQueue
BlockingQueue 接口的有界队列实现类,底层采用数组来实现。
LinkedBlockingQueue
底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用。
PriorityBlockingQueue
支持优先级的无界阻塞队列。
ConcurrentSkipListMap
跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
跳表是一种可以用来快速查找的数据结构,有点类似于平衡树,是一种利用空间换时间的算法。
Atomic 原子类
具有原子/原子操作特征的类,都存放在java.util.concurrent.atomic下。
基本类型
AtomicInteger:整型原子类
AtomicLong:长整型原子类
AtomicBoolean:布尔型原子类
数组类型
AtomicIntegerArray:整型数组原子类
AtomicLongArray:长整型数组原子类
AtomicReferenceArray:引用类型数组原子类
引用类型
AtomicReference:引用类型原子类
AtomicMarkableReference:原子更新带有标记的引用类型。
AtomicStampedReference:原子更新带有版本号的引用类型。
对象的属性修改类型
AtomicIntegerFieldUpdater:原子更新整型字段的更新器
AtomicLongFieldUpdater:原子更新长整型字段的更新器
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
IO
定义
IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储的过程即输出。
数据传输过程类似于水流,因此称为 IO 流。
IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
抽象类基类
InputStream/Reader/OutputStream/Writer
字节流和字符流
必要性
字符流是由 Java 虚拟机将字节转换得到的,这个过程比较耗时。
如果不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。
字节流
音频文件、图片等媒体文件用字节流
InputStream(字节输入流)
用于从源头(通常是文件)读取数据(字节信息)到内存中
FileInputStream
读取文件单字节数据
DataInputStream
读取指定类型数据,不能单独使用
ObjectInputStream
读取 Java 对象(反序列化),不能单独使用
OutputStream(字节输出流)
用于将数据(字节信息)写入到目的地(通常是文件)
FileOutputStream
输出单字节数据到文件
DataOutputStream
写入指定类型数据,不能单独使用
ObjectOutputStream
写入 Java 对象(反序列化),不能单独使用
字符流
涉及到字符的话使用字符流
字符流默认采用的是 Unicode 编码
Unicode:任何字符都占 2 个字节
Utf8:英文占 1 字节,中文占 3 字节
Gbk:英文占 1 字节,中文占 2 字节
Reader(字符输入流)
用于从源头(通常是文件)读取数据(字符信息)到内存中
InputStreamReader
字节流转换为字符流的桥梁
FileReader
基于InputStreamReader的封装
Writer(字符输出流)
用于将数据(字符信息)写入到目的地(通常是文件)
OutputStreamWriter
字符流转换为字节流的桥梁
FileWriter
基于OutputStreamWriter的封装
字节缓冲流
IO 操作很消耗性能,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,避免频繁操作来提高效率。
原理
装饰器模式
内部维护了一个字节数组作为缓冲区,默认大小为 8192 字节
BufferedInputStream(字节缓冲输入流)、BufferedOutputStream(字节缓冲输出流)
ZipInputStream 和 ZipOutputStream 可以分别增强字节缓冲流
字符缓冲流
类似于字节缓冲流,内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
打印流
System.out 实际是用于获取一个 PrintStream 对象
PrintStream(字节打印流)、PrintWriter (字符打印流)
随机访问流
随意跳转到文件的任意位置进行读写
RandomAccessFile
应用
实现大文件的断点续传
RandomAccessFile 的实现依赖于 FileDescriptor (文件描述符) 和 FileChannel (内存映射文件)。
IO设计模式
装饰器(Decorator)模式
可以在不改变原有对象的情况下拓展其功能。
适配器(Adapter Pattern)模式
用于接口互不兼容的类的协调工作
概念
适配者(Adaptee)
存在被适配的对象或者类
适配器(Adapter)
作用于适配者的对象或者类
分类
类适配器
使用继承关系来实现
对象适配器
使用组合关系来实现
工厂模式
用于创建对象
NIO 中大量用到了工厂模式
观察者模式
NIO 中的文件目录监听服务
IO模型
IO 操作依赖内核空间的能力,应用程序只是IO调用,具体执行是操作系统的内核。
同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O
BIO (Blocking I/O)
属于同步阻塞 IO 模型 。
应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
客户端连接数量不高则可用,但十万甚至百万级连接数则不可用。
NIO (Non-blocking/New I/O)
属于 I/O 多路复用模型或同步非阻塞 IO 模型,Java 1.4 引入
同步非阻塞 IO 模型
应用程序会一直发起 read 调用,只有最后的read会一直阻塞,直到内核把数据拷贝到用户空间。
调用轮询数据,十分消耗 CPU 资源。
I/O 多路复用模型
应用程序先查询内核数据是否准备就绪,就绪后再发起read调用,调用过程中还是阻塞的。
性能优势主要体现在高并发和高延迟的网络环境下。
Buffer(缓冲区)
NIO 读写数据都是通过缓冲区进行操作的。
变量
容量(capacity)、界限(limit)、位置(position)、标记(mark)
0 <= mark <= position <= limit <= capacity
模式
读/写模式分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。
默认写模式,调用 flip() 切换到读模式。调用 clear() 或者 compact() 切换到写模式。切换后 position 都会归零。
读模式
limit 等于 Buffer 中实际写入的数据大小。
写模式
limit 代表最多能写入的数据,一般等于 capacity。
Channel(通道)
Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。
read :读取 Channel 数据并写入到 Buffer 中。
write :将 Buffer 中的数据写入到 Channel 中。
选择器/多路复用器 ( Selector )
允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。
原理
通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。
JDK 使用了 epoll() 代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。
事件类型
SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel。
SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel。
SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读。
SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。
Selector是抽象类,可以通过调用此类的 open() 静态方法来创建 Selector 实例。
NIO 零拷贝
执行 IO 操作时,避免 CPU 存储区域之间的数据复制,从而减少上下文切换以及 CPU 的拷贝时间来提升性能。
应用
ActiveMQ、Kafka 、RocketMQ、QMQ、Netty 等顶级开源项目
常见实现技术
mmap+write、sendfile和 sendfile + DMA gather copy 。
Java支持
MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷⻉⽅式的提供的⼀种实现。
FileChannel 的transferTo()/transferFrom()是 NIO 基于发送文件(sendfile)这种零拷贝方式的提供的一种实现。
不建议直接使用原生 NIO,推荐使用一些成熟的基于 NIO 的网络编程框架比如 Netty。
适用于连接数目多且连接比较短(轻操作)的架构。例如:聊天服务器。
AIO (Asynchronous I/O)
属于异步 IO 模型,Java 7 引入,也就是 NIO 2。
基于事件和回调机制实现的。
适用于连接数目多且连接比较长(重操作)的架构。例如:相册服务器。
JVM
Java内存区域
运行时数据区域
概要
JDK 1.7
JDK 1.8
线程私有的
程序计数器、虚拟机栈、本地方法栈
线程共享的
堆、方法区、直接内存 (非运行时数据区的一部分)
程序计数器
一块较小的内存空间,作为当前线程所执行的字节码的行号指示器。
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,以便切换回来后继续执行。
唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期与线程保持一致。
Java 虚拟机栈
JVM 运行时数据区域的一个核心,生命周期与线程保持一致。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
拥有先进后出的数据结构,只支持出栈和入栈两种操作。
局部变量表
主要存放编译期可知的各种数据类型、对象引用。
操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。
计算过程中产生的临时变量也会放在操作数栈中。
动态链接
主要服务一个方法需要调用其他方法的场景。
作用
为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。
StackOverFlowError
栈的内存大小不允许动态扩展,栈深度超过最大深度
OutOfMemoryError
虚拟机在动态扩展栈时无法申请到足够的内存空间
本地方法栈
只为虚拟机使用到的 Native 方法服务的Java虚拟机栈。
堆
最大一块内存区域,在虚拟机启动时创建,唯一目的就是存放对象实例。
注意:随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,并不是所有对象都在堆中分配,JDK 1.7 默认开启逃逸分析。
垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。
结构
新生代和老年代
Eden、Survivor、Old
JDK 1.7
新生代(Young Generation)
Eden、S0、S1
老年代(Old Generation)
Tenured
永久代(Permanent Generation)
PermGen
JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。
永久代替换为元空间原因
永久代受 JVM 固定大小限制,元空间使用的本地内存,溢出几率小。
存放类的元数据,加载类数量不由 MaxPermSize 控制,而由系统空间控制,可加载更多类。
JDK8 合并 HotSpot 和 JRockit 的代码,JRockit 没有永久代。
元空间不指定大小时,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
对象在 Eden 区域分配,首次GC后若存活则进入 S0 或 S1,并且对象的年龄还会加 1,年龄超过设置(默认15)则晋升到老年代中。
OutOfMemoryError
GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。
MetaSpace:当元空间溢出时,就会引发此错误。
方法区
JVM 运行时数据区域的一块逻辑区域。
类加载时,存储类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区和永久代以及元空间关系
类似 Java 中接口和类的关系
方法区看作接口,永久代以及元空间为不同实现。
运行时常量池
常量池表(Constant Pool Table)
用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)
字面量包括整数、浮点数和字符串字面量。
符号引用包括类符号引用、字段符号引用、方法符号引用、接口方法符号。
常量池表会在类加载后存放到方法区的运行时常量池中。
运行时常量池是方法区的一部分,受到方法区内存的限制,也会抛出 OutOfMemoryError 错误。
字符串常量池
JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
原理
固定大小的HashTable ,容量为 StringTableSize(可以通过 -XX:StringTableSize 参数来设置)。
保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
JDK 1.7 将字符串常量池移动到堆中原因
永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。
Java 程序中通常会有大量的被创建的字符串等待回收,放在堆中,能更高效回收。
直接内存
一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
不属于虚拟机。
堆外内存
把内存对象分配在堆外的内存,这些内存直接受操作系统管理(而不是虚拟机),一定程度上减少垃圾回收对应用程序造成的影响。
HotSpot 虚拟机
对象的创建过程
类加载检查
检查类的符号引用是否能在常量池中定位,再检查类是否已被加载过、解析和初始化过,如没有就需要执行类加载过程。
分配内存
虚拟机将为新生对象分配内存。
对象所需的内存大小在类加载完成后便可确定。
方式
指针碰撞
适用场合:堆内存规整(即没有内存碎片)的情况下。
原理:内存用过和没用过的分两边,中间一个分界指针,分配时,向没用过区域移动指针即可。
使用该分配方式的 GC 收集器:Serial, ParNew
空闲列表
适用场合:堆内存不规整的情况下。
原理:虚拟机维护了会记录哪些内存块是可用的列表,分配时,更新列表即可。
使用该分配方式的 GC 收集器:CMS
Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩")。
内存分配并发问题
虚拟机采用两种方式来保证线程安全。
CAS+失败重试: CAS 是乐观锁的一种实现方式。
TLAB:首先在 TLAB 分配,当 TLAB 的内存不够时,再采用上述的 CAS 进行内存分配。
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
目的:保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置。
例如:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。
执行 init 方法
初始化后,一个真正可用的对象才算完全产生出来。
对象的内存布局
对象头
用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等)。
类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充
不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
原因:虚拟机要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。
对象的访问定位
Java 程序通过栈上的 reference 数据来操作堆上的具体对象,对象的访问方式由虚拟机实现而定。
句柄
Java 堆分配句柄池,reference 存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
优势:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
直接指针
使用直接指针访问,reference 中存储的直接就是对象的地址。
优势:速度快,它节省了一次指针定位的时间开销。
JVM垃圾回收
内存分配和回收原则
对象优先在 Eden 区分配
新生代无足够空间时,虚拟机通过 分配担保机制 把新生代的对象提前转移到老年代中。
大对象直接进入老年代
优化策略:减少新生代的垃圾回收频率和成本。
长期存活的对象将进入老年代
对象年龄(Age)计数器,每次GC,年龄就增加1,年龄超过设置(默认15)则晋升到老年代中。
主要进行 GC 的区域
部分收集 (Partial GC)
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
空间分配担保
为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
死亡对象判断方法
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数法
给对象中添加一个引用计数器,引用+1,引用失效-1,0代表无引用。
简单高效,但使用少,原因:很难解决对象之间循环引用的问题。
可达性分析算法
以 “GC Roots” 的对象为起点向下搜索,节点所走过的路径称为引用链,当对象到 GC Roots 没有任何引用链相连则证明此对象不可达。
可以作为 GC Roots 的对象
虚拟机栈(栈帧中的局部变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象
JNI(Java Native Interface)引用的对象
不可达对象在死亡前,仍需要经历两次标记才能真正死亡被回收:finalize 方法和队列标记。
引用类型总结
JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)
强引用(StrongReference)
我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象。
大部分引用实际上都是强引用,这是使用最普遍的引用。
软引用(SoftReference)
使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收。
可用来实现内存敏感的高速缓存。
可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
弱引用(WeakReference)
使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收。
弱引用与软引用的区别:只具有弱引用的对象拥有更短暂的生命周期。
虚引用(PhantomReference)
虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知。
主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别:虚引用必须和引用队列(ReferenceQueue)联合使用。
除了强引用外,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多。
废弃常量
没有任何对象引用的常量就是废弃常量,如果这时发生内存回收的话而且有必要的话,废弃常量就会被系统清理。
无用的类
条件
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足条件的无用的类进行回收,而不是必然被回收。
垃圾收集算法
标记-清除(Mark-and-Sweep)算法
首先标记(Mark)出所有不需要回收的对象,在标记完成后统一清除(Sweep)掉所有没有被标记的对象。
最基础的收集算法,后续的算法都是对其不足进行改进得到。
问题
效率问题:标记和清除两个过程效率都不高。
空间问题:标记清除后会产生大量不连续的内存碎片。
复制(Copying)算法
将内存分为大小相同的两块A和B,只使用A,当A使用完后,就将还存活的对象复制到B,然后再把A一次清理掉。
问题
可用内存变小:可用内存缩小为原来的一半。
不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
标记-整理(Mark-and-Compact)算法
标记过程与“标记-清除”算法一样,但后续是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
由于多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景。
分代收集算法
根据对象存活周期的不同将内存分为几块(新生代和老年代),以便根据各个年代的特点选择合适的垃圾收集算法。
当前虚拟机的垃圾收集都采用分代收集算法。
垃圾收集器
没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。
JDK 默认垃圾收集器
JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
JDK 9 ~ JDK20: G1
Serial 收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。
单线程收集器,只会使用一条线程去完成工作同时,必须暂停其他所有的工作线程( "Stop The World" ),直到它工作结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
优点:简单而高效(与其他收集器的单线程相比),运行在 Client 模式下的虚拟机的不错选择。
ParNew 收集器
Serial 收集器的多线程版本,运行在 Server 模式下的虚拟机的首选。
新生代采用标记-复制算法,老年代采用标记-整理算法。
Parallel Scavenge 收集器
几乎和 ParNew 收集器一样,但其关注点是吞吐量(高效率的利用 CPU),其它关注更多的是用户线程的停顿时间(提高用户体验)。
吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
新生代采用标记-复制算法,老年代采用标记-整理算法。
JDK1.8 默认使用的垃圾收集器是 Parallel Scavenge + Parallel Old。
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。
用途
在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备方案。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。
在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep)收集器
以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
步骤
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快。
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。
重新标记: 为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
优点
并发收集、低停顿。
缺点
对 CPU 资源敏感。
无法处理浮动垃圾。
使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
从 JDK9 开始,CMS 收集器已被弃用。
G1(Garbage-First) 收集器
面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
特点
并行与并发、分代收集、空间整合、可预测的停顿
步骤
初始标记、并发标记、最终标记、筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region。
从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
ZGC 收集器
与 CMS 中的 ParNew 和 G1 类似,ZGC 也采用标记-复制算法,不过 ZGC 对该算法做了重大改进。
ZGC 可以将暂停时间控制在几毫秒以内,且暂停时间不受堆内存大小的影响,出现 Stop The World 的情况会更少,但代价是牺牲了一些吞吐量。
ZGC 在 Java11 中引入,处于试验阶段,在 Java15 已经可以正式使用了,最大支持 16TB 的堆内存。但默认的垃圾回收器依然是 G1。
类文件结构
魔数(Magic Number)
每个 Class 文件的头 4 个字节称为魔数(Magic Number)。
唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。
Java 规范规定魔数为固定值:0xCAFEBABE。非此值,Java 虚拟机将拒绝加载它。
Class 文件版本号(Minor&Major Version)
魔数后的四个字节存储的是 Class 文件的版本号:第 5 ~ 6 个字节是次版本号,第 7 ~ 8 个字节是主版本号。
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。可以使用 javap -v 命令来快速查看 Class 文件的版本号信息。
常量池(Constant Pool)
主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1。
主要存放两大常量:字面量和符号引用。
字面量
接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。
符号引用
属于编译原理方面的概念。包括 类和接口的全限定名、字段的名称和描述符、方法的名称和描述符 这三类常量。
访问标志(Access Flags)
在常量池之后,紧接着的两个字节代表访问标志。
用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。
字段表集合(Fields)
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
方法表集合(Methods)
方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
属性表集合(Attributes)
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
类加载过程
类的生命周期
加载(Loading)、验证(Verification)、准备(Preparation)、
解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
验证、准备和解析这三个阶段可以统称为连接(Linking)。
类加载过程
系统加载 Class 类型的文件主要三步:加载->连接->初始化。
连接过程又可分为三步:验证->准备->解析。
加载
通过全类名获取定义此类的二进制字节流。
将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。
验证
确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求。
组成
文件格式验证(Class 文件格式检查)
元数据验证(字节码语义检查)
字节码验证(程序语义检查)
符号引用验证(类的正确性检查)
准备
正式为类变量分配内存并设置类变量初始值的阶段,在方法区中分配。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。
初始化
执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步。
必须对类进行初始化的6种情况
当遇到 new、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时。
使用 java.lang.reflect 包的方法对类进行反射调用时。
初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类)。
MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
就必须先使用 findStaticVarHandle 来初始化要调用的类。
就必须先使用 findStaticVarHandle 来初始化要调用的类。
当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时。
注意
<clinit> ()方法是编译之后自动生成的。
<clinit> () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。
类卸载
卸载类即该类的 Class 对象被 GC。
要求
该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
该类没有在其他任何地方被引用。
该类的类加载器的实例已被 GC。
类加载器
定义
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
每个 Java 类都有一个引用指向加载它的 ClassLoader。
数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
作用
加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。
除了加载类,还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。
加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。
类加载器
BootstrapClassLoader(启动类加载器)
最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级。
作用
加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 jar 包和类)及被 -Xbootclasspath 参数指定的路径下的所有类。
拓展
rt.jar:rt 代表“RunTime”,rt.jar是 Java 基础类库,常用内置库 java.xxx.*都在里面。
ExtensionClassLoader(扩展类加载器)
加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。
AppClassLoader(应用程序类加载器)
面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
自定义类加载器
需要继承 ClassLoader抽象类。
重写
loadClass(String name, boolean resolve) 方法
打破双亲委派模型
findClass(String name)方法
建议,不想打破双亲委派模型
线程上下文类加载器(ThreadContextClassLoader)
将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。
双亲委派模型
定义
ClassLoader 类使用委托模型来搜索类和资源。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。
面向对象编程设计原则:组合优于继承,多用组合少用继承。(非常经典)
执行流程
在类加载的时候,系统会首先判断当前类是否被加载过。
类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。
拓展
JVM 判定两个 Java 类是否相同的具体规则
类的全名和加载此类的类加载器都一致。
优点
保证了 Java 程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。
打破模型
自定义类加载器并重写 loadClass(String name, boolean resolve) 方法。
单纯依靠自定义类加载器没办法满足某些场景的要求,例如,有些情况下,高层的类加载器需要加载低层的加载器才能加载的类。
应用
Tomcat 服务器为了能够优先加载 Web 应用目录下的类。
SPI 接口的实现(如com.mysql.cj.jdbc.Driver)是由第三方供应商提供的,需要先加载。
JVM参数
堆内存相关
显式指定堆内存–Xms和-Xmx
-Xms<heap size>[unit]
-Xmx<heap size>[unit]
-Xmx<heap size>[unit]
heap size 表示要初始化内存的具体大小。
unit 表示要初始化内存的单位。单位为 “ g” (GB)、“ m”(MB)、“ k”(KB)。
显式新生代内存(Young Generation)
默认情况下,YG 的最小大小为 1310 MB,最大大小为 无限制。
方式
通过-XX:NewSize和-XX:MaxNewSize指定。
通过-Xmn<young size>[unit]指定。
通过 -XX:NewRatio=<int> 来设置老年代与新生代内存的比值。
GC调优策略
最大限度将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC。
显式指定永久代/元空间的大小
永久代
通过-XX:PermSize=N和-XX:MaxPermSize=N指定。
元空间
通过-XX:MetaspaceSize=N和-XX:MaxMetaspaceSize=N指定。
注意
Metaspace 的初始容量并不是 -XX:MetaspaceSize 设置,对于64位 JVM 来说,初始容量都是 21807104(约 20.8m)。
MetaspaceSize 表示 Metaspace 使用过程中触发 Full GC 的阈值,只对触发起作用。
垃圾收集相关
垃圾回收器
通过-XX:+Use<垃圾回收器标识>指定。
串行垃圾收集器(SerialGC)、并行垃圾收集器(ParallelGC)、CMS 垃圾收集器(ParNewGC)、G1 垃圾收集器(G1GC)
GC 日志记录
通过-XX:+Print<打印内容标识>指定。
# 必选
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M
# 打印基本 GC 信息
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 打印对象分布
-XX:+PrintTenuringDistribution
# 打印堆数据
-XX:+PrintHeapAtGC
# 打印Reference处理信息
# 强引用/弱引用/软引用/虚引用/finalize 相关的方法
-XX:+PrintReferenceGC
# 打印STW时间
-XX:+PrintGCApplicationStoppedTime
# 可选
# 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=50M
处理 OOM
JVM 提供了一些参数,将堆内存转储到一个物理文件中,以后可以用来查找泄漏。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"
-XX:+UseGCOverheadLimit
其他
- -server : 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM
- -XX:+UseStringDeduplication : Java 8u20 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 char [] 数组来优化堆内存。
- -XX:+UseLWPSynchronization: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。
- -XX:LargePageSizeInBytes: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。
- -XX:MaxHeapFreeRatio : 设置 GC 后, 堆空闲的最大百分比,以避免收缩。
- -XX:SurvivorRatio : eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6 设置每个 survivor 和 eden 之间的比例为 1:6。
- -XX:+UseLargePages : 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。
- -XX:+UseStringCache : 启用 String 池中可用的常用分配字符串的缓存。
- -XX:+UseCompressedStrings : 对 String 对象使用 byte [] 类型,该类型可以用纯 ASCII 格式表示。
- -XX:+OptimizeStringConcat : 它尽可能优化字符串串联操作。
JDK工具
命令行工具
jps (JVM Process Status): 类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。
jstat(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据。
jinfo (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息。
jmap (Memory Map for Java) : 生成堆转储快照。
jhat (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。
jstack (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
可视化分析工具
JConsole:Java 监视与管理控制台
Visual VM:多合一故障处理工具
JVM问题及调优案例
新特性
Java 8
Oracle 于 2014 发布了 Java8(jdk1.8),使用最多的 jdk 版本。
Interface 的方法可以用 default 或 static 修饰,为了解决接口的修改与现有的实现不兼容的问题。
functional interface 函数式接口,也称 SAM 接口,即 Single Abstract Method interfaces,有且只有一个抽象方法,但可以有多个非抽象方法的接口。
Lambda 表达式,最重要新特性。
Stream,java.util.stream 包下,可以检索(Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。
Optional,防止 NPE 问题。
Date-Time API,对java.util.Date强有力的补充,解决了 Date 类的大部分痛点。
《Java8 指南》中文翻译
Java 9
发布于 2017 年 9 月 21 日,最重要的改动是 Java 平台模块系统的引入。
JShell,是 Java 9 新增的一个实用工具。为 Java 提供了类似于 Python 的实时命令行交互工具。
模块系统,是Jigsaw Project的一部分,把模块化开发实践引入到了 Java 平台中,可以让我们的代码可重用性更好!
G1 成为默认垃圾回收器。
快速创建不可变集合,增加了List.of()、Set.of()、Map.of() 和 Map.ofEntries()等工厂方法来创建不可变集合。
String 存储结构优化,Java 8 及之前的版本,String 一直是用 char[] 存储。在 Java 9 之后,String 的实现改用 byte[] 数组存储字符串,节省了空间。
接口私有方法,接口的使用就更加灵活了,有点像是一个简化版的抽象类。
try-with-resources 增强,在 try-with-resources 语句中可以使用 effectively-final 变量。
Stream & Optional 增强
Stream 中增加了新的方法 ofNullable()、dropWhile()、takeWhile() 以及 iterate() 方法的重载方法。
Optional 类中新增了 ifPresentOrElse()、or() 和 stream() 等方法。
进程 API,增加了 java.lang.ProcessHandle 接口来实现对原生进程进行管理,尤其适合于管理长时间运行的进程。
响应式流 ( Reactive Streams )
变量句柄,是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等。
平台日志 API 改进,Java 9 允许为 JDK 和应用配置同样的日志实现。
CompletableFuture类增强,新增了几个新的方法(completeAsync ,orTimeout 等)。
I/O 流的新特性,增加了新的方法来读取和复制 InputStream 中包含的数据。
改进应用的安全性能,Java 9 新增了 4 个 SHA- 3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。
Java 10
发布于 2018 年 3 月 20 日,最知名的特性应该是 var 关键字(局部变量类型推断)的引入。
局部变量类型推断(var),var 并不会改变 Java 是一门静态类型语言的事实,编译器负责推断出类型。
垃圾回收器接口,通过引入一套纯净的垃圾收集器接口来将不同垃圾收集器的源代码分隔开。
G1 并行 Full GC,为了最大限度地减少 Full GC 造成的应用停顿的影响。
集合增强,List,Set,Map 提供了静态方法copyOf()返回入参集合的一个不可变拷贝。
Optional 增强,新增了orElseThrow()方法来在没有值时抛出指定的异常。
应用程序类数据共享(扩展 CDS 功能),允许应用类放置在共享存档中。
实验性的基于 Java 的 JIT 编译器、线程-局部管控、备用存储装置上的堆分配等
Java 11
于 2018 年 9 月 25 日正式发布,这是很重要的一个版本!这是据 Java 8 以后支持的首个长期版本,持续至 2026 年 9 月。
HTTP Client 标准化,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。
String 增强,增加了一系列的字符串处理方法。
Optional 增强,新增了isEmpty()方法来判断指定的 Optional 对象是否为空。
ZGC(可伸缩低延迟垃圾收集器),是一个可伸缩的、低延迟的垃圾收集器。处在实验阶段,只支持 Linux/x64 平台。
Lambda 参数的局部变量语法,允许开发者在 Lambda 表达式中使用 var 进行参数声明。
启动单文件源代码程序,允许使用 Java 解释器直接执行 Java 源代码。
新的垃圾回收器 Epsilon、低开销的 Heap Profiling、TLS1.3 协议、飞行记录器(Java Flight Recorder) 等。
Java 12
String 增强,增加了两个的字符串处理方法 indent() 和 transform() 。
Files 增强(文件比较),添加了方法( mismatch() )来比较两个文件。
数字格式化工具类,NumberFormat 新增了对复杂的数字进行格式化的支持。
Shenandoah GC,Redhat 主导开发的 Pauseless GC 实现,主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关等。落地可能性大。
G1 收集器优化,默认的垃圾收集器 G1 带来了两项更新:可中止的混合收集集合 和 及时返回未使用的已分配内存。
增强 Switch,使用类似 lambda 语法条件匹配成功后的执行块,不需要多写 break 。
instanceof 模式匹配,在判断是否属于具体的类型同时完成转换。
Java 13
增强 ZGC(释放未使用内存),将向操作系统返回被标识为长时间未使用的页面,这样它们将可以被其他进程重用。
SocketAPI 重构,将 Socket API 的底层进行了重写,在 Java 13 中是默认使用新的 Socket 实现。
FileSystems,添加了以下三种新方法,以便更容易地使用将文件内容视为文件系统的文件系统提供程序。
动态 CDS 存档,允许在 Java 应用程序执行结束时动态进行类归档。
预览新特性
文本块,支持两个 """ 符号中间的任何内容都会被解释为字符串的一部分,包括换行符。
增强 Switch(引入 yield 关键字到 Switch 中)
Java 14
空指针异常精准提示,通过 JVM 中添加参数,可以在空指针异常中获取更为详细的调用信息,更快的定位和解决问题。
转正
switch 的增强。
预览新特性
record 关键字,record 关键字可以简化 数据类(一个 Java 类一旦实例化就不能再修改)的定义方式。
文本块,引入了两个新的转义字符。
instanceof 增强。
从 Java11 引入的 ZGC 作为继 G1 过后的下一代 GC 算法,从支持 Linux 平台到 Java14 开始支持 MacOS 和 Windows。
移除了 CMS(Concurrent Mark Sweep) 垃圾收集器(功成而退)。
新增了 jpackage 工具,标配将应用打成 jar 包外,还支持不同平台的特性包。
Java 15
CharSequence,接口添加了一个默认方法 isEmpty() 来判断字符序列为空。
TreeMap,新引入了5个方法。
EdDSA(数字签名算法),新加入了一个安全性和性能都更强的基于 Edwards-Curve Digital Signature Algorithm (EdDSA)实现的数字签名算法。
隐藏类(Hidden Classes),是为框架(frameworks)所设计的,隐藏类不能直接被其他类的字节码使用,只能在运行时生成类并通过反射间接使用它们。
转正
ZGC,但默认的垃圾回收器依然是 G1。
文本块。
预览新特性
密封类,可以对继承或者实现它们的类进行限制,这样这个类就只能被指定的类继承。
instanceof 模式匹配。
Nashorn JavaScript 引擎彻底移除、DatagramSocket API 重构、禁用和废弃偏向锁(Biased Locking)等。
Java 16
在 2021 年 3 月 16 日正式发布,非长期支持(LTS)版本。
启用 C++ 14 语言特性,允许在 JDK 的 C++ 源代码中使用 C++14 语言特性,并提供在 HotSpot 代码中可以使用哪些特性的具体指导。
ZGC 并发线程堆栈处理,消除 ZGC 垃圾收集器中最后一个延迟源可以极大地提高应用程序的性能和效率。
弹性元空间,可将未使用的 HotSpot 类元数据(即元空间,metaspace)内存更快速地返回到操作系统,从而减少元空间的占用空间。
对基于值的类发出警告。
默认强封装 JDK 内部元素,默认强封装 JDK 的所有内部元素,但关键内部 API(例如 sun.misc.Unsafe)除外。
转正
打包工具。
instanceof 模式匹配。
记录类型。
孵化
向量 API(第一次孵化)。
外部内存访问 API(第三次孵化)。
外部链接器 API。
预览新特性
密封类
Unix-Domain 套接字通道、从 Mercurial 迁移到 Git、迁移到 GitHub、移植 Alpine Linux、Windows/AArch64 移植 等。
Java 17
在 2021 年 9 月 14 日正式发布,是一个长期支持(LTS)版本,到 2029 年 9 月份。Spring 6.x 和 Spring Boot 3.x 最低支持的就是 Java 17。
增强的伪随机数生成器,增加了新的接口类型和实现,使得开发者更容易在应用程序中互换使用各种 PRNG 算法。
弃用/删除
弃用 Applet API,用于编写在 Web 浏览器端运行的 Java 小程序。
删除 远程方法调用激活机制。
删除 实验性的 AOT 和 JIT 编译器。
弃用 安全管理器。
转正
密封类。
孵化
Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。
向量 API(第二次孵化)。
预览新特性
switch 的类型匹配,增加了类型匹配自动转换功能。
Java 18
在 2022 年 3 月 22 日正式发布,非长期支持版本。新特性少了很多。
默认字符集为 UTF-8,JDK 终于将 UTF-8 设置为默认字符集。
简易的 Web 服务器,可以使用 jwebserver 命令启动一个简易的静态 Web 服务器。
优化 Java API 文档中的代码片段,可以通过 @snippet 标签来引入代码段。
使用方法句柄重新实现反射核心,改进了 java.lang.reflect.Method、Constructor 的实现逻辑,使之性能更好,速度更快。
孵化
向量 API(第三次孵化)。
Foreign Function & Memory API(第二次孵化)。
互联网地址解析 SPI,定义了一个全新的 SPI(service-provider interface),用于主要名称和地址的解析。
Java 19
于 2022 年 9 月 20 日正式发布,非长期支持版本,有比较重要的新特性。
孵化
向量 API(第四次孵化)。
结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent。
预览新特性
外部函数和内存 API,Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。
虚拟线程(Virtual Thread),是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP)。
Java 20
于 2023 年 3 月 21 日发布,非长期支持版本。
孵化
作用域值(Scoped Values),可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。
结构化并发(第二次孵化)。
向量 API(第五次孵化)。
预览新特性
记录模式(Record Patterns,第二次预览),可对 record 的值进行解构,也就是更方便地从记录类(Record Class)中提取数据。
switch 模式匹配(第四次预览)。
外部函数和内存 API(第二次预览),Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。
虚拟线程(第二次预览)。
Java 21
于 2023 年 9 月 19 日 发布,是一个长期支持(LTS)版本,这是一个非常重要的版本,里程碑式。
转正
记录模式。
switch 的模式匹配。
虚拟线程。
预览新特性
字符串模板(String Templates),提供了一种更简洁、更直观的方式来动态构建字符串。
外部函数和内存 API(第三次预览)。
未命名模式和变量(预览),可以使用下划线 _ 表示未命名的变量以及模式匹配时不使用的组件,旨在提高代码的可读性和可维护性。
未命名类和实例 main 方法 (预览),主要简化了 main 方法的的声明。
序列化集合,引入了一种新的集合类型:Sequenced Collections(序列化集合,也叫有序集合),这是一种具有确定出现顺序的集合。
分代 ZGC,对 ZGC 进行了功能扩展,增加了分代 GC 功能。不过,默认是关闭的,需要通过配置打开。
计算机基础
网络
网络分层模型
OSI 七层模型
国际标准化组织提出一个网络分层模型。
详细介绍
OSI 七层模型虽厉害,但因复杂且效率低等原因,没有 TCP/IP 四层模型 更适用。
TCP/IP 四层模型
目前被广泛采用的一种模型,可以将 TCP / IP 模型看作是 OSI 七层模型的精简版本。
每一层包含的协议和核心技术
划分
应用层(Application layer)
提供两个终端设备上的应用程序之间信息交换的服务,它定义了信息交换的格式。
把应用层交互的数据单元称为报文。
传输层(Transport layer)
负责向两台终端设备进程之间的通信提供通用的数据传输服务。
网络层(Network layer)
负责为分组交换网上的不同主机提供通信服务。
选择合适的路由,使源主机运输层所传下来的分组,能通过网络层中的路由器找到目的主机。
网络接口层(Network interface layer)
看作是数据链路层和物理层的合体。
复杂的系统需要分层,因为每一层都需要专注于一类事情。网络分层的原因也是一样,每一层只专注于做一类事情。
分层原因
各层之间相互独立。
提高了灵活性和可替换性,高内聚、低耦合。
大问题化小,将复杂的问题分解为容易理解的更小的问题。
常见网络协议
应用层
HTTP(Hypertext Transfer Protocol,超文本传输协议)
基于 TCP 协议,是一种用于传输超文本和多媒体内容的协议。
SMTP(Simple Mail Transfer Protocol,简单邮件发送协议)
基于 TCP 协议,是一种用于发送电子邮件的协议。
POP3/IMAP(邮件接收协议)
基于 TCP 协议,两者都是负责邮件接收的协议。
FTP(File Transfer Protocol,文件传输协议)
基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。
Telnet(远程登陆协议)
基于 TCP 协议,用于通过一个终端登陆到其他服务器。
SSH(Secure Shell Protocol,安全的网络传输协议)
基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务。
RTP(Real-time Transport Protocol,实时传输协议)
通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能。
DNS(Domain Name System,域名管理系统)
基于 UDP 协议,用于解决域名和 IP 地址的映射问题。
传输层
TCP(Transmission Control Protocol,传输控制协议 )
提供 面向连接 的,可靠的数据传输服务。
UDP(User Datagram Protocol,用户数据协议)
提供 无连接 的,尽最大努力的数据传输服务(不保证数据传输的可靠性),简单高效。
网络层
IP(Internet Protocol,网际协议)
TCP/IP 协议中最重要的协议之一。分为两种:IPv4 和 IPv6。
主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。
ARP(Address Resolution Protocol,地址解析协议)
解决网络层地址和链路层地址之间的转换问题。
ICMP(Internet Control Message Protocol,互联网控制报文协议)
一种用于传输网络状态和错误消息的协议,常用于网络诊断和故障排除。
NAT(Network Address Translation,网络地址转换协议)
应用于内部网到外部网的地址转换过程中。
OSPF(Open Shortest Path First,开放式最短路径优先)
一种内部网关协议(Interior Gateway Protocol,IGP),也是广泛使用的一种动态路由协议。
基于链路状态算法,考虑了链路的带宽、延迟等因素来选择最佳路径。
RIP(Routing Information Protocol,路由信息协议)
一种内部网关协议(Interior Gateway Protocol,IGP),也是一种动态路由协议。
基于距离向量算法,使用固定的跳数作为度量标准,选择跳数最少的路径作为最佳路径。
BGP(Border Gateway Protocol,边界网关协议)
一种用来在路由选择域之间交换网络层可达性信息(Network Layer Reachability Information,NLRI)的路由选择协议。
HTTP
过程
在浏览器中输入指定网页的 URL。
浏览器通过 DNS 协议,获取域名对应的 IP 地址。
浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求。
浏览器在 TCP 连接上,向服务器发送一个 HTTP 请求报文,请求获取网页的内容。
服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。
浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,
同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。
同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。
浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。
状态码
1xx Informational(信息性状态码)
平时你大概率不会碰到。
2xx Success(成功状态码)
200 OK:请求被成功处理。
201 Created:请求被成功处理并且在服务端创建了一个新的资源。
202 Accepted:服务端已经接收到了请求,但是还未处理。
204 No Content:服务端已经成功处理了请求,但是没有返回任何内容。
3xx Redirection(重定向状态码)
301 Moved Permanently:资源被永久重定向了。
302 Found:资源被临时重定向了。
4xx Client Error(客户端错误状态码)
400 Bad Request:发送的 HTTP 请求存在问题。
401 Unauthorized:未认证却请求需要认证之后才能访问的资源。
403 Forbidden:直接拒绝 HTTP 请求,不处理。
404 Not Found:你请求的资源未在服务端找到。
409 Conflict:表示请求的资源与服务端当前的状态存在冲突,请求无法被处理。
5xx Server Error(服务端错误状态码)
500 Internal Server Error:服务端出问题了(通常是服务端出 Bug 了)。
502 Bad Gateway:我们的网关将请求转发到服务端,但是服务端返回的却是一个错误的响应。
Header
Accept
能够接受的回应内容类型(Content-Types)。
Accept-Charset
能够接受的字符集。
Accept-Datetime
能够接受的按照时间来表示的版本。
Accept-Encoding
能够接受的编码方式列表。参考 HTTP 压缩。
Accept-Language
能够接受的回应内容的自然语言列表。
Authorization
用于超文本传输协议的认证的认证信息。
Cache-Control
用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令。
Connection
该浏览器想要优先使用的连接类型。
Content-Length
以 八位字节数组 (8 位的字节)表示的请求体的长度。
Content-MD5
请求体的内容的二进制 MD5 散列值,以 Base64 编码的结果。
Content-Type
请求体的 多媒体类型 (用于 POST 和 PUT 请求中)。
Cookie
之前由服务器通过 Set- Cookie (下文详述)发送的一个 超文本传输协议 Cookie。
Date
发送该消息的日期和时间(按照 RFC 7231 中定义的"超文本传输协议日期"格式来发送)。
Expect
表明客户端要求服务器做出特定的行为。
From
发起此请求的用户的邮件地址。
Host
服务器的域名(用于虚拟主机 ),以及服务器所监听的传输控制协议端口号。
If-Match
仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。
If-Modified-Since
允许在对应的内容未被修改的情况下返回 304 未修改( 304 Not Modified )
If-None-Match
允许在对应的内容未被修改的情况下返回 304 未修改( 304 Not Modified )
If-Range
如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体
If-Unmodified-Since
仅当该实体自某个特定时间已来未被修改的情况下,才发送回应。
Max-Forwards
限制该消息可被代理及网关转发的次数。
Origin
发起一个针对 跨来源资源共享 的请求。
Pragma
与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。
Proxy-Authorization
用来向代理进行认证的认证信息。
Range
仅请求某个实体的一部分。字节偏移以 0 开始。参见字节服务。
Referer
表示浏览器所访问的前一个页面,正是那个页面上的某个链接将浏览器带到了当前所请求的这个页面。
TE
浏览器预期接受的传输编码方式:可使用回应协议头 Transfer-Encoding 字段中的值;
Upgrade
要求服务器升级到另一个协议。
User-Agent
浏览器的浏览器身份标识字符串
Via
向服务器告知,这个请求是由哪些代理发出的。
Warning
一个一般性的警告,告知,在实体内容体中可能存在错误。
HTTP 和 HTTPS
HTTP 协议
定义
超文本传输协议(Hypertext Transfer Protocol),无状态(stateless)协议。
主要是来规范浏览器和服务器端的行为的。
通信过程
服务器在 80 端口等待客户的请求。
浏览器发起到服务器的 TCP 连接(创建套接字 Socket)。
服务器接收来自浏览器的 TCP 连接。
浏览器(HTTP 客户端)与 Web 服务器(HTTP 服务器)交换 HTTP 消息。
关闭 TCP 连接。
优点
扩展性强、速度快、跨平台支持性好。
HTTPS 协议
定义
HTTPS 协议(Hyper Text Transfer Protocol Secure)基于 HTTP,使用 SSL/TLS 协议用作加密和安全认证。
SSL 通道通常使用基于密钥的加密算法,密钥长度通常是 40 比特或 128 比特。
优点
保密性好、信任度高。
SSL/TLS 协议
全称为 SSL(Secure Sockets Layer)/TLS(Transport Layer Security)协议。
SSL 指安全套接字协议,首次发布于1996年就是 3.0 版本。
在 1999 年,SSL 3.0 进一步升级,新版本被命名为 TLS 1.0。两者混称为 SSL/TLS。
原理
非对称加密
一个公钥,一个私钥。
公私钥对的生成算法依赖于单向陷门函数。
对称加密
唯一密钥
公钥传输的信赖性
证书颁发机构(CA,Certificate Authority)
默认是受信任的第三方。
CA 会给各个服务器颁发证书,证书存储在服务器上,并附有 CA 的数字签名。
数字签名
CA 在给服务器颁发证书时,使用散列+加密的组合技术,在证书上盖个章,以此来提供验伪的功能。
带有证书的公钥传输机制
区别
端口号:HTTP 默认是 80,HTTPS 默认是 443。
URL 前缀:HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://。
安全性和资源消耗:HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
SEO(搜索引擎优化):使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响。
HTTP/1.1 和 HTTP/2.0 区别
多路复用(Multiplexing):HTTP/2.0 在同一连接上可以同时传输多个请求和响应,互不干扰。HTTP/1.1 则使用串行方式。
二进制帧(Binary Frames):HTTP/2.0 使用二进制帧进行数据传输,而 HTTP/1.1 则使用文本格式的报文。
头部压缩(Header Compression):HTTP/1.1 支持Body压缩,Header不支持压缩。HTTP/2.0 支持对Header压缩。
服务器推送(Server Push):HTTP/2.0 支持服务器推送,可以在客户端请求一个资源时,将其他相关资源一并推送给客户端。
HTTP/2.0 和 HTTP/3.0 区别
传输协议:HTTP/2.0 是基于 TCP 协议实现的,HTTP/3.0 新增了 QUIC(Quick UDP Internet Connections) 协议来实现可靠的传输。
队头阻塞:由于 QUIC 协议的特性,HTTP/3.0 在一定程度上解决了队头阻塞(Head-of-Line blocking, 简写:HOL blocking)问题。
错误恢复:HTTP/3.0 具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传。
安全性:HTTP/3.0 基于 QUIC 协议,包含了内置的加密和身份验证机制,可以提供更强的安全性。
保存用户状态方式
Session 机制,作用就是通过服务端记录用户的状态。
Cookie 被禁用怎么办?
最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。
URI 和 URL
URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。
URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI。
Cookie 和 Session 区别
Session 的主要作用就是通过服务端记录用户的状态。
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
GET 和 POST 区别
语义(主要区别):GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源。
幂等:GET 请求是幂等的,POST 请求是不幂等的。
格式:GET 请求的参数通常放在 URL 中,POST 请求的参数通常放在请求体(body)中。
缓存:由于 GET 请求是幂等的,可以缓存。而 POST 请求则不适合被缓存。
安全性:GET 请求相比 POST 请求更容易泄露敏感数据。
WebSocket
定义
一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。
在 2008 年诞生,2011 年成为国际标准,几乎所有主流较新版本的浏览器都支持该协议。
本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。
客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
应用场景
视频弹幕、实时消息推送、实时游戏对战、多用户协同编辑、社交聊天等。
WebSocket 和 HTTP
WebSocket 和 HTTP 两者都是基于 TCP 的应用层协议,都可以在网络中传输数据。
区别
WebSocket 是一种双向实时通信协议,而 HTTP 是一种单向通信协议。
WebSocket 使用 ws:// 或 wss:// 作为协议前缀,HTTP 使用 http:// 或 https:// 作为协议前缀。
WebSocket 可以支持扩展,用户可以扩展协议,实现部分自定义的子协议,如支持压缩、加密等。
WebSocket 通信数据格式比较轻量,而 HTTP 通信每次都要携带完整的头部,网络开销较大。
工作过程
客户端向服务器发送一个 HTTP 请求,请求头中包含 Upgrade: websocket 和 Sec-WebSocket-Key等字段,表示要求升级协议为 WebSocket;
服务器收到请求后会升级协议,若支持 WebSocket,它将回复状态码(101)及对应响应头来表示成功升级成功。
客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的以帧(frames)形式的数据传输。
客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。
另外,建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。
SSE 与 WebSocket
SSE 与 WebSocket 作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息。
区别
SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。
SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。
SSE 默认支持断线重连;WebSocket 则需要自己实现。
SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。
当只需要一些服务器操作的更新。比如:站内信、未读消息数、状态更新、股票行情、监控数量等场景,SEE 更加有优势。
PING
PING 命令是一种常用的网络诊断工具,经常用来测试网络中主机之间的连通性和网络延迟。
结果结构
ICMP Echo Request(请求报文)信息:序列号、TTL(Time to Live)值。
目标主机的域名或 IP 地址:输出结果的第一行。
往返时间(RTT,Round-Trip Time):从发送 ICMP(请求报文)到接收到 ICMP(响应报文)的总时间,用来衡量网络连接的延迟。
统计结果(Statistics):包括发送/接收的 ICMP 数据包数量、丢包率、往返时间(RTT)的最小、平均、最大和标准偏差值。
注意
禁用了对 ICMP 请求的回复,这样会导致无法得到正确的响应。
原理
基于网络层的 ICMP(Internet Control Message Protocol,互联网控制报文协议),其主要原理就是通过在网络上发送和接收 ICMP 报文实现的。
ICMP 报文类型
查询报文类型:向目标主机发送请求并期望得到响应。PING属于此类型。
差错报文类型:向源主机发送错误信息,用于报告网络中的错误情况。
DNS
DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是域名和 IP 地址的映射问题。
设计采用的是分布式、层次数据库结构,DNS 是应用层协议,它可以在 UDP 或 TCP 协议之上运行,端口为 53 。
服务器分类
根 DNS 服务器。根 DNS 服务器提供 TLD 服务器的 IP 地址。目前世界上只有 13 组根服务器,我国境内目前仍没有根服务器。
顶级域 DNS 服务器(TLD 服务器)。顶级域是指域名的后缀,如com、org、net和edu等。
权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录。
本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。
注意
每组根服务器都有多个服务器,截止到 2023 年底,所有根服务器之和达到了 600 多台,未来还会继续增加。
查询解析过程(两种模式)
迭代
实践中常采用的方式,从请求主机到本地 DNS 服务器的查询是递归的,其余的查询时迭代的。
递归
具体过程和迭代类似,只是顺序有所不同。
报文格式
报文分为查询和回答报文,两种形式的报文结构相同。
DNS 记录
在响应查询时,需要查询自己的数据库,数据库中的条目被称为资源记录(Resource Record,RR)。
RR 提供了主机名到 IP 地址的映射。RR 是一个包含了Name, Value, Type, TTL四个字段的四元组。
TTL是该记录的生存时间,它决定了资源记录应当从缓存中删除的时间。
Name和Value字段的取值取决于Type。
DNS 劫持
DNS 劫持有时也被称为 DNS 重定向、DNS 欺骗或 DNS 污染。
一种网络攻击,它通过修改 DNS 服务器的解析结果,使用户访问的域名指向错误的 IP 地址。
TCP 与 UDP
区别
是否面向连接:UDP 在传送数据之前不需要先建立连接。
是否是可靠传输:UDP 传输不可靠,TCP 传输可靠。
是否有状态:TCP 传输是有状态的,而 UDP 是无状态服务。
传输效率:由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。
传输形式:TCP 是面向字节流的,UDP 是面向报文的。
首部开销:TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。
是否提供广播或多播服务:TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多;
应用
TCP
用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。
HTTP 协议(HTTP/3.0 之前)、HTTPS 协议、FTP 协议、SMTP 协议、POP3/IMAP 协议、Telnet 协议、SSH 协议等等。
UDP
一般用于即时通信,比如:语音、 视频、直播等等。
HTTP 协议(HTTP/3.0 )、DHCP 协议、DNS 域名系统等等。
HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 基于 UDP 的 QUIC 协议 。
TCP
建立连接-TCP 三次握手
一次握手:客户端发送带有 SYN(SEQ=x)标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务器的确认。
二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态。
三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入ESTABLISHED 状态,完成 TCP 三次握手。
目的
建立可靠的通信信道。
定义
SYN 同步序列编号(Synchronize Sequence Numbers) 是 TCP/IP 建立连接时使用的握手信号。
ACK 确认字符(Acknowledgement),是 TCP/IP 建立连接时使用的已确认接收无误的信号。
断开连接-TCP 四次挥手
第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后客户端进入 FIN-WAIT-1 状态。
第二次挥手:服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。
然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。
第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入TIME-WAIT状态,
服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。
此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。
此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。
目的
TCP 是全双工通信,可以双向传输数据,避免一方数据没有发完就关闭通道。
服务器发送的 ACK 和 FIN 不能合并原因
可能还有一些数据没有发完,先回复 ACK,等数据发完,再回复 FIN。
第二次挥手时,客户端没有收到 ACK 确认,会重新发送 FIN 请求。
第四次挥手客户端需要等待 2*MSL时间原因
防止 Server 没有收到 ACK 而不断重发 FIN。
定义
MSL(Maximum Segment Lifetime) : 一个片段在网络中最大的存活时间,2MSL 就是一个发送和一个回复所需的最大时间。
FIN(Finish):TCP 协议中的 Finish 标志,用于触发连接的关闭过程。
保证传输可靠性
基于数据块传输、对失序数据包重新排序以及去重、校验和、重传机制、流量控制、拥塞控制。
流量控制
利用滑动窗口实现流量控制。
为了控制发送方发送速率,保证接收方来得及接收。
如果接收方处理不过来,就只能把处理不过来的数据存在 接收缓冲区(Receiving Buffers) 里(失序的数据包也会被存放在缓存区里)。
如果缓存区满了发送方还在狂发数据的话,接收方只能把收到的数据包丢掉。
如果缓存区满了发送方还在狂发数据的话,接收方只能把收到的数据包丢掉。
TCP 为全双工(Full-Duplex, FDX)通信,两端各有一个发送缓冲区与接收缓冲区,两端都各自维护一个发送窗口和一个接收窗口。
接收窗口的大小是根据接收端处理数据的速度动态调整的。
拥塞控制
若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。
拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。
拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。
拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。
为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。
算法
采用了四种算法,即 慢开始、 拥塞避免、快重传 和 快恢复。
在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。
在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。
慢开始: 先探测一下,即由小到大逐渐增大发送,cwnd 初始值为 1,每个传播轮次加倍,从而避免引起网络阻塞。
拥塞避免:让拥塞窗口 cwnd 缓慢增大,即每经过一个往返时间 RTT 就把发送方的 cwnd 加 1。
快重传与快恢复(fast retransmit and recovery,FRR):一种拥塞控制算法,能快速恢复丢失的数据包。
ARQ 协议
自动重传请求(Automatic Repeat-reQuest,ARQ)是 OSI 模型中数据链路层和传输层的错误纠正协议之一。
它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。
它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。
ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。
停止等待 ARQ 协议
为了实现可靠传输的,原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。
连续 ARQ 协议
可提高信道利用率,发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。
优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。
缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。
超时重传原理
当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。
定义
RTT(Round Trip Time):往返时间,也就是数据包从发出去到收到对应 ACK 的时间。
RTO(Retransmission Time Out):重传超时时间,即从数据发送时刻算起,超过这个时间便执行重传。
计算
RTO 的确定是一个关键问题,因为它直接影响到 TCP 的性能和效率。
动态地调整 RTO 算法:加权移动平均(EWMA)算法,Karn 算法,Jacobson 算法等。
这些算法都是根据往返时延(RTT)的测量和变化来估计 RTO 的值。
这些算法都是根据往返时延(RTT)的测量和变化来估计 RTO 的值。
IP
IP(Internet Protocol,网际协议) 是 TCP/IP 协议中最重要的协议之一,属于网络层的协议。
作用
定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。
分类
IPv4
IPv4(Internet Protocol version 4) 是目前广泛使用的 IP 地址版本,其格式是四组由点分隔的数字。32位,总量约42亿。
IPv6
IPv6(Internet Protocol version 6)为了解决 IP 地址耗尽问题,使用由单或双冒号分隔的一组数字和字母的复杂格式。128位,总量2^128。
IPv6 优势
无状态地址自动配置(Stateless Address Autoconfiguration,简称 SLAAC),无需依赖 DHCP 服务器,简化了网络配置和管理。
NAT(Network Address Translation,网络地址转换) 成为可选项,地址资源充足,每个设备都可以是独立地址。
对标头结构进行了改进:IPv6 标头结构相较于 IPv4 更加简化和高效,减少了处理开销,提高了网络性能。
可选的扩展头:允许在 IPv6 标头中添加不同的扩展头(Extension Headers),用于实现不同类型的功能和选项。
ICMPv6(Internet Control Message Protocol for IPv6):IPv6 中的 ICMPv6 相较于 IPv4 中的 ICMP 有了一些改进。
IP 地址
定义
每个连入互联网的设备或域(如计算机、服务器、路由器等)都被分配一个 IP 地址(Internet Protocol address),作为唯一标识符。
每个 IP 地址都是一个字符序列,如 192.168.1.1(IPv4)、2001:0db8:85a3:0000:0000:8a2e:0370:7334(IPv6) 。
每个 IP 地址都是一个字符序列,如 192.168.1.1(IPv4)、2001:0db8:85a3:0000:0000:8a2e:0370:7334(IPv6) 。
工作方式
网络设备根据目的 IP 地址来判断数据包的目的地,并将数据包转发到正确的目的地网络或子网络,从而实现了设备间的通信。
IP 地址过滤
限制或阻止特定 IP 地址或 IP 地址范围的访问。
获取客户端真实 IP
应用层方法 :通过 X-Forwarded-For 请求头获取,但容易被伪造。
传输层方法
利用 TCP Options 字段承载真实源 IP 信息,但需要双方都进行改造。
利用 Proxy Protocol 协议来传递客户端 IP 和 Port 信息。
网络层方法:隧道 +DSR 模式,适用于任何协议,但实施麻烦,很少用。
NAT
NAT(Network Address Translation,网络地址转换) 主要用于在不同网络之间转换 IP 地址。
允许将私有 IP 地址映射为公有 IP 地址或者反向映射,从而实现局域网内的多个设备通过单一公有 IP 地址访问互联网。
不光可以缓解 IPv4 地址资源短缺的问题,还可以隐藏内部网络的实际拓扑结构,从而提高了内部网络的安全性。
原理
NAT 转换表,IP + 端口的映射关系。
特点
NAT 协议通过对 WAN 屏蔽 LAN,有效地缓解了 IPv4 地址分配压力。
LAN 主机 IP 地址的变更,无需通告 WAN。
WAN 的 ISP 变更接口地址时,无需通告 LAN 内主机。
LAN 主机对 WAN 不可见,不可直接寻址,可以保证一定程度的安全性。
NAT 协议在 LAN 以外标识一个内部主机时,使用的是端口号,因为 IP 地址都是相同的。不规范。
ARP
MAC 地址
媒体访问控制地址(Media Access Control Address),一切网络设备都由 MAC 地址唯一标识。
MAC 地址是身份证号,IP 地址是邮政地址。MAC 地址也有一些别称,如 LAN 地址、物理地址、以太网地址等。
ARP 地址解析协议(Address Resolution Protocol),解决的是网络层地址和链路层地址之间的转换问题。
在协议栈中属于一个偏底层的、非常重要的、又非常简单的通信协议。
原理
ARP 表
每个网络设备都维护了一个 ARP 表,记录其他网络设备的 IP 地址-MAC 地址映射关系,并以 <IP, MAC, TTL> 三元组的形式存储。
ARP 协议是一个广播问询,单播响应协议。
场景
同一局域网内的 MAC 寻址。
不同局域网内的 MAC 寻址。
网络攻击
IP 欺骗
伪造某台主机的 IP 地址的技术。
原理
伪造大量的 IP 地址,向目标发送 RST 数据,使服务器不对合法用户服务。
缓解
入口过滤是一种数据包过滤形式,通常在网络边缘设备上实施,用于检查传入的 IP 数据包并确定其源标头。
SYN Flood(洪水)
互联网上最原始、最经典的 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击之一。
目的
耗尽可用服务器资源,致使服务器无法传输合法流量。
原理
利用了 TCP 协议的三次握手机制,攻击者通常利用工具或者控制僵尸主机向服务器发送海量的变源 IP 地址或变源端口的 TCP SYN 报文,
服务器响应了这些报文后就会生成大量的半连接,当系统资源被耗尽后,服务器将无法提供正常的服务。
服务器响应了这些报文后就会生成大量的半连接,当系统资源被耗尽后,服务器将无法提供正常的服务。
形式
直接攻击: 不伪造 IP 地址的 SYN 洪水攻击称为直接攻击。
欺骗攻击: 恶意用户还可以伪造其发送的各个 SYN 数据包的 IP 地址,以便阻止缓解措施并加大身份暴露难度。
分布式攻击(DDoS): 如果使用僵尸网络发起攻击,则追溯攻击源头的可能性很低。
缓解
扩展积压工作队列:利用每个操作系统都允许一定数量的半开连接,内存耗尽,总比直接拒绝服务强。
回收最先创建的 TCP 半开连接:在填充积压工作后覆盖最先创建的半开连接。
SYN Cookie:要求服务器创建 Cookie,为避免在填充积压工作时断开连接,服务器使用 SYN-ACK 数据包响应每一项连接请求,
而后从积压工作中删除 SYN 请求,同时从内存中删除请求,保证端口保持打开状态并做好重新建立连接的准备。
而后从积压工作中删除 SYN 请求,同时从内存中删除请求,保证端口保持打开状态并做好重新建立连接的准备。
UDP Flood(洪水)
一种拒绝服务攻击,将大量的用户数据报协议(UDP)数据包发送到目标服务器,目的是压倒该设备的处理和响应能力。
服务器接收 UDP 数据包处理步骤
服务器首先检查是否正在运行正在侦听指定端口的请求的程序。
如果没有程序在该端口接收数据包,则服务器使用 ICMP(ping)数据包进行响应,以通知发送方目的地不可达。
原理
利用服务器响应发送到其中一个端口的 UDP 数据包所采取的步骤。
缓解
操作系统部分限制了 ICMP 报文的响应速率,以中断需要 ICMP 响应的 DDoS 攻击。
HTTP Flood(洪水)
一种大规模的 DDoS(Distributed Denial of Service,分布式拒绝服务)攻击,旨在利用 HTTP 请求使目标服务器不堪重负。
形式
HTTP GET 攻击:在这种攻击形式下,多台计算机或其他设备相互协调,向目标服务器发送对图像、文件或其他资产的多个请求。
HTTP POST 攻击:一般而言,在网站上提交表单时,服务器必须处理传入的请求并将数据推送到持久层。
缓解
对发出请求的设备实施质询,以测试它是否是机器人。
使用 Web 应用程序防火墙 (WAF)、管理 IP 信誉数据库以跟踪和有选择地阻止恶意流量,以及由工程师进行动态分析。
DNS Flood(洪水)
一种分布式拒绝服务(DDoS)攻击,攻击者用大量流量淹没某个域的 DNS 服务器,以尝试中断该域的 DNS 解析。
DNS 放大攻击
反射并放大不安全 DNS 服务器的流量,以便隐藏攻击的源头并提高攻击的有效性。
防护
唯一方法是使用一个超大型、高度分布式的 DNS 系统,以便实时监测、吸收和阻止攻击流量。
TCP 重置攻击
攻击者通过向通信的一方或双方发送伪造的消息,告诉它们立即断开连接,从而使通信双方连接中断。
一般情况,仅长链接有效。
防护
暂无
中间人攻击
攻击中间人攻击英文名叫 Man-in-the-MiddleAttack,简称「MITM 攻击」。
原理
攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据。
第三方验证
摘要算法
消息摘要算法
MD5 是将任意长度的文章转化为一个 128 位的散列值,易碰撞。
安全散列算法(SHA)
数字签名和证书
对称加密
加密方与解密方使用同一钥匙(秘钥)。
算法
DES、IDEA、AES、SM1、SM4
非对称加密
发送方与接收方使用的不同的秘钥。
算法
RSA(性能好)、ECC(最安全)、SM2(国标)
散列算法
MD5、SHA、SM3
DDOS
分布式拒绝服务(Distributed Denial of Service),处于不同位置的多个攻击者同时向一个或数个目标发动攻击,是一种分布的、协同的大规模攻击方式。
方式
ICMP Flood、UDP Flood、NTP Flood、SYN Flood、CC 攻击、DNS Query Flood等等。
应对
高防服务器、黑名单、DDoS 清洗、CDN 加速等等。
操作系统
定义
操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基石。
操作系统本质上是一个运行在计算机上的软件程序 ,主要用于管理计算机硬件和软件资源。
操作系统存在屏蔽了硬件层的复杂性。
操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存、硬件设备、文件系统的管理以及应用程序的管理。
功能
进程和线程的管理:进程的创建、撤销、阻塞、唤醒,进程间的通信等。
存储管理:内存的分配和管理、外存(磁盘等)的分配和管理等。
文件管理:文件的读、写、创建及删除等。
设备管理:完成设备(输入输出设备和外部存储设备等)的请求或释放,以及设备启动等功能。
网络管理:操作系统负责管理计算机网络的使用。
安全管理:用户的身份认证、访问控制、文件加密等,以防止非法用户对系统资源的访问和操作。
种类
Windows、Unix、Linux、Mac OS 等等。
用户态和内核态
用户态(User Mode) : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。
内核态(Kernel Mode):内核态运行的进程几乎可以访问计算机的任何资源,拥有非常高的权限。
进入内核态需要付出较高的开销(需要进行一系列的上下文切换和权限检查)。
同时具有用户态和内核态主要是为了保证计算机系统的安全性、稳定性和性能。
用户态切换到内核态的 3 种方式:系统调用(Trap)、中断(Interrupt)、异常(Exception)。
进程和线程
定义
进程(Process) 是指计算机中正在运行的一个程序实例。
线程(Thread) 也被称为轻量级进程,更加轻量。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
区别
线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。
基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
线程执行开销小,但不利于资源的管理和保护;而进程正相反。
线程必要性
进程切换是一个开销很大的操作,线程切换的成本较低。
线程更轻量,一个进程可以创建多个线程。
多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。
同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核。
线程间的同步的方式
线程同步是两个或多个共享关键资源的线程的并发执行。同步线程可以避免关键的资源使用冲突。
互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。
读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
信号量(Semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。
事件(Event,Wait/Notify):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
PCB
PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。
主要内容
进程的描述信息,包括进程的名称、标识符等等。
进程的调度信息,包括进程阻塞原因、进程状态、进程优先级等等。
进程对资源的需求情况,包括 CPU 时间、内存空间、I/O 设备等等。
进程打开的文件信息,包括文件描述符、文件类型、打开模式等等。
处理机的状态信息,包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
进程的状态
创建状态(new):进程正在被创建,尚未到就绪状态。
就绪状态(ready):进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
运行状态(running):进程正在处理器上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
阻塞状态(waiting):又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。
结束状态(terminated):进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
进程间的通信方式
管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
有名管道(Named Pipes) : 以磁盘文件的方式存在,可以实现本机任意两个进程通信。
严格遵循 先进先出(First In First Out)。
信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
消息队列(Message Queuing)
消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。
消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。
信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。
套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。
进程的调度算法
先到先服务调度算法(FCFS,First Come, First Served)
短作业优先的调度算法(SJF,Shortest Job First)
时间片轮转调度算法(RR,Round-Robin)
多级反馈队列调度算法(MFQ,Multi-level Feedback Queue)
优先级调度算法(Priority)
僵尸进程:子进程已经终止,但是其父进程仍在运行,且父进程不释放子进程,导致其一直被占用。
孤儿进程:一个进程的父进程已经终止或者不存在,但是该进程仍在运行。
死锁
死锁(Deadlock),多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
必要条件
互斥:资源必须处于非共享模式,即一次只有一个进程可以使用。
占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
非抢占:资源不能被抢占。
循环等待。
解决方法
预防
采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。
破坏第一个条件(互斥条件):简单,磁盘可以使用该方法,但绝大数场景不适用。
破坏第二个条件(占有并等待):静态分配策略,逻辑简单,实现容易,但严重地降低了资源利用率。
破坏第三个条件(非抢占):剥夺式调度算法,仅适用于 主存资源 和 处理器资源 的分配,会导致 资源利用率下降。
破坏第四个条件(循环等待):层次分配策略,所有的资源被分成了多个层次,一个进程需要层层申请资源。
Dijkstra 的银行家算法:改善了 资源使用率低的问题 ,但是需要花费较多的时间。
内存管理
作用
内存的分配与回收:对进程所需的内存进行分配和释放,malloc 函数:申请内存,free 函数:释放内存。
地址转换:将程序中的虚拟地址转换成内存中的物理地址。
内存扩充:当系统没有足够的内存时,利用虚拟内存技术或自动覆盖技术,从逻辑上扩充内存。
内存映射:将一个文件直接映射到进程的进程空间中,这样可以通过内存指针用读写内存的办法直接存取文件内容,速度更快。
内存优化:通过调整内存分配策略和回收算法来优化内存使用效率。
内存安全:保证进程之间使用内存互不干扰,避免一些恶意程序通过修改内存来破坏系统的安全性。
内存碎片
由内存的申请和释放产生的。
分类
内部内存碎片(Internal Memory Fragmentation,简称为内存碎片):已经分配给进程使用但未被使用的内存。
外部内存碎片(External Memory Fragmentation,简称为外部碎片):那些并未分配给进程但又不能使用的内存。
内存碎片会导致内存利用率下降。
方式
连续内存管理
为一个用户程序分配一个连续的内存空间,内存利用率一般不高。
方式
块式管理,早期计算机操作系统采用的方式,存在严重的内存碎片问题。
伙伴系统(Buddy System)算法,一种经典的连续内存分配算法,可以有效解决外部内存碎片的问题。
非连续内存管理
允许一个程序使用的内存分布在离散或者说不相邻的内存中,相对更加灵活一些。
方式
段式管理:以段(—段连续的物理内存)的形式管理/分配物理内存。
页式管理:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页。
段页式管理机制:结合段和页的管理方式,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
虚拟内存
定义
计算机系统内存管理非常重要的一个技术, 本质只是逻辑存在的,是一个假想出来的内存空间。
作用
作为进程访问主存(物理内存)的桥梁并简化内存管理。
能力
隔离进程:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。
提升物理内存利用率:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
简化内存管理:进程都有一个一致且私有的虚拟地址空间,可以借助虚拟地址空间访问物理内存,从而简化了内存管理。
多个进程共享物理内存:进程在运行过程中,会加载许多操作系统的动态库。
提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
提供更大的可使用内存空间:可以让程序拥有超过系统物理内存大小的可用内存空间。
物理地址(Physical Address):真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。
虚拟地址(Virtual Address):程序中访问的内存地址。
虚拟地址空间:虚拟地址的集合,是虚拟内存的范围。每一个进程都有一个一致且私有的虚拟地址空间。
物理地址空间:物理地址的集合,是物理内存的范围。
MMU
MMU(Memory Management Unit,内存管理单元),CPU 芯片中的一个重要组件,将虚拟地址转换为物理地址,过程被称为 地址翻译/地址转换。
通过 MMU 将虚拟地址转换为物理地址后,再通过总线传到物理内存设备,进而完成相应的物理内存读写请求。
机制
分段机制
以段(—段 连续 的物理内存)的形式管理/分配物理内存。
分段管理通过 段表(Segment Table) 映射虚拟地址和物理地址。
虚拟地址组成
段号:标识着该虚拟地址属于整个虚拟地址空间中的哪一个段。
段内偏移量:相对于该段起始地址的偏移量。
分页机制
把主存(物理内存)分为连续等长的物理页,应用程序的虚拟地址空间划也被分为连续等长的虚拟页。
注意:这里的页是连续等长的,不同于分段机制下不同长度的段。
分页管理通过 页表(Page Table) 映射虚拟地址和物理地址。
虚拟地址组成
页号:通过虚拟页号可以从页表中取出对应的物理页号
页内偏移量:物理页起始地址+页内偏移量=物理内存地址。
分段和分页机制
共同点
都是非连续内存管理的方式。
都采用了地址映射的方法,将虚拟地址映射到物理地址,以实现对内存的管理和保护。
区别
分页机制以页面为单位进行内存管理,而分段机制以段为单位进行内存管理。
页是物理单位,而段则是逻辑单位。
分段机制容易出现外部内存碎片,分页机制解决了外部内存碎片的问题,但仍然可能会出现内部内存碎片。
分页采用页表,而分段采用段表,来完成虚拟地址到物理地址的映射。
分页机制对程序没有任何要求,分段则需要将程序分为多个段,并且显式地使用段寄存器来访问不同的段。
TLB
转址旁路缓存(Translation Lookaside Buffer,TLB,也被称为快表),为了提高虚拟地址到物理地址的转换速度。
本质上就是一块高速缓存(Cache),缓存了虚拟页号到物理页号的映射关系。
换页机制:当物理内存不够用的时候,操作系统选择将一些物理页的内容放到磁盘上去,等要用到的时候再将它们读取到物理内存中。
段页机制:结合分段和分页机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
页缺失
页缺失(Page Fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)
指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。
指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。
分类
硬性页缺失(Hard Page Fault):物理内存中没有对应的物理页。
软性页缺失(Soft Page Fault):物理内存中有对应的物理页,但虚拟页还未和物理页建立映射。
无效缺页错误(Invalid Page Fault):应用程序访问的是无效的物理内存。
页面置换算法
定义:淘汰物物理页的规则。
最佳页面置换算法(OPT,Optimal),理论最优,无法实现。
先进先出页面置换算法(FIFO,First In First Out),性能不好。
最近最久未使用页面置换算法(LRU ,Least Recently Used),易实现,应用较多。
最少使用页面置换算法(LFU,Least Frequently Used),和 LRU 算法比较像。
时钟页面置换算法(Clock)
局部性原理
指在程序执行过程中,数据和指令的访问存在一定的空间和时间上的局部性特点。
时间局部性:指一个数据项或指令在一段时间内被反复使用的特点。
空间局部性:指一个数据项或指令在一段时间内与其相邻的数据项或指令被反复使用的特点。
计算机体系结构设计的重要原则之一,也是许多优化算法的基础。
文件系统
主要负责管理和组织计算机存储设备上的文件和目录。
作用
存储管理:将文件数据存储到物理存储介质中,并且管理空间分配,以确保每个文件都有足够的空间存储,并避免文件之间发生冲突。
文件管理:文件的创建、删除、移动、重命名、压缩、加密、共享等等。
目录管理:目录的创建、删除、移动、重命名等等。
文件访问控制:管理不同用户或进程对文件的访问权限,以确保用户只能访问其被授权访问的文件,以保证文件的安全性和保密性。
硬链接和软链接
索引节点(inode)
在 Linux/类 Unix 文件系统中,每个文件和目录都有一个唯一的索引节点(inode)号,用来标识该文件或目录。
硬链接(Hard Link)
通过 inode 节点号建立连接,硬链接和源文件的 inode 节点号相同,两者对文件系统来说是完全平等的。
只有删除了源文件和所有对应的硬链接文件,该文件才会被真正删除。
硬链接具有一些限制,不能对目录以及不存在的文件创建硬链接,并且,硬链接也不能跨越文件系统。
ln 命令用于创建硬链接。
软链接(Symbolic Link 或 Symlink)
软链接和源文件的 inode 节点号不同,而是指向一个文件路径。
源文件删除后,软链接依然存在,但是指向的是一个无效的文件路径。
软连接类似于 Windows 系统中的快捷方式。
不同于硬链接,可以对目录或者不存在的文件创建软链接,并且,软链接可以跨越文件系统。
ln -s 命令用于创建软链接。
硬链接不能跨文件系统原因
可能会导致 inode 节点号冲突的问题,即目标文件的 inode 节点号已经在该文件系统中被使用。
提高性能的方式
优化硬件、选择合适的文件系统选型、运用缓存、避免磁盘过度使用、对磁盘进行合理的分区
磁盘调度算法
定义
操作系统中对磁盘访问请求进行排序和调度的算法,其目的是提高磁盘的访问效率。
算法
先来先服务算法(First-Come First-Served,FCFS)
最短寻道时间优先算法(Shortest Seek Time First,SSTF):也被称为最佳服务优先(Shortest Service Time First,SSTF)算法
扫描算法(SCAN):也被称为电梯(Elevator)算法
循环扫描算法(Circular Scan,C-SCAN):SCAN 算法的变体
边扫描边观察算法(LOOK)
均衡循环扫描算法(C-LOOK)
数据结构
线性
数组
数组(Array)由相同类型的元素(Element)组成,并且是使用一块连续的内存来存储。
特点
提供随机访问 并且容量有限。
应用
需要支持随机访问。
存储的数据元素的个数确定,并且不需要经常添加和删除数据。
链表
链表(LinkedList)虽然是一种线性表,但是并不会按线性的顺序存储数据,使用的不是连续的内存空间来存储数据。
特点
可以克服数组需要预先知道数据大小的缺点,但比数组会占用更多的空间。
不具有数组随机读取的优点。
复杂度
插入和删除 O(1)
查找一个节点或者访问特定位置的节点 O(n)
分类
单链表
只有一个方向,结点只有一个后继指针 next 指向后面的节点。
循环链表
一种特殊的单链表,循环链表的尾结点不是指向 null,而是指向链表的头结点。
双向链表
包含两个指针,一个 prev 指向前一个节点,一个 next 指向后一个节点。
双向循环链表
最后一个节点的 next 指向 head,而 head 的 prev 指向最后一个节点,构成一个环。
应用
存储的数据元素的个数不确定,并且需要经常添加和删除数据。
与数组区别
数组支持随机访问,而链表不支持。
数组使用的是连续内存空间对 CPU 的缓存机制友好,链表则相反。
数组的大小固定,而链表则天然支持动态扩容。
栈
栈 (Stack) 只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。
特点
按照 后进先出(LIFO, Last In First Out) 的原理运作。在栈中,push 和 pop 的操作都发生在栈顶。
复杂度
O(1)
分类
顺序栈(数组实现)
链式栈(链表实现)
应用
实现浏览器的回退和前进功能。
检查符号是否成对出现。
反转字符串。
维护函数调用。
队列
队列(Queue) 是 先进先出 (FIFO,First In, First Out) 的线性表。
特点
队列只允许在后端(rear)进行插入操作也就是入队 enqueue,在前端(front)进行删除操作也就是出队 dequeue。
队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。
分类
单队列
常见的队列,每次添加元素时,都是添加到队尾。
顺序队列(数组实现)
存在“假溢出”的问题也就是明明有位置却不能添加的情况。
链式队列(链表实现)
循环队列
队列从头开始,形成头尾相接的循环,可以解决顺序队列的假溢出和越界问题。
双端队列
双端队列 (Deque) 是一种在队列的两端都可以进行插入和删除操作的队列,相比单队列来说更加灵活。
优先队列
优先队列 (Priority Queue) 从底层结构上来讲并非线性的数据结构,它一般是由堆来实现的。
原理
在每个元素入队时,优先队列会将新元素其插入堆中并调整堆。
队头出队时,优先队列会返回堆顶元素并调整堆。
特点
有序性
应用
阻塞队列: 阻塞队列可以看成在队列基础上加了阻塞操作的队列。
线程池中的请求/任务队列。
图
定义
由顶点的有穷非空集合和顶点之间的边组成的集合。通常表示为:G(V,E),其中,G 表示一个图,V 表示顶点的集合,E 表示边的集合。
顶点
图中的数据元素,我们称之为顶点,图至少有一个顶点(非空有穷集合)。
边
顶点之间的关系用边表示。
度
表示一个顶点包含多少条边,在有向图中,还分为出度和入度。
出度表示从该顶点出去的边的条数,入度表示进入该顶点的边的条数。
无向图和有向图
无向图:用不带箭头的边表示不关注方向。
有向图:用带箭头的边表示二者的关系。
无权图和带权图
无权图:只关心关系的有无,而不关心关系有多强。
带权图:既关心关系的有无,也关心关系的强度。
存储
邻接矩阵存储
邻接矩阵将图用二维矩阵存储,如果第 i 个顶点和第 j 个顶点之间有关系,且关系权值为 n,则 A[i][j]=n 。
优点是简单直接,缺点是比较浪费空间。
邻接表存储
使用一个链表来存储某个顶点的所有后继相邻顶点。
对于图中每个顶点 Vi,把所有邻接于 Vi 的顶点 Vj 链成一个单链表,这个单链表称为顶点 Vi 的 邻接表。
对于图中每个顶点 Vi,把所有邻接于 Vi 的顶点 Vj 链成一个单链表,这个单链表称为顶点 Vi 的 邻接表。
优点是节省空间。
搜索
广度优先搜索
就像水面上的波纹一样一层一层向外扩展。
具体实现方式用到了线性数据结构——队列 。
深度优先搜索
从源顶点开始,一直走到没有后继节点,才回溯到上一顶点,然后继续上一过程。
和广度优先搜索类似,具体实现用到了另一种线性数据结构——栈 。
堆
堆是一种满足一定条件的树:堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。
注意
堆不一定是完全二叉树,只是为了方便存储和索引,我们通常用完全二叉树的形式来表示堆。
(二叉)堆是一个数组,它可以被看成是一个 近似的完全二叉树。
分类
最大堆:堆中的每一个节点的值都大于等于子树中所有节点的值。
最小堆:堆中的每一个节点的值都小于等于子树中所有节点的值。
存储
由于完全二叉树的优秀性质,利用数组存储二叉树即节省空间,又方便索引。
操作
插入元素:先将元素放至数组末尾,再自底向上堆化,将末尾元素上浮。
堆化:删除堆顶元素后,为了保持堆的性质,需要对堆的结构进行调整,这个过程即为 堆化。
删除堆顶元素:删除堆顶元素,将末尾元素放至堆顶,再自顶向下堆化,将堆顶元素下沉。
也可以自底向上堆化,只是会产生“气泡”,浪费存储空间。最好采用自顶向下堆化的方式。
也可以自底向上堆化,只是会产生“气泡”,浪费存储空间。最好采用自顶向下堆化的方式。
排序
第一步是建堆,将一个无序的数组建立为一个堆。
第二步是排序,将堆顶元素取出,然后对剩下的元素进行堆化,反复迭代,直到所有元素被取出为止。
应用
当我们只关心所有数据中的最大值或者最小值,存在多次获取最大值或者最小值,多次插入或删除数据时,就可以使用堆。
树
树就是一种类似现实生活中的树的数据结构(倒置的树)。任何一颗非空树只有一个根节点。
特点
一棵树中的任意两个结点有且仅有唯一的一条路径连通。
一棵树如果有 n 个结点,那么它一定恰好有 n - 1 条边。
一棵树不包含回路。
二叉树
定义
二叉树(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。
二叉树 的分支通常被称作“左子树”或“右子树”。并且,二叉树 的分支具有左右次序,不能随意颠倒。
分类
满二叉树
一个二叉树,如果每一个层的结点数都达到最大值。
完全二叉树
除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点。
平衡二叉树
定义
平衡二叉树 是一棵二叉排序树。
特点
可以是一棵空树。
如果不是空树,它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。
实现
红黑树、AVL 树、替罪羊树、加权平衡树、伸展树 等。
存储
链式存储
依靠指针将各个节点串联起来,不需要连续的存储空间。
顺序存储
利用数组进行存储,数组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。
遍历
先序遍历
先输出根结点,再遍历左子树,最后遍历右子树,遍历左子树和右子树的时候,同样遵循先序遍历的规则,即递归。
中序遍历
先递归中序遍历左子树,再输出根结点的值,再递归中序遍历右子树,即把树压扁,父结点被拍到了左子节点和右子节点的中间。
后序遍历
先递归后序遍历左子树,再递归后序遍历右子树,最后输出根结点的值。
红黑树
定义
红黑树(Red Black Tree)是一种自平衡二叉查找树。
在 1972 年由 Rudolf Bayer 发明的,当时被称为平衡二叉 B 树(symmetric binary B-trees)。
红黑树的诞生就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
特点
每个节点非红即黑。黑色决定平衡,红色不决定平衡。
根节点总是黑色的。
每个叶子节点都是黑色的空节点(NIL 节点)。
如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。
从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
结构
建立在 BST 二叉搜索树的基础上,AVL、2-3 树、红黑树都是自平衡二叉树(统称 B-树)。
实现
左倾染色、右倾染色、左旋调衡、右旋调衡
布隆过滤器
定义
布隆过滤器(Bloom Filter,BF),于1970年由 Bloom 提出,一种来检索元素是否在给定大集合中的数据结构。
组成
由二进制向量(位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。
优缺点
相比于我们平时常用的 List、Map、Set 等数据结构,它占用空间更少并且效率更高。
缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下,添加到集合中的元素越多,误报的可能性就越大。
布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
原理
对存入元素进行哈希计算,根据计算结果对位数组进行标记。
应用
判断给定数据是否存在:大量数据判重、防止缓存穿透、邮箱的垃圾邮件过滤、黑名单功能等等。
去重:比如爬给定网址的时候对已经爬取过的 URL 去重、对巨量的 QQ 号/订单号去重。
隆过滤器主要是为了解决海量数据的存在性问题。
利用 Google 开源的 Guava 中自带的布隆过滤器。
Redis v4.0 之后有了 Module(模块/插件) 功能,可集成布隆过滤器。
算法
贪心算法
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
步骤
将问题分解为若干个子问题。
找出适合的贪心策略。
求解每一个子问题的最优解。
将局部最优解堆叠成全局最优解。
动态规划
动态规划中每一个状态一定是由上一个状态推导出来的,而贪心没有状态推导,而是从局部直接选最优的。
经典题目:01 背包、完全背包
步骤
确定 dp 数组(dp table)以及下标的含义。
确定递推公式。
dp 数组如何初始化。
确定遍历顺序。
举例推导 dp 数组。
回溯算法
在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。其本质就是穷举。
经典题目:8 皇后
步骤
针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间 。
以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
分治算法
将一个规模为 N 的问题分解为 K 个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
经典题目:二分查找、汉诺塔问题
步骤
将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。
若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题。
将各个子问题的解合并为原问题的解。
排序算法
分类
内部排序
数据记录在内存中进行排序。
插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
外部排序
因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
数据库
数据库基础
库概念
数据库
数据库(DataBase 简称 DB)就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。
数据库管理系统
数据库管理系统(Database Management System 简称 DBMS)是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。
数据库系统
数据库系统(Data Base System,简称 DBS)通常由软件、数据库和数据管理员(DBA)组成。
数据库管理员
数据库管理员(Database Administrator, 简称 DBA)负责全面管理和控制数据库系统。
表概念
元组
元组(tuple)是关系数据库中的基本概念,
关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。
码
码就是能唯一标识实体的属性,对应表中的列。
候选码
若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。
主码
主码也叫主键。主码是从候选码中选出来的。 一个实体集中只能有一个主码,但可以有多个候选码。
主键用于唯一标识一个元组,不能有重复,不允许为空。一个表只能有一个主键。
外码
外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。
外键用来和其他表建立联系用,外键是另一表的主键,外键是可以有重复的,可以是空值。一个表可以有多个外键。
主属性
候选码中出现过的属性称为主属性。
非主属性
不包含在任何一个候选码中的属性称为非主属性。
ER 图
ER 图 全称是 Entity Relationship Diagram(实体联系图),提供了表示实体类型、属性和联系的方法。
组成
实体:通常是现实世界的业务对象,当然使用一些逻辑对象也可以。
属性:即某个实体拥有的属性,属性用来描述组成实体的要素,对于产品设计来说可以理解为字段。
联系:即实体与实体之间的关系,在 ER 图中用菱形表示,这个关系不仅有业务关联关系,还能通过数字表示实体之间的数量对照关系。
实体关系
1 对 1(1 : 1)、1 对多(1 : N)、多对多(M : N)
数据库范式
重要概念
函数依赖(functional dependency)
若在一张表中,在属性(或属性组)X 的值确定的情况下,必定能确定属性 Y 的值,那么就可以说 Y 函数依赖于 X,写作 X → Y。
部分函数依赖(partial functional dependency)
如果 X→Y,并且存在 X 的一个真子集 X0,使得 X0→Y,则称 Y 对 X 部分函数依赖。
完全函数依赖(Full functional dependency)
在一个关系中,若某个非主属性数据项依赖于全部关键字称之为完全函数依赖。
传递函数依赖
在关系模式 R(U)中,设 X,Y,Z 是 U 的不同的属性子集,如果 X 确定 Y、Y 确定 Z,且有 X 不包含 Y,Y 不确定 X,
(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。
(X∪Y)∩Z=空集合,则称 Z 传递函数依赖(transitive functional dependency) 于 X。
传递函数依赖会导致数据冗余和异常。
1NF(第一范式)
属性不可再分。
1NF 是所有关系型数据库的最基本要求。
2NF(第二范式)
1NF 的基础之上,消除了非主属性对于码的部分函数依赖。
3NF(第三范式)
3NF 在 2NF 的基础之上,消除了非主属性对于码的传递函数依赖 。
例:在关系 R(学号 , 姓名, 系名,系主任)中,学号 → 系名,系名 → 系主任,存在非主属性系主任对于学号的传递函数依赖,所以该表不符合 3NF 的要求。
优点
满足后,基本上解决了数据冗余过大,插入异常,修改异常,删除异常的问题。
2NF 和 3NF 区别
2NF:非主键列是否完全依赖于主键,还是依赖于主键的一部分。
3NF:非主键列是直接依赖于主键,还是直接依赖于非主键列。
外键与级联
缺点
增加了复杂性: a. 每次做 DELETE 或者 UPDATE 都必须考虑外键约束;b. 外键的主从关系是定的,变化很麻烦。
增加了额外工作:数据库需要增加维护外键的工作。
对分库分表不友好:因为分库分表下外键是无法生效的。
优点
保证了数据库数据的一致性和完整性。
级联操作方便,减轻了程序代码量。
外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
存储过程
可以把存储过程看成是一些 SQL 语句的集合,中间加了点逻辑控制语句。
存储过程在互联网公司应用不多,因为存储过程难以调试和扩展,而且没有移植性,还会消耗数据库资源。
DML 和 DDL
DML(Data Manipulation Language)数据库操作语言,是指对数据库中表记录的操作。
DDL (Data Definition Language)数据定义语言,是对数据库内部的对象进行创建、删除、修改的操作语言。
设计步骤
需求分析 : 分析用户的需求,包括数据、功能和性能需求。
概念结构设计 : 主要采用 E-R 模型进行设计,包括画 E-R 图。
逻辑结构设计 : 通过将 E-R 图转换成表,实现从 E-R 模型到关系模型的转换。
物理结构设计 : 主要是为所设计的数据库选择合适的存储结构和存取路径。
数据库实施 : 包括编程、测试和试运行。
数据库的运行和维护 : 系统的运行与数据库的日常维护。
NoSQL基础
NoSQL(Not Only SQL 的缩写)泛指非关系型的数据库,主要针对的是键值、文档以及图形类型数据存储。
特性
天生支持分布式,数据冗余和数据分片等,旨在提供可扩展的高可用高性能数据存储解决方案。
存储结构绝大部分(比如 HBase、Cassandra、RocksDB)都是基于 LSM 树。
MongoDB 默认使用 B+ 树作为其存储结构,还支持 LSM(Log Structured Merge) 树作为存储结构。
NoSQL 数据库通常不支持事务,但 MongoDB 支持事务。
代表
HBase、Cassandra、MongoDB、Redis
优势
灵活性: 提供灵活的架构,以实现更快速、更多的迭代开发。
可扩展性:通过使用分布式硬件集群来横向扩展,而不是通过添加昂贵和强大的服务器来纵向扩展。
高性能: 针对特定的数据模型和访问模式进行了优化。
强大的功能: 提供功能强大的 API 和数据类型,专门针对其各自的数据模型而构建。
分类
键值:键值数据库是一种较简单的数据库,其中每个项目都包含键和值。
Redis、DynanoDB
文档:文档数据库中的数据被存储在类似于 JSON 对象的文档中,非常清晰直观。
MongoDB
图形:图形数据库旨在轻松构建和运行与高度连接的数据集一起使用的应用程序。
Neo4j、Giraph
宽列:宽列存储数据库非常适合需要存储大量的数据。
Cassandra、HBase
字符集
定义
字符
各种文字和符号的统称,包括各个国家文字、标点符号、表情、数字等等。
字符集
一系列字符的集合。
字符编码
将字符对应二进制数据的过程。
字符解码
二进制数据解析成字符的过程。
ASCII
ASCII (American Standard Code for Information Interchange,美国信息交换标准代码) 是一套主要用于现代美国英语的字符集。
ASCII 字符集
共定义了 128 个字符,其中有 33 个控制字符(比如回车、删除)无法显示。
ASCII 扩展字符集
可以定义 256(2^8)个字符。
GB2312
一种对汉字比较友好的字符集,共收录 6700 多个汉字,基本涵盖了绝大部分常用汉字。
不支持绝大部分的生僻字和繁体字。
GBK
GB2312 字符集的扩展,兼容 GB2312 字符集,共收录了 20000 多个汉字。
GB18030
完全兼容 GB2312 和 GBK 字符集,纳入中国国内少数民族的文字,且收录了日韩汉字,最全面的汉字字符集,共收录汉字 70000 多个。
BIG5
针对的是繁体中文,收录了 13000 多个汉字。
Unicode
包含了世界上几乎所有已知的字符。不过,Unicode 字符集并没有规定如何存储这些字符(也就是如何使用二进制数据表示这些字符)。
UTF-8
UTF-8(8-bit Unicode Transformation Format),包含了世界上几乎所有已知的字符。类似的还有 UTF-16、 UTF-32。使用最广泛。
MySQL字符集
默认
在 MySQL5.7 中,默认字符集是 latin1 ;在 MySQL8.0 中,默认字符集是 utf8mb4。
字符集层次级别
server(MySQL 实例级别)
database(库级别)
table(表级别)
column(字段级别)
连接字符集
和 MySQL 服务器的通信相关的字符集。
JDBC
JDBC 驱动会影响连接字符集。
两套 UTF-8 编码实现
utf8
utf8编码只支持1-3个字节 。但 emoji 符号占 4 个字节,一些较复杂的文字、繁体字也是 4 个字节。
utf8mb4
UTF-8 的完整实现,正版!最多支持使用 4 个字节表示字符,因此,可以用来存储 emoji 符号。
SQL
概念
SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。
数据库(database)
保存有组织的数据的容器(通常是一个文件或一组文件)。
数据表(table)
某种特定类型数据的结构化清单。
模式(schema)
关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储。数据库和表都有模式。
列(column)
表中的一个字段。所有表都是由一个或多个列组成的。
行(row)
表中的一个记录。
主键(primary key)
一列(或一组列),其值能够唯一标识表中每一行。
语法结构
子句
语句和查询的组成成分。
表达式
可以产生任何标量值,或由列和行的数据库表。
谓词
给需要评估的 SQL 三值逻辑(3VL)(true/false/unknown)或布尔真值指定条件,并限制语句和查询的效果,或改变程序流程。
查询
基于特定条件检索数据。这是 SQL 的一个重要组成部分。
语句
可以持久地影响纲要和数据,也可以控制数据库事务、程序流程、连接、会话或诊断。
执行顺序
FROM:对FROM子句中的前两个表执行笛卡尔积(Cartesian product)(交叉联接),生成虚拟表VT1。
ON:对VT1应用ON筛选器。只有那些使<join_condition>为真的行才被插入VT2。
OUTER(JOIN):如果指定了 OUTER JOIN(相对于 CROSS JOIN 或(INNER JOIN),保留表(preserved table:左外部联接把左表标记为保留表,右外部联接把右表标记为保留表,完全外部联接把两个表都标记为保留表)中未找到匹配的行将作为外部行添加到 VT2,生成VT3。如果 FROM 子句包含两个以上的表,则对上一个联接生成的结果表和下一个表重复执行步骤1到步骤3,直到处理完所有的表为止。
WHERE:对 VT3 应用 WHERE 筛选器。只有使<where_condition>为 true 的行才被插入 VT4。
GROUP BY:按 GROUP BY 子句中的列列表对 VT4 中的行分组,生成 VT5。
CUBE | ROLLUP:把超组(Suppergroups)插入 VT5,生成 VT6。
HAVING:对 VT6 应用 HAVING 筛选器。只有使<having_condition>为 true 的组才会被插入 VT7。
SELECT:处理 SELECT 列表,产生 VT8。
DISTINCT:将重复的行从 VT8 中移除,产生 VT9。
ORDER BY:将 VT9 中的行按 ORDER BY 子句中的列列表排序,生成游标(VC10)。
TOP:从 VC10 的开始处选择指定数量或比例的行,生成表 VT11,并返回调用者。
语法要点
SQL 语句不区分大小写,但是数据库表名、列名和值是否区分,依赖于具体的 DBMS 以及配置。
多条 SQL 语句必须以分号(;)分隔。
处理 SQL 语句时,所有空格都被忽略。
分类
数据定义语言(DDL)
数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言。
DDL 的主要功能是定义数据库对象。
DDL 的核心指令是 CREATE、ALTER、DROP。
数据操纵语言(DML)
数据操纵语言(Data Manipulation Language, DML)是用于数据库操作,对数据库其中的对象和数据运行访问工作的编程语句。
DML 的主要功能是 访问数据,因此其语法都是以读写数据库为主。
DML 的核心指令是 INSERT、UPDATE、DELETE、SELECT。这四个指令合称 CRUD(Create, Read, Update, Delete),即增删改查。
事务控制语言(TCL)
事务控制语言 (Transaction Control Language, TCL) 用于管理数据库中的事务。用于管理由 DML 语句所做的更改。它还允许将语句分组为逻辑事务。
TCL 的核心指令是 COMMIT、ROLLBACK。
数据控制语言(DCL)
数据控制语言 (Data Control Language, DCL) 是一种可对数据访问权进行控制的指令,
它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。
它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。
DCL 的核心指令是 GRANT、REVOKE。
DCL 以控制用户的访问权限为主,因此其指令作法并不复杂,
可利用 DCL 控制的权限有:CONNECT、SELECT、INSERT、UPDATE、DELETE、EXECUTE、USAGE、REFERENCES。
可利用 DCL 控制的权限有:CONNECT、SELECT、INSERT、UPDATE、DELETE、EXECUTE、USAGE、REFERENCES。
增删改查
又称为 CRUD,数据库基本操作中的基本操作。
INSERT INTO、UPDATE、DELETE、TRUNCATE TABLE、SELECT、DISTINCT、LIMIT
排序
ORDER BY 用于对结果集按照一个列或者多个列进行排序。
ORDER BY、DESC、ASC
分组
GROUP BY、HAVING
子查询
嵌套在较大查询中的 SQL 查询,也称内部查询或内部选择,包含子查询的语句也称为外部查询或外部选择。
WHERE、IN、BETWEEN、AND、OR、NOT、LIKE
连接
JOIN 是“连接”的意思,顾名思义,SQL JOIN 子句用于将两个或者多个表联合起来进行查询。
INNER JOIN、LEFT/RIGHT/FULL JOIN、 LEFT/RIGHT/FULL OUTER JOIN、SELF JOIN、CROSS JOIN
组合
UNION 运算符将两个或更多查询的结果组合起来,并生成一个结果集。
UNION、UNION ALL
函数
不同数据库的函数往往各不相同,因此不可移植。
LOWER()、UPPER() 等等
数据定义
CREATE DATABASE、DROP DATABASE、USE
CREATE TABLE、DROP TABLE
ALTER TABLE ... ADD/DROP/MODIFY
视图(VIEW)
定义
视图是基于 SQL 语句的结果集的可视化的表。
视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。
作用
简化复杂的 SQL 操作,比如复杂的联结。
只使用实际表的一部分数据。
通过只给用户访问视图的权限,保证数据的安全性。
更改数据格式和表示。
CREATE VIEW ... AS、DROP VIEW
索引(INDEX)
一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。
优点
使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
缺点
创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
索引需要使用物理文件存储,也会耗费一定空间。
CREATE INDEX ... ON、ALTER TABLE ... ADD/DROP INDEX
约束
用于规定表中的数据规则。如果存在违反约束的数据行为,行为会被约束终止。
NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK、DEFAULT
事务处理
MySQL 默认是隐式提交(可设定),每执行一条语句就把这条语句当成一个事务然后进行提交。
权限控制
要授予用户帐户权限,可以用 GRANT 命令。有撤销用户的权限,可以用 REVOKE 命令。
存储过程(procedure)
可以看成是对一系列 SQL 操作的批处理。不推荐,没有可移植性。
游标(cursor)
一个存储在 DBMS 服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。
触发器(trigger)
一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。
MySQL
定义
关系型数据库(RDB,Relational Database)就是一种建立在关系模型的基础上的数据库。
关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。
关系模型表明了数据库中所存储的数据之间的联系(一对一、一对多、多对多)。
数据库
MySQL、PostgreSQL、Oracle、SQL Server、SQLite(微信本地的聊天记录的存储就是用的 SQLite) ……。
MySQL 是一种关系型数据库,主要用于持久化存储我们的系统中的一些数据比如用户信息。
优点
成熟稳定,功能完善。
开源免费。
文档丰富,既有详细的官方文档,又有非常多优质文章可供参考学习。
开箱即用,操作简单,维护成本低。
兼容性好,支持常见的操作系统,支持多种开发语言。
社区活跃,生态完善。
事务支持优秀, InnoDB 存储引擎不会有任何性能损失,并且可以解决幻读问题发生的。
支持分库分表、读写分离、高可用。
字段类型
数值类型:整型(TINYINT、SMALLINT、MEDIUMINT、INT 和 BIGINT)、浮点型(FLOAT 和 DOUBLE)、定点型(DECIMAL)
字符串类型:CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT、TINYBLOB、BLOB、MEDIUMBLOB 和 LONGBLOB 等。
日期时间类型:YEAR、TIME、DATE、DATETIME 和 TIMESTAMP 等。
UNSIGNED
表示不允许负值的无符号整数,将正整数的上限提高一倍。
CHAR 和 VARCHAR
CHAR 是定长字符串,VARCHAR 是变长字符串。
DECIMAL 和 FLOAT/DOUBLE
DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。
TEXT 和 BLOB
TEXT 类型类似于 CHAR(0-255 字节)和 VARCHAR(0-65,535 字节),但可以存储更长的字符串,即长文本数据,例如博客内容。
BLOB 类型主要用于存储二进制大对象,例如图片、音视频等文件。
不推荐使用,很少使用 TEXT 类型,只偶尔会用到,而 BLOB 类型则基本不常用。
DATETIME 和 TIMESTAMP
DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。
NULL 和 ''
NULL 代表一个不确定的值,就算是两个 NULL,它俩也不一定相等。
''的长度是 0,是不占用空间的,而 NULL 是需要占用空间的。
NULL 会影响聚合函数的结果。
查询 NULL 值时,必须使用 IS NULL 或 IS NOT NULLl 来判断。
结构
MySQL 主要分为 Server 层和引擎层。
Server 层
连接器: 身份认证和权限相关(登录 MySQL 的时候)。
查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器分为词法分析和语法分析。
优化器: 按照 MySQL 认为最优的方案去执行。
执行器: 执行语句,然后从存储引擎返回数据。 执行语句之前会先判断是否有权限,如果没有权限的话,就会报错。
引擎层
插件式存储引擎:主要负责数据的存储和读取,采用的是插件式架构,支持 InnoDB、MyISAM、Memory 等多种存储引擎。
查询语句的执行流程:权限校验(如果命中缓存)--->查询缓存--->分析器--->优化器--->权限校验--->执行器--->引擎。
更新语句执行流程:分析器---->权限校验---->执行器--->引擎---redo log(prepare 状态)--->binlog--->redo log(commit 状态)。
存储引擎
MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。
存储引擎采用的是 插件式架构 ,支持多种存储引擎。存储引擎是基于表的,而不是数据库。
MyISAM 和 InnoDB
MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
MyISAM 不支持事务,而 InnoDB 支持,并实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。
MyISAM 不支持外键,而 InnoDB 支持。
MyISAM 不支持数据库异常崩溃后的安全恢复,而 InnoDB 支持。
MyISAM 不支持 MVCC,而 InnoDB 支持。
MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。
虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的。
InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的。
InnoDB 的性能比 MyISAM 更强大,随着 CPU 核数的增加,差距更明显。
索引
索引(Index)是一种用于快速查询和检索数据的数据结构,其本质可以看成是一种排序好的数据结构。
优点
使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
缺点
创建索引和维护索引需要耗费许多时间。
索引需要使用物理文件存储,也会耗费一定空间。
结构
B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。
Hash 表
哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
原理
哈希算法(也叫散列算法)
Hash 冲突
InnoDB 存储引擎中存在一种特殊的“自适应哈希索引”(Adaptive Hash Index),结合了 B+Tree 和哈希索引的特点。
因为 Hash 索引不支持顺序和范围查询,MySQL 没有使用其作为索引的数据结构。
二叉查找树(BST)
二叉查找树(Binary Search Tree)是一种基于二叉树的数据结构。
特点
左子树所有节点的值均小于根节点的值。
右子树所有节点的值均大于根节点的值。
左右子树也分别为二叉查找树。
二叉查找树的性能非常依赖于它的平衡程度,这就导致其不适合作为 MySQL 底层索引的数据结构。
AVL 树
计算机科学中最早被发明的自平衡二叉查找树。
特点
保证任何节点的左右子树高度之差不超过 1,因此也被称为高度平衡二叉树。
由于 AVL 树需要频繁地进行旋转操作来保持平衡,因此会有较大的计算开销进而降低了查询性能。
红黑树
红黑树是一种自平衡二叉查找树,通过在插入和删除节点时进行颜色变换和旋转操作,使得树始终保持平衡状态。
特点
每个节点非红即黑。
根节点总是黑色的。
每个叶子节点都是黑色的空节点(NIL 节点)。
如果节点是红色的,则它的子节点必须是黑色的(反之不一定)。
从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)。
因为红黑树的平衡性相对较弱,高度较高的树的查询可能会导致多次磁盘 IO 操作,这也是 MySQL 没有选择红黑树的主要原因。
B 树 & B+树
B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。
大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。
B+树与 B 树相比,具备更少的 IO 次数、更稳定的查询效率和更适于范围查询这些优势。
类型
按数据结构
BTree 索引:MySQL 里默认和最常用的索引类型。
哈希索引:类似键值对的形式,一次即可定位。
RTree 索引:一般不会使用,仅支持 geometry 数据类型,优势在于范围查找,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
对文本的内容进行分词,进行搜索。目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。
一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。
按底层存储方式
聚簇索引(聚集索引):索引结构和数据一起存放的索引,InnoDB 中的主键索引就属于聚簇索引。
非聚簇索引(非聚集索引):索引结构和数据分开存放的索引,二级索引(辅助索引)就属于非聚簇索引。
按应用维度
主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个。
普通索引:仅加速查询。
唯一索引:加速查询 + 列值唯一(可以有 NULL)。
覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。
联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。
全文索引:对文本的内容进行分词,进行搜索。
MySQL 8.x 新特性
隐藏索引:也称为不可见索引,不会被优化器使用,但是仍然需要维护,通常会软删除和灰度发布的场景中使用。主键不能设置为隐藏。
降序索引:MySQL 8.x 版本才开始真正支持降序索引。另外,在 MySQL 8.x 版本中,不再对 GROUP BY 语句进行隐式排序。
函数索引:从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值,也就是在索引中可以包含函数或者表达式。
主键索引(Primary Key)
数据表的主键列使用的就是主键索引。
一张数据表有只能有一个主键,并且主键不能为 null,不能重复。
二级索引
二级索引(Secondary Index)又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。
唯一索引,普通索引,前缀索引等索引属于二级索引。
唯一索引(Unique Key):唯一索引也是一种约束。不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。
普通索引(Index):普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。
前缀索引(Prefix):前缀索引只适用于字符串类型的数据。
全文索引(Full Text):全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。
聚簇索引(聚集索引)
聚簇索引(Clustered Index)即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引。
优点
查询速度非常快、对排序查找和范围查找优化。
缺点
依赖于有序的数据、更新代价大。
非聚簇索引(非聚集索引)
非聚簇索引(Non-Clustered Index)即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。
MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引。
优点
更新代价比聚簇索引要小 。
缺点
依赖于有序的数据、可能会二次查询(回表)。
覆盖索引
覆盖索引(Covering Index),一个索引包含(或者说覆盖)所有需要查询的字段的值。
联合索引
使用表中的多个字段创建索引,就是 联合索引,也叫 组合索引 或 复合索引。
最左前缀匹配原则
在使用联合索引时,MySQL 会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配,
如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,
直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 >、< )才会停止匹配。
对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
如果查询条件中存在与联合索引中最左侧字段相匹配的字段,则就会使用该字段过滤一批数据,
直至联合索引中全部字段匹配完成,或者在执行过程中遇到范围查询(如 >、< )才会停止匹配。
对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
索引下推
索引下推(Index Condition Pushdown) 是 MySQL 5.6 版本中提供的一项索引优化功能,
可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
使用建议
选择合适的字段创建索引
不为 NULL 的字段。
被频繁查询的字段。
被作为条件查询的字段。
频繁需要排序的字段。
被经常频繁用于连接的字段。
被频繁更新的字段应该慎重建立索引。
限制每张表上的索引数量
建议单张表索引不超过 5 个!
尽可能的考虑建立联合索引而不是单列索引。
注意避免冗余索引。
字符串类型的字段使用前缀索引代替普通索引。
避免索引失效
SELECT * 不会直接导致索引失效,但它可能会带来浪费、无法使用索引覆盖。
创建了组合索引,但查询条件未遵守最左匹配原则。
在索引列上进行计算、函数、类型转换等操作。
以 % 开头的 LIKE 查询比如 LIKE '%abc'。
查询条件中使用 OR,且 OR 的前后条件中有一个列没有索引,涉及的索引都不会被使用到。
IN 的取值范围较大时会导致索引失效,走全表扫描(NOT IN 和 IN 的失效场景相同)。
发生隐式转换。
删除长期未使用的索引。
知道如何分析语句是否走索引查询,EXPLAIN 执行计划。
日志
主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。
其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
redo log(重做日志)
InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。
redo log 是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。
作用
MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。
数据页
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页。
过程
组成
每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成。
刷盘时机
事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘。
log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。
Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。
后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页刷新到磁盘,并将相关的重做日志一同刷新。
正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。
刷盘策略
设置参数 innodb_flush_log_at_trx_commit,默认值为1。
0:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。性能最高,但也最不安全。
1:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。性能最低,但也最安全。
2:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。
日志文件组
硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写。
重要属性
write pos:是当前记录的位置,一边写一边后移。
checkpoint:是当前要擦除的位置,也是往后推移。
如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log 记录。
MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
binlog(归档日志)
binlog(归档日志)保证了 MySQL 集群架构的数据一致性。
MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。
binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。
binlog会记录所有涉及更新数据的逻辑操作,并且是顺序写。
记录格式
binlog 日志有三种格式,可以通过 binlog_format 参数指定。
statement
记录的内容是SQL语句原文。
问题
update_time=now() 这里会获取当前系统时间,直接执行会导致与原库的数据不一致。
row
记录的内容不再是简单的SQL语句了,还包含操作的具体数据。
问题
需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗IO资源,影响执行速度。
mixed
statement 和 row 的混合,属于折中方案。
MySQL会判断这条SQL语句是否可能引起数据不一致,如果是,就用row格式,否则就用statement格式。
写入机制
事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
fsync,才是将数据持久化到磁盘的操作。
write 和 fsync 的时机,可以由参数 sync_binlog 控制,默认是1。
为0时,表示每次提交事务都只 write,由系统自行判断什么时候执行 fsync。
为1时,表示每次提交事务都会执行 fsync,就如同 redo log 日志刷盘流程 一样。
为N(N>1)时,表示每次提交事务都 write,但累积N个事务后才 fsync。
为0时,表示每次提交事务都只 write,由系统自行判断什么时候执行 fsync。
为1时,表示每次提交事务都会执行 fsync,就如同 redo log 日志刷盘流程 一样。
为N(N>1)时,表示每次提交事务都 write,但累积N个事务后才 fsync。
两阶段提交
为了解决两份日志(redo log 和 binlog)之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。
原理
将 redo log 的写入拆成了两个步骤 prepare 和 commit,这就是两阶段提交。
MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于 prepare 阶段,并且没有对应 binlog 日志,就会回滚该事务。
undo log(回滚日志)
在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。
目的
保证事务的原子性。
事务
事务是逻辑上的一组操作,要么都执行,要么都不执行。
ACID特性
原子性(Atomicity):事务是最小的执行单位,不允许分割。
一致性(Consistency):执行事务前后,数据保持一致。
隔离性(Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
持久性(Durability):一个事务被提交之后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
问题
脏读(Dirty read)
一个事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交。
这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据。
这时另外一个事务读取了这个还未提交的数据,但第一个事务突然回滚,导致数据并没有被提交到数据库,那第二个事务读取到的就是脏数据。
丢失修改(Lost to modify)
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,
第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
不可重复读(Unrepeatable read)
指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,
由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
幻读(Phantom read)
幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。
在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
控制方式
锁
锁可以看作是悲观控制的模式。
锁控制方式下会通过锁来显示控制共享资源而不是通过调度手段,MySQL 中主要是通过 读写锁 来实现并发控制。
共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。
MVCC
多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。
MVCC 是一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。
原理
通过在每个数据行上维护多个版本的数据来实现的。
MVCC 的实现依赖于:隐藏字段、Read View、undo log。
当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
隔离级别
READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,可以阻止脏读和不可重复读,但幻读仍有可能发生。
SERIALIZABLE(可串行化):最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,可以防止脏读、不可重复读以及幻读。
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。
InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况:
快照读:由 MVCC 机制来保证不出现幻读。
当前读:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,
行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。
快照读:由 MVCC 机制来保证不出现幻读。
当前读:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,
行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。
锁
锁是一种常见的并发事务的控制方式。
MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性非常差。
InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。
InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。
表级锁
MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁。
实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。
表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。
表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
行级锁
MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。
行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
行级锁和存储引擎有关,是在存储引擎层面实现的。
行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
行级锁和存储引擎有关,是在存储引擎层面实现的。
InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。
行锁定方式
记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题。
在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。
但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
共享锁和排他锁
共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。
意向锁
由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
表级锁
分类
意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
当前读和快照读
快照读
快照读(一致性非锁定读)就是单纯的 SELECT 语句。
比较适合对于数据一致性要求不是特别高且追求极致性能的业务场景。
当前读
当前读 (一致性锁定读)就是给行记录加 X 锁或 S 锁。
自增锁(AUTO-INC Locks)
InnoDB 中的自增主键会涉及一种比较特殊的表级锁。
规范
不建议用 MySQL 直接存储文件(比如图片),推荐使用 FastDFS、MinIO(推荐)。
可以将 IP 地址转换成整形数据存储,性能更好,占用空间也更小。
INET_ATON(),INET_NTOA()
执行计划
MySQL 为我们提供了 EXPLAIN 命令,来获取执行计划的相关信息。
EXPLAIN 语句并不会真的去执行相关的语句,而是通过查询优化器对语句进行分析,找出最优的查询方案,并显示对应的信息。
EXPLAIN 执行计划支持 SELECT、DELETE、INSERT、REPLACE 以及 UPDATE 语句。
结果分析
id
SELECT 标识符,是查询中 SELECT 的序号,用来标识整个查询中 SELELCT 语句的顺序。
id 如果相同,从上往下依次执行。id 不同,id 值越大,执行优先级越高,如果行引用其他行的并集结果,则该值可以为 NULL。
select_type
查询的类型,主要用于区分普通查询、联合查询、子查询等复杂的查询。
SIMPLE:简单查询,不包含 UNION 或者子查询。
PRIMARY:查询中如果包含子查询或其他部分,外层的 SELECT 将被标记为 PRIMARY。
SUBQUERY:子查询中的第一个 SELECT。
UNION:在 UNION 语句中,UNION 之后出现的 SELECT。
DERIVED:在 FROM 中出现的子查询将被标记为 DERIVED。
UNION RESULT:UNION 查询的结果。
table
查询用到的表名,每行都有对应的表名。
<unionM,N> : 本行引用了 id 为 M 和 N 的行的 UNION 结果。
<derivedN> : 本行引用了 id 为 N 的表所产生的的派生表结果。派生表有可能产生自 FROM 语句中的子查询。
<subqueryN> : 本行引用了 id 为 N 的表所产生的的物化子查询结果。
type
查询执行的类型,描述了查询是如何执行的。所有值的顺序从最优到最差排序为:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
system:如果表使用的引擎对于表行数统计是精确的(如:MyISAM),且表中只有一行记录的情况下,访问方法是 system ,是 const 的一种特例。
const:表中最多只有一行匹配的记录,一次查询就可以找到,常用于使用主键或唯一索引的所有字段作为查询条件。
eq_ref:当连表查询时,前一张表的行在当前这张表中只有一行与之对应。是除了 system 与 const 之外最好的 join 方式。
ref:使用普通索引作为查询条件,查询结果可能找到多个符合条件的行。
index_merge:当查询条件使用了多个索引时,表示开启了 Index Merge 优化,此时执行计划中的 key 列列出了使用到的索引。
range:对索引列进行范围查询,执行计划中的 key 列表示哪个索引被使用了。
index:查询遍历了整棵索引树,与 ALL 类似,只不过扫描的是索引,而索引一般在内存中,速度更快。
ALL:全表扫描。
possible_keys
possible_keys 列表示 MySQL 执行查询时可能用到的索引。如果这一列为 NULL ,则表示没有可能用到的索引。
key
key 列表示 MySQL 实际使用到的索引。如果为 NULL,则表示未用到索引。
key_len
key_len 列表示 MySQL 实际使用的索引的最大长度;当使用到联合索引时,有可能是多个列的长度和。
在满足需求的前提下越短越好。如果 key 列显示 NULL ,则 key_len 列也显示 NULL 。
rows
rows 列表示根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好。
Extra
包含了 MySQL 解析查询的额外信息,通过这些信息,可以更准确的理解 MySQL 到底是如何执行查询的。
Using filesort:在排序时使用了外部的索引排序,没有用到表内索引进行排序。
Using temporary:MySQL 需要创建临时表来存储查询的结果,常见于 ORDER BY 和 GROUP BY。
Using index:表明查询使用了覆盖索引,不用回表,查询效率非常高。
Using index condition:表示查询优化器选择使用了索引条件下推这个特性。
Using where:表明查询使用了 WHERE 子句进行条件过滤。一般在没有使用到索引的时候会出现。
Using join buffer (Block Nested Loop):连表查询的方式,表示当被驱动表的没有使用索引的时候,
MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。
MySQL 会先将驱动表读出来放到 join buffer 中,再遍历被驱动表与驱动表进行查询。
注意
当 Extra 列包含 Using filesort 或 Using temporary 时,MySQL 的性能可能会存在问题,需要尽可能避免。
数据库命名规范
所有数据库对象名称必须使用小写字母并用下划线分割。
所有数据库对象名称禁止使用 MySQL 保留关键字。
数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符。
临时库表必须以 tmp_ 为前缀并以日期为后缀,备份表必须以 bak_ 为前缀并以日期 (时间戳) 为后缀。
所有存储相同数据的列名和列类型必须一致。
数据库基本设计规范
所有表必须使用 InnoDB 存储引擎。
数据库和表的字符集统一使用 UTF8,支持emoji,则需要采用 utf8mb4。
所有表和字段都需要添加注释。
尽量控制单表数据量的大小,建议控制在 500 万以内(并不是数据库限制)。
谨慎使用 MySQL 分区表,建议采用物理分表的方式管理大数据。
经常一起使用的列放到一个表中。
禁止在表中建立预留字段。
禁止在数据库中存储文件(比如图片)这类大的二进制数据。
不要被数据库范式所束缚。
禁止在线上做数据库压力测试。
禁止从开发环境,测试环境直接连接生产环境数据库。
数据库字段设计规范
优先选择符合存储需要的最小的数据类型。
避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据。
避免使用 ENUM 类型。
尽可能把所有列定义为 NOT NULL。
一定不要用字符串存储日期。
同财务相关的金额类数据必须使用 decimal 类型。
单表不要包含过多字段。
索引设计规范
限制每张表上的索引数量,建议单张表索引不超过 5 个。
禁止使用全文索引。
禁止给表中的每一列都建立单独的索引。
每个 InnoDB 表必须有个主键。
不要使用 UUID、MD5、HASH 字符串列作为主键,主键建议使用自增 ID 值。
常见索引列建议
1、出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列。
2、包含在 ORDER BY、GROUP BY、DISTINCT 中的字段。
3、并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好。
4、多表 join 的关联列。
选择索引列的顺序
区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)。
尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)。
使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)。
避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)。
对于频繁的查询优先考虑使用覆盖索引
覆盖索引:就是包含了所有查询字段 (where,select,order by,group by 包含的字段) 的索引。
好处
避免 InnoDB 表进行索引的二次查询,也就是回表操作。
可以把随机 IO 变成顺序 IO 加快查询效率。
索引 SET 规范
不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引。
外键可用于保证数据的参照完整性,但建议在业务端实现。
外键会影响父表和子表的写操作从而降低性能。
数据库 SQL 开发规范
尽量不在数据库做运算,复杂运算需移到业务应用里完成。
优化对性能影响较大的 SQL 语句。
充分利用表上已经存在的索引。
禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询。
禁止使用不含字段列表的 INSERT 语句。
建议使用预编译语句进行数据库操作。
避免数据类型的隐式转换。
避免使用子查询,可以把子查询优化为 join 操作。
避免使用 JOIN 关联太多的表。
减少同数据库的交互次数。
对应同一列进行 or 判断时,使用 in 代替 or。
in 的值不要超过 500 个。
禁止使用 order by rand() 进行随机排序。
WHERE 从句中禁止对列进行函数转换和计算。
在明显不会有重复值时使用 UNION ALL 而不是 UNION。
拆分复杂的大 SQL 为多个小 SQL。
程序连接不同的数据库使用不同的账号,禁止跨库查询。
数据库操作行为规范
超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作。
对于大表使用 pt-online-schema-change 修改表结构。
禁止为程序使用的账号赋予 super 权限。
对于程序连接数据库账号,遵循权限最小原则。
其它
查询缓存
执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用。
自增主键
让聚集索引尽量地保持递增顺序插入,避免了随机查询,从而提高了查询效率。
存储位置
MyISAM 引擎的自增值保存在数据文件中。
InnoDB 引擎的自增值,其实是保存在了内存里,并没有持久化。
首次打开表时,会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。
首次打开表时,会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。
MySQL 8.0 版本后,自增值的变更记录被放在了 redo log 中,提供了自增值持久化的能力。
自增行为
在 MySQL 里面,如果字段 id 被定义为 AUTO_INCREMENT,在插入一行数据的时候,自增值的行为。
如果插入数据时 id 字段指定为 0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段。
如果插入数据时 id 字段指定了具体的值,就直接使用语句里指定的值。
根据要插入的值和当前自增值的大小关系,自增值的变更结果也会有所不同。
假设某次要插入的值是 insert_num,当前的自增值是 autoIncrement_num:
如果 insert_num < autoIncrement_num,那么这个表的自增值不变。
如果 insert_num >= autoIncrement_num,就需要把当前自增值修改为新的自增值,
假设某次要插入的值是 insert_num,当前的自增值是 autoIncrement_num:
如果 insert_num < autoIncrement_num,那么这个表的自增值不变。
如果 insert_num >= autoIncrement_num,就需要把当前自增值修改为新的自增值,
自增值不连续场景 1
分布式 id 为了避免两个库生成的主键发生冲突,我们可以让一个库的自增 id 都是奇数,另一个库的自增 id 都是偶数。
自增值不连续场景 2
自增初始值和自增步长设置不为 1。
唯一键冲突。
事务回滚。
自增值不连续场景 3
批量插入(如 insert...select 语句)。
隐式转换
当操作符左右两边的数据类型不一致时,会发生隐式转换。
当 where 查询操作符左边为数值类型时发生了隐式转换,那么对效率影响不大,但还是不推荐这么做。
当 where 查询操作符左边为字符类型时发生了隐式转换,那么会导致索引失效,造成全表扫描效率极低。
字符串转换为数值类型时,非数字开头的字符串会转化为0,以数字开头的字符串会截取从第一个字符到第一个非数字内容为止的值为转化结果。
Redis
Redis (REmote DIctionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。
快的原因
Redis 基于内存,内存的访问速度是磁盘的上千倍。
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用。
Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
分布式缓存选型
Redis
首选
Memcached
分布式缓存最开始兴起的那会,比较常用的。
Dragonfly
针对现代应用程序负荷需求而构建的内存数据库,完全兼容 Redis 和 Memcached 的 API,迁移时无需修改任何代码,号称全世界最快的内存数据库。
KeyDB
Redis 的一个高性能分支,专注于多线程、内存效率和高吞吐量。
Tendis
腾讯开源,基于 RocksDB,兼容 Redis,但关注度不高。
Redis 和 Memcached
共同点
都是基于内存的数据库,一般都用来当做缓存使用。
都有过期策略。
两者的性能都非常高。
区别
Redis 支持更丰富的数据类型(支持更复杂的应用场景)。Memcached 只支持最简单的 k/v 数据类型。
Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 把数据全部存在内存之中。
Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但是,Memcached 在服务器内存使用完之后,就会直接报异常。
Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前是原生支持 cluster 模式的。
Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型。 (Redis 6.0 针对网络数据的读写引入了多线程)
Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言。
Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除。
用 Redis 原因
高性能
使用内存,访问速度非常快。
高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,
但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。
但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数。
读写策略
Cache Aside Pattern(旁路缓存模式)
服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
写
读
缺陷
首次请求数据一定不在 cache 的问题。
解决:可以将热点数据可以提前放入 cache 中。
解决:可以将热点数据可以提前放入 cache 中。
写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。
解决:
数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间。
解决:
数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间。
Read/Write Through Pattern(读写穿透)
服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。
在平时在开发过程中非常少见,因为 Redis 并没有提供 cache 将数据写入 db 的功能。
Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。
写(Write Through)
读(Read Through)
Write Behind Pattern(异步缓存写入)
与 Read/Write Through Pattern 很相似,但其只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。
在平时开发过程中也非常少见,因其更新db方式对数据一致性带来了更大的挑战。
应用
消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制。
db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
模块化
Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中。
常用
实现搜索引擎、处理 JSON 数据、实现图形数据库、处理时间序列数据、实现布隆过滤器、实现分布式限流模块等等。
应用
分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。基于 Redisson 来实现分布式锁。
限流:一般是通过 Redis + Lua 脚本的方式来实现限流。
消息队列:Redis 自带的 List/Stream 数据结构可以作为一个简单的队列使用。不建议。
延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景。
搜索引擎
RediSearch + RedisJSON 可实现小型项目的简单搜索场景。
比较复杂或者数据规模较大的搜索场景还是建议使用 Elasticsearch。
数据类型
概要
5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
其它特殊数据类型:Bloom filter(布隆过滤器)、Bitfield(位域)。
基础数据类型的底层 8 种数据结构:简单动态字符串(SDS)、LinkedList(双向链表)、
Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
Dict(哈希表/字典)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)。
String(字符串)
String 是 Redis 中最简单同时也是最常用的一个数据类型。
String 是一种二进制安全的数据类型,可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(base64 编码或路径)、序列化后的对象。
底层实现
简单动态字符串(Simple Dynamic String,SDS)
应用
常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存。
计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
分布式锁(利用 SETNX key value 命令可以实现一个最简易的分布式锁)。
与 Hash 对比
对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 非常适合。
系统对性能和资源消耗非常敏感的话,String 非常适合。
在绝大部分情况,推荐使用 String 来存储对象数据。
List(列表)
Redis 的 List 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
底层实现
LinkedList(双向链表)/ ZipList(压缩列表)/ QuickList(快速列表)
应用
信息流展示(最新文章、最新动态)
消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
Hash(哈希)
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象。
底层实现
Dict(哈希表/字典)、ZipList(压缩列表)
应用
对象数据存储场景(用户信息、商品信息、文章信息、购物车信息)
Set(集合)
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一。
底层实现
Dict(哈希表/字典)、Intset(整数集合)
应用
需要存放的数据不能重复的场景(网站 UV 统计、文章点赞、动态点赞)
需要获取多个数据源交集、并集和差集的场景(共同好友(交集)、好友推荐(差集)、订阅号推荐(差集+交集))
需要随机获取数据源中的元素的场景(抽奖系统、随机点名)
Sorted Set(有序集合)
类似于 Set,但增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
底层实现
ZipList(压缩列表)、SkipList(跳跃表)
应用
需要随机获取数据源中的元素根据某个权重进行排序的场景(各种排行榜)
需要存储的数据有优先级或者重要程度的场景(优先级任务队列)
Bitmap (位图)
Bitmap 存储的是连续的二进制数字(0 和 1),只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。
应用
需要保存状态信息(0/1 即可表示)的场景(用户签到情况、活跃用户情况、用户行为统计)
HyperLogLog(基数统计)
HyperLogLog 是一种有名的基数计数概率算法 ,Redis 只是实现了这个算法并提供了一些开箱即用的 API。
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64个不同元素。
计数方式
稀疏矩阵:计数较少的时候,占用空间很小。
稠密矩阵:计数达到某个阈值的时候,占用 12k 的空间。
应用
数量量巨大(百万、千万级别以上)的计数场景(热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计)
Geospatial (地理位置)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。
通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
应用
需要管理使用地理空间数据的场景(附近的人)
持久化
使用缓存的时候,我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中。
方式
快照(snapshotting,RDB)
只追加文件(append-only file,AOF)
RDB 和 AOF 的混合持久化(Redis 4.0 新增)
RDB 持久化
Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。默认方式。
应用
将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能)。
将快照留在原地以便重启服务器的时候使用。
生成 RDB 快照命令
save : 同步保存操作,会阻塞 Redis 主线程。
bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
优势
RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。
使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。
劣势
RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
AOF 持久化
与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了)。
流程
命令追加(append):所有的写命令会追加到 AOF 缓冲区中。
文件写入(write):将 AOF 缓冲区的数据写入到 AOF 文件中。
文件同步(fsync):AOF 缓冲区根据对应的持久化方式( fsync 策略)向硬盘做同步操作。
文件重写(rewrite):随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
重启加载(load):当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
定义
write:写入系统内核缓冲区之后直接返回(仅仅是写到缓冲区),不会立即同步到硬盘。
fsync:用于强制刷新系统内核缓冲区(同步到到磁盘),确保写磁盘操作结束才会返回。
方式( fsync策略)
appendfsync always:write + fsync,这样会严重降低 Redis 的性能。
appendfsync everysec:write + fsync,fsync 间隔为 1 秒,兼顾数据和写入性能。
appendfsync no:write 但不 fsync,fsync 的时机由操作系统决定。
这 3 种持久化方式的主要区别在于 fsync 同步 AOF 文件的时机(刷盘)。
从 Redis 7.0.0 开始,Redis 使用了 Multi Part AOF 机制。顾名思义,Multi Part AOF 就是将原来的单个 AOF 文件拆分成多个 AOF 文件。
在 Multi Part AOF 中,AOF 文件被分为三种类型,分别为:
BASE:表示基础 AOF 文件,它一般由子进程通过重写产生,该文件最多只有一个。
INCR:表示增量 AOF 文件,它一般会在 AOFRW 开始执行时被创建,该文件可能存在多个。
HISTORY:表示历史 AOF 文件,它由 BASE 和 INCR AOF 变化而来,每次 AOFRW 成功完成时,本次 AOFRW 之前对应的 BASE 和 INCR AOF 都将变为 HISTORY,HISTORY 类型的 AOF 会被 Redis 自动删除。
在 Multi Part AOF 中,AOF 文件被分为三种类型,分别为:
BASE:表示基础 AOF 文件,它一般由子进程通过重写产生,该文件最多只有一个。
INCR:表示增量 AOF 文件,它一般会在 AOFRW 开始执行时被创建,该文件可能存在多个。
HISTORY:表示历史 AOF 文件,它由 BASE 和 INCR AOF 变化而来,每次 AOFRW 成功完成时,本次 AOFRW 之前对应的 BASE 和 INCR AOF 都将变为 HISTORY,HISTORY 类型的 AOF 会被 Redis 自动删除。
执行完命令之后记录日志原因
避免额外的检查开销,AOF 记录日志不会对命令进行语法检查。
在命令执行完之后再记录,不会阻塞当前的命令执行。
风险
如果刚执行完命令 Redis 就宕机会导致对应的修改丢失。
可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。
AOF 重写
当 AOF 变得太大时,Redis 在后台自动重写 AOF 产生一个新的 AOF 文件,新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
AOF 校验机制
Redis 在启动时对 AOF 文件进行检查,以判断文件是否完整,是否有损坏或者丢失的数据。
原理
校验和(checksum),通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。
优势
RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。
AOF 以一种易于理解和解析的格式包含所有操作的日志。
混合持久化
由于 RDB 和 AOF 各有优势,于是,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
RDB 和 AOF
Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。
不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。
如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。
线程模型
定义
对于读写命令来说,Redis 一直是单线程模型。
Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作。
Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。
单线程模型
定义
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 ,该模型对应的是 Redis 中的文件事件处理器(file event handler)。
由于文件事件处理器(file event handler)是单线程方式运行的,所以一般都说 Redis 是单线程模型。
文件事件处理器
多个 socket(客户端连接)
IO 多路复用程序(支持多个客户端连接的关键)
文件事件分派器(将 socket 关联到相应的事件处理器)
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
Redis6.0 虽然引入了多线程,但是只是在网络数据的读写这类耗时操作上使用,执行命令仍然是单线程顺序执行,因此不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。并且开启后,性能不能有太大提升,因此一般情况下并不建议开启。
后台线程
用于执行一些比较耗时的操作。
通过 bio_close_file 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
通过 bio_aof_fsync 后台线程调用 fsync 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
通过 bio_lazy_free 后台线程释放大对象(已删除)占用的内存空间。
内存管理
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。
过期数据的删除策略
惰性删除:只会在取出 key 的时候才对数据进行过期检查。
定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
内存淘汰机制
解决可能存在定期删除和惰性删除漏掉了很多过期 key 的情况,导致大量过期 key 堆积在内存里而 Out of memory 问题。
数据淘汰策略
volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。不推荐!
volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
事务
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(Transaction)功能。
Redis 事务是不支持回滚(roll back)操作,Redis 事务不满足原子性。
Redis 事务的持久性也是没办法保证。
通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。
Redis 事务实际开发中使用的非常少,功能比较鸡肋,不建议在日常开发中使用。
性能优化
使用批量操作减少网络传输
原生批量操作命令
pipeline(流水线)
Lua 脚本
大量 key 集中过期问题
给 key 设置随机过期时间。推荐!
开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的。
Redis bigkey(大 Key)
标准
String 类型的 value 超过 1MB。
复合类型(List、Hash、Set、Sorted Set 等)的 value 包含的元素超过 5000 个。
问题
客户端超时阻塞、网络阻塞、工作线程阻塞
定位
使用 Redis 自带的 --bigkeys 参数来查找。
使用 Redis 自带的 SCAN 命令。
借助开源工具分析 RDB 文件。
借助公有云的 Redis 分析服务。
处理
分割 bigkey:将一个 bigkey 分割为多个小 key。
手动清理:Redis 4.0+ 可以使用 UNLINK 命令来异步删除一个或多个指定的 key。
采用合适的数据结构。
开启 lazy-free(惰性删除/延迟释放) :lazy-free 特性是 Redis 4.0 开始引入的。
Redis hotkey(热 Key)
如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key)。
问题
处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。
如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。
定位
使用 Redis 自带的 --hotkeys 参数来查找。
使用MONITOR 命令。
借助开源项目。
京东零售的 hotkey 项目。
根据业务情况提前预估。
业务代码中记录分析。
借助公有云的 Redis 分析服务。
处理
读写分离:主节点处理写请求,从节点处理读请求。
使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
二级缓存:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。
公有云,通过代理查询缓存功能(Proxy Query Cache)优化热点 Key 问题。
慢查询命令
Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。
时间复杂度 O(n) 的命令
KEYS *:会返回所有符合规则的 key。
HGETALL:会返回一个 Hash 中所有的键值对。
LRANGE:会返回 List 中指定范围内的元素。
SMEMBERS:返回 Set 中的所有元素。
SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。
时间复杂度 O(n) 以上的命令
ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有元素。
ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。
定位
在 redis.conf 文件中,我们可以使用 slowlog-log-slower-than 参数设置耗时命令的阈值,并使用 slowlog-max-len 参数设置耗时命令的最大记录条数。
内存碎片
那些不可用的空闲内存。虽然不会影响 Redis 性能,但是会增加内存消耗。
原因
Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
频繁修改 Redis 中的数据也会产生内存碎片。
查看
使用 info memory 命令即可查看 Redis 内存相关的信息。
内存碎片率的计算公式:mem_fragmentation_ratio = used_memory_rss / used_memory
mem_fragmentation_ratio (内存碎片率)的值越大代表内存碎片率越严重。
mem_fragmentation_ratio > 1.5 才需要清理内存碎片。
清理
直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。
注意性能影响,可通过配置控制。
重启节点可以做到内存碎片重新整理。
生产问题
缓存穿透
大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。
解决
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。
缓存无效 key、布隆过滤器、接口限流
缓存击穿
请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
解决
设置热点数据永不过期或者过期时间比较长。
针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
缓存雪崩
缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。
解决
服务不可用
采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
限流,避免同时处理大量的请求。
多级缓存,例如:本地缓存 + Redis 缓存的组合。
热点缓存失效
设置不同的失效时间比如随机设置缓存的失效时间。
缓存永不失效(不太推荐,实用性太差)。
缓存预热,也就是在程序启动后或运行过程中,主动将热点数据加载到缓存中。
使用定时任务,比如 xxl-job。
使用消息队列,比如 Kafka。
Redis 阻塞
O(n) 命令
SAVE 创建 RDB 快照、AOF 日志记录阻塞、AOF 刷盘阻塞、AOF 重写阻塞
大 Key、清空数据库、集群扩容、Swap(内存交换)、CPU 竞争、网络问题
集群
Redis 集群是一种通过将多个 Redis 节点连接在一起以实现高可用性、数据分片和负载均衡的技术。
主要有三种模式:主从复制模式(Master-Slave)、哨兵模式(Sentinel)和 Cluster 模式。
作用
高可用性:Redis集群可以在某个节点发生故障时,自动进行故障转移,保证服务的持续可用。
负载均衡:Redis集群可以将客户端请求分发到不同的节点上,有效地分摊节点的压力,提高系统的整体性能。
容灾恢复:通过主从复制或哨兵模式,Redis集群可以在主节点出现故障时,快速切换到从节点,实现业务的无缝切换。
数据分片:在Cluster模式下,Redis集群可以将数据分散在不同的节点上,从而突破单节点内存限制,实现更大规模的数据存储。
易于扩展:Redis集群可以根据业务需求和系统负载,动态地添加或移除节点,实现水平扩展。
主从复制模式(Master-Slave)
一种基本集群模式,它通过将一个Redis节点(主节点)的数据复制到一个或多个其他Redis节点(从节点)来实现数据的冗余和备份。
优点
配置简单,易于实现。
实现数据冗余,提高数据可靠性。
读写分离,提高系统性能。
缺点
主节点故障时,需要手动切换到从节点,故障恢复时间较长。
主节点承担所有写操作,可能成为性能瓶颈。
无法实现数据分片,受单节点内存限制。
应用
数据备份和容灾恢复:通过从节点备份主节点的数据,实现数据冗余。
读写分离:将读操作分发到从节点,减轻主节点压力,提高系统性能。
在线升级和扩展:在不影响主节点的情况下,通过增加从节点来扩展系统的读取能力。
哨兵模式(Sentinel)
在主从复制基础上加入了哨兵节点,实现了自动故障转移。
优点
自动故障转移,提高系统的高可用性。
具有主从复制模式的所有优点,如数据冗余和读写分离。
缺点
配置和管理相对复杂。
依然无法实现数据分片,受单节点内存限制。
应用
高可用性要求较高的场景:通过自动故障转移,确保服务的持续可用。
数据备份和容灾恢复:在主从复制的基础上,提供自动故障转移功能。
Cluster 模式
定义
Redis的一种高级集群模式,它通过数据分片和分布式存储实现了负载均衡和高可用性。
在Cluster模式下,Redis将所有的键值对数据分散在多个节点上。每个节点负责一部分数据,称为槽位。
通过对数据的分片,Cluster模式可以突破单节点的内存限制,实现更大规模的数据存储。
数据分片与槽位
Redis Cluster将数据分为16384个槽位,每个节点负责管理一部分槽位。
当客户端向Redis Cluster发送请求时,Cluster会根据键的哈希值将请求路由到相应的节点。
具体来说,Redis Cluster使用CRC16算法计算键的哈希值,然后对16384取模,得到槽位编号。
优点
数据分片,实现大规模数据存储。
负载均衡,提高系统性能。
自动故障转移,提高高可用性。
缺点
配置和管理较复杂。
一些复杂的多键操作可能受到限制。
应用
大规模数据存储:通过数据分片,突破单节点内存限制。
高性能要求场景:通过负载均衡,提高系统性能。
高可用性要求场景:通过自动故障转移,确保服务的持续可用。
规范
使用连接池:避免频繁创建关闭客户端连接。
尽量不使用 O(n)指令,使用 O(n) 命令时要关注 n 的数量。
使用批量操作减少网络传输:原生批量操作命令(比如 MGET、MSET等等)、pipeline、Lua 脚本。
尽量不适用 Redis 事务:Redis 事务实现的功能比较鸡肋,可以使用 Lua 脚本代替。
禁止长时间开启 monitor:对性能影响比较大。
控制 key 的生命周期:避免 Redis 中存放了太多不经常被访问的数据。
ElasticSearch
Elasticsearch 全称叫全文搜索引擎,简称 ES,一个分布式可扩展的实时搜索和分析引擎,一个建立在搜索引擎 Apache Lucene™ 基础上的搜索引擎。
原理
使用 Java 编写的,它的内部使用 Lucene 做索引与搜索。
Lucene 就是一个 Jar 包,里面包含了各种建立倒排索引的方法。
作用
一个分布式的实时文档存储,每个字段可以被索引与搜索。
一个分布式实时分析搜索引擎。
能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。
组件
Client(客户端)
支持 Http 和 Tcp 两种协议去访问,不过目前 Tcp 只支持 Java。Http 和 Tcp 端口默认9200,如果是 Java 应用建议是 Tcp 方式访问 ES,性能更好。
Cluster(集群)
ES 天生就是集群方式,哪怕只有一个节点。
一个集群有一个唯一的名字标志,默认为“elasticsearch”。
集群名称非常重要,具体相同集群名的节点才会组成一个集群,集群名称可以在配置文件中指定。
EsMaster(Master 节点)
EsMaster 负责存放 Elasticsearch 的元数据。
ES元数据包括(身份元数据、索引元数据、文档元数据、路由元数据以及其他类型的元数据),管理集群节点状态。
EsNode(Node 节点)
Node 节点:存储集群的数据,参与集群的索引和搜索功能。
同一个集群内节点的名字不能重复,ES会自动分配,也可以在配置文件中指定。通常在一个节点上分配一个或者多个分片。
Shards(分片)
分片,就是把索引的数据做水平切分,类似于mysql的分区。
在一个多分片的索引中写入数据时,通过路由来确定具体写入那一个分片中,所以在创建索引时需要指定分片的数量,并且分片的数量一旦确定就不能更改。
分片还有可以有副本,解决当某个节点宕机后不影响索引正常访问。分片数据量可按照每个分片<30G设置,默认5个分片。
Replicas(副本)
副本,是指对主分片的备份。主分片和备份分片都可以对外提供查询服务,写操作时先在主分片上完成,然后分发到备份上。
当主分片不可用时,会在备份的分片中选举出一个作为主分片,所以备份不仅可以提升系统的高可用性能,还可以提升搜索时的并发性能。
副本数不宜太多,会增加数据写入负担,副本数要 <= 集群节点数 - 1。
Index(索引-数据库/表)
索引: 一个索引是一个文档的集合。每个索引有唯一的名字,通过这个名字来操作它。
Index 可以理解为 RDBMS 里的数据库,也可理解一张表,要看我们怎么使用。
Type(类型-表)
类型,指索引内部的逻辑分区,通过Type的名字在索引内进行唯一标识。
在查询时如果没有该值,则表示在整个索引中查询。可以理解为表。
在 ES6.x 中一个 Index 只能有一个 Type,在 ES7.x 后就取消了 Type,逐渐减少 Type 是为了提高查询效率。
Document(文档-行)
文档,索引中的每一条数据叫作一个文档。
ES 中一个可以被检索的基本单位,每一 Document 都有一个唯一的 ID 作为区分,以 Json 格式来表示。
Field(字段-列)
好比关系型数据库中列的概念,一个 Document 有一个或者多个 Field 组成。
Mapping(映射-表结构)
类似于关系型数据库中的表结构信息,用于定义索引中字段(Field)的存储类型、分词方式、是否存储等信息。
ES 中的 Mapping 是可以动态识别的,根据插入的数据自动识别字段类型。
一个索引的 Mapping 一旦创建,若已经存储了数据,就不可修改了。
可以创建索引 Mapping 模板,只要索引名字匹配了就会按照该 Mapping 插入数据。
Status(集群状态)
Green(绿色):所有的主分片和副本分片都正常运行。
Yellow(黄色):所有的主分片都正常运行,但不是所有的副本分片都正常运行。
Red(红色):有主分片没能正常运行。
ES数据架构的主要概念与关系数据库Mysql对比表。
核心技术
分词
字符串按照一定规则分成多个独立的词元(token)。
Elasticsearch 内置的分词器对中文不友好,会把中文分成单个字来进行全文检索,不能达到想要的结果 。其中IK分词器对中文很好,一般都使用它。
倒排索引
也可以称反向索引,倒排索引是搜索引擎到核心。
组成
单词词典(Term Dictionary)
记录所有文档的单词,一般都比较大。
记录单词到倒排列表的关联信息(文档ID)。
倒排列表(Posting List)
记录了单词对应的文档集合,由倒排索引项(Posting)组成。
单词词典的实现一般是 B+Tree。
倒排索引项组成
文档ID,用于获取原始信息。
单词频率(TF, Term Frequency),记录该单词在该文档中的出现次数,用于后续相关性算法。
位置(Position),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query)。
偏移(Offset),记录单词在文档的开始和结束位置,用于做高亮显示。
优点
天生分片和集群,从 ES 出生开始就天然的支持分布式的特征,且无需第三方组件,自带。
天生索引,ES 所有数据都是默认进行索引的,这点和 MySQL 正好相反,ES 只有不加索引才需要说明。
支持PB级海量数据实时全文搜索。
支持多语言访问,支持 TCP 和 RESTFUL API两种方式访问。
缺点
不适合做复杂聚合,会影响ES集群性能。
不支持高并发写入数据。
ES 耗 CPU 和内存资源,需要用高配置的机器来搭建集群,使用成本比较高。
MongoDB
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的,属于文档类型数据库。
存储结构
文档(Document):MongoDB 中最基本的单元,由 BSON 键值对(key-value)组成,类似于关系型数据库中的行(Row)。
集合(Collection):一个集合可以包含多个文档,类似于关系型数据库中的表(Table)。
数据库(Database):一个数据库中可以包含多个集合,可以在 MongoDB 中创建多个数据库,类似于关系型数据库中的数据库(Database)。
文档
MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
BSON [bee·sahn] 是 Binary JSON 的简称,是 JSON 文档的二进制表示,
支持将文档和数组嵌入到其他文档和数组中,还包含允许表示不属于 JSON 规范的数据类型的扩展。
支持将文档和数组嵌入到其他文档和数组中,还包含允许表示不属于 JSON 规范的数据类型的扩展。
BJSON 的遍历速度优于 JSON,但 BJSON 需要更多的存储空间。
键命名规则
键不能含有 \0(空字符)。这个字符用来表示键的结尾。
. 和 $ 有特别的意义,只有在特定环境下才能使用。
以下划线_开头的键是保留的(不是严格要求的)。
集合
MongoDB 集合存在于数据库中,没有固定的结构,也就是 无模式 的,这意味着可以往集合插入不同格式和类型的数据。
集合命名条件
集合名不能是空字符串""。
集合名不能含有 \0 (空字符),这个字符表示集合名的结尾。
集合名不能以"system."开头,这是为系统集合保留的前缀。
集合名必须以下划线或者字母符号开始,并且不能包含 $。
数据库
数据库用于存储所有集合,而集合又用于存储所有文档。一个 MongoDB 中可以创建多个数据库,每一个数据库都有自己的集合和权限。
预留库
admin : admin 数据库主要是保存 root 用户和角色。
local : local 数据库是不会被复制到其他分片的,因此可以用来存储本地单台服务器的任意 collection。
config : 当 MongoDB 使用分片设置时,config 数据库可用来保存分片的相关信息。
test : 默认创建的测试库,连接 mongod 服务时,如果不指定连接的具体数据库,默认就会连接到 test 数据库。
数据库命名条件
不能是空字符串""。
不得含有' '(空格)、.、$、/、\和 \0 (空字符)。
应全部小写。
最多 64 字节。
特点
数据记录被存储为文档:MongoDB 中的记录就是一个 BSON 文档,它是由键值对组成的数据结构,类似于 JSON 对象,是 MongoDB 中的基本数据单元。
模式自由:集合的概念类似 MySQL 里的表,但它不需要定义任何模式,能够用更少的数据对象表现复杂的领域模型对象。
支持多种查询方式:MongoDB 查询 API 支持读写操作 (CRUD)以及数据聚合、文本搜索和地理空间查询。
支持 ACID 事务:NoSQL 数据库通常不支持事务,为了可扩展和高性能进行了权衡。但MongoDB 支持事务,同样具有 ACID 特性。
高效的二进制存储:存储在集合中的文档,是以键值对的形式存在的。
自带数据压缩功能:存储同样的数据所需的资源更少。
支持 mapreduce:通过分治的方式完成复杂的聚合任务。从 MongoDB 5.0 开始,推荐其替代方案 聚合管道。
支持多种类型的索引:MongoDB 支持多种类型的索引,包括单字段索引、复合索引、多键索引、哈希索引、文本索引、 地理位置索引等。
支持 failover:提供自动故障恢复的功能,主节点故障时,自动选举新的主节点,客户无感知。
支持分片集群:MongoDB 支持集群自动切分数据,让集群存储更多的数据,具备更强的性能。
支持存储大文件:MongoDB 的单文档存储空间要求不超过 16MB。提供了 GridFS 来进行分块存储。
存储引擎
数据库的核心组件,负责管理数据在内存和磁盘中的存储方式。与 MySQL 一样,MongoDB 采用的也是 插件式的存储引擎架构。
最初默认是使用 MMAPV1 存储引擎,MongoDB4.x 版本不再支持 MMAPv1 存储引擎。
分类
WiredTiger 存储引擎:自 MongoDB 3.2 以后,默认的存储引擎为 WiredTiger 存储引擎 。
非常适合大多数工作负载,建议用于新部署。WiredTiger 提供文档级并发模型、检查点和数据压缩等功能。
非常适合大多数工作负载,建议用于新部署。WiredTiger 提供文档级并发模型、检查点和数据压缩等功能。
In-Memory 存储引擎:In-Memory 存储引擎 在 MongoDB Enterprise 中可用。
它不是将文档存储在磁盘上,而是将它们保留在内存中以获得更可预测的数据延迟。
它不是将文档存储在磁盘上,而是将它们保留在内存中以获得更可预测的数据延迟。
MongoDB 3.0 提供了 可插拔的存储引擎 API ,允许第三方为 MongoDB 开发存储引擎,这点和 MySQL 也比较类似。
WiredTiger
默认使用 B+ 树作为其存储结构,还支持 LSM(Log Structured Merge) 树作为存储结构。
使用 B+ 树时,WiredTiger 以 page 为基本单位往磁盘读写数据。B+ 树的每个节点为一个 page。
page分类
root page(根节点):B+ 树的根节点。
internal page(内部节点):不实际存储数据的中间索引节点。
leaf page(叶子节点):真正存储数据的叶子节点,包含一个页头(page header)、块头(block header)和真正的数据(key/value)。
聚合
将多个文档甚至是多个集合汇总到一起计算分析(比如求和、取最大值)并返回计算后的结果,这个过程被称为 聚合操作 。
分类
聚合管道(Aggregation Pipeline):执行聚合操作的首选方法。
单一目的聚合方法(Single purpose aggregation methods):也就是单一作用的聚合函数比如 count()、distinct()、estimatedDocumentCount()。
mapreduce:通过分治的方式完成复杂的聚合任务。从 MongoDB 5.0 开始,推荐其替代方案 聚合管道。
聚合管道
由多个阶段组成,每个阶段在文档通过管道时转换文档。
工作流程
接受一系列原始数据文档。
对这些文档进行一系列运算。
结果文档输出给下一个阶段。
操作符
$match
匹配操作符,用于对文档集合进行筛选。
$project
投射操作符,用于重构每一个文档的字段,可以提取字段,重命名字段,甚至可以对原有字段进行操作后新增字段。
$sort
排序操作符,用于根据一个或多个字段对文档进行排序。
$limit
限制操作符,用于限制返回文档的数量。
$skip
跳过操作符,用于跳过指定数量的文档。
$count
统计操作符,用于统计文档的数量。
$group
分组操作符,用于对文档集合进行分组。
$unwind
拆分操作符,用于将数组中的每一个值拆分为单独的文档。
$lookup
连接操作符,用于连接同一个数据库中另一个集合,并获取指定的文档,类似于 populate。
事务
NoSQL 数据库通常不支持事务,但 MongoDB 支持事务。
MongoDB 单文档原生支持原子性,也具备事务的特性。通常指的是 多文档 。
MongoDB 4.0 加入了对多文档 ACID 事务的支持,但只支持复制集部署模式下的 ACID 事务,也就是说事务的作用域限制为一个副本集内。
MongoDB 4.2 引入了 分布式事务 ,增加了对分片集群上多文档事务的支持,并合并了对副本集上多文档事务的现有支持。
在大多数情况下,多文档事务比单文档写入会产生更大的性能成本。对于大部分场景来说,适当地对数据进行建模可以最大限度地减少对多文档事务的需求。
从 MongoDB 4.2 开始,多文档事务支持副本集和分片集群,其中:主节点使用 WiredTiger 存储引擎,
同时从节点使用 WiredTiger 存储引擎或 In-Memory 存储引擎。在 MongoDB 4.0 中,只有使用 WiredTiger 存储引擎的副本集支持事务。
同时从节点使用 WiredTiger 存储引擎或 In-Memory 存储引擎。在 MongoDB 4.0 中,只有使用 WiredTiger 存储引擎的副本集支持事务。
在 MongoDB 4.2 及更早版本中,你无法在事务中创建集合。从 MongoDB 4.4 开始,您可以在事务中创建集合和索引。
数据压缩
借助 WiredTiger 存储引擎,MongoDB 支持对所有集合和索引进行压缩。压缩以额外的 CPU 为代价最大限度地减少存储使用。
算法
Snappy:谷歌开源的压缩算法(压缩比 3 ~ 5 倍),WiredTiger 默认使用其对所有集合使用块压缩,对所有索引使用前缀压缩。
zlib:高度压缩算法,压缩比 5 ~ 7 倍。
Zstandard(简称 zstd):Facebook 开源的一种快速无损压缩算法,
针对 zlib 级别的实时压缩场景和更好的压缩比,提供更高的压缩率和更低的 CPU 使用率,MongoDB 4.2 开始可用。
针对 zlib 级别的实时压缩场景和更好的压缩比,提供更高的压缩率和更低的 CPU 使用率,MongoDB 4.2 开始可用。
WiredTiger 日志也会被压缩,默认使用的也是 Snappy 压缩算法。如果日志记录小于或等于 128 字节,WiredTiger 不会压缩该记录。
索引
MongoDB 可以使用该索引来限制它必须检查的文档数量,以便提高查询效率。
分类
单字段索引: 建立在单个字段上的索引,索引创建的排序顺序无所谓,MongoDB 可以头/尾开始遍历。
复合索引: 建立在多个字段上的索引,也可以称之为组合索引、联合索引。字段的顺序非常重要,且遵循左前缀原则。
多键索引:MongoDB 的一个字段可能是数组,在对这种字段创建索引(为数组的每个值创建索引)时,就是多键索引。
哈希索引:按数据的哈希值索引,用在哈希分片集群上。
文本索引: 支持对字符串内容的文本搜索查询。支持全文索引,但性能低下,暂不推荐。
地理位置索引: 基于经纬度的索引,适合 2D 和 3D 的位置查询。
唯一索引:确保索引字段不会存储重复值。
TTL 索引:TTL 索引提供了一个过期机制,允许为每一个文档设置一个过期时间,当一个文档达到预设的过期时间之后就会被删除。
TTL 索引限制
TTL 索引是单字段索引。复合索引不支持 TTL。
_id字段不支持 TTL 索引。
无法在上限集合(Capped Collection)上创建 TTL 索引,因为 MongoDB 无法从上限集合中删除文档。
如果某个字段已经存在非 TTL 索引,那么在该字段上无法再创建 TTL 索引。
覆盖索引查询
所有的查询字段是索引的一部分。
结果中返回的所有字段都在同一索引中。
查询中没有字段等于null。
高可用
复制集群
又称为副本集群,是一组维护相同数据集合的 mongod 进程。
一个复制集群包含 1 个主节点(Primary),多个从节点(Secondary)以及零个或 1 个仲裁节点(Arbiter)。
组成
主节点:整个集群的写操作入口,接收所有的写操作,并将集合所有的变化记录到操作日志中,即 oplog。主节点挂掉之后会自动选出新的主节点。
从节点:从主节点同步数据,在主节点挂掉之后选举新节点。不过,从节点可以配置成 0 优先级,阻止它在选举中成为主节点。
仲裁节点:这个是为了节约资源或者多机房容灾用,只负责主节点选举时投票不存数据,保证能有节点获得多数赞成票。
主节点与备节点之间是通过 oplog(操作日志) 来同步数据的。
oplog 是 local 库下的一个特殊的 上限集合(Capped Collection) ,用来保存写操作所产生的增量日志,类似于 MySQL 中 的 Binlog。
oplog 是 local 库下的一个特殊的 上限集合(Capped Collection) ,用来保存写操作所产生的增量日志,类似于 MySQL 中 的 Binlog。
优势
实现 failover:提供自动故障恢复的功能。
实现读写分离:减轻了主节点读写压力过大的问题。
分片集群
MongoDB 的分布式版本,相较副本集,分片集群数据被均衡的分布在不同分片中。
作用
不仅大幅提升了整个集群的数据容量上限,也将读写的压力分散到不同分片,以解决副本集性能瓶颈的难题。
组成
Config Servers:配置服务器,本质上是一个 MongoDB 的副本集,负责存储集群的各种元数据和配置,如分片地址、Chunks 等。
Mongos:路由服务,不存具体数据,从 Config 获取集群配置讲请求转发到特定的分片,并且整合分片结果返回给客户端。
Shard:每个分片是整体数据的一部分子集,从 MongoDB3.6 版本开始,每个 Shard 必须部署为副本集(replica set)架构。
拓展方式
垂直扩展:通过增加单个服务器的能力来实现,比如磁盘空间、内存容量、CPU 数量等。
水平扩展:通过将数据存储到多个服务器上来实现,根据需要添加额外的服务器以增加容量。
场景
存储容量受单机限制,即磁盘资源遭遇瓶颈。
读写能力受单机限制,可能是 CPU、内存或者网卡等资源遭遇瓶颈,导致读写能力无法扩展。
分片键
分片键(Shard Key) 是数据分区的前提, 从而实现数据分发到不同服务器上,减轻服务器的负担。
分片键就是文档里面的一个字段,但是这个字段不是普通的字段,有一定的要求。
要求
它必须在所有文档中都出现。
它必须是集合的一个索引,可以是单索引或复合索引的前缀索引,不能是多索引、文本索引或地理空间位置索引。
MongoDB 4.2 之前的版本,文档的分片键字段值不可变。
MongoDB 4.2 版本开始,除非分片键字段是不可变的 _id 字段,否则您可以更新文档的分片键值。
MongoDB 5.0 版本开始,实现了实时重新分片(live resharding),可以实现分片键的完全重新选择。
MongoDB 4.2 版本开始,除非分片键字段是不可变的 _id 字段,否则您可以更新文档的分片键值。
MongoDB 5.0 版本开始,实现了实时重新分片(live resharding),可以实现分片键的完全重新选择。
它的大小不能超过 512 字节。
选择条件
选择合适的片键对 sharding 效率影响很大,主要基于四个因素。
取值基数:取值基数建议尽可能大,如果用小基数的片键,因为备选值有限,
那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难。
那么块的总数量就有限,随着数据增多,块的大小会越来越大,导致水平扩展时移动块会非常困难。
取值分布:取值分布建议尽量均匀,分布不均匀的片键会造成某些块的数据量非常大,同样有上面数据分布不均匀,性能瓶颈的问题。
查询带分片:查询时建议带上分片,使用分片键进行条件查询时,可以直接定位到具体分片,否则需要将查询分发到所有分片,再等待响应返回。
避免单调递增或递减:单调递增的 sharding key,数据文件挪动小,但写入会集中,导致最后一篇的数据量持续增大,不断发生迁移,递减同理。
分片策略
基于范围的分片
MongoDB 按照分片键(Shard Key)的值的范围将数据拆分为不同的块(Chunk),每个块包含了一段范围内的数据。
当分片键的基数大、频率低且值非单调变更时,范围分片更高效。
当分片键的基数大、频率低且值非单调变更时,范围分片更高效。
优点
Mongos 可以快速定位请求需要的数据,并将请求转发到相应的 Shard 节点中。
缺点
可能导致数据在 Shard 节点上分布不均衡,容易造成读写热点,且不具备写分散性。
场景
分片键的值不是单调递增或单调递减、分片键的值基数大且重复的频率低、需要范围查询等业务场景。
基于 Hash 值的分片
MongoDB 计算单个字段的哈希值作为索引值,并以哈希值的范围将数据拆分为不同的块(Chunk)。
优点
可以将数据更加均衡地分布在各 Shard 节点中,具备写分散性。
缺点
不适合进行范围查询,进行范围查询时,需要将读请求分发到所有的 Shard 节点。
场景
分片键的值存在单调递增或递减、片键的值基数大且重复的频率低、需要写入的数据随机分发、数据读取随机性较大等业务场景。
除了上述两种分片策略,还可以配置 复合片键 ,例如由一个低基数的键和一个单调递增的键组成。
分片数据存储
Chunk(块)
MongoDB 分片集群的一个核心概念,其本质上就是由一组 Document 组成的逻辑数据单元。
每个 Chunk 包含一定范围片键的数据,互不相交且并集为全部数据,即离散数学中划分的概念。
每个 Chunk 包含一定范围片键的数据,互不相交且并集为全部数据,即离散数学中划分的概念。
分片集群不会记录每条数据在哪个分片上,而是记录 Chunk 在哪个分片上一级这个 Chunk 包含哪些数据。
默认情况下,一个 Chunk 的最大值默认为 64MB(可调整,取值范围为 1~1024 MB。如无特殊需求,建议保持默认值)。
Chunk 分裂
进行数据插入、更新、删除时,如果此时 Mongos 感知到了目标 Chunk 的大小或者其中的数据量超过上限,则会触发 Chunk 分裂。
再平衡(Rebalance)
数据的增长会让 Chunk 分裂得越来越多。此时各个分片上的 Chunk 数量可能会不平衡。
Mongos 中的 均衡器(Balancer) 组件就会执行自动平衡,尝试使各个 Shard 上 Chunk 的数量保持均衡,这个过程就是 再平衡(Rebalance)。
默认情况下,数据库和集合的 Rebalance 是开启的。
Mongos 中的 均衡器(Balancer) 组件就会执行自动平衡,尝试使各个 Shard 上 Chunk 的数量保持均衡,这个过程就是 再平衡(Rebalance)。
默认情况下,数据库和集合的 Rebalance 是开启的。
Rebalance 操作是比较耗费系统资源的,我们可以通过在业务低峰期执行、预分片或者设置 Rebalance 时间窗等方式来减少其影响。
Chunk 迁移
MongoDB默认情况下会开启一个balancer模块用于定期检测各个shard上的chunk数量分布,
当检测到各个shard上的chunk数量存在分布不均匀的情况时,就会触发chunk迁移。
当检测到各个shard上的chunk数量存在分布不均匀的情况时,就会触发chunk迁移。
chunk迁移操作通过moveChunk命令发起,可以被balancer自动调用(balancer每隔10s扫描哪些chunk需要被迁移),也支持用户主动发起。
迁移chunk的整个过程实际上就是一次两个shard进行数据交换的过程,分别有 chunk 的发送方和接收方。
应用
MongoDB 的优势在于其数据模型和存储引擎的灵活性、架构的可扩展性以及对强大的索引支持。
开发工具
Maven
本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以管理项目的构建、报告和文档。
POM
项目对象模型 (Project Object Model,POM)
每一个 Maven 工程都有一个 pom.xml 文件,位于根目录中,包含项目构建生命周期的详细信息。
通过 pom.xml 文件,可以定义项目的坐标、项目依赖、项目信息、插件信息等等配置。
作用
项目构建:提供标准的、跨平台的自动化项目构建方式。
依赖管理:方便快捷的管理项目依赖的资源(jar 包),避免资源间的版本冲突问题。
统一开发结构:提供标准的、统一的项目结构。
坐标
groupId(必须):定义了当前 Maven 项目隶属的组织或公司。一般分为多段,通常情况下,第一段为域,第二段为公司名称。
artifactId(必须):定义了当前 Maven 项目的名称,项目的唯一的标识符,对应项目根目录的名称。
version(必须):定义了 Maven 项目当前所处版本。
packaging(可选):定义了 Maven 项目的打包方式(比如 jar,war...),默认使用 jar。
classifier(可选):常用于区分从同一 POM 构建的具有不同内容的构件,可以是任意的字符串,附加在版本号之后。
依赖
配置
dependencies:一个 pom.xml 文件中只能存在一个这样的标签,是用来管理依赖的总标签。
dependency:包含在 dependencies 标签中,可以有多个,每一个表示项目的一个依赖。
groupId,artifactId,version(必要):依赖的基本坐标。
type(可选):依赖的类型,对应于项目坐标定义的 packaging。大多不必声明,默认值 jar。
scope(可选):依赖的范围,默认值是 compile。
optional(可选):标记依赖是否可选。
exclusions(可选):用来排除传递性依赖,例如 jar 包冲突。
范围
classpath 用于指定 .class 文件存放的位置,类加载器会从该路径中加载所需的 .class 文件到内存中。
compile:编译依赖范围(默认),使用此依赖范围对于编译、测试、运行三种都有效。
test:测试依赖范围,只能用于测试,而在编译和运行项目时无法使用此类依赖,典型的是 JUnit。
provided:此依赖范围,对于编译和测试有效,而对运行时无效。例如 比如 Tomcat 的 servlet-api.jar
runtime:运行时依赖范围,对于测试和运行有效,但是在编译主代码时无效,典型的就是 JDBC 驱动实现。
system:系统依赖范围,必须通过 systemPath 元素显示地指定依赖文件的路径,不依赖 Maven 仓库解析,所以可能会造成建构的不可移植。
传递依赖性
依赖冲突
对于 Maven 而言,同一个 groupId 同一个 artifactId 下,只能使用一个 version。
若相同类型但版本不同的依赖存在于同一个 pom 文件,只会引入后一个声明的依赖。
若相同类型但版本不同的依赖存在于同一个 pom 文件,只会引入后一个声明的依赖。
项目的两个依赖同时引入了某个依赖。
依赖调解
遵循 路径最短优先 和 声明顺序优先 两大原则解决问题。
排除依赖
单纯依赖 Maven 来进行依赖调解,在很多情况下是不适用的,需要我们手动排除依赖。
一般我们在解决依赖冲突的时候,都会优先保留版本较高的。因为大部分 jar 在升级的时候都会做到向下兼容。
仓库
定义
坐标和依赖是构件在 Maven 世界中的逻辑表示方式,构件的物理表示方式是文件,Maven 通过仓库来统一管理这些文件。
构件
在 Maven 世界中,任何一个依赖、插件或者项目构建的输出,都可以称为 构件 。
分类
本地仓库
运行 Maven 的计算机上的一个目录,它缓存远程下载的构件并包含尚未发布的临时构件。
本地仓库路径配置
settings.xml 文件
默认:${user.home}/.m2/repository
远程仓库
官方或者其他组织维护的 Maven 仓库。
分类
中央仓库:由 Maven 社区来维护,里面存放了绝大多数开源软件的包,并且是作为 Maven 的默认配置,不需要开发者额外配置。
私服:一种特殊的远程 Maven 仓库,架设在局域网内的仓库服务,私服一般被配置为互联网远程仓库的镜像,供局域网内的 Maven 用户使用。
其他的公共仓库:有一些公共仓库是为了加速访问(比如阿里云 Maven 镜像仓库)或者部分构件不存在于中央仓库中。
依赖包寻找顺序
先去本地仓库找寻,有的话,直接使用。
本地仓库没有找到的话,会去远程仓库找寻,下载包到本地仓库。
远程仓库没有找到的话,会报错。
生命周期
为了对所有的构建过程进行抽象和统一,包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。
Maven 定义了 3 个生命周期 META-INF/plexus/components.xml
分类
default
在没有任何关联插件的情况下定义的,是 Maven 的主要生命周期,用于构建应用程序,共包含 23 个阶段。
validate
验证项目是否正确,并且所有必要的信息可用于完成构建过程。
initialize
建立初始化状态,例如设置属性。
generate-sources
生成要包含在编译阶段的源代码。
process-sources
处理源代码。
generate-resources
生成要包含在包中的资源。
process-resources
将资源复制并处理到目标目录中,为打包阶段做好准备。
compile
编译项目的源代码。
process-classes
对编译生成的文件进行后处理,例如对 Java 类进行字节码增强/优化。
generate-test-sources
生成要包含在编译阶段的任何测试源代码。
process-test-sources
处理测试源代码。
generate-test-resources
生成要包含在编译阶段的测试源代码。
process-test-resources
处理从测试代码文件编译生成的文件
test-compile
编译测试源代码。
process-test-classes
处理从测试代码文件编译生成的文件。
test
使用合适的单元测试框架(Junit 就是其中之一)运行测试。
prepare-package
在实际打包之前,执行任何的必要的操作为打包做准备。
package
获取已编译的代码并将其打包成可分发的格式,例如 JAR、WAR 或 EAR 文件。
pre-integration-test
在执行集成测试之前执行所需的操作。 例如,设置所需的环境。
integration-test
处理并在必要时部署软件包到集成测试可以运行的环境。
post-integration-test
执行集成测试后执行所需的操作。 例如,清理环境。
verify
运行任何检查以验证打的包是否有效并符合质量标准。
install
将包安装到本地仓库中,可以作为本地其他项目的依赖。
deploy
将最终的项目包复制到远程仓库中与其他开发者和项目共享。
clean
clean 生命周期的目的是清理项目,共包含 3 个阶段。
pre-clean
执行一些需要在clean之前完成的工作。
clean
移除所有上一次构建生成的文件。
post-clean
执行一些需要在clean之后立刻完成的工作。
site
site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段。
pre-site
执行一些需要在生成站点文档之前完成的工作。
site
生成项目的站点文档作。
post-site
执行一些需要在生成站点文档之后完成的工作,并且为部署做准备。
site-deploy
将生成的站点文档部署到特定的服务器上。
特点
这些生命周期是相互独立的,每个生命周期包含多个阶段(phase)。
这些阶段是有序的,也就是说,后面的阶段依赖于前面的阶段。当执行某个阶段的时候,会先执行它前面的阶段。
因此当我们执行 mvn test命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。
因此当我们执行 mvn test命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。
插件
Maven 本质上是一个插件执行框架,所有的执行过程,都是由一个一个插件独立完成的。
分类
Build plugins:在构建时执行。
Reporting plugins:在网站生成过程中执行。
举例
maven-surefire-plugin:配置并执行单元测试。
maven-failsafe-plugin:配置并执行集成测试。
maven-javadoc-plugin:生成 Javadoc 格式的项目文档。
maven-checkstyle-plugin:强制执行编码标准和最佳实践。
jacoco-maven-plugin: 单测覆盖率。
sonar-maven-plugin:分析代码质量。
多模块管理
将一个项目分为多个模块,每个模块只负责单一的功能实现。
表现
一个 Maven 项目中不止有一个 pom.xml 文件,会在不同的目录中有多个 pom.xml 文件,进而实现多模块管理。
优点
降低代码之间的耦合性(从类级别的耦合提升到 jar 包级别的耦合)。
减少重复,提升复用性。
每个模块都可以是自解释的(通过模块名或者模块文档)。
模块还规范了代码边界的划分,开发者很容易通过模块确定自己所负责的内容。
多模块管理下,会有一个父模块,其他的都是子模块。父模块通常只有一个 pom.xml,没有其他内容。
父模块的 pom.xml 一般只定义了各个依赖的版本号、包含哪些子模块以及插件有哪些。
Gradle
定义
Gradle 就是一个运行在 JVM 上的自动化的项目构建工具,用来帮助我们自动构建项目。
Gradle 构建脚本是使用 Groovy 或 Kotlin 语言编写的,表达能力非常强,也足够灵活。
Groovy 是运行在 JVM 上的脚本语言,是基于 Java 扩展的动态语言,它的语法和 Java 非常的相似,可以使用 Java 的类库。
作用
项目构建:提供标准的、跨平台的自动化项目构建方式。
依赖管理:方便快捷的管理项目依赖的资源(jar 包),避免资源间的版本冲突问题。
统一开发结构:提供标准的、统一的项目结构。
优点
在灵活性上,Gradle 支持基于 Groovy 语言编写脚本,侧重于构建过程的灵活性,适合于构建复杂度较高的项目。
在粒度性上,Gradle 构建的粒度细化到了每一个 task 之中。并且它所有的 Task 源码都是开源的。
在扩展性上,Gradle 支持插件机制,所以我们可以复用这些插件,就如同复用库一样简单方便。
包装器
Gradle 包装器(Gradle Wrapper),它将 Gradle 再次包装,让所有的 Gradle 构建方法在 Gradle 包装器的帮助下运行。
流程
首先当我们刚创建的时候,如果指定的版本没有被下载,就先会去 Gradle 的服务器中下载对应版本的压缩包。
下载完成后需要先进行解压缩并且执行批处理文件。
后续项目每次构建都会重用这个解压过的 Gradle 版本。
优点
在给定的 Gradle 版本上标准化项目,从而实现更可靠和健壮的构建。
可以让我们的电脑中不安装 Gradle 环境也可以运行 Gradle 项目。
为不同的用户和执行环境提供新的 Gradle 版本就像更改 Wrapper 定义一样简单。
生成
本地配置好 Gradle 环境变量,因内置了 Wrapper Task,在项目根目录执行 gradle wrapper 命令即生成 Gradle Wrapper。
生成的文件
gradle-wrapper.jar:包含了 Gradle 运行时的逻辑代码。
gradle-wrapper.properties:定义了 Gradle 的版本号和 Gradle 运行时的行为属性。
gradlew:Linux 平台下,用于执行 Gralde 命令的包装器脚本。
gradlew.bat:Windows 平台下,用于执行 Gralde 命令的包装器脚本。
gradle-wrapper.properties 文件的内容如下:
distributionBase:Gradle 解包后存储的父目录。
distributionPath:distributionBase指定目录的子目录。distributionBase+distributionPath就是 Gradle 解包后的存放的具体目录。
distributionUrl:Gradle 指定版本的压缩包下载地址。
zipStoreBase:Gradle 压缩包下载后存储父目录。
zipStorePath:zipStoreBase指定目录的子目录。zipStoreBase+zipStorePath就是 Gradle 压缩包的存放位置。
distributionBase:Gradle 解包后存储的父目录。
distributionPath:distributionBase指定目录的子目录。distributionBase+distributionPath就是 Gradle 解包后的存放的具体目录。
distributionUrl:Gradle 指定版本的压缩包下载地址。
zipStoreBase:Gradle 压缩包下载后存储父目录。
zipStorePath:zipStoreBase指定目录的子目录。zipStoreBase+zipStorePath就是 Gradle 压缩包的存放位置。
更新
方式
接修改distributionUrl字段,然后执行 Gradle 命令。
执行 gradlew 命令 gradlew wrapper –-gradle-version [version]。
自定义
Gradle 已经内置了 Wrapper Task,因此构建 Gradle Wrapper 会生成其属性文件,该属性文件可以通过自定义 Wrapper Task 来设置。
也可以设置 Gradle 发行版压缩包的下载地址和 Gradle 解包后的本地存储路径等配置。
任务
任务(Task)是构建执行的单个工作单元。
Gradle 的构建是基于 Task 进行的,当你运行项目的时候,实际就是在执行了一系列的 Task 比如编译 Java 源码的 Task、生成 jar 文件的 Task。
Action
创建一个 Task 后,可以根据需要给 Task 添加不同的 Action。
一个 Task 中可以有多个 Action,从队列头部开始向队列尾部执行 Action。
Action 代表的是一个个函数、方法,每个 Task 都是一堆 Action 按序组成的执行图。
dependsOn
Task 声明依赖的关键字是dependsOn,支持声明一个或多个依赖。
执行 Task 之前,会先执行它的依赖 Task。
还可以设置默认 Task,脚本中我们不调用默认 Task ,也会执行。
Gradle 本身也内置了很多 Task 比如 copy(复制文件)、delete(删除文件)。
插件
Gradle 提供的是一套核心的构建机制,而 Gradle 插件则是运行在这套机制上的一些具体构建逻辑,其本质上和 .gradle 文件是相同。
分类
脚本插件:脚本插件就是一个普通的脚本文件,它可以被导入都其他构建脚本中。
二进制插件 / 对象插件:在一个单独的插件模块中定义,其他模块通过 Plugin ID 应用插件。大多是该插件。
Gradle 插件与 .gradle 文件
逻辑复用: 将相同的逻辑提供给多个相似项目复用,减少重复维护类似逻辑开销。当然 .gradle 文件也能做到逻辑复用,但 Gradle 插件的封装性更好。
组件发布: 可以将插件发布到 Maven 仓库进行管理,其他项目可以使用插件 ID 依赖。当然 .gradle 文件也可以放到一个远程路径被其他项目引用。
构建配置: Gradle 插件可以声明插件扩展来暴露可配置的属性,提供定制化能力。当然 .gradle 文件也可以做到,但实现会麻烦些。
构建生命周期
Gradle 构建的生命周期有三个阶段:初始化阶段,配置阶段和运行阶段。
在初始化阶段与配置阶段之间、配置阶段结束之后、执行阶段结束之后,可以加一些定制化的 Hook。
初始化阶段
Gradle 支持单项目和多项目构建。在初始化阶段,Gradle 确定哪些项目将参与构建,并为每个项目创建一个 Project 实例 。
本质
就是执行 settings.gradle 脚本,从而读取整个项目中有多少个 Project 实例。
配置阶段
Gradle 会解析每个工程的 build.gradle 文件,
创建要执行的任务子集和确定各种任务之间的关系,以供执行阶段按照顺序执行,并对任务的做一些初始化配置。
创建要执行的任务子集和确定各种任务之间的关系,以供执行阶段按照顺序执行,并对任务的做一些初始化配置。
每个 build.gradle 对应一个 Project 对象,配置阶段执行的代码包括 build.gradle 中的各种语句、闭包以及 Task 中的配置语句。
在配置阶段结束后,Gradle 会根据 Task 的依赖关系会创建一个 有向无环图 。
运行阶段
在运行阶段,Gradle 根据配置阶段创建和配置的要执行的任务子集,执行任务。
与 Maven 差异。
Git
版本控制
一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
作用
可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态。
可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因。
分类
本地版本控制系统
复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别。
唯一的好处就是简单,但是特别容易犯错。
集中化的版本控制系统
集中化的版本控制系统(Centralized Version Control Systems,简称 CVCS)。
一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。
问题
单点故障: 中央服务器宕机,则其他人无法使用;如果中心数据库磁盘损坏又没有进行备份,你将丢失所有数据。
必须联网才能工作: 受网络状况、带宽影响。
分布式版本控制系统
分布式版本控制系统(Distributed Version Control System,简称 DVCS)。
客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。
应用
Git
实现方式
差异比较
以文件变更列表的方式存储信息。将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。
原理
每次更新文件,只记录更新内容,最终版本只需要将这些原文件和这些增加进行相加就行了。
问题
更新特别多的话,如果我们要得到最终的文件会耗费时间和性能。
应用
CVS、Subversion、Perforce、Bazaar 等等。
记录快照
把数据看作是对小型文件系统的一组快照。对待数据更像是一个 快照流。
原理
提交更新或保存项目状态时,对当时的全部文件制作一个快照并保存这个快照的索引。
为了高效,如果文件没有修改,就不再重新存储该文件,而是只保留一个链接指向之前存储的文件。
应用
Git
状态
已提交(committed):数据已经安全的保存在本地数据库中。
已修改(modified):已修改表示修改了文件,但还没保存到数据库中。
已暂存(staged):表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
工作区
Git 仓库(.git directory)、工作目录(Working Directory) 、暂存区域(Staging Area) 。
流程
在工作目录中修改文件。
暂存文件,将文件的快照放入暂存区域。
提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。
使用
获取 Git 仓库
在现有目录中初始化仓库:进入项目目录运行 git init 命令,该命令将创建一个名为 .git 的子目录。
从一个服务器克隆一个现有的 Git 仓库:git clone [url] 自定义本地仓库的名字。
记录每次更新到仓库
检测当前文件状态:git status。
提出更改(把它们添加到暂存区):git add filename (针对特定文件)、git add *(所有文件)、git add *.txt(支持通配符,所有 .txt 文件)。
忽略文件:.gitignore 文件。
提交更新:git commit -m "代码提交信息" (每次提交前,先用 git status 看下是否都已暂存起来了, 然后再运行提交命令 git commit)。
跳过使用暂存区域更新的方式 : git commit -a -m "代码提交信息"。
移除文件:git rm filename (从暂存区域移除,然后提交。)
对文件重命名:git mv A.md B.md(这个命令相当于mv A.md B.md、git rm A.md、git add B.md 这三条命令的集合)。
推送改动到远程仓库
没有远端仓库:使用如下命令添加:git remote add origin <server>,然后再提交改动到远端仓库。
将这些改动提交到远端仓库:git push origin master (可以把 master 换成你想要推送的任何分支)
Docker
容器
容器就是将软件打包成标准化单元,以用于开发、交付和部署。
容器镜像是轻量的、可执行的独立软件包 ,包含软件运行所需的所有内容:代码、运行时环境、系统工具、系统库和设置。
容器化软件适用于基于 Linux 和 Windows 的应用,在任何环境中都能够始终如一地运行。
容器赋予了软件独立性,使其免受外在环境差异的影响,从而有助于减少团队间在相同基础设施上运行不同软件时的冲突。
容器虚拟化的是操作系统而不是硬件,容器之间是共享同一套操作系统资源的。
定义
Docker 是世界领先的软件容器平台。
Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核 提供的 CGroup 功能和 namespace 来实现的,以及 AUFS 类的 UnionFS 等技术。
由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。
Docker 能够自动执行重复性任务,例如搭建和配置开发环境,从而解放了开发人员以便他们专注在真正重要的事情上:构建杰出的软件。
用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。
思想
集装箱、标准化( ① 运输方式 ② 存储方式 ③ API 接口)、隔离。
特点
轻量 : 在一台机器上运行的多个 Docker 容器可以共享这台机器的操作系统内核;它们能够迅速启动,只需占用很少的计算和内存资源。
镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。
镜像是通过文件系统层进行构造的,并共享一些公共文件。这样就能尽量降低磁盘用量,并能更快地下载镜像。
标准 : Docker 容器基于开放式标准,能够在所有主流 Linux 版本、Microsoft Windows 以及包括 VM、裸机服务器和云在内的任何基础设施上运行。
安全 : Docker 赋予应用的隔离性不仅限于彼此隔离,还独立于底层的基础设施。Docker 默认最强的隔离,因此问题只会出现在单个容器而不是整台机器。
优点
一致的运行环境
Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性。
更快速的启动时间
可以做到秒级、甚至毫秒级的启动时间。大大的节约了开发、测试、部署的时间。
隔离性
避免公用的服务器,资源会容易受到其他用户的影响。
弹性伸缩,快速扩展
善于处理集中爆发的服务器使用压力。
迁移方便
以很轻易的将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。
持续交付和部署
使用 Docker 可以通过定制应用镜像来实现持续集成、持续交付、部署。
容器 VS 虚拟机
传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程。
容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。
因此容器要比传统虚拟机更为轻便。
容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。
因此容器要比传统虚拟机更为轻便。
虚拟机更擅长于彻底隔离整个运行环境。例如,云服务提供商通常采用虚拟机技术隔离不同的用户。
Docker 通常用于隔离不同的应用 ,例如前端,后端以及数据库。
Docker 通常用于隔离不同的应用 ,例如前端,后端以及数据库。
容器与虚拟机两者是可以共存的。
基本概念
镜像(Image)
一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,
还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
原理
Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构 。镜像实际是由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。
容器(Container)
容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等 。
实质
实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间。容器也是分层存储。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
最佳实践
容器不应该向其存储层内写入任何数据 ,容器存储层要保持无状态化。
所有的文件写入操作,都应该使用数据卷(Volume)、或者绑定宿主目录。使用后,容器可以随意删除、重新 run ,数据却不会丢失。
注意:直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
仓库(Repository)
如果需要在其它服务器上使用这个镜像,就需要一个集中的存储、分发镜像的服务,这个服务就是仓库(Repository)。
一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。
一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。
Docker Registry 公开服务
开放给用户使用、允许用户管理镜像的 Registry 服务。
官方的 Docker Hub(默认的 Registry)、时速云镜像库、网易云镜像服务、DaoCloud 镜像市场、阿里云镜像库等。
私有 Docker Registry
本地搭建私有 Docker Registry 。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。
常见命令
docker version # 查看docker版本
docker images # 查看所有已下载镜像,等价于:docker image ls 命令
docker container ls # 查看所有容器
docker ps #查看正在运行的容器
docker image prune # 清理临时的、没有被使用的镜像文件。-a, --all: 删除所有没有用的镜像,而不仅仅是临时文件;
docker search mysql # 查看mysql相关镜像
docker pull mysql:5.7 # 拉取mysql镜像
docker image ls # 查看所有已下载镜像
docker rmi f6509bac4980 # 或者 docker rmi mysql
docker images # 查看所有已下载镜像,等价于:docker image ls 命令
docker container ls # 查看所有容器
docker ps #查看正在运行的容器
docker image prune # 清理临时的、没有被使用的镜像文件。-a, --all: 删除所有没有用的镜像,而不仅仅是临时文件;
docker search mysql # 查看mysql相关镜像
docker pull mysql:5.7 # 拉取mysql镜像
docker image ls # 查看所有已下载镜像
docker rmi f6509bac4980 # 或者 docker rmi mysql
搬运工人
Build(构建镜像):镜像就像是集装箱包括文件以及运行环境等等资源。
Ship(运输镜像):主机和仓库间运输,这里的仓库就像是超级码头一样。
Run (运行镜像):运行的镜像就是一个容器,容器就是运行程序的地方。
底层原理
虚拟化技术
一种资源管理技术,是将计算机的各种实体资源,予以抽象、转换后呈现出来并可供分割、组合为一个或多个电脑配置环境。
LXC
名称来自 Linux 软件容器(Linux Containers)的缩写,一种操作系统层虚拟化(Operating system–level virtualization)技术。
它将应用软件系统打包成一个软件容器(Container),内含应用软件本身的代码,以及所需要的操作系统核心和库。
通过统一的名字空间和共用 API 来分配不同软件容器的可用硬件资源,创造出应用程序的独立沙箱运行环境。
LXC 技术主要是借助 Linux 内核中提供的 CGroup 功能和 namespace 来实现的,通过 LXC 可以为软件提供一个独立的操作系统运行环境。
cgroup 和 namespace
namespace 是 Linux 内核用来隔离内核资源的方式。
CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物力资源的机制。
对比
两者都是将进程进行分组,但 namespace 是为了隔离进程组之间的资源,而 cgroup 是为了对一组进程进行统一的资源监控和限制。
Docker 技术是基于 LXC(Linux container- Linux 容器)虚拟容器技术的。
K8S
由 Google 基于 Borg 系统,在2014年6月正式发布并开源的基于容器的集群管理平台,它的全称是 Kubernetes。
作用
自动化部署、扩展和管理容器化应用程序。
组成
控制平面(Master)
负责整个集群的运维和资源调度,
包括API服务器(负责集群入口)、调度器(根据资源需求分配Pod)、控制器管理器(执行副本集等操作)和etcd(键值存储,用于存储集群配置信息)。
包括API服务器(负责集群入口)、调度器(根据资源需求分配Pod)、控制器管理器(执行副本集等操作)和etcd(键值存储,用于存储集群配置信息)。
工作节点(Worker)
运行容器,并通过kubelet、kube-proxy与控制平面通信。
支持有状态和无状态应用,通过ConfigMap、Secret等对象管理配置数据,通过Pod、Service等对象管理应用生命周期。
在K8s中,应用程序被打包成容器镜像,通过Pod进行部署和管理,而Pod又由一组容器和共享数据卷组成。
K8s的部署架构具有高度的灵活性和可扩展性,能够支撑各种复杂的企业级应用需求。
IDEA
IDEA 高效使用指南
常用框架
Spring
定义
Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。
Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。
应用
IoC(Inversion of Control:控制反转)。
AOP(Aspect-Oriented Programming:面向切面编程)。
可以很方便地对数据库进行访问。
可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)。
对单元测试支持比较好。
支持 RESTful Java 应用程序的开发。
Spring
Spring MVC
Spring Boot
Spring MVC
Spring Boot
Spring
Spring 包含了多个功能模块,其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块,
Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。
Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。
Spring MVC
Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。
Spring Boot
Spring Boot 是⼀个微服务框架,旨在简化 Spring 开发(减少配置文件,开箱即用!)。
Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架。
模块
Core Container
Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。
spring-core:Spring 框架基本的核心工具类。
spring-beans:提供对 bean 的创建、配置和管理等功能的支持。
spring-context:提供对国际化、事件传播、资源加载等功能的支持。
spring-expression:提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。
AOP
spring-aspects:该模块为与 AspectJ 的集成提供支持。
spring-aop:提供了面向切面的编程实现。
spring-instrument:提供了为 JVM 添加代理(agent)的功能。
Data Access/Integration
spring-jdbc:提供了对数据库访问的抽象 JDBC。
spring-tx:提供对事务的支持。
spring-orm:提供对 Hibernate、JPA、iBatis 等 ORM 框架的支持。
spring-oxm:提供一个抽象层支撑 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。
spring-jms : 消息服务。自 Spring Framework 4.1 以后,它还提供了对 spring-messaging 模块的继承。
Spring Web
spring-web:对 Web 功能的实现提供一些最基础的支持。
spring-webmvc:提供对 Spring MVC 的实现。
spring-websocket:提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
spring-webflux:提供对 WebFlux 的支持。
WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。
WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。
Messaging
spring-messaging 是从 Spring4.0 开始新加入的一个模块,主要职责是为 Spring 框架集成一些基础的报文传送应用。
Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。
支持
JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、
PowerMock(解决 Mockito 的问题比如无法模拟 final,static,private 方法)等等。
PowerMock(解决 Mockito 的问题比如无法模拟 final,static,private 方法)等等。
Spring IoC
IoC(Inversion of Control:控制反转/反转控制) 是一种设计思想(Java 开发领域对象的创建以及管理的问题),而不是一个具体的技术实现。
思想
将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。
实现
最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。
原理:将对象的依赖关系由外部容器来管理和注入。
控制反转
控制:指的是对象创建(实例化、管理)的权力。
反转:控制权交给外部环境(Spring 框架、IoC 容器)。
IoC 容器
Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
IoC 容器类似工厂,当需要创建一个对象时,只需配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
线程安全
Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。
prototype 作用域
每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。
singleton 作用域
IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。
Bean 状态
指的是否含可变的成员变量的对象,含即有状态,不含即无状态。
有状态
存在线程安全问题。
无状态
线程是安全的(比如 Dao、Service)。
解决办法
在 Bean 中尽量避免定义可变的成员变量。
在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐)。
Spring Bean
Bean 代指的就是那些被 IoC 容器所管理的对象。
注解
@Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用 @Component 注解标注。
@ComponentScan:定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中。
@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
@Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
@Component 和 @Bean
@Component 注解作用于类,而 @Bean 注解作用于方法。
@Component 通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中。
@Bean 通常是在标有该注解的方法中定义产生这个 bean,@Bean 告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。
@Bean 通常是在标有该注解的方法中定义产生这个 bean,@Bean 告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。
@Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。
比如当我们引用第三方库中的类需要装配到 Spring 容器时,则只能通过 @Bean 来实现。
比如当我们引用第三方库中的类需要装配到 Spring 容器时,则只能通过 @Bean 来实现。
注入
Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。
@Autowired
属于 Spring 内置的注解,默认注入方式为 byType(根据类型进行匹配),优先根据接口类型去匹配并注入 Bean (接口的实现类)。
当一个接口存在多个实现类时,byType 方式就无法正确注入对象,而 byName(根据名称进行匹配)可以正常注入。
byName 注入方式
隐式:通过实现类名(首字母小写)。
显式:通过 @Qualifier 注解,推荐。
使用
支持在构造函数、方法、字段和参数上使用。
@Resource
属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType。
有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。
如果仅指定 name 属性则注入方式为 byName,
如果仅指定 type 属性则注入方式为 byType,
如果同时指定 name 和 type 属性(不建议这么做)则注入方式为 byType + byName。
如果仅指定 type 属性则注入方式为 byType,
如果同时指定 name 和 type 属性(不建议这么做)则注入方式为 byType + byName。
使用
主要用于字段和方法上的注入,不支持在构造函数或参数上使用。
作用域
singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
配置
xml 方式
<bean id="..." class="..." scope="singleton"></bean>
注解
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
生命周期
Bean 容器找到配置文件中 Spring Bean 的定义。
Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。
如果涉及到一些属性值 利用 set()方法设置一些属性值。
如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字。
如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。
如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法。
如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法。
如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法
当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
Spring Bean 生命周期
BeanFactory 和 ApplicationContext
BeanFactory:延迟注入(使用到某个 bean 的时候才会注入),相比于ApplicationContext 来说会占用更少的内存,程序启动速度更快。
ApplicationContext:容器启动的时候,不管你用没用到,一次性创建所有 bean 。
BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory,除了有 BeanFactory 的功能还有额外更多功能。
BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory,除了有 BeanFactory 的功能还有额外更多功能。
ApplicationContext 的三个实现类
ClassPathXmlApplication:把上下文文件当成类路径资源。
FileSystemXmlApplication:从文件系统中的 XML 文件载入上下文定义信息。
XmlWebApplicationContext:从 Web 系统中的 XML 文件载入上下文定义信息。
Spring AOP
定义
AOP (Aspect Orient Programming): 面向切面编程,AOP 是 OOP(面向对象编程)的一种延续,二者互补,并不对立。
⾯向对象编程将程序抽象成各个层次的对象,⽽⾯向切⾯编程是将程序抽象成各个切⾯。
⾯向对象编程将程序抽象成各个层次的对象,⽽⾯向切⾯编程是将程序抽象成各个切⾯。
原理
基于动态代理
有接口实现
JDK Proxy 创建代理对象。
无接口实现
Cglib 生成一个被代理对象的子类来作为代理。
作用
将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,
通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。
通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。
概念
目标(Target)
被通知的对象。
代理(Proxy)
向目标对象应用通知之后创建的代理对象。
连接点(JoinPoint)
目标对象的所属类中,定义的所有方法均为连接点。
切入点(Pointcut)
被切面拦截 / 增强的连接点(切入点一定是连接点,连接点不一定是切入点)。
通知(Advice)
增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情。
切面(Aspect)
切入点(Pointcut) + 通知(Advice)。
织入(Weaving)
将通知应用到目标对象,进而生成代理对象的过程动作。
横切关注点(cross-cutting concerns) :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
AspectJ
⼀个功能强⼤且成熟的AOP框架。
通知类型
Before(前置通知):目标对象的方法调用之前触发。
After (后置通知):目标对象的方法调用之后触发。
AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发。
AfterThrowing(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。
Around (环绕通知):编程式控制目标对象的方法调用。所有通知类型中可操作范围最大的一种。
执行顺序
通常使用 @Order 注解直接定义切面顺序。
实现 Ordered 接口重写 getOrder 方法。
Spring AOP 和 AspectJ AOP
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。
Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。
切面比较少时,两者性能差异不大。但当切面太多时,最好选择 AspectJ ,它比 Spring AOP 快很多。
场景
日志记录:自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。
性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。
事务管理:基于 AOP 实现的 @Transactional 注解可以让 Spring 为我们进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。
权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限。例如,SpringSecurity 利用@PreAuthorize 注解一行代码即可自定义权限校验。
接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。
缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。
Spring MVC
Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
发展
Model 1 时代
整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。
JSP 即是控制层(Controller)又是表现层(View)。
问题
控制逻辑和表现逻辑混杂在一起,导致代码重用率极低。
前端和后端相互依赖,难以进行测试维护并且开发效率极低。
Model 2 时代
“Java Bean(Model)+ JSP(View)+ Servlet(Controller)”开发模式,这就是早期的 JavaWeb MVC 开发模式。
问题
抽象和封装程度还远远不够。
开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。
同时期,为了解决这些问题,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。
Spring MVC 时代
随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。
一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。
核心组件
DispatcherServlet:核心的中央处理器,负责接收请求、分发,并给予客户端响应。
HandlerMapping:处理器映射器,根据 URL 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。
HandlerAdapter:处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler。
Handler:请求处理器,处理实际请求的处理器。
ViewResolver:视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端。
View:视图,提供了很多的 View 视图类型的⽀持,包括:JstlView、FreemarkerView、PdfView等。最常⽤的视图就是 Jsp。
拦截器
Spring MVC 中,所有的拦截器都需要实现 HandlerInterceptor 接⼝,该接⼝包含如下三个⽅法:preHandle()、postHandle()、afterCompletion()。
工作原理
客户端(浏览器)发送请求, DispatcherServlet 拦截请求。
DispatcherServlet 根据请求信息调用 HandlerMapping 。
HandlerMapping 根据 URL 去匹配查找能处理的 Handler(Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
HandlerMapping 根据 URL 去匹配查找能处理的 Handler(Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
DispatcherServlet 调用 HandlerAdapter 适配器执行 Handler 。
Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给 DispatcherServlet。
ModelAndView 包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
ModelAndView 包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
ViewResolver 会根据逻辑 View 查找实际的 View。
DispaterServlet 把返回的 Model 传给 View(视图渲染)。
把 View 返回给请求者(浏览器)。
统一异常处理
推荐使用注解的方式统一异常处理,具体会使用到 @ControllerAdvice + @ExceptionHandler 这两个注解 。
ExceptionHandlerMethodResolver 中 getMappedMethod 方法决定了异常具体被哪个被 @ExceptionHandler 注解修饰的方法处理异常。
getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。
getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。
设计模式
工厂设计模式 : Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、Spring MVC 中也是用到了适配器模式适配 Controller。
事务
管理方式
编程式事务:在代码中硬编码(在分布式系统中推荐使用) 。
通过 TransactionTemplate 或者 TransactionManager 手动管理事务,事务范围过大会出现事务未提交导致超时,因此事务要比锁的粒度更小。
通过 TransactionTemplate 或者 TransactionManager 手动管理事务,事务范围过大会出现事务未提交导致超时,因此事务要比锁的粒度更小。
声明式事务:在 XML 配置文件中配置或者直接基于注解(单体应用或者简单业务系统推荐使用)。
实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)。
实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)。
管理接口
PlatformTransactionManager
(平台)事务管理器,Spring 事务策略的核心。
Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是:PlatformTransactionManager 。
通过此接口,Spring 提供了各个平台对应的事务管理器接口
JDBC(DataSourceTransactionManager)
Hibernate(HibernateTransactionManager)
JPA(JpaTransactionManager)
TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
TransactionStatus:事务运行状态。
隔离级别
ISOLATION_DEFAULT
使用后端数据库默认的隔离级别,MySQL 默认 REPEATABLE_READ,Oracle 默认 READ_COMMITTED。
ISOLATION_READ_UNCOMMITTED
最低的隔离级别,使用很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
ISOLATION_READ_COMMITTED
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
ISOLATION_REPEATABLE_READ
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
ISOLATION_SERIALIZABLE
最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,可以防止脏读、不可重复读以及幻读。(严重影响性能)
传播行为
事务传播行为是为了解决业务层方法之间互相调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
PROPAGATION_REQUIRED
如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。(默认)
PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。事务相互独立,互不干扰。
PROPAGATION_NESTED
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行。
如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(使用少)
PROPAGATION_SUPPORTS
如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。(无回滚)
PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。(无回滚)
PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。(无回滚)
超时属性
指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。
只读属性
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。
只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
如果不加 Transactional,每条 sql 会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性。
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,此时,应该启用事务支持。
回滚规则
默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但在遇到检查型(Checked)异常时不会回滚。
@Transactional
作用范围
方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
接口:不推荐在接口上使用。
配置参数
propagation
事务的传播行为,默认值为 REQUIRED,可选的值在 TransactionDefinition 中。
isolation
事务的隔离级别,默认值采用 DEFAULT,可选的值在 TransactionDefinition 中。
timeout
事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly
指定事务是否为只读事务,默认值为 false。
rollbackFor
用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。
原理
基于 AOP 实现的,AOP 又是使用动态代理实现的。
注意
@Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用。
避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效。
无法避免时则使用 AspectJ 取代 Spring AOP 代理。
正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败。
被 @Transactional 注解的方法所在的类必须被 Spring 管理,否则不生效。
底层使用的数据库必须支持事务机制,否则不生效。
Spring Data JPA
JPA(Java Persistence API)
一种规范,通过注解或 xml 描述对象关系表的映射关系,并将运行期间的实体对象持久化到数据库中。
技术
ORM 映射元数据:支持 XML 和 JDK5.0 注解两种元数据形式。
API:用来操作实体对象,可以进行 CURD 操作,从 JDBC 和 SQL 中解放开发者。
查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序与 SQL 紧耦合。
非持久化字段
静态
static String transient1;
常量
final String transient2 = "Satish";
关键字
transient String transient3;
注解
@Transient
String transient4;
String transient4;
审计
@CreatedDate:表示该字段为创建时间字段,在这个实体被 insert 的时候,会设置值。
@CreatedBy:表示该字段为创建人,在这个实体被 insert 的时候,会设置值。
@LastModifiedDate、@LastModifiedBy 同理。
实体关联关系
@OneToOne : 一对一。
@ManyToMany:多对多。
@OneToMany : 一对多。
@ManyToOne:多对一。
利用 @ManyToOne 和 @OneToMany 也可以表达多对多的关联关系。
Spring Security
Spring Security 是基于 Spring 的身份认证(Authentication)和用户授权(Authorization)框架,提供了一套 Web 应用安全性的完整解决方案。
核心技术
使用了 Servlet 过滤器、IOC 和 AOP 等。
身份认证(Authentication)
验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。
用户授权(Authorization)
验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。
控制请求权限
permitAll():无条件允许任何形式访问,不管你登录还是没有登录。
anonymous():允许匿名访问,也就是没有登录才可以访问。
denyAll():无条件决绝任何形式的访问。
authenticated():只允许已认证的用户访问。
fullyAuthenticated():只允许已经登录或者通过 remember-me 登录的用户访问。
hasRole(String) : 只允许指定的角色访问。
hasAnyRole(String) : 指定一个或者多个角色,满足其一的用户即可访问。
hasAuthority(String):只允许具有指定权限的用户访问。
hasAnyAuthority(String):指定一个或者多个权限,满足其一的用户即可访问。
hasIpAddress(String) : 只允许指定 ip 的用户访问。
加密
Spring Security 提供了多种加密算法的实现,开箱即用,非常方便。
这些加密算法实现类的父类是 PasswordEncoder ,如果你想要自己实现一个加密算法的话,也需要继承 PasswordEncoder。
官方推荐使用基于 BCrypt 强哈希函数的加密算法实现类。
优雅更换加密算法
通过 DelegatingPasswordEncoder 兼容多种不同的密码加密方案,以适应不同的业务需求。
DelegatingPasswordEncoder 其实就是一个代理类,并非是一种全新的加密算法,它做的事情就是代理上面提到的加密算法实现类。
在 Spring Security 5.0 之后,默认就是基于 DelegatingPasswordEncoder 进行密码加密的。
SpringBoot
定义
Spring Boot 是⼀个微服务框架,旨在简化 Spring 开发(减少配置文件,开箱即用!)。
Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架。
优点
开发基于 Spring 的应⽤程序很容易。
Spring Boot 项⽬所需的开发或⼯程时间明显减少,通常会提⾼整体⽣产⼒。
Spring Boot 不需要编写⼤量样板代码、XML 配置和注释。
Spring 引导应⽤程序可以很容易地与 Spring ⽣态系统集成,如 SpringJDBC、Spring ORM、Spring Data、Spring Security 等。
Spring Boot 遵循“固执⼰⻅的默认配置”,以减少开发⼯作(默认配置可以修改)。
Spring Boot 应⽤程序提供嵌⼊式 HTTP 服务器,如 Tomcat 和 Jetty,可以轻松地开发和测试 web 应⽤程序。
Spring Boot 提供命令⾏接⼝(CLI)⼯具,⽤于开发和测试 Spring Boot 应⽤程序,如 Java 或 Groovy。
Spring Boot 提供了多种插件,可以使⽤内置⼯具(如 Maven 和 Gradle)开发和测试 Spring Boot 应⽤程序。
Starter
⼀种预配置的模块,它封装了特定功能的依赖项和配置,开发者只需引⼊相关的 Starter 依赖,⽆需⼿动配置⼤量的参数和依赖项。
管理了相关功能的依赖项,包括其他 Starter 和第三⽅库,确保它们能够良好地协同⼯作,避免版本冲突和依赖问题。
应⽤可以通过引⼊不同的 Starter 来实现模块化的开发。每个 Starter 都关注⼀个特定的功能领域,如数据库访问、消息队列、Web 开发等。
开发者可以创建⾃定义的 Starter,以便在项⽬中共享和重⽤特定功能的配置和依赖项。
内嵌 Servlet 容器
Tomcat 9.0(默认嵌入)
Servlet Ver 4.0
Jetty 9.4
Servlet Ver 3.1
Undertow 2.0
Servlet Ver 4.0
切换
只需要修改 pom.xml(Maven)或者 build.gradle(Gradle)就可以了。
启动流程
自动装配
通过注解或者一些简单的配置(引入 Starters)就能在 Spring Boot 的帮助下实现某块功能。
@EnableAutoConfiguration 是启动⾃动配置的关键。
过程
Spring Boot 通过 @EnableAutoConfiguration 注解开启⾃动配置,加载 spring.factories 中注册的各种 AutoConfiguration 类。
当某个 AutoConfiguration 类满⾜其注解 @Conditional 指定的⽣效条件(Starters 提供的依赖、配置或 Spring 容器中是否存在某个 Bean 等)时,
实例化该 AutoConfiguration 类中定义的 Bean(组件等),并注⼊ Spring 容器,就可以完成依赖框架的⾃动配置。
当某个 AutoConfiguration 类满⾜其注解 @Conditional 指定的⽣效条件(Starters 提供的依赖、配置或 Spring 容器中是否存在某个 Bean 等)时,
实例化该 AutoConfiguration 类中定义的 Bean(组件等),并注⼊ Spring 容器,就可以完成依赖框架的⾃动配置。
配置文件
application.properties 和 application.yml
YAML
YAML 是⼀种⼈类可读的数据序列化语⾔。它通常⽤于配置⽂件。
相⽐于 Properties 配置的⽅式,YAML 配置的⽅式更加直观清晰,简介明了,有层次感。
缺点
不⽀持 @PropertySource 注解导⼊⾃定义的 YAML 配置。
读取
通过 @value 读取⽐较简单的配置信息
使⽤ @Value("${property}") 读取⽐较简单的配置信息
@value 这种⽅式是不被推荐的。
通过 @ConfigurationProperties 读取并与 bean 绑定。
通过 @ConfigurationProperties 读取并校验。
@PropertySource 读取指定的 properties ⽂件
加载优先级
bootstrap.properties --> bootstrap.yml --> application.properties --> application.yml
配置文件优先级(由高到低)
命令行参数。
java:comp/env的JNDI属性(当前J2EE应用的环境)。
JAVA系统的环境属性。
操作系统的环境变量。
JAR包外部的 application-xxx.properties 或 application-xxx.yml 配置文件。
JAR包内部的 application-xxx.properties 或 application-xxx.yml 配置文件。
JAR包外部的 application.properties 或 application.yml 配置文件。
JAR包内部的 application.properties 或 application.yml 配置文件。
@Configuration 注解类上的 @PropertySource 指定的配置文件。
通过 SpringApplication.setDefaultProperties 指定的默认属性。
设置初始化数据
实现 ApplicationRunner 接⼝ 或 实现 CommandLineRunner接⼝。
Spring & SpringBoot 常用注解
@SpringBootApplication
⽤于标识主应⽤程序类,通常位于项⽬的顶级包中。它包含了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan。
@EnableAutoConfiguration:启用 SpringBoot 的自动配置机制。
@ComponentScan:扫描被 @Component(@Repository,@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。
@Configuration:允许在 Spring 上下文中注册额外的 bean 或导入其他配置类。
@Controller
⽤于标识类作为 Spring MVC 的控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。
单独使用 @Controller 而不加 @ResponseBody 一般用在返回一个视图的情况,属于比较传统的 Spring MVC 的应用,对应于前后端不分离的情况。
@Controller + @ResponseBody 返回 JSON 或 XML 形式数据。
@RestController
类似于 @Controller,但它是专⻔⽤于 RESTful Web 服务的。它包含了@Controller 和 @ResponseBody。
@RequestMapping
⽤于将 HTTP 请求映射到 Controller 的处理⽅法。可以⽤在类级别和⽅法级别。
可以使用 @[Get/Post/Put/Patch/Delete]Mapping 注解平替对应的请求方法。
@PathVariable
用于获取路径参数。
@RequestParam
用于获取查询参数。
@RequestBody
用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据。
@Autowired
⽤于⾃动注⼊ Spring 容器中的 Bean,可以⽤在构造⽅法、字段、Setter ⽅法上。
@Service
⽤于标识类作为服务层的 Bean,主要涉及一些复杂的逻辑,需要用到 Dao 层。
@Repository
⽤于标识类作为数据访问层的 Bean,通常⽤于与数据库交互。
@Component
通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用 @Component 注解标注。
@Configuration
⽤于定义配置类,类中可能包含⼀些 @Bean 注解⽤于定义 Bean。可以使用 @Component 注解替代。
@EnableAutoConfiguration
⽤于启⽤ Spring Boot 的⾃动配置机制,根据项⽬的依赖和配置⾃动配置 Spring 应⽤程序。
@Value
⽤于从属性⽂件或配置中读取值,将值注⼊到成员变量中。
@Qualifier
与 @Autowired ⼀起使⽤,指定注⼊时使⽤的 Bean 名称。
@ConfigurationProperties
⽤于将配置⽂件中的属性映射到 Java Bean。
@PropertySource
读取指定 properties 文件。
@Profile
⽤于定义不同环境下的配置,可以标识在类或⽅法上。
@Async
⽤于将⽅法标记为异步执⾏。
字段验证注解
@NotEmpty 被注释的字符串的不能为 null 也不能为空
@NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是 Email 格式。
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits(integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@NotBlank 被注释的字符串非 null,并且必须包含一个非空白字符
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是 Email 格式。
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max=, min=) 被注释的元素的大小必须在指定的范围内
@Digits(integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Valid
参数加 @Valid 注解,如果验证失败,它将抛出 MethodArgumentNotValidException。
@Validated
类加 @Validated 注解,可以让 Spring 去校验方法参数。
全局处理 Controller 层异常
@ControllerAdvice:注解定义全局异常处理类。
@ExceptionHandler:注解声明异常处理方法。
JPA相关
@Entity
声明一个类对应一个数据库实体。
@Table
设置表名。
@Id
声明一个字段为主键。
@GeneratedValue
定义主键的生成策略。
@GenericGenerator
声明一个主键策略。
@Column
声明字段。
@Transient
声明不需要与数据库映射的字段,在保存的时候不需要保存进数据库 。
@Lob
声明某个字段为大字段。
@Basic
指定 Lob 类型数据的获取策略( fetch ), FetchType.EAGER 表示非延迟加载,而 FetchType.LAZY 表示延迟加载 。
@Enumerated
声明使用枚举类型的字段。
审计功能
只要继承了 AbstractAuditBase 的类都会默认加上四个字段。
@CreatedDate、@CreatedBy、@LastModifiedDate、@LastModifiedBy
@EnableJpaAuditing
开启 JPA 审计功能。
@Modifying
提示 JPA 该操作是修改操作,注意还要配合 @Transactional 注解使用。
实体关联关系
@OneToOne : 一对一。
@ManyToMany:多对多。
@OneToMany : 一对多。
@ManyToOne:多对一。
利用 @ManyToOne 和 @OneToMany 也可以表达多对多的关联关系。
@Transactional
声明开启事务。
作用于类:当把 @Transactional 注解放在类上时,表示所有该类的 public 方法都配置相同的事务属性信息。
作用于方法:当类配置了 @Transactional,方法也配置了 @Transactional,方法的事务会覆盖类的事务配置信息。
Json 数据处理
@JsonIgnoreProperties
作用在类上用于过滤掉特定字段不返回或者不解析。
@JsonIgnore
作用在属性上用于过滤掉特定字段不返回或者不解析。
@JsonFormat
用来格式化 json 数据。
@JsonUnwrapped
用来扁平化对象。
测试相关
@ActiveProfiles
一般作用于测试类上, 用于声明生效的 Spring 配置文件。
@Test
声明一个方法为测试方法。
@Transactional
被声明的测试方法的数据会回滚,避免污染测试数据。
@WithMockUser
pring Security 提供的,用来模拟一个真实用户,并且可以赋予权限。
MyBatis
⼀个应⽤⼴泛的优秀的 ORM 框架,已经成了 JavaWeb 世界近乎标配的部分,这个框架具有强⼤的灵活性。
JDBC 和 MyBatis
JDBC 问题:数据库连接创建、释放频繁造成系统资源浪费从⽽影响系统性能,如果使⽤数据库连接池可解决此问题。
MyBatis 解决:在 mybatis-config.xml 中配置数据连接池,使⽤连接池管理数据库连接。
MyBatis 解决:在 mybatis-config.xml 中配置数据连接池,使⽤连接池管理数据库连接。
JDBC 问题:Sql 语句写在代码中造成代码不易维护,实际应⽤ sql 变化的可能较⼤,sql 变动需要改变 java 代码。
MyBatis 解决:将 Sql 语句配置在 XXXXmapper.xml ⽂件中与 java 代码分离。
MyBatis 解决:将 Sql 语句配置在 XXXXmapper.xml ⽂件中与 java 代码分离。
JDBC 问题:向 sql 语句传参数麻烦,因为 sql 语句的 where 条件不⼀定,可能多也可能少,占位符需要和参数⼀⼀对应。
MyBatis 解决:Mybatis ⾃动将 java 对象映射⾄ sql 语句。
MyBatis 解决:Mybatis ⾃动将 java 对象映射⾄ sql 语句。
JDBC 问题:对结果集解析麻烦,sql 变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成 pojo 对象解析⽐较⽅便。
MyBatis 解决:Mybatis ⾃动将 sql 执⾏结果映射⾄ java 对象。
MyBatis 解决:Mybatis ⾃动将 sql 执⾏结果映射⾄ java 对象。
插件
MyBatis 基于四大组件(核心对象)提供了简单易⽤的插件扩展机制。
MyBatis ⽀持⽤插件对四⼤核⼼对象进⾏拦截,⽤来增强核⼼对象的功能,增强功能本质上是借助于底层的动态代理实现的。
核心对象
ParameterHandler
处理SQL的参数对象。
ResultSetHandler
处理SQL的返回结果集。
StatementHandler
数据库的处理对象,⽤于执⾏SQL语句。
Executor
MyBatis 的执⾏器,⽤于执⾏增删改查操作。
插件原理
Mybatis 的插件借助于 JDK 动态代理和责任链设计模式进⾏对拦截的处理。
使⽤动态代理对⽬标对象进⾏包装,达到拦截的⽬的。
作⽤于 Mybatis 的作⽤域对象之上。
插件接⼝
Intercept ⽅法,插件的核⼼⽅法。
plugin ⽅法,⽣成 target 的代理对象。
setProperties ⽅法,传递插件所需参数。
Dao 和 XML
Mybatis 会把每个 SQL 标签封装成 SqlSource 对象,XML ⽂件中的每⼀个 SQL 标签就对应⼀个 MappedStatement 对象。
MappedStatement 对象属性
id:全限定类名 + ⽅法名组成的 ID。
sqlSource:当前 SQL 标签对应的 SqlSource 对象。
过程
Dao 接⼝的⼯作原理是 JDK 动态代理,Mybatis 运⾏时会使⽤ JDK 动态代理为 Dao 接⼝⽣成代理 proxy 对象,
代理对象 proxy 会拦截接⼝⽅法,转⽽执⾏ MappedStatement 所代表的 sql,然后将 sql 执⾏结果返回。
代理对象 proxy 会拦截接⼝⽅法,转⽽执⾏ MappedStatement 所代表的 sql,然后将 sql 执⾏结果返回。
重载
Mybatis 的 Dao 接口可以有多个重载方法,但是多个接口对应的映射必须只有一个,否则启动会报错。
条件
仅有一个无参方法和一个有参方法。
多个有参方法时,参数数量必须一致。且使用相同的 @Param ,或者使用 param1 这种。
不同 xml 中的 id 重复问题
不同 namespace 可以重复,无 namespace 则不可以重复。
接口绑定
通过注解绑定:在接⼝的⽅法上⾯加上 @Select、@Update 等注解⾥⾯包含 Sql 语句来绑定( Sql 语句⽐较简单的时候,推荐注解绑定)。
通过 xml ⾥⾯写 Sql 来绑定:指定 xml 映射⽂件⾥⾯的 namespace 必须为接⼝的全路径名(Sql 语句⽐较复杂的时候,推荐xml绑定)。
XML 和 内部数据结构映射
MyBatis 将所有 xml 配置信息都封装到 All-In-One 重量级对象 Configuration 内部。
<parameterMap>
ParameterMap 对象,其每个子元素会被解析为 ParameterMapping 对象。
<resultMap>
ResultMap 对象,其每个子元素会被解析为 ResultMapping 对象。
<select>、<insert>、<update>、<delete>
MappedStatement 对象,标签内的 sql 会被解析为 BoundSql 对象。
结果映射
使⽤标签(<resultMap>),逐⼀定义列名和对象属性名之间的映射关系。
使⽤ sql 列的别名(AS)功能,将列别名书写为对象属性名。
映射类型
resultType
直接表示返回类型,自动提交。
resultMap
对外部 ResultMap 的引⽤,手动提交
resultType 跟 resultMap 不能同时存在。
关联对象查询方式
单独发送一个 sql 去查询关联对象,赋给主对象,然后返回主对象。
使用嵌套查询,嵌套查询的含义为使用 join 查询,一部分列是 A 对象的属性值,另外一部分列是关联对象 B 的属性值。
映射枚举类
自定义一个 TypeHandler ,实现 TypeHandler 的 setParameter() 和 getResult() 接口方法。
动态 Sql
Mybatis 动态 sql 可以让我们在 Xml 映射⽂件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能。
原理
使用 OGNL 从 sql 参数对象中计算表达式的值,根据表达式的值动态拼接 sql,以此来完成动态 sql 的功能。
标签
<if></if>
<where></where>(trim,set)
<choose></choose>(when, otherwise)
<foreach></foreach>
<bind/>
实现
解释器模式:初始化过程中构建出抽象语法树,请求处理时根据参数对象解释语法树,⽣成 sql 语句。
⼯⼚模式:为动态标签的处理⽅式创建⼯⼚类(SqlTagHandlerFactory),根据标签名称获取对应的处理⽅式。
策略模式:将动态标签处理⽅式抽象为接⼝,针对不同标签有相应的实现类。
解释抽象语法树时, 定义统⼀的解释流程,再调⽤标签对应的处理⽅式完成解释中的各个⼦环节。
解释抽象语法树时, 定义统⼀的解释流程,再调⽤标签对应的处理⽅式完成解释中的各个⼦环节。
缓存
一级缓存
SqlSession 级别的缓存,默认开启。
在操作数据库时需要构造 sqlSession 对象,在对象中有⼀个数据结构⽤于存储缓存数据。
不同的 sqlSession 之间的缓存数据区域是互相不影响的。
二级缓存
Mapper 级别的缓存,默认不开启。
多个 SqlSession 去操作同⼀个 Mapper 的 sql 语句,可共⽤⼆级缓存,⼆级缓存是跨 SqlSession 的。⼆级缓存的作⽤范围更⼤。
MyBatis 和 Spring 整合开发后,如果没有事务,⼀级缓存是没有意义的。
每⼀个 namespace 的 mapper 都有⼀个⼆级缓存区域,如果相同两个 mapper 执⾏ sql 查询到数据将存在相同的⼆级缓存区域中。
执行器
分类
SimpleExecutor: 每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。
ReuseExecutor: 执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,
用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement>内,供下一次使用。简言之,就是重复使用 Statement 对象。
用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement>内,供下一次使用。简言之,就是重复使用 Statement 对象。
BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),
等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。
等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。
作用范围
Executor 的这些特点,都严格限制在 SqlSession 生命周期范围内。
指定
在 MyBatis 配置文件中,可以指定默认的 ExecutorType 执行器类型。
可以手动给 DefaultSqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数。
初始化过程
在 MyBatis 初始化过程中,会加载配置⽂件(mybatis-config.xml)、 映射配置⽂件(Mapper.xml)以及 Mapper 接⼝中的注解信息,
解析后的配置信息会形成相应的对象并全部保存到 Configuration 对象中,
并创建 DefaultSqlSessionFactory 供 Sql 执⾏过程创建出顶层接⼝ SqlSession 供给⽤户进⾏操作。
解析后的配置信息会形成相应的对象并全部保存到 Configuration 对象中,
并创建 DefaultSqlSessionFactory 供 Sql 执⾏过程创建出顶层接⼝ SqlSession 供给⽤户进⾏操作。
解析 mybatis-config.xml 配置⽂件
SqlSessionFactoryBuilder
XMLConfigBuilder
Configuration
解析 Mapper.xml 映射配置⽂件
XMLMapperBuilder::parse()
XMLStatementBuilder::parseStatementNode ()
XMLLanguageDriver
SqlSource
MappedStatement
解析 Mapper 接⼝中的注解
MapperRegistry
MapperAnnotationBuilder::parse()
延迟加载
MyBatis 中的延迟加载,也称为懒加载,是指在进⾏表的关联查询时,按照设置延迟规则推迟对关联对象的 select 查询。
要求
关联对象的查询与主加载对象的查询必须是分别进⾏的 select 语句,不能是使⽤多表连接所进⾏的 select 查询。
加载时机
直接加载:执⾏完对主加载对象的 select 语句,⻢上执⾏对关联对象的 select 查询。
侵⼊式延迟:执⾏对主加载对象的查询时,不会执⾏对关联对象的查询。但当要访问主加载对象的详情属性时,就会⻢上执⾏关联对象的 select 查询。
深度延迟:执⾏对主加载对象的查询时,不会执⾏对关联对象的查询。
访问主加载对象的详情时也不会执⾏关联对象的select查询。只有当真正访问关联对象的详情时,才会执⾏对关联对象的 select 查询。
访问主加载对象的详情时也不会执⾏关联对象的select查询。只有当真正访问关联对象的详情时,才会执⾏对关联对象的 select 查询。
分页
原理
使用 MyBatis 提供的插件接口,实现自定义插件,
在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。
在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。
方式
MyBatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。
可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能。
可以使用分页插件来完成物理分页。
区别
MyBatis 和 Hibernate
Mybatis 和 Hibernate不同,它不完全是⼀个 ORM 框架,因为 MyBatis 需要程序员⾃⼰编写 Sql 语句。
Mybatis 直接编写原⽣态 sql,可以严格控制 sql 执⾏性能,灵活度⾼,⾮常适合对关系数据模型要求不⾼的软件开发。
Hibernate 对象/关系映射能⼒强,数据库⽆关性好,对于关系模型要求⾼的软件,如果⽤ Hibernate 开发可以节省很多代码,提⾼效率。
Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
MyBatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
#{} 和 ${}
#{} 是 sql 的参数占位符,MyBatis 会将 sql 中的 #{} 替换为 ? 号,
在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的 ? 号占位符设置参数值。
在 sql 执行前会使用 PreparedStatement 的参数设置方法,按序给 sql 的 ? 号占位符设置参数值。
${} 是 Properties 文件中的变量占位符,它可以用于标签属性值和 sql 内部,属于原样文本替换,可以替换任意内容。
Netty
一个Java NIO技术的开源异步事件驱动的网络编程框架,用于快速开发可维护的高性能协议服务器和客户端。
特点
高并发:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高。
传输快:Netty 的传输依赖于零拷贝特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。
封装好:Netty 封装了 NIO 操作的很多细节,提供了易于使用调用接口。
优势
使用简单:封装了 NIO 的很多细节,使用更简单。
功能强大:预置了多种编解码功能,支持多种主流协议。
定制能力强:可以通过 ChannelHandler 对通信框架进行灵活地扩展。
性能高:通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优。
稳定:Netty 修复了已经发现的所有 NIO 的 bug,让开发人员可以专注于业务本身。
社区活跃:Netty 是活跃的开源项目,版本迭代周期短,bug 修复速度快。
应用
阿里分布式服务框架 Dubbo 的 RPC 框架。
RocketMQ 也是使用 Netty 作为通讯的基础。
Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架。
开源集群运算框架 Spark。
分布式计算框架 Storm。
高性能
IO 线程模型:同步非阻塞,用最少的资源做更多的事。
内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输。
内存池设计:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。默认堆外内存,开启池化管理。
串形化处理读写:避免使用锁带来的性能开销。
高性能序列化协议:支持 protobuf 等高性能序列化协议。
线程模型
Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss 线程池和 work 线程池。
boss 线程池
负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work 线程池。
work 线程池
负责请求的 read 和 write 事件,由对应的 Handler 处理。
分类
单线程模型
所有 I/O 操作都由一个线程完成,即多路复用、事件分发和处理都是在一个 Reactor 线程上完成的。
多线程模型
Acceptor线程:只负责监听服务端,接收客户端的 TCP 连接请求。
NIO 线程池:负责网络 IO 的操作,即消息的读取、解码、编码和发送。
1 个 NIO 线程可以同时处理 N 条链路,但是1个链路只对应1个 NIO 线程,这是为了防止发生并发操作问题。
在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。
主从多线程模型
多个 Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上移除,
重新注册到 Sub 线程池的线程上,用于处理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作。
重新注册到 Sub 线程池的线程上,用于处理 I/O 的读写等操作,从而保证 mainReactor 只负责接入认证、握手等操作。
TCP 粘包/拆包
TCP 是以流的方式来处理数据,一个完整的包可能会被 TCP 拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
原因
应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象。
应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象。
解决
消息定长:FixedLengthFrameDecoder 类。
自定义分隔符类 :DelimiterBasedFrameDecoder 类。
将消息分为消息头和消息体:LengthFieldBasedFrameDecoder 类。
分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
零拷贝
Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。
Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,
用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
Netty 的文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。
重要组件
Bootstrap,ServerBootstrap:客户端和服务端的启动引导程序。
Channel:数据传送的通道,Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等。
ChannelHandler:Channel 中有相关事件发生的时候会触发执行,充当了所有处理入站和出站数据的逻辑容器。
ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。
ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。
ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。
EventLoop:Io 异步执行的任务队列和线程池,主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。
ChannelFuture:Io 任务执行以后未来的返回结果。Netty 框架中所有的 I/O 操作都为异步的,
因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。
因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。
ChannelHandlerContext:ChannelHandler相关联的上下文信息对象。
发送消息方式
直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动。
写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。
默认线程数
Netty 默认是 CPU 处理器数的两倍,bind 完之后启动。
序列化协议
定义
序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等。
反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。
性能因素
序列化后的码流大小(网络带宽的占用)、序列化的性能(CPU资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。
XML
优点:人机可读性好,可指定元素或特性的名称。
缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;
只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
适用场景:当做配置文件存储数据,实时数据转换。
JSON
一种轻量级的数据交换格式。
优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与 XML 相比,其协议比较简单,解析速度比较快。
缺点:数据的描述性比 XML 差、不适合性能要求为ms级别的情况、额外空间开销比较大。
适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于 Web browser 的 Ajax 请求、传输数据量相对小,实时性要求相对低(秒级别)的服务。
Fastjson
采用一种“假定有序快速匹配”的算法。
优点:接口简单易用、目前 java 语言中最快的 json 库。
缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。
适用场景:协议交互、Web 输出、Android 客户端。
Thrift
不仅是序列化协议,还是一个 RPC 框架。
优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。
缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,
调试代码时相对困难、不能与其他传输层协议共同使用(例如 HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。
调试代码时相对困难、不能与其他传输层协议共同使用(例如 HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。
适用场景:分布式系统的 RPC 解决方案。
Avro
Hadoop 的一个子项目,解决了 JSON 的冗长和没有 IDL 的问题。
优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、
提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。
提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。
缺点:对于习惯于静态类型语言的用户不直观。
适用场景:在 Hadoop 中做 Hive、Pig 和 MapReduce 的持久化数据格式。
Protobuf
将数据结构以 .proto 文件进行描述,通过代码生成工具可以生成对应数据结构的 POJO 对象和 Protobuf 相关的方法和属性。
优点:序列化后码流小,性能高、结构化数据存储格式(XML、JSON等)、
通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。
通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。
缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持 Java 、C++ 、python。
适用场景:对性能要求高的 RPC 调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化。
protostuff
基于 protobuf 协议,但不需要配置 proto 文件,直接导包即可。
Jboss marshaling
可以直接序列化 java 类, 无须实 java.io.Serializable 接口。
Message pack
一个高效的二进制序列化格式。
Hessian
采用二进制协议的轻量级 remoting onhttp 工具。
kryo
基于 protobuf 协议,只支持 java 语言,需要注册(Registration),然后序列化(Output),反序列化(Input)。
选择
对于公司间的系统调用,如果性能要求在100ms以上的服务,基于 XML 的 SOAP 协议是一个值得考虑的方案。
基于 Web browser 的 Ajax,以及 Mobile app 与服务端之间的通讯,JSON 协议是首选。
对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON也是非常不错的选择。
对于调试环境比较恶劣的场景,采用 JSON 或 XML 能够极大的提高调试效率,降低系统开发成本。
当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro 之间具有一定的竞争关系。
对于 T 级别的数据的持久化应用场景,Protobuf 和 Avro 是首要选择。如果持久化后的数据存储在 hadoop 子项目里,Avro 会是更好的选择。
对于持久层非 Hadoop 项目,以静态类型语言为主的应用场景,Protobuf 会更符合静态类型语言工程师的开发习惯。
由于 Avro 的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro 是更好的选择。
由于 Avro 的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro 是更好的选择。
如果需要提供一个完整的 RPC 解决方案,Thrift 是一个好的选择。
如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf 可以优先考虑。
心跳类型设置
readerIdleTime:为读超时时间(即测试端一定时间内未接受到被测试端消息)。
writerIdleTime:为写超时时间(即测试端一定时间内向被测试端发送消息)。
allIdleTime:所有类型的超时时间。
Netty 和 Tomcat
作用不同:Tomcat 是 Servlet 容器,可以视为 Web 服务器,
而 Netty 是异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。
而 Netty 是异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。
协议不同:Tomcat 是基于 http 协议的 Web 服务器,而 Netty 能通过编程自定义各种协议,因为 Netty 本身自己能编码/解码字节流,
所有 Netty 可以实现 HTTP 服务器、FTP 服务器、UDP 服务器、RPC 服务器、WebSocket 服务器、Redis 的 Proxy 服务器、MySQL 的 Proxy 服务器等等。
所有 Netty 可以实现 HTTP 服务器、FTP 服务器、UDP 服务器、RPC 服务器、WebSocket 服务器、Redis 的 Proxy 服务器、MySQL 的 Proxy 服务器等等。
工作流程
Netty Reactor 工作架构图
系统设计
基础知识
RESTful API
API
API(Application Programming Interface) 翻译过来是应用程序编程接口的意思。
把 API 理解为程序与程序之间通信的桥梁,其本质就是一个函数而已。
定义
RESTful API 经常也被叫做 REST API,它是基于 REST 构建的 API。
REST
REST 的全称是 Resource Representational State Transfer ,直白翻译就是 “资源”在网络传输中以某种“表现形式”进行“状态转移” 。
资源(Resource):我们可以把真实的对象数据称为资源。一个资源既可以是一个集合,也可以是单个个体。
表现形式(Representational):"资源"是一种信息实体,它可以有多种外在表现形式。
状态转移(State Transfer):REST 中的状态转移更多地描述的服务器端资源的状态。
RESTful 架构
每一个 URI 代表一种资源。
客户端和服务器之间,传递这种资源的某种表现形式比如 json,xml,image,txt 等等。
客户端通过特定的 HTTP 动词,对服务器端资源进行操作,实现"表现层状态转化"。
动作
GET:请求从服务器获取特定资源。举个例子:GET /classes(获取所有班级)
POST:在服务器上创建一个新的资源。举个例子:POST /classes(创建班级)
PUT:更新服务器上的资源(客户端提供更新后的整个资源)。举个例子:PUT /classes/12(更新编号为 12 的班级)
DELETE:从服务器删除特定的资源。举个例子:DELETE /classes/12(删除编号为 12 的班级)
PATCH:更新服务器上的资源(客户端提供更改的属性,可以看做作是部分更新),使用的比较少,这里就不举例子了。
路径(接口命名)
路径又称"终点"(endpoint),表示 API 的具体网址。
规范
网址中不能有动词,只能有名词,API 中的名词也应该使用复数。
不用大写字母,建议用中杠 - 不用下杠 _ 。
善用版本化 API。
接口尽量使用名词,避免使用动词。
过滤信息(Filtering)
如果我们在查询的时候需要添加特定条件的话,建议使用 url 参数的形式。
状态码(Status Codes)
2xx:成功
200 成功
201 创建
3xx:重定向
301 永久重定向
304 资源未修改
4xx:客户端错误
400 错误请求
401 未授权
403 禁止访问
404 未找到
405 请求方法不对
5xx:服务器错误
500 服务器错误
502 网关错误
504 网关超时
HATEOAS
返回结果中提供链接,连向其他 API 方法,使得用户不查文档,也知道下一步应该做什么。即 Hypermedia API 设计,也被称为 HATEOAS。
软件工程
在更少资源消耗的情况下,创造出更好、更容易维护的软件。
开发过程
软件开发过程(software development process)或软件过程(software process)是软件开发的开发生命周期(software development life cycle),
其各个阶段实现了软件的需求定义与分析、设计、实现、测试、交付和维护。软件过程是在开发与构建系统时应遵循的步骤,是软件开发的路线图。
其各个阶段实现了软件的需求定义与分析、设计、实现、测试、交付和维护。软件过程是在开发与构建系统时应遵循的步骤,是软件开发的路线图。
需求分析:分析用户的需求,建立逻辑模型。
软件设计:根据需求分析的结果对软件架构进行设计。
编码:编写程序运行的源代码。
测试 : 确定测试用例,编写测试报告。
交付:将做好的软件交付给客户。
维护:对软件进行维护比如解决 bug,完善功能。
开发模型
瀑布模型(Waterfall Model)、快速原型模型(Rapid Prototype Model)、V 模型(V-model)、W 模型(W-model)、敏捷开发模型。
敏捷开发核心
持续集成、重构、小版本发布、低文档、站会、结对编程、测试驱动开发。
基本策略
软件复用
通过复用已有的一些轮子(框架、第三方库等)、设计模式、设计原则等等现成的物料,可以更快地构建出一个满足要求的软件。
分而治之
将一些比较复杂的问题拆解为一些小问题,然后,一一攻克。
在领域驱动(Domain Driven Design,简称 DDD)设计中,很重要的一个概念就是领域(Domain),它就是我们要解决的问题。
在领域驱动设计中,我们要做的就是把比较大的领域(问题)拆解为若干的小领域(子域)。
在领域驱动设计中,我们要做的就是把比较大的领域(问题)拆解为若干的小领域(子域)。
逐步演进
软件开发是一个逐步演进的过程,我们需要不断进行迭代式增量开发,最终交付符合客户价值的产品。
MVP(Minimum Viable Product,最小可行产品),刚好能够满足客户需求的产品。
优化折中
软件开发是一个不断优化改进的过程,要学会折中,在有限的投入内,以最有效的方式提高现有软件的质量。
代码命名
好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。
规范
驼峰命名法(CamelCase)
使用大小写混合的格式来区别各个单词,并且单词之间不使用空格隔开或者连接字符连接的命名方式。
大驼峰命名法(UpperCamelCase)
类名
小驼峰命名法(lowerCamelCase)
方法名、参数名、成员变量、局部变量
蛇形命名法(snake_case)
各个单词之间通过下划线“_”连接。
优点:命名所需要的单词比较多时,更易读。
应用:测试方法名、常量、枚举名称。
串式命名法(kebab-case)
各个单词之间通过连接符“-”连接。
应用:项目文件夹名称。
基本命名规范
类名需要使用大驼峰命名法(UpperCamelCase)风格。方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)。
测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case)。并且,测试方法名称要求全部小写,常量以及枚举名称需要全部大写。
项目文件夹名称使用串式命名法(kebab-case),比如dubbo-registry。
包名统一使用小写,尽量使用单个名词作为包名,各个单词通过 "." 分隔符连接,并且各个单词必须为单数。
抽象类命名使用 Abstract 开头。
异常类命名使用 Exception 结尾。
测试类命名以它要测试的类的名称开始,以 Test 结尾。
POJO 类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。
如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。
命名易读性规范
为了能让命名更加易懂和易读,尽量不要缩写/简写单词,除非这些单词已经被公认可以被这样缩写/简写。
命名不像函数一样要尽量追求短,可读性强的名字优先于简短的名字,虽然可读性强的名字会比较长一点。
避免无意义的命名,你起的每一个名字都要能表明意思。
避免命名过长(50 个字符以内最好),过长的命名难以阅读并且丑陋。
不要使用拼音,更不要使用中文。
代码重构
定义
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
利用设计模式(如组合模式、策略模式、责任链模式)、
软件设计原则(如 SOLID 原则、YAGNI 原则、KISS 原则)和重构手段(如封装、继承、构建测试体系)来让代码更容易理解,更易于修改。
软件设计原则(如 SOLID 原则、YAGNI 原则、KISS 原则)和重构手段(如封装、继承、构建测试体系)来让代码更容易理解,更易于修改。
核心
步子一定要小,每一步的重构都不会影响软件的正常运行,可以随时停止重构。
常见的设计模式
单例模式
⼀种确保⼀个类只有⼀个实例,并提供⼀个全局访问点来访问该实例的创建模式。
原理
⼀个私有构造函数(确保只能单例类⾃⼰创建实例):单例类通常会将其构造函数设为私有,以防⽌外部代码直接实例化对象。
⼀个私有静态变量(确保只有⼀个实例):单例类通常包含⼀个私有的静态变量,⽤于保存该类的唯⼀实例。
⼀个公有静态函数(给使⽤者提供调⽤⽅法)。
优点
使⽤单例模式就可以避免⼀个全局使⽤的类,频繁的创建与销毁,耗费系统资源。
分类
懒汉式(线程不安全)
先不创建实例,当第⼀次被调⽤时,再创建实例,所以被称为懒汉式。
优点
延迟了实例化,如果不需要使⽤该类,就不会被实例化,只有在需要时才创建实例,避免了资源浪费。
缺点
线程不安全,多线程环境下会实例化多个实例。
饿汉式(线程安全)
先不管需不需要使⽤这个实例,直接先实例化好实例(饿死⻤⼀样,所以称为饿汉式),然后当需要使⽤的时候,直接调⽅法就可以使⽤了。
优点
提起实例化好了⼀个实例,避免了线程不安全问题的出现。
缺点
直接实例化了实例,不再延迟实例化。若系统没有使⽤这个实例,或者系统运⾏很久之后才需要使⽤这个实例,都会使操作系统的资源浪费。
懒汉式(线程安全)
实现和线程不安全的懒汉式⼏乎⼀样,唯⼀不同的点是,在 get ⽅法上加了⼀把锁(synchronized)。
优点
延迟实例化,节约了资源,并且是线程安全的。
缺点
虽然解决了线程安全问题,但是因为有锁导致性能降低了。
双重检查锁实现(线程安全)
双重检查锁相当于是改进了线程安全的懒汉式。获取锁之前增加空判断,并且增加 volatile 关键字修饰静态实例。
volatile 作用
禁⽌ JVM 的指令重排,可以保证多线程环境下的安全运⾏。
优点
延迟实例化,节约了资源;线程安全;并且相对于线程安全的懒汉式,性能提⾼了。
缺点
volatile 关键字,对性能也有⼀些影响。
静态内部类实现(线程安全)
新增静态内部类持有实例,外部类提供静态方法返回内部类持有的实例。仅在获取实例时,才会被初始化,⽽且 JVM 会确保只被实例化⼀次。
优点
延迟实例化,节约了资源,且线程安全,性能也提⾼了。
枚举类实现(线程安全)
默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。
优点
写法简单,线程安全,天然防⽌反射和反序列化调⽤。
序列化
把 java 对象转换为字节序列的过程。
反序列化
通过这些字节序列在内存中新建 java 对象的过程。
将⼀个单例实例对象写到磁盘再读回来,从⽽获得了⼀个新的实例。枚举类天然防⽌反序列化。
其他单例模式 可以通过 重写 readResolve() ⽅法,从⽽防⽌反序列化,使实例唯⼀重写。
应用
资源共享:当多个模块或系统需要共享某⼀资源时,可以使⽤单例模式确保该资源只被创建⼀次,避免重复创建和浪费资源。
控制资源访问:单例模式可以⽤于控制对特定资源的访问,例如数据库连接池、线程池等。
配置管理器:当整个应⽤程序需要共享⼀些配置信息时,可以使⽤单例模式将配置信息存储在单例类中,⽅便全局访问和管理。
⽇志记录器:单例模式可以⽤于创建⼀个全局的⽇志记录器,⽤于记录系统中的⽇志信息。
线程池:在多线程环境下,使⽤单例模式管理线程池,确保线程池只被创建⼀次,提⾼线程池的利⽤率。
缓存:单例模式可以⽤于实现缓存系统,确保缓存只有⼀个实例,避免数据不⼀致性和内存浪费。
⼯⼚模式
定义
⼯⼚模式是⼀种⾮常常⽤的创建型设计模式,其提供了创建对象的最佳⽅式。
在创建对象时,不会对客户端暴露对象的创建逻辑,⽽是通过使⽤共同的接⼝来创建对象。
原理
⽤来封装和管理类的创建,本质是对获取对象过程的抽象。
优点
解耦:将对象的创建和使⽤进⾏分离,客户端代码与具体产品类的实例化过程解耦,客户端只需知道⼯⼚和抽象产品的接⼝,⽽不需要关⼼具体的实现类。
可复⽤:对于创建过程⽐较复杂且在很多地⽅都使⽤到的对象,通过⼯⼚模式可以提⾼对象创建的代码的复⽤性。
易于扩展:添加新的产品类时,只需要扩展相应的具体产品和具体⼯⼚,⽽不需要修改已有的代码,符合开闭原则。
更符合⾯向对象的设计原则:通过⼯⼚模式,将对象的创建封装在⼯⼚类中,使得系统更符合单⼀职责原则。
分类
简单⼯⼚
定义
简单⼯⼚模式也被称为静态⼯⼚⽅法模式。
简单⼯⼚模式并不是⼀个标准的设计模式,更像是⼀种编程习惯。
在简单⼯⼚模式中,⼀个⼯⼚类负责创建多个产品类的实例,通过传⼊不同的参数来决定创建哪种产品。
同时在简单⼯⼚模式中会定义⼀个类负责创建其他类的实例,被创建的实例也通常具有共同的⽗类。
缺点
虽实现了对象的创建和使⽤的分离,但不够灵活,⼯⼚类集合了所有产品的创建逻辑,职责过重。
新增⼀个产品就需要在原⼯⼚类内部添加⼀个分⽀,违反了开闭原则。
若是有多个判断条件共同决定创建对象,则后期修改会越来越复杂。
应用
JDK 中的 DateFormate、Calendar 类都有使⽤,通过不同参数返回我们需要的对象。
⼯⼚⽅法
定义
将简单⼯⼚中的⼯⼚类变为⼀个抽象接⼝。
负责给出不同⼯⼚应该实现的⽅法,⾃身不再负责创建各种产品,⽽是将具体的创建操作交给实现该接⼝的⼦⼯⼚类来做。
优点
通过多态的形式解决了简单⼯⼚模式过多的分⽀问题。
虽然在新增产品时不仅要新增⼀个产品类还要实现与之对应的⼦⼯⼚,但是相较于简单⼯⼚模式更符合开闭原则。
应用
JDK 中的 Collection 接⼝中 Iterator 的实现。
抽象⼯⼚
定义
提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定它们的具体类。
通常涉及多个抽象产品、多个具体产品和多个具体⼯⼚。
缺点
虽然对于新增⼀个产品族很方便,并且也符合开闭原则,但是新增⼀个产品等级结构,会对整个⼯⼚结构进⾏⼤改。
应用
Spring 中的 BeanFactory。
条件
当⼀个类不知道它所需要的类的时候。
当⼀个类希望通过其⼦类来指定创建对象的时候。
当类将创建对象的职责委托给多个帮助⼦类中的某⼀个,并且希望将哪⼀个帮助⼦类是代理者的信息局部化时。
应用
在数据库操作中,通过⼯⼚模式可以根据不同的数据库类型(MySQL、Oracle等)创建对应的数据库连接对象。
通过⼯⼚模式可以根据配置⽂件或其他条件选择不同类型的⽇志记录器,如⽂件⽇志记录器、数据库⽇志记录器等。
在图形⽤户界⾯(GUI)库中,可以使⽤⼯⼚模式创建不同⻛格或主题的界⾯元素,如按钮、⽂本框等。
在加密算法库中,可以使⽤⼯⼚模式根据需要选择不同的加密算法,例如对称加密、⾮对称加密等。
在⽂件解析过程中,可以使⽤⼯⼚模式根据⽂件类型选择不同的解析器,如XML解析器、JSON解析器等。
在⽹络通信库中,可以使⽤⼯⼚模式创建不同类型的⽹络连接对象,如TCP连接、UDP连接等。
观察者模式
定义
属于⾏为型模式的⼀种,它定义了⼀种⼀对多的依赖关系,让多个观察者对象同时监听某⼀个主题对象。
这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够⾃动更新⾃⼰。
构成
Subject(主题)
主题是被观察的对象,它包含了⼀组观察者对象,并提供了添加、删除和通知观察者的⽅法。主题通常有⼀个状态,当状态改变时,通知所有观察者。
Observer(观察者)
观察者是依赖于主题的对象,当主题的状态发⽣改变时,观察者得到通知并进⾏相应的更新。观察者的具体实现类需要实现更新的⽅法。
ConcreteSubject(具体主题)
具体主题是主题的具体实现类,它维护了⼀个状态,并在状态改变时通知观察者。
ConcreteObserver(具体观察者)
具体观察者是观察者的具体实现类,实现了更新的⽅法,以便在接收到通知时进⾏相应的处理。
优点
解除耦合,让耦合的双⽅都依赖于抽象,从⽽使得各⾃的变换都不会影响另⼀边的变换。
缺点
调试复杂,⽽且在Java中消息的通知⼀般是顺序执⾏,那么⼀个观察者卡顿,会影响整体的执⾏效率,在这种情况下,⼀般会采⽤异步实现。
实现
定义主题接⼝(Subject),包括注册、移除和通知观察者的⽅法。
定义观察者接⼝(Observer),包括更新的⽅法。
创建具体主题类(Concrete Subject),维护⼀组观察者对象,并在状态改变时通知观察者。
创建具体观察者类(Concrete Observer),实现更新的⽅法。
客户端代码中创建主题对象和观察者对象,注册观察者到主题中,然后通过主题改变状态,观察者得到通知并进⾏更新。
应用
事件处理:当⼀个对象的状态发⽣改变时,观察者模式可以⽤于通知和处理与该对象相关的事件。
这在图形⽤户界⾯(GUI)开发中是很常⻅的,例如按钮点击事件、⿏标移动事件等。
这在图形⽤户界⾯(GUI)开发中是很常⻅的,例如按钮点击事件、⿏标移动事件等。
发布订阅:观察者模式可以⽤于实现发布-订阅模型,其中⼀个主题(发布者)负责发送通知,
⽽多个观察者(订阅者)监听并响应这些通知。这种模型在消息队列系统、事件总线等场景中经常使⽤。
⽽多个观察者(订阅者)监听并响应这些通知。这种模型在消息队列系统、事件总线等场景中经常使⽤。
MVC架构:观察者模式常被⽤于实现MVC架构中的模型和视图之间的通信。
当模型的状态发⽣改变时,所有相关的视图都会得到通知并更新显示。
当模型的状态发⽣改变时,所有相关的视图都会得到通知并更新显示。
异步编程:观察者模式可以⽤于处理异步任务的完成事件。任务完成时,通知所有相关的观察者进⾏后续处理。
代理模式
定义
代理模式(Proxy Pattern)是⼀种结构型设计模式,其主要⽬的是在访问某个对象时引⼊⼀种代理对象,通过代理对象控制对原始对象的访问。
结构
抽象主题(Subject):定义了代理对象和真实对象的共同接⼝,使得代理对象能够替代真实对象。
真实主题(Real Subject):是实际执⾏业务逻辑的对象,是代理模式中的被代理对象。
代理(Proxy):包含⼀个指向真实主题的引⽤,提供与真实主题相同的接⼝,可以控制对真实主题的访问,并在需要时负责创建或删除真实主题的实例。
作用
实现懒加载、控制访问、监控对象等场景。
策略模式
定义
策略模式(Strategy Pattern)是⼀种⾏为设计模式,它定义了⼀系列算法,把它们单独封装起来并可以互相替换,使算法独⽴于使⽤它的客户端⽽变化。
这些算法所完成的功能类型是⼀样的,对外接⼝也是⼀样的,只是不同的策略为引起环境⻆⾊表现出不同的⾏为。
相⽐于使⽤⼤量的if...else,使⽤策略模式可以降低复杂度,使得代码更容易维护。
构成
策略接⼝(Stragety): 策略接⼝定义了算法的抽象,具体的策略类实现了这个接⼝。
具体策略类(ConcreteStragety): 具体策略类实现了策略接⼝,封装了具体的算法。
环境类(Context):⽤来操作策略的上下⽂环境类,环境类的构造函数包含了 Strategy 类,
通过多态传进来不同的具体策略(ConcreteStrategyA。ConcreteStrategyB)来调⽤不同策略的⽅法。
通过多态传进来不同的具体策略(ConcreteStrategyA。ConcreteStrategyB)来调⽤不同策略的⽅法。
实现
定义⼀个策略接⼝,声明算法的抽象⽅法。
创建具体的策略类,实现策略接⼝,封装具体的算法。
创建环境类,包含对策略接⼝的引⽤,以及⼀个⽤于设置具体策略对象的⽅法。
在客户端中创建环境类的对象,并调⽤其⽅法来执⾏具体的算法。
优点
使⽤策略模式可以避免使⽤多重条件转移语句。多重转移语句将算法或⾏为的逻辑混合在⼀起,不易维护。
缺点
可能需要定义⼤量的策略类,并且这些策略类都要提供给客户端。
客户端必须知道所有的策略类,并⾃⾏决定使⽤哪⼀个策略类,策略模式只适⽤于客户端知道所有的算法或⾏为的情况。
应用
个商场销售系统根据不同的促销策略计算最终价格。
装饰模式
定义
装饰模式(Decorator Pattern)是⼀种结构型设计模式,它允许在不改变原始类接⼝的情况下,动态地添加功能或责任。
装饰模式通过创建⼀个装饰类,包裹原始类的实例,并在保持原始类接⼝不变的情况下,提供额外的功能。就增加功能来说,装饰模式⽐⽣成⼦类更为灵活。
结构
组件接⼝:定义了具体组件和装饰器共同的接⼝,确保它们可以互相替换。
具体组件:实现了组件接⼝,是被装饰的具体对象。
装饰器:持有⼀个组件对象的引⽤,并实现了组件接⼝。装饰器通常是⼀个抽象类,它的具体⼦类实现具体的装饰逻辑。
具体装饰器:继承⾃装饰器,实现了具体的装饰逻辑,并调⽤⽗类的⽅法以保持接⼝⼀致。
应用
装饰模式把每个要装饰的功能放在单独的类中,并让这个类包装它所要装饰的对象。
因此,当需要执⾏特殊⾏为时,客户代码就可以在运⾏时根据需要有选择地、按顺序地使⽤装饰功能包装对象了。
因此,当需要执⾏特殊⾏为时,客户代码就可以在运⾏时根据需要有选择地、按顺序地使⽤装饰功能包装对象了。
23种设计模式
创建型模式
⼯⼚⽅法(factory method)模式
定义⼀个创建对象的接⼝,但由⼦类决定需要实例化哪⼀个类。⼯⼚⽅法使得⼦类实例化的过程推迟。
抽象⼯⼚(abstract factory)模式
提供⼀个接⼝,可以创建⼀系列相关或相互依赖的对象,⽽⽆需指定他们具体的类。
原型(prototype)模式
⽤原型实例指定创建对象的类型,并且通过拷⻉这个原型来创建新的对象。
单例(singleton)模式
保证⼀个类只有⼀个实例,并提供⼀个访问它的全局访问点。
构建器(builder)模式
将⼀个复杂类的表示与其构造相分离,使得相同的构建过程能够得出不同的表示。
结构型模式
适配器(adapter)模式
将⼀个类的接⼝转换成⽤户希望得到的另⼀个接⼝。它使原本不相容的接⼝得以协同⼯作——速记关键字:转换接⼝。
桥接(bridge)模式
将类的抽象部分和它的实现部分分离开来,使它们可以独⽴地变化——速记关键字:继承树拆分。
组合(composite)模式
将对象组合成树型结构以表示“整体-部分”的层次结构,使得⽤户对单个对象和组合对象的使⽤具有⼀致性——速记关键字:树形⽬录结构。
装饰(decorator)模式
动态地给⼀个对象添加⼀些额外的职责。它提供了⽤⼦类扩展功能的⼀个灵活的替代,⽐派⽣⼀个⼦类更加灵活——速记关键字:附加职责。
外观(facade)模式
定义⼀个⾼层接⼝,为⼦系统中的⼀组接⼝提供⼀个⼀致的外观,从⽽简化了该⼦系统的使⽤——速记关键字:对外统⼀接⼝。
享元(flyweight)模式
提供⽀持⼤量细粒度对象共享的有效⽅法。
代理(proxy)模式
为其他对象提供⼀种代理以控制这个对象的访问。
⾏为型模式
职责链(chain of responsibility)模式
通过给多个对象处理请求的机会,减少请求的发送者与接收者之间的耦合。
将接收对象链接起来,在链中传递请求,直到有⼀个对象处理这个请求——速记关键字:传递职责。
将接收对象链接起来,在链中传递请求,直到有⼀个对象处理这个请求——速记关键字:传递职责。
命令(command)模式
将⼀个请求封装为⼀个对象,从⽽可⽤不同的请求对客户进⾏参数化,将请求排队或记录请求⽇志,⽀持可撤销的操作——速记关键字:⽇志记录,可撤销。
解释器(interpreter)模式
给定⼀种语⾔,定义它的⽂法表示,并定义⼀个解释器,该解释器⽤来根据⽂法表示来解释语⾔中的句⼦。
迭代器(iterator)模式
提供⼀种⽅法来顺序访问⼀个聚合对象中的各个元素⽽不需要暴露该对象的内部表示。
中介者(mediator)模式
⽤⼀个中介对象来封装⼀系列的对象交互。
它使各对象不需要显式地相互调⽤,从⽽达到低耦合,还可以独⽴地改变对象间的交互——速记关键字:不直接引⽤。
它使各对象不需要显式地相互调⽤,从⽽达到低耦合,还可以独⽴地改变对象间的交互——速记关键字:不直接引⽤。
备忘录(memento)模式
在不破坏封装性的前提下,捕获⼀个对象的内部状态,并在该对象之外保存这个状态,从⽽可⽤在以后将该对象恢复到原先保存的状态。
观察者(observer)模式
定义对象间的⼀种⼀对多的依赖关系,当⼀个对象的状态发⽣改变时,所有依赖于它的对象都得到通知并⾃动更新。
状态(state)模式
允许⼀个对象在其内部状态改变时改变它的⾏为——速记关键字:状态变成类。
策略(strategy)模式
定义⼀系列算法,把它们⼀个个封装起来,并且使它们之间可互相替换,从⽽让算法可以独⽴于使⽤它的⽤户⽽变化。
模板⽅法(template method)模式
定义⼀个操作中的算法⻣架,⽽将⼀些步骤延迟到⼦类中,使得⼦类可以不改变⼀个算法的结构即可重新定义算法的某些特定步骤。
访问者(visitor)模式
表示⼀个作⽤于某对象结构中的各元素的操作,使得在不改变各元素的类的前提下定义作⽤于这些元素的新操作。
常见的软件设计原则
单⼀职责
在设计类的时候要尽量缩⼩粒度,使功能明确、单⼀,不要做多余的事情(⾼内聚,低耦合)。
开闭原则
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着可以通过扩展来添加新功能,⽽不必修改现有代码。
⾥⽒替换
⼦类型必须能够替换掉它们的基类型。在程序中,如果有⼀个基类和⼀个⼦类,那么可以⽤⼦类对象替换基类对象,⽽程序的⾏为仍然是正确的。
接⼝隔离
不应该强迫⼀个类实现它不需要的接⼝。⼀个类不应该对它⽤不到的⽅法负责。
依赖倒置
⾼层模块不应该依赖于低层模块,⽽是应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
⽐如Java的操作数据库,Java定义了⼀组接⼝,由各个数据库去实现它,Java不依赖于他们,数据库依赖于Java。
⽐如Java的操作数据库,Java定义了⼀组接⼝,由各个数据库去实现它,Java不依赖于他们,数据库依赖于Java。
组合优于继承
继承耦合度⾼,组合耦合度低。继承基类是固定好的,但是组合通过组合类的指针,可以传⼊不同的类,避免⾼耦合。
组合复⽤原则
概念
优先使⽤组合 contains a(聚合 has a),⽽不是继承 is a 来达到⽬的。
原因
继承会将实现细节暴露给⼦类,继承复⽤破坏了封装性,是⽩箱复⽤。使⽤继承时需要考虑⾥⽒替换原则。
优点
新类对象存取成员对象只通过成员对象的接⼝,是⿊箱复⽤,系统更灵活,降低耦合度。
可以在运⾏时动态进⾏,新对象可动态引⽤与成员对象类型相同的对象。
可以在运⾏时动态进⾏,新对象可动态引⽤与成员对象类型相同的对象。
缺点
需要管理较多对象。
迪⽶特法则
概念
⼀个对象应当对其他对象有尽可能少的了解,即不和陌⽣⼈说话,“朋友圈”概念。
this
该对象⽅法中的参数。
实例变量直接引⽤的对象。
实例变量如果是⼀个聚集(聚合对象),聚集中的元素。
该对象⽅法中创建的变量。
要求
优先考虑将⼀个类设计成不变类。
尽量降低⼀个类的访问权限。
谨慎使⽤ Serializable(持久化,通过序列化⼀个对象,将其写⼊磁盘,以后程序调⽤时重新恢复该对象)。
尽量降低成员的访问权限。
优点
降低类之间的耦合。
缺点
过多使⽤迪⽶特法则,会产⽣⼤量中介类,设计变复杂。
YAGNI(你不需要它原则)
You Aren't Gonna Need It 的缩写。
极限编程原则告诫开发人员,他们应该只实现当前所需的功能,并避免实现未来需要的功能,仅在必要时才实现。
遵守这一原则可以减小代码库大小,同时避免时间和生产力浪费在没有价值的功能上。
DRY(不要重复你自己原则)
系统中,每一块知识都必须是单一、明确而权威的。
DRY 是 Do not Repeat Yourself 的缩写。这个原则旨在帮助开发人员减少代码的重复性,并将公共代码保存在一个地方。
与 DRY 相反的是 WET(功能实现两次或者喜欢打字 Write Everything Twice or We Enjoy Typing)。
实际上,如果你在两个或更多的地方有相同的功能,你可以使用 DRY 原则将它们合并为一个,并在任何你需要的地方重复使用。
KISS 原则
保持简单和直白。
KISS 原则指明了如果大多数的系统能够保持简单而非复杂化,那么他们便能够工作在最佳状态。
因此,简单性应该是设计时的关键指标,同时也要避免不必要的复杂度。
因此,简单性应该是设计时的关键指标,同时也要避免不必要的复杂度。
目的
主要目的主要是提升代码 & 架构的灵活性 / 可扩展性以及复用性。最终目标是提高软件开发速度和质量 。
让代码更容易理解:通过添加注释、命名规范、逻辑优化等手段可以让我们的代码更容易被理解。
避免代码腐化:通过重构干掉坏味道代码。
加深对代码的理解:重构代码的过程会加深你对某部分代码的理解。
发现潜在 bug:是这样的,很多潜在的 bug ,都是我们在重构的过程中发现的。
时机
提交代码之前
营地法则:保证你离开时的代码库一定比来时更健康。
当我们离开营地(项目代码)的时候,请不要留下垃圾(代码坏味道)!尽量确保营地变得更干净了!
开发一个新功能之后&之前
Code Review 之后
捡垃圾式重构
阅读理解代码的时候
注意事项
单元测试是重构的保护网。
不要为了重构而重构。
遵循方法。
单元测试
单元测试(Unit Testing)是针对程序模块(软件设计的最小单位)进行的正确性检验测试工作。
作用
为重构保驾护航、提高代码质量、减少 bug、快速定位 bug、持续集成依赖单元测试
TDD
Test-Driven Development( 测试驱动开发),这是敏捷开发的一项核心实践和技术,也是一种设计方法论。
原理
开发功能代码之前,先编写测试用例代码,然后针对测试用例编写功能代码,使其能够通过。
优点
帮你整理需求,梳理思路。
帮你设计出更合理的接口(空想的话很容易设计出屎)。
减小代码出现 bug 的概率。
提高开发效率(前提是正确且熟练使用 TDD)。
缺点
能用好 TDD 的人非常少,看似简单,实则门槛很高。
投入开发资源(时间和精力)通常会更多。
由于测试用例在未进行代码设计前写,很有可能限制开发者对代码整体设计。
可能引起开发人员不满情绪,这点很严重,毕竟不是人人都喜欢单元测试,尽管单元测试会带给我们相当多的好处。
单测框架
JUnit、Mockito、Spock、PowerMock、JMockit、TestableMock 等等。
JUnit 几乎是默认选择,但是其不支持 Mock,因此还需要选择一个 Mock 工具。Mockito 和 Spock 是最主流的两款 Mock 工具,一般都是在这两者中选择。
Mockito 和 Spock
Spock 没办法 Mock 静态方法和私有方法 ,Mockito 3.4.0 以后,支持静态方法的 Mock。
Spock 基于 Groovy,写出来的测试代码更清晰易读,比较规范(自带 given-when-then 的常用测试结构规范)。
Mockito 没有具体的结构规范,需要项目组自己约定一个或者遵守比较好的测试代码实践。
Mockito 没有具体的结构规范,需要项目组自己约定一个或者遵守比较好的测试代码实践。
Mockito 使用的人群更广泛,稳定可靠。并且,Mockito 是 SpringBoot Test 默认集成的 Mock 工具。
认证授权
认证授权
认证 (Authentication)
验证身份的凭据(例如用户名/用户 ID 和密码),通过这个凭据证明系统存在这个用户。所以 Authentication 被称为身份/用户验证。
授权 (Authorization)
授权发生在认证之后,管理用户访问系统的权限。
Cookie
定义
某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。
Cookie 存放在客户端,一般用来保存用户信息。
内容
名字,值,过期时间,路径和域,路径与域一起构成cookie的作用范围。
生命周期
表示当前cookie的生命周期为浏览器会话期间,关闭浏览器则cookie消失。
应用
Cookie 保存已经登录过的用户信息,下次再访问网站时,页面可以自动登录并自动填写一些基本信息。
Cookie 能保存用户首选项,主题和其他设置信息。
Cookie 能保存用户首选项,主题和其他设置信息。
使用 Cookie 保存 SessionId 或者 Token ,向后端发送请求的时候带上 Cookie,这样后端就能取到 Session 或者 Token 了。
这样就能记录用户当前的状态了,因为 HTTP 协议是无状态的。
这样就能记录用户当前的状态了,因为 HTTP 协议是无状态的。
Cookie 可以用来记录和分析用户行为。
Cookie 和 Session
Session 的主要作用就是通过服务端记录用户的状态,典型的场景是购物车。
Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。
Session-Cookie 认证
单体环境认证过程
用户向服务器发送用户名、密码、验证码用于登陆系统。
服务器验证通过后,服务器为用户创建一个 Session,并将 Session 信息存储起来。
服务器向用户返回一个 SessionID,写入用户的 Cookie。
当用户保持登录状态时,Cookie 将与每个后续请求一起被发送出去。
服务器可以将存储在 Cookie 上的 SessionID 与存储在内存中或者数据库中的 Session 信息进行比较,
以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。
以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。
注意
依赖 Session 的关键业务一定要确保客户端开启了 Cookie。
注意 Session 的过期时间。
多节点认证方案
某个用户的所有请求都通过特性的哈希策略分配给同一个服务器处理。
每一个服务器保存的 Session 信息都是互相同步的,也就是说每一个服务器都保存了全量的 Session 信息。
单独使用一个所有服务器都能访问到的数据节点(比如缓存)来存放 Session 信息。
Spring Session 是一个用于在多个服务器之间管理会话的项目。
没有 Cookie 时 Session 还能用吗?
可以将 SessionID 放在请求的 url 里面。
攻击
CSRF(Cross Site Request Forgery)跨站请求伪造 ,说简单点,就是用你的身份去发送一些对你不友好的请求。
XSS(Cross Site Scripting)跨站脚本攻击,攻击者会用各种方式将恶意代码注入到其他用户的页面中,通过脚本盗用信息。
因与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆,因此将跨站脚本攻击缩写为 XSS。
因与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆,因此将跨站脚本攻击缩写为 XSS。
Cookie 无法防止 CSRF 攻击,而 Token 可以。
不论是 Cookie 还是 Token 都无法避免 跨站脚本攻击(Cross Site Scripting)XSS 。
JWT
定义
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。
从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT 自身包含了身份验证所需要的所有信息,因此服务器不需要存储 Session 信息。这增加了系统的可用性和伸缩性,减轻了服务端的压力。
JWT 更符合设计 RESTful API 时的「Stateless(无状态)」原则 。
使用 JWT 认证可以有效避免 CSRF 攻击,因为 JWT 一般是存在 localStorage 中,使用 JWT 进行身份验证的过程中是不会涉及到 Cookie 的。
组成
JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分,例:xxxxx.yyyyy.zzzzz。
Header
描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。
组成
typ(Type):令牌类型,也就是 JWT。
alg(Algorithm):签名算法,比如 HS256。
Payload
用来存放实际需要传递的数据。
Payload 是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。
Claims 分类
Registered Claims(注册声明):预定义的一些声明,建议使用,但不是强制性的。
Public Claims(公有声明):JWT 签发方可以自定义的声明,但是为了避免冲突,应该在 IANA JSON Web Token Registry 中定义它们。
Private Claims(私有声明):JWT 签发方因为项目需要而自定义的声明,更符合实际项目场景使用。
常见注册声明
iss(issuer):JWT 签发方。
iat(issued at time):JWT 签发时间。
sub(subject):JWT 主题。
aud(audience):JWT 接收方。
exp(expiration time):JWT 的过期时间。
nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
jti(JWT ID):JWT 唯一标识。
注意
Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!
Signature
服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。
要素
Header + Payload。
存放在服务端的密钥(一定不要泄露出去)。
签名算法。
公式
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
认证过程
用户向服务器发送用户名、密码以及验证码用于登陆系统。
如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
服务端检查 JWT 并从中获取用户相关信息。
建议
建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 Authorization 字段中(Authorization: Bearer Token)。
优势
相比于 Session 认证的方式。
无状态
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。
由于 JWT 的无状态,也导致了它最大的缺点:不可控(用户 Logout 后,JWT 仍然有效)。
由于 JWT 的无状态,也导致了它最大的缺点:不可控(用户 Logout 后,JWT 仍然有效)。
有效避免了 CSRF 攻击
使用 JWT 进行身份验证不需要依赖 Cookie ,因此可以避免 CSRF 攻击。
适合移动端应用
只要 JWT 可以被客户端存储就能够使用,而且 JWT 还可以跨语言使用。
单点登录友好
单点登录 Cookie 有跨域问题,但因 JWT 被保存在客户端,不会存在这问题。
安全性
使用安全系数高的加密算法。
使用成熟的开源库,没必要造轮子。
JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。
一定不要将隐私信息存放在 Payload 当中。
防篡改,密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
Payload 要加入 exp (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。
问题
主动失效问题
场景
退出登录、修改密码、修改权限/角色、用户的帐户被封禁/删除、用户被服务端强制注销、用户被踢下线等。
解决
将 JWT 存入内存数据库(Redis),但违背了 JWT 的无状态原则。
黑名单机制(Redis),JWT 失效就加入黑名单,但违背了 JWT 的无状态原则。
修改密钥 (Secret),为每个用户都创建一个专属密钥,如果让某个 JWT 失效,直接修改对应用户的密钥即可。
不推荐
保持令牌的有效期限短并经常轮换,但是会导致用户登录状态不会被持久记录,而且需要用户经常登录。
续签问题
类似于 Session 认证中的做法
Session:假如 Session 的有效期 30 分钟,如果 30 分钟内用户有访问,就把 Session 有效期延长 30 分钟。
JWT:如果马上快过期了,服务端就重新生成 JWT,客户端检查新旧 JWT,不一致就进行替换。
每次请求都返回新 JWT
思路很简单,但开销会比较大,尤其是在服务端要存储维护 JWT 的情况下。
JWT 有效期设置到半夜
一种折衷的方案,保证了大部分用户白天可以正常登录,适用于对安全性要求不高的系统。
用户登录返回两个 JWT
accessJWT 过期时间半小时,refreshJWT 过期时间1天。accessJWT 失效就校验 refreshJWT,并生成新的 accessJWT。
问题
需要客户端来配合。
用户注销的时候需要同时保证两个 JWT 都无效。
重新请求获取 JWT 的过程中会有短暂 JWT 不可用的情况。
解决:可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT。
解决:可以通过在客户端设置定时器,当 accessJWT 快过期的时候,提前去通过 refreshJWT 获取新的 accessJWT。
存在安全问题,只要拿到了未过期的 refreshJWT 就一直可以获取到 accessJWT。
SSO
SSO(Single Sign On)单点登录,即用户登陆多个子系统的其中一个就有权访问与其相关的其他系统。
优点
用户角度:用户能够做到一次登录多次使用,无需记录多套用户名和密码,省心。
系统管理员角度:管理员只需维护好一个统一的账号中心就可以了,方便。
新系统开发角度:新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时。
设计与实现
设计
实现
登录
登录信息获取/登录状态校验
登出
跨域登录(主域名已登录)
跨域登录(主域名未登录)
跨域登出
OAuth 2.0
定义
OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。
OAuth 2.0 是对 OAuth 1.0 的完全重新设计,OAuth 2.0 更快,更容易实现,OAuth 1.0 已经被废弃。
目的
一种授权机制,目的是为第三方应用颁发一个有时效性的令牌 Token,使得第三方应用能够通过该令牌获取相关的资源。
应用
第三方登录。
支付场景(微信支付、支付宝支付)。
开发平台(微信开放平台、阿里开放平台等等)。
权限系统设计
RBAC 模型
定义
系统权限控制最常采用的访问控制模型
RBAC 即基于角色的权限访问控制(Role-Based Access Control),一种通过角色关联权限,角色同时又关联用户的授权的方式。
一个用户可以拥有若干角色,每一个角色又可以被分配若干权限,这样就构造成“用户-角色-权限” 的授权模型。
实现
数据库表的常见设计:一共 5 张表,2 张用户建立表之间的联系。
ABAC 模型
定义
基于属性的访问控制(Attribute-Based Access Control,简称 ABAC) 是一种比 RBAC模型 更加灵活的授权模型
这个模型在云系统中使用的比较多,比如 AWS,阿里云等。
原理
通过各种属性来动态判断一个操作是否可以被允许。
一个操作是否被允许是基于对象、资源、操作和环境信息共同动态计算决定的。
对象:对象是当前请求访问资源的用户。用户的属性包括 ID,个人资源,角色,部门和组织成员身份等。
资源:资源是当前用户要访问的资产或对象,例如文件,数据,服务器,甚至 API。
操作:操作是用户试图对资源进行的操作。常见的操作包括“读取”,“写入”,“编辑”,“复制”和“删除”。
环境:环境是每个访问请求的上下文。环境属性包含访问的时间和位置,对象的设备,通信协议和加密强度等。
应用
授权某个人具体某本书的编辑权限。
当一个文档的所属部门跟用户的部门相同时,用户可以访问这个文档。
新权限模型
用户最终权限 = 用户拥有的角色带来的权限 + 用户独立配置的权限,两者取并集。
数据安全
加密算法
定义
一种用数学方法对数据进行变换的技术,目的是保护数据的安全,防止被未经授权的人读取或修改。
加密算法可以分为三大类:对称加密算法、非对称加密算法和哈希算法(也叫摘要算法)。
应用
保存在数据库中的密码需要加盐之后使用哈希算法(比如 BCrypt)进行加密。
保存在数据库中的银行卡号、身份号这类敏感数据需要使用对称加密算法(比如 AES)保存。
网络传输的敏感数据比如银行卡号、身份号需要用 HTTPS + 非对称加密算法(如 RSA)来保证传输数据的安全性。
哈希算法
哈希算法也叫散列函数或摘要算法,作用是对任意长度的数据生成一个固定长度的唯一标识,也叫哈希值、散列值或消息摘要。
作用
用来验证数据的完整性和一致性。
特点
不可逆:不能从哈希值还原出原始数据。
原始数据的任何改变都会导致哈希值的巨大变化。
分类
加密哈希算法
定义
安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,
能够抵御一定的攻击手段,安全性相对较高,适用于对安全性要求较高的场景。例如,SHA-256、SHA-512、SM3、Bcrypt 等等。
能够抵御一定的攻击手段,安全性相对较高,适用于对安全性要求较高的场景。例如,SHA-256、SHA-512、SM3、Bcrypt 等等。
哈希算法一般是不需要密钥的,但也存在部分特殊哈希算法需要密钥。
例如,MAC 和 SipHash 就是一种基于密钥的哈希算法,它在哈希算法的基础上增加了一个密钥,使得只有知道密钥的人才能验证数据的完整性和来源。
例如,MAC 和 SipHash 就是一种基于密钥的哈希算法,它在哈希算法的基础上增加了一个密钥,使得只有知道密钥的人才能验证数据的完整性和来源。
常见算法
MD(Message Digest,消息摘要算法):MD2、MD4、MD5 等,已经不被推荐使用。
SHA(Secure Hash Algorithm,安全哈希算法):SHA-1 系列安全性低,SHA2,SHA3 系列安全性较高。
国密算法:SM2、SM3、SM4,其中 SM2 为非对称加密算法,SM4 为对称加密算法,SM3 为哈希算法(安全性及效率和 SHA-256 相当,更适合国内)。
Bcrypt(密码哈希算法):基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高。
MAC(Message Authentication Code,消息认证码算法):HMAC 是一种基于哈希的 MAC,可以与任何安全的哈希算法结合使用,例如 SHA-256。
CRC(Cyclic Redundancy Check,循环冗余校验):CRC32 是一种 CRC 算法,特点是生成 32 位的校验值,通常用于数据完整性校验、文件校验等场景。
SipHash:加密哈希算法,它的设计目的是在速度和安全性之间达到一个平衡,用于防御哈希泛洪 DoS 攻击。
Rust 默认使用 SipHash 作为哈希算法,从 Redis4.0 开始,哈希算法被替换为 SipHash。
Rust 默认使用 SipHash 作为哈希算法,从 Redis4.0 开始,哈希算法被替换为 SipHash。
MurMurHash:经典快速的非加密哈希算法,目前最新的版本是 MurMurHash3,可以生成 32 位或者 128 位哈希值。
MD
定义
MD 算法有多个版本,包括 MD2、MD4、MD5 等,其中 MD5 是最常用的版本,它可以生成一个 128 位(16 字节)的哈希值。
除了这些版本,还有一些基于 MD4 或 MD5 改进的算法,如 RIPEMD、HAVAL 等。
安全性
MD5 > MD4 > MD2。
MD5 算法本身存在弱碰撞(Collision)问题,即多个不同的输入产生相同的 MD5 值。
破解
攻击者可以通过暴力破解或彩虹表攻击等方式,找到与原始数据相同的哈希值,从而破解数据。
增加破解难度
加盐。
盐(Salt)
在密码学中,是指通过在密码任意固定位置插入特定的字符串,让哈希后的结果和使用原始密码的哈希结果不相符,这种过程称之为“加盐”。
推荐
更安全的哈希算法比如 SHA-2、Bcrypt。
SHA
定义
SHA(Secure Hash Algorithm)系列算法是一组密码哈希算法,用于将任意长度的数据映射为固定长度的哈希值。
SHA 系列算法由美国国家安全局(NSA)于 1993 年设计,目前共有 SHA-1、SHA-2、SHA-3 三种版本。
版本
SHA-1
将任意长度的数据映射为 160 位的哈希值。
缺点
安全性低,容易受到碰撞攻击和长度扩展攻击。(不推荐)
SHA-2
在 SHA-1 算法的基础上改进而来的,它们采用了更复杂的运算过程和更多的轮次,使得攻击者更难以通过预计算或巧合找到碰撞。
家族
SHA-256、SHA-384、SHA-512 等。
SHA-3
美国国家标准与技术研究院(National Institute of Standards and Technology,简称 NIST)在 2007 年公开征集 SHA-3 的候选算法。
一共 64 个算法方案,最终在 2012 年 Keccak 算法胜出,成为 SHA-3 的标准算法(SHA-3 与 SHA-2 算法没有直接的关系)。
Keccak 算法
具有与 MD 和 SHA-1/2 完全不同的设计思路,即海绵结构(Sponge Construction),
使得传统攻击方法无法直接应用于 SHA-3 的攻击中(能够抵抗目前已知的所有攻击方式包括碰撞攻击、长度扩展攻击、差分攻击等)。
使得传统攻击方法无法直接应用于 SHA-3 的攻击中(能够抵抗目前已知的所有攻击方式包括碰撞攻击、长度扩展攻击、差分攻击等)。
由于 SHA-2 算法还没有出现重大的安全漏洞,而且在软件中的效率更高,所以大多数人还是倾向于使用 SHA-2 算法。
SHA-2 和 MD5
哈希值长度更长:例如 SHA-256 算法的哈希值长度为 256 位,而 MD5 算法的哈希值长度为 128 位,这就提高了攻击者暴力破解或者彩虹表攻击的难度。
更强的碰撞抗性:SHA 算法采用了更复杂的运算过程和更多的轮次,使得攻击者更难以通过预计算或巧合找到碰撞。目前 SHA-256 还没有产生碰撞。
注意
SHA-2 也不是绝对安全的,也有被暴力破解或者彩虹表攻击的风险,所以,在实际的应用中,加盐还是必不可少的。
Bcrypt
一种基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高。
原理
由于 Bcrypt 采用了 salt(盐) 和 cost(成本) 两种机制,它可以有效地防止彩虹表攻击和暴力破解攻击,从而保证密码的安全性。
salt 是一个随机生成的字符串,用于和密码混合,增加密码的复杂度和唯一性。
cost 是一个数值参数,用于控制 Bcrypt 算法的迭代次数,增加密码哈希的计算时间和资源消耗。
Bcrypt 算法可以根据实际情况进行调整加密的复杂度,可以设置不同的 cost 值和 salt 值,从而满足不同的安全需求,灵活性很高。
Spring Security 支持多种密码编码器,其中 BCryptPasswordEncoder 是官方推荐的一种,它使用 BCrypt 算法对用户的密码进行加密存储。
非加密哈希算法
安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,
但性能较高,适用于对安全性没有要求的业务场景。例如,CRC32、MurMurHash3 等等。
但性能较高,适用于对安全性没有要求的业务场景。例如,CRC32、MurMurHash3 等等。
对称加密
定义
对称加密算法是指加密和解密使用同一个密钥的算法,也叫共享密钥加密算法。
可以用来保护数据的安全性和保密性,常见的对称加密算法有 DES、3DES、AES 等。
DES 和 3DES
DES
定义
DES(Data Encryption Standard)使用 64 位的密钥(有效秘钥长度为 56 位,8 位奇偶校验位)和 64 位的明文进行加密。
虽然 DES 一次只能加密 64 位,但我们只需要把明文划分成 64 位一组的块,就可以实现任意长度明文的加密。
填充
如果明文长度不是 64 位的倍数,必须进行填充,常用的模式有 PKCS5Padding, PKCS7Padding, NOPADDING。
思想
将 64 位的明文分成两半,然后对每一半进行多轮的变换,最后再合并成 64 位的密文。
变换包括置换、异或、选择、移位等操作,每一轮都使用了一个子密钥,而这些子密钥都是由同一个 56 位的主密钥生成的。
总共进行了 16 轮变换,最后再进行一次逆置换,得到最终的密文。
缺点
虽经典,但也有明显的缺陷,即 56 位的密钥安全性不足,已被证实可以在短时间内破解。
3DES
3DES(Triple DES)是 DES 向 AES 过渡的加密算法,它使用 2 个或者 3 个 56 位的密钥对数据进行三次加密。
3DES 相当于是对每个数据块应用三次 DES 的对称加密算法。
为了兼容普通的 DES,3DES 并没有直接使用 加密->加密->加密 的方式,而是采用了加密->解密->加密 的方式。
当三种密钥均相同时,前两步相互抵消,相当于仅实现了一次加密,因此可实现对普通 DES 加密算法的兼容。
3DES 比 DES 更为安全,但其处理速度不高。
AES
定义
AES(Advanced Encryption Standard)算法是一种更先进的对称密钥加密算法,
它使用 128 位、192 位或 256 位的密钥对数据进行加密或解密,密钥越长,安全性越高。
它使用 128 位、192 位或 256 位的密钥对数据进行加密或解密,密钥越长,安全性越高。
AES 也是一种分组(或者叫块)密码,分组长度只能是 128 位,也就是说,每个分组为 16 个字节。
AES 加密算法有多种工作模式(mode of operation),如:ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM(目前使用最广泛的模式)。
不同的模式参数和加密流程不同,但是核心仍然是 AES 算法。
填充
对于不是 128 位倍数的明文需要进行填充,常用的填充模式有 PKCS5Padding, PKCS7Padding, NOPADDING。
AES-GCM 是流加密算法,可以对任意长度的明文进行加密,所以对应的填充模式为 NoPadding,即无需填充。
优点
AES 的速度比 3DES 快,而且更安全。
DES 算法和 AES 算法简单对比
非对称加密
定义
非对称加密算法是指加密和解密使用不同的密钥的算法,也叫公开密钥加密算法。
这两个密钥互不相同,一个称为公钥,另一个称为私钥。公钥可以公开给任何人使用,私钥则要保密。
如果用公钥加密数据,只能用对应的私钥解密(加密);如果用私钥加密数据,只能用对应的公钥解密(签名)。
可以实现数据的安全传输和身份认证。常见的非对称加密算法有 RSA、DSA、ECC 等。
RSA
RSA(Rivest–Shamir–Adleman algorithm)算法是一种基于大数分解的困难性的非对称加密算法,
原理
选择两个大素数作为私钥的一部分,然后计算出它们的乘积作为公钥的一部分(寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难)。
安全性
依赖于大数分解的难度,目前已经有 512 位和 768 位的 RSA 公钥被成功分解,因此建议使用 2048 位或以上的密钥长度。
优点
简单易用,可以用于数据加密和数字签名。
缺点
运算速度慢,不适合大量数据的加密。
DSA
DSA(Digital Signature Algorithm)算法是一种基于离散对数的困难性的非对称加密算法。
原理
选择一个素数 q 和一个 q 的倍数 p 作为私钥的一部分,然后计算出一个模 p 的原根 g 和一个模 q 的整数 y 作为公钥的一部分。
安全性
依赖于离散对数的难度,目前已经有 1024 位的 DSA 公钥被成功破解,因此建议使用 2048 位或以上的密钥长度。
优点
数字签名速度快,适合生成数字证书。
缺点
不能用于数据加密,且签名过程需要随机数。
算法签名过程
敏感词过滤
系统需要对用户输入的文本进行敏感词过滤如色情、政治、暴力相关的词汇。
算法实现
Trie 树
Trie 树也称为字典树、单词查找树,哈系树的一种变种,
通常被用于字符串匹配,用来解决在一组字符串集合中快速查找某个字符串的问题。
通常被用于字符串匹配,用来解决在一组字符串集合中快速查找某个字符串的问题。
原理
通过公共前缀来提高字符串匹配效率。
缺点
一种利用空间换时间的数据结构,占用的内存会比较大。
推荐
改进版 Trie 树,例如:双数组 Trie 树(Double-Array Trie,DAT)。
DAT
相比较于 Trie 树,DAT 的内存占用极低,可以达到 Trie 树内存的 1%左右。
DAT 在中文分词、自然语言处理、信息检索等领域有广泛的应用,是一种非常优秀的数据结构。
AC 自动机
Aho-Corasick(AC)自动机是一种建立在 Trie 树上的一种改进算法,是一种多模式匹配算法。
原理
使用 Trie 树来存放模式串的前缀,通过失败匹配指针(失配指针)来处理匹配失败的跳转。
DFA
DFA(Deterministic Finite Automata)即确定有穷自动机,一个识别器,它对每个输入的字符做识别和判断,以确定其能到达的最终状态或状态集和路径。
与之对应的是 NFA(Non-Deterministic Finite Automata,不确定有穷自动机)。
数据脱敏
定义
对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。
在数据脱敏过程中,通常会采用不同的算法和技术,以根据不同的需求和场景对数据进行处理。
规则
替换(常用):将敏感数据中的特定字符或字符序列替换为其他字符。例如,将信用卡号中的中间几位数字替换为星号(*)或其他字符。
删除:将敏感数据中的部分内容随机删除。比如,将电话号码的随机 3 位数字进行删除。
重排:将原始数据中的某些字符或字段的顺序打乱。例如,将身份证号码的随机位交错互换。
加噪:在数据中注入一些误差或者噪音,达到对数据脱敏的效果。例如,在敏感数据中添加一些随机生成的字符。
加密(常用):使用加密算法将敏感数据转换为密文。例如,将银行卡号用 MD5 或 SHA-256 等哈希函数进行散列。
工具
Hutool
一个 Java 基础工具类,对文件、流、加密解密、转码、正则、线程、XML 等 JDK 方法进行封装,组成各种 Util 工具类。
数据脱敏工具就是在 hutool.core 模块,DesensitizedUtil 工具基本覆盖了常见的敏感信息。
全局脱敏
基于 Spring Boot 的 web 项目,则可以利用 Spring Boot 自带的 jackson 自定义序列化实现。
原理
在 json 进行序列化渲染给前端时,进行脱敏。
步骤
脱敏策略的枚举。
定义一个用于脱敏的 Desensitization 注解。
@JsonSerialize
创建自定的序列化类,继承 JsonSerializer,实现 ContextualSerializer 接口,并重写两个方法。
Apache ShardingSphere
一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)相互独立的产品组成。
作用
提供标准化的数据分片、分布式事务和数据库治理功能 。
原理
Apache ShardingSphere 下面存在一个数据脱敏模块,此模块集成的常用的数据脱敏的功能。
对用户输入的 SQL 进行解析拦截,并依靠用户的脱敏配置进行 SQL 的改写,从而实现对原文字段的加密及加密字段的解密。
FastJSON
一个很常用的 Spring Web Restful 接口序列化的工具。
方式
基于注解 @JSONField 实现
需要自定义一个用于脱敏的序列化的类,然后在需要脱敏的字段上通过 @JSONField 中的 serializeUsing 指定为我们自定义的序列化类型即可。
基于序列化过滤器
需要实现 ValueFilter 接口,重写 process 方法完成自定义脱敏,然后在 JSON 转换时使用自定义的转换策略。
Mybatis-mate
MybatisPlus 也提供了数据脱敏模块 mybatis-mate。其为 MybatisPlus 企业级模块,使用之前需要配置授权码(付费),旨在更敏捷优雅处理数据。
MyBatis-Flex
类似于 MybatisPlus,MyBatis-Flex 也是一个 MyBatis 增强框架。MyBatis-Flex 同样提供了数据脱敏功能,并且是可以免费使用的。
MyBatis-Flex 提供了 @ColumnMask() 注解,以及内置的 9 种脱敏规则,开箱即用。
定时任务
定义
定时任务:在指定时间点执行特定的任务。
延时任务:一定的延迟时间后执行特定的任务。
单机定时任务
Timer
java.util.Timer 是 JDK 1.3 开始就已经支持的一种定时任务的实现方式。
原理
Timer 内部使用一个叫做 TaskQueue 的类存放定时任务,它是一个基于最小堆实现的优先级队列。
TaskQueue 会按照任务距离下一次执行时间的大小将任务排序,保证在堆顶的任务最先执行。
缺点
一个 Timer 一个线程,这就导致 Timer 的任务的执行只能串行执行,一个任务执行时间过长的话会影响其他任务(性能非常差)。
发生异常时任务直接停止(Timer 只捕获了 InterruptedException )。
无法使用 Cron 表达式指定任务执行的具体时间。
ScheduledThreadPoolExecutor 支持多线程执行定时任务并且功能更强大,是 Timer 的替代品。
ScheduledExecutorService
ScheduledExecutorService 是一个接口,有多个实现类,比较常用的是 ScheduledThreadPoolExecutor 。
ScheduledThreadPoolExecutor 本身就是一个线程池,支持任务并发执行。并且,其内部使用 DelayedWorkQueue 作为任务队列。
缺点
无法使用 Cron 表达式指定任务执行的具体时间。
DelayQueue
DelayQueue 是 JUC 包(java.util.concurrent)为我们提供的延迟队列,用于实现延时任务。
原理
它是 BlockingQueue 的一种,底层是一个基于 PriorityQueue 实现的一个无界队列,是线程安全的。
DelayQueue 和 Timer/TimerTask
DelayQueue 和 Timer/TimerTask 都可以用于实现定时任务调度,但是它们的实现方式不同。
DelayQueue 是基于优先级队列和堆排序算法实现的,可以实现多个任务按照时间先后顺序执行。
Timer/TimerTask 是基于单线程实现的,只能按照任务的执行顺序依次执行,如果某个任务执行时间过长,会影响其他任务的执行。
DelayQueue 还支持动态添加和移除任务,而 Timer/TimerTask 只能在创建时指定任务。
Spring Task
Spring 提供的 @Scheduled 注解即可定义定时任务。
优点
简单,轻量,支持 Cron 表达式。
Cron 表达式
用于定时作业(定时任务)系统定义执行时间或执行频率的表达式。
缺点
Spring 自带的定时调度只支持单机,并且提供的功能比较单一。
时间轮
定义
一个环形的队列(底层一般基于数组实现),队列中的每一个元素(时间格)都可以存放一个定时任务列表。
Kafka、Dubbo、ZooKeeper、Netty、Caffeine、Akka 中都有对时间轮的实现。
原理
时间轮中的每个时间格代表了时间轮的基本时间跨度或者说时间精度,
假如时间一秒走一个时间格的话,那么这个时间轮的最高精度就是 1 秒(也就是说 3 s 和 3.9s 会在同一个时间格中)。
假如时间一秒走一个时间格的话,那么这个时间轮的最高精度就是 1 秒(也就是说 3 s 和 3.9s 会在同一个时间格中)。
超过一圈时,则可以记录圈数/轮数,来表示更久的时间。
圈数/轮数
多层次时间轮 (类似手表)
应用
时间轮比较适合任务数量比较多的定时任务场景,它的任务写入和执行的时间复杂度都是 O(1)。
分布式定时任务
Redis
Redis 用来做延时任务。
方案
Redis 过期事件监听。
Redisson 内置的延时队列。
MQ
大部分消息队列,例如 RocketMQ、RabbitMQ,都支持定时/延时消息。
优点
可以与 Spring 集成、支持分布式、支持集群、性能不错。
缺点
功能性较差、不灵活、需要保障消息可靠性。
分布式任务调度框架
需要一些高级特性比如支持任务在分布式场景下的分片和高可用的话,我们就需要用到分布式任务调度框架了。
角色
任务:首先肯定是要执行的任务,这个任务就是具体的业务逻辑比如定时发送文章。
调度器:其次是调度中心,调度中心主要负责任务管理,会分配任务给执行器。
执行器:最后就是执行器,执行器接收调度器分派的任务并执行。
Quartz
Quartz 是 Java 定时任务领域的参考标准,其他的任务调度框架基本都是基于 Quartz 开发的,如当当网的 elastic-job。
优点
使用 Quartz 可以很方便地与 Spring 集成,并且支持动态添加任务和集群。
缺点
Quartz 使用起来也比较麻烦,API 繁琐。
Quartz 并没有内置 UI 管理控制台。
Quartz 虽然支持分布式任务,但在数据库层面,通过数据库的锁机制做的,有非常多的弊端比如系统侵入性严重、节点负载不均衡。
Elastic-Job
当当网开源的一个面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。
原理
Elastic-Job 没有调度中心这一概念,而是使用 ZooKeeper 作为注册中心,注册中心负责协调分配任务到不同的节点上。
Elastic-Job 中的定时调度都是由执行器自行触发,这种设计也被称为去中心化设计(调度和处理都是执行器单独完成)。
ElasticJob-Lite 和 ElasticJob-Cloud
ElasticJob-Lite 的架构设计
优点
可以与 Spring 集成、支持分布式、支持集群、性能不错、支持任务可视化管理。
缺点
依赖了额外的中间件比如 Zookeeper(复杂度增加,可靠性降低、维护成本变高)
XXL-JOB
XXL-JOB 于 2015 年开源,是一款优秀的轻量级分布式任务调度框架,支持任务可视化管理、弹性扩容缩容、任务失败重试和告警、任务分片等功能,
特性
架构设计
组成
调度中心
主要负责任务管理、执行器管理以及日志管理。进行任务调度时,是通过自研 RPC 来实现的。
执行器
主要是接收调度信号并处理。
使用
只需要重写 IJobHandler 自定义任务执行逻辑就可以了,非常易用!
优点
开箱即用(学习成本比较低)、与 Spring 集成、支持分布式、支持集群、支持任务可视化管理。
缺点
不支持动态添加任务(如果一定想要动态创建任务也是支持的)。
PowerJob
非常值得关注的一个分布式任务调度框架,分布式任务调度领域的新星。目前,已经有很多公司接入比如 OPPO、京东、中通、思科。
消息推送
定义
指网站的运营工作等人员,通过某种工具对用户当前网页或移动设备 APP 进行的主动消息推送。
一般又分为 Web 端消息推送和移动端消息推送。
轮询
轮询(polling) 应该是实现消息推送方案中最简单的一种,分为短轮询和长轮询。
短轮询
指定的时间间隔,由浏览器向服务器发出 HTTP 请求,服务器实时返回未读消息数据给客户端,浏览器再做渲染显示。
优缺点
虽简单,但由于推送数据并不会频繁变更,无论是否有新的消息产生,客户端都会进行请求,会对服务端造成很大压力,浪费带宽和服务器资源。
长轮询
对上边短轮询的一种改进版本,在尽可能减少对服务器资源浪费的同时,保证消息的相对实时性。
原理
如果服务端的数据没有发生变更,会一直 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。
返回后,客户端又会立即再次发起下一次长轮询。
返回后,客户端又会立即再次发起下一次长轮询。
应用
在中间件中应用的很广泛,比如 Nacos 和 Apollo 配置中心,消息队列 Kafka、RocketMQ 中都有用到长轮询。
iframe 流
定义
在页面中插入一个隐藏的<iframe>标签,通过在src中请求消息数量 API 接口,由此在服务端和客户端之间创建一条长连接,服务端持续向iframe传输数据。
传输的数据通常是 HTML、或是内嵌的 JavaScript 脚本,来达到实时更新页面的效果。
缺点
iframe 流的服务器开销很大,而且 IE、Chrome 等浏览器一直会处于 loading 状态,图标会不停旋转,非常不友好,不推荐。
SSE
定义
SSE(Server-Sent Events)服务器发送事件,一种服务器端到客户端(浏览器)的单向消息推送。
原理
SSE 基于 HTTP 协议。
在服务器和客户端之间打开一个单向通道,
服务端响应的不再是一次性的数据包而是 text/event-stream 类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
服务端响应的不再是一次性的数据包而是 text/event-stream 类型的数据流信息,在有数据变更时从服务器流式传输到客户端。
SSE 与 WebSocket
作用相似,都可以建立服务端与浏览器之间的通信,实现服务端向客户端推送消息。
SSE 是基于 HTTP 协议的,它们不需要特殊的协议或服务器实现即可工作;WebSocket 需单独服务器来处理协议。
SSE 单向通信,只能由服务端向客户端单向通信;WebSocket 全双工通信,即通信的双方可以同时发送和接受信息。
SSE 实现简单开发成本低,无需引入其他组件;WebSocket 传输数据需做二次解析,开发门槛高一些。
SSE 默认支持断线重连;WebSocket 则需要自己实现。
SSE 只能传送文本消息,二进制数据需要经过编码后传送;WebSocket 默认支持传送二进制数据。
兼容性
应用
ChatGPT
Websocket
定义
一种在 TCP 连接上进行全双工通信的协议,建立客户端和服务器之间的通信渠道。
浏览器和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
步骤
客户端向服务器发送一个 HTTP 请求,请求头中包含 Upgrade: websocket 和 Sec-WebSocket-Key 等字段,表示要求升级协议为 WebSocket。
服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,
响应头中包含 ,Connection: Upgrade和 Sec-WebSocket-Accept: xxx 等字段、表示成功升级到 WebSocket 协议。
响应头中包含 ,Connection: Upgrade和 Sec-WebSocket-Accept: xxx 等字段、表示成功升级到 WebSocket 协议。
客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。
数据以帧(frames)的形式进行传送,而不是传统的 HTTP 请求和响应。
WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。
发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。
数据以帧(frames)的形式进行传送,而不是传统的 HTTP 请求和响应。
WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。
发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。
客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。
注意
建立 WebSocket 连接之后,通过心跳机制来保持 WebSocket 连接的稳定性和活跃性。
MQTT
定义
MQTT(Message Queue Telemetry Transport)是一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,
通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。
通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。
该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,
因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似。
因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的 MQ 有点类似。
TCP 协议位于传输层,MQTT 协议位于应用层,MQTT 协议构建于 TCP/IP 协议上,也就是说只要支持 TCP/IP 协议栈的地方,都可以使用 MQTT 协议。
优势
首先 HTTP 协议它是一种同步协议,客户端请求后需要等待服务器的响应。
而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合 IOT 应用程序。
而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合 IOT 应用程序。
HTTP 是单向的,如果要获取消息客户端必须发起连接,
而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。
而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。
通常需要将一条命令或者消息,发送到网络上的所有设备上。HTTP 要实现这样的功能不但很困难,而且成本极高。
分布式
理论
CAP 理论
定义
CAP 理论/定理起源于 2000 年,由 Eric Brewer 教授在分布式计算原理研讨会(PODC)上提出,因此又被称作 布鲁尔定理(Brewer’s theorem)。
2 年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 发表了布鲁尔猜想的证明,CAP 理论正式成为分布式领域的定理。
CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。
组成
一致性(Consistency) : 所有节点访问同一份最新的数据副本。
可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。
网络分区
分布式系统中,多个节点之间的网络本来是连通的,但是因为某些故障某些节点之间不连通了,整个网络就分成了几块区域,这就叫 网络分区。
分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。
CP 架构
ZooKeeper、HBase
AP 架构
Cassandra、Eureka
CP + AP 架构
Nacos
总结
在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等。
如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。
BASE 理论
定义
BASE 理论起源于 2008 年, 由 eBay 的架构师 Dan Pritchett 在 ACM 上发表。
BASE 是 Basically Available(基本可用)、Soft-state(软状态) 和 Eventually Consistent(最终一致性) 三个短语的缩写。
BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的。
本质
即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。
对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。
三要素
基本可用
分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
允许损失部分可用性
响应时间上的损失:正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。
软状态
允许系统中的数据存在中间状态(CAP 理论中的数据不一致),
并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
最终一致性
系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。
本质
需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
分布式一致性级别
强一致性:系统写入了什么,读出来的就是什么。
弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
最终一致性:弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。(推荐)
实现方式
读时修复 : 在读取数据时,检测数据的不一致,进行修复。
比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
写时修复 : 在写入数据,检测数据的不一致时,进行修复。
比如 Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。(推荐)
比如 Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。(推荐)
异步修复 : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。
总结
ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。
算法
Paxos 算法
Paxos 算法是第一个被证明完备的分布式系统共识算法。
作用
让分布式系统中的多个节点之间对某个提案(Proposal)达成一致的看法。
组成
Basic Paxos 算法:描述的是多节点之间如何就某个值(提案 Value)达成共识。
Multi-Paxos 思想:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。Multi-Paxos 说白了就是执行多次 Basic Paxos ,核心还是 Basic Paxos 。
Raft 算法
比 Paxos 算法更易理解和实现的共识算法。
Raft 是 Multi-Paxos 的一个变种,其简化了 Multi-Paxos 的思想,变得更容易被理解以及工程实现。
恶意节点
没有
除了 Raft 算法之外,当前最常用的一些共识算法比如 ZAB 协议、 Fast Paxos 算法都是基于 Paxos 算法改进的。
有
一般使用的是 工作量证明(POW,Proof-of-Work)、 权益证明(PoS,Proof-of-Stake ) 等共识算法。
应用
区块链
Basic Paxos 算法
角色
提议者(Proposer):也可以叫做协调者(coordinator),
提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID) 和提议的值 (Value)。
提议者负责接受客户端的请求并发起提案。提案信息通常包括提案编号 (Proposal ID) 和提议的值 (Value)。
接受者(Acceptor):也可以叫做投票员(voter),负责对提议者的提案进行投票,同时需要记住自己的投票历史。
学习者(Learner):若有超过半数接受者就某个提议达成了共识,学习者就需要接受这个提议,并就该提议作出运算,然后将运算结果返回给客户端。
为了减少实现该算法所需的节点数,一个节点可以身兼多个角色。
一个提案被选定需要被半数以上的 Acceptor 接受。
Basic Paxos 算法具备容错性,在少于一半的节点出现故障时,集群仍能正常工作。
Multi Paxos 思想
Multi-Paxos 只是一种思想。
核心
通过多个 Basic Paxos 实例就一系列值达成共识。也就是说,Basic Paxos 是 Multi-Paxos 思想的核心,Multi-Paxos 就是多执行几次 Basic Paxos。
Raft 算法
背景
拜占庭将军
假设多位拜占庭将军中没有叛军,信使的信息可靠但有可能被暗杀的情况下,将军们如何达成是否要进攻的一致性决定?
解决
先在所有的将军中选出一个大将军,用来做出所有的决定。
共识算法
可容错系统中的一个基本问题:即使面对故障,服务器也可以在共享状态上达成一致。
允许一组节点像一个整体一样一起工作,即使其中的一些节点出现故障也能够继续工作下去,
其正确性主要是源于复制状态机的性质:一组Server的状态机计算相同状态的副本,即使有一部分的Server宕机了它们仍然能够继续运行。
其正确性主要是源于复制状态机的性质:一组Server的状态机计算相同状态的副本,即使有一部分的Server宕机了它们仍然能够继续运行。
一般通过使用复制日志来实现复制状态机。因此共识算法的工作就是保持复制日志的一致性。
特性
安全。确保在非拜占庭条件下的安全性,包括网络延迟、分区、包丢失、复制和重新排序。
高可用。只要大多数服务器都是可操作的,并且可以相互通信,也可以与客户端进行通信,那么这些服务器就可以看作完全功能可用的。
一致性不依赖时序。错误的时钟和极端的消息延迟,在最坏的情况下也只会造成可用性问题,而不会产生一致性问题。
在集群中大多数服务器响应,命令就可以完成,不会被少数运行缓慢的服务器来影响整体系统性能。
基础
节点类型
Leader:负责发起心跳,响应客户端,创建日志,同步日志。
Candidate:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。
Follower:接受 Leader 的心跳和日志同步数据,投票给 Candidate。
规则
在正常的情况下,只有一个服务器是 Leader,剩下的服务器是 Follower。
Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。
Follower 是被动的,它们不会发送任何请求,只是响应来自 Leader 和 Candidate 的请求。
任期
raft 算法将时间划分为任意长度的任期(term),任期用连续的数字表示,看作当前 term 号。
过程
每一个任期的开始都是一次选举,在选举开始时,一个或多个 Candidate 会尝试成为 Leader。
如果一个 Candidate 赢得了选举,它就会在该任期内担任 Leader。
如果没有选出 Leader,将会开启另一个任期,并立刻开始下一次选举。
raft 算法保证在给定的一个任期最少要有一个 Leader。
term 号
每个节点都会存储当前的 term 号,当服务器之间进行通信时会交换当前的 term 号。
过程
如果有服务器发现自己的 term 号比其他人小,那么他会更新到较大的 term 值。
如果一个 Candidate 或者 Leader 发现自己的 term 过期了,他会立即退回成 Follower。
如果一台服务器收到的请求的 term 号是过期的,那么它会拒绝此次请求。
日志
entry:每一个事件成为 entry,只有 Leader 可以创建 entry。entry 的内容为<term,index,cmd>其中 cmd 是可以应用到状态机的操作。
log:由 entry 构成的数组,每一个 entry 都有一个表明自己在 log 中的 index。
只有 Leader 才可以改变其他节点的 log。
entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。
Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。
只有 Leader 才可以改变其他节点的 log。
entry 总是先被 Leader 添加到自己的 log 数组中,然后再发起共识请求,获得同意后才会被 Leader 提交给状态机。
Follower 只能从 Leader 获取新日志和当前的 commitIndex,然后把对应的 entry 应用到自己的状态机中。
领导人选举
raft 使用心跳机制来触发 Leader 的选举。
如果一台服务器能够收到来自 Leader 或者 Candidate 的有效信息,那么它会一直保持为 Follower 状态,并且刷新自己的 electionElapsed,重新计时。
Leader 会向所有的 Follower 周期性发送心跳来保证自己的 Leader 地位。
如果一个 Follower 在一个周期内没有收到心跳信息,就叫选举超时,它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。
如果一个 Follower 在一个周期内没有收到心跳信息,就叫选举超时,它就会认为此时没有可用的 Leader,并且开始进行一次选举以选出一个新的 Leader。
为了开始新的选举,Follower 会自增自己的 term 号并且转换状态为 Candidate。
然后他会向所有节点发起 RequestVoteRPC 请求, Candidate 的状态会持续到以下情况发生:
赢得选举、其他节点赢得选举、一轮选举结束,无人胜出。
然后他会向所有节点发起 RequestVoteRPC 请求, Candidate 的状态会持续到以下情况发生:
赢得选举、其他节点赢得选举、一轮选举结束,无人胜出。
赢得选举的条件是:一个 Candidate 在一个任期内收到了来自集群内的多数选票(N/2+1),就可以成为 Leader。
在 Candidate 等待选票的时候,它可能收到其他节点声明自己是 Leader 的心跳,此时有两种情况:
该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。
该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。
该 Leader 的 term 号大于等于自己的 term 号,说明对方已经成为 Leader,则自己回退为 Follower。
该 Leader 的 term 号小于自己的 term 号,那么会拒绝该请求并让该节点更新 term。
由于可能同一时刻出现多个 Candidate,导致没有 Candidate 获得大多数选票,如果没有其他手段来重新分配选票的话,那么可能会无限重复下去。
raft 使用了随机的选举超时时间来避免上述情况。
每一个 Candidate 在发起选举后,都会随机化一个新的选举超时时间,
这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时,它会在其他服务器超时之前赢得选举。
每一个 Candidate 在发起选举后,都会随机化一个新的选举超时时间,
这种机制使得各个服务器能够分散开来,在大多数情况下只有一个服务器会率先超时,它会在其他服务器超时之前赢得选举。
日志复制
一旦选出了 Leader,它就开始接受客户端的请求。每一个客户端的请求都包含一条需要被复制状态机(Replicated State Machine)执行的命令。
Leader 收到客户端请求后,会生成一个 entry,包含<index,term,cmd>,
再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。
再将这个 entry 添加到自己的日志末尾后,向所有的节点广播该 entry,要求其他服务器复制这条 entry。
如果 Follower 接受该 entry,则会将 entry 添加到自己的日志后面,同时返回给 Leader 同意。
如果 Leader 收到多数的成功响应,Leader 会将这个 entry 应用到自己的状态机中,之后可以成为这个 entry 是 committed 的,并且向客户端返回执行结果。
raft 保证以下两个性质:
在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd。
在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同。
在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd。
在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同。
通过“仅有 Leader 可以生成 entry”来保证第一个性质,第二个性质需要一致性检查来进行保证。
一般情况下,Leader 和 Follower 的日志保持一致,然后,Leader 的崩溃会导致日志不一样,这样一致性检查会产生失败。
Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。
Leader 通过强制 Follower 复制自己的日志来处理日志的不一致。这就意味着,在 Follower 上的冲突日志会被领导者的日志覆盖。
为了使得 Follower 的日志和自己的日志一致,
Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。
Leader 需要找到 Follower 与它日志一致的地方,然后删除 Follower 在该位置之后的日志,接着把这之后的日志发送给 Follower。
Leader 给每一个Follower 维护了一个 nextIndex,它表示 Leader 将要发送给该追随者的下一条日志条目的索引。
当一个 Leader 开始掌权时,它会将 nextIndex 初始化为它的最新的日志条目索引数+1。
如果一个 Follower 的日志和 Leader 的不一致,AppendEntries 一致性检查会在下一次 AppendEntries RPC 时返回失败。
在失败之后,Leader 会将 nextIndex 递减然后重试 AppendEntries RPC。
最终 nextIndex 会达到一个 Leader 和 Follower 日志一致的地方。
这时,AppendEntries 会返回成功,Follower 中冲突的日志条目都被移除了,并且添加所缺少的上了 Leader 的日志条目。
一旦 AppendEntries 返回成功,Follower 和 Leader 的日志就一致了,这样的状态会保持到该任期结束。
当一个 Leader 开始掌权时,它会将 nextIndex 初始化为它的最新的日志条目索引数+1。
如果一个 Follower 的日志和 Leader 的不一致,AppendEntries 一致性检查会在下一次 AppendEntries RPC 时返回失败。
在失败之后,Leader 会将 nextIndex 递减然后重试 AppendEntries RPC。
最终 nextIndex 会达到一个 Leader 和 Follower 日志一致的地方。
这时,AppendEntries 会返回成功,Follower 中冲突的日志条目都被移除了,并且添加所缺少的上了 Leader 的日志条目。
一旦 AppendEntries 返回成功,Follower 和 Leader 的日志就一致了,这样的状态会保持到该任期结束。
安全性
选举限制
Leader 需要保证自己存储全部已经提交的日志条目。使日志条目只有一个流向:从 Leader 流向 Follower,Leader 永远不会覆盖已经存在的日志条目。
每个 Candidate 发送 RequestVoteRPC 时,都会带上最后一个 entry 的信息。
所有节点收到投票信息时,会对该 entry 进行比较,如果发现自己的更新,则拒绝投票给该 Candidate。
所有节点收到投票信息时,会对该 entry 进行比较,如果发现自己的更新,则拒绝投票给该 Candidate。
判断日志新旧的方式:如果两个日志的 term 不同,term 大的更新;如果 term 相同,更长的 index 更新。
节点崩溃
如果 Leader 崩溃,集群中的节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会触发新一轮的选主,在选主期间整个集群对外是不可用的。
如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVoteRPC 和 AppendEntriesRPC 会失败。
由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。
由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。
时间与可用性
raft 的要求之一就是安全性不依赖于时间:系统不能仅仅因为一些事件发生的比预想的快一些或者慢一些就产生错误。
时间条件
broadcastTime << electionTimeout << MTBF
broadcastTime:向其他节点并发发送消息的平均响应时间。
electionTimeout:选举超时时间。
MTBF(mean time between failures):单台机器的平均健康时间。
broadcastTime应该比electionTimeout小一个数量级,为的是使Leader能够持续发送心跳信息(heartbeat)来阻止Follower开始选举。
electionTimeout也要比MTBF小几个数量级,为的是使得系统稳定运行。
当Leader崩溃时,大约会在整个electionTimeout的时间内不可用;我们希望这种情况仅占全部时间的很小一部分。
当Leader崩溃时,大约会在整个electionTimeout的时间内不可用;我们希望这种情况仅占全部时间的很小一部分。
由于broadcastTime和MTBF是由系统决定的属性,因此需要决定electionTimeout的时间。
一般来说,broadcastTime 一般为 0.5~20ms,electionTimeout 可以设置为 10~500ms,MTBF 一般为一两个月。
协议
Gossip 协议
定义
在分布式系统中,不同的节点进行数据/信息共享是一个基本的需求。
共享方式
集中式发散消息
一个主节点同时共享最新信息给其他所有节点,比较适合中心化系统。(简单粗暴)
缺点
节点多的时候不光同步消息的效率低,还太依赖与中心节点,存在单点风险问题。
分散式发散消息
Gossip 协议。
Gossip 协议 也叫 Epidemic 协议(流行病协议)或者 Epidemic propagation 算法(疫情传播算法)。
Gossip 协议是一种允许在分布式系统中共享状态的去中心化通信协议,通过这种通信协议,我们可以将信息传播给网络或集群中的所有成员。
特点
随机传播特性 (联想一下病毒传播、癌细胞扩散等生活中常见的情景)。
应用
NoSQL 数据库 Redis 和 Apache Cassandra、服务网格解决方案 Consul 等。
Redis Cluster
一个典型的分布式系统,分布式系统中的各个节点需要互相通信。
其中各个节点基于 Gossip 协议 来进行通信共享信息,每个 Redis 节点都维护了一份集群的状态信息。
消息分类
MEET:在 Redis Cluster 中的某个 Redis 节点上执行 CLUSTER MEET ip port 命令,
可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。
可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。
PING/PONG:Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,
来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。
来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。
FAIL:Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以上的节点将 B 节点标记为 PFAIL,
节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。
节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。
有了 Redis Cluster 之后,不需要专门部署 Sentinel 集群服务了。
Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。
Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。
消息传播模式
反熵(Anti-entropy)
熵
熵的概念最早起源于物理学,用于度量一个热力学系统的混乱程度。
熵最好理解为不确定性的量度而不是确定性的量度,因为越随机的信源的熵越大。
反熵中的熵
理解为节点之间数据的混乱程度/差异性。
反熵就是指消除不同节点中数据的差异,提升节点间数据的相似度,从而降低熵值。
实现
集群中的节点,每隔段时间就随机选择某个其他节点,然后通过互相交换自己的所有数据来消除两者之间的差异,实现数据的最终一致性。
方式
推方式,就是将自己的所有副本数据,推给对方,修复对方副本中的熵。
拉方式,就是拉取对方的所有副本数据,修复自己副本中的熵。
推拉式,就是同时修复自己副本和对方副本中的熵。
实际应用场景中,一般不会采用随机的节点进行反熵,而是需要可以的设计一个闭环。
闭环优势
能够在一个确定的时间范围内实现各个节点数据的最终一致性,而不是基于随机的概率。
优缺点
反熵很简单实用,但节点过多或者节点动态变化的话,反熵就不太适用了。
传谣(Rumor-Mongering)
分布式系统中的一个节点一旦有了新数据之后,就会变为活跃节点,活跃节点会周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据。
应用
比较适合节点数量比较多或者节点动态变化的场景。
注意
要尽量避免传播的信息包不能太大,避免网络消耗太大。
优势
相比于其他分布式协议/算法来说,Gossip 协议理解起来非常简单。
能够容忍网络上节点的随意地增加或者减少,宕机或者重启,因为 Gossip 协议下这些节点都是平等的,去中心化的。
新增加或者重启的节点在理想情况下最终是一定会和其他节点的状态达到一致。
新增加或者重启的节点在理想情况下最终是一定会和其他节点的状态达到一致。
速度相对较快。节点数量比较多的情况下,扩散速度比一个主节点向其他节点传播信息要更快(多播)。
缺陷
消息需要通过多个传播的轮次才能传播到整个网络中,因此,必然会出现各节点状态不一致的情况。
毕竟,Gossip 协议强调的是最终一致,至于达到各个节点的状态一致需要多长时间,谁也无从得知。
毕竟,Gossip 协议强调的是最终一致,至于达到各个节点的状态一致需要多长时间,谁也无从得知。
由于拜占庭将军问题,不允许存在恶意节点。
可能会出现消息冗余的问题。由于消息传播的随机性,同一个节点可能会重复收到相同的消息。
API网关
API网关(API Gateway)是一种架构模式,它是将一些服务共有的功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。
本质
请求转发 + 请求过滤。
功能
请求转发:将请求转发到目标微服务。
负载均衡:根据各个微服务实例的负载情况或者具体的负载均衡策略配置对请求实现动态的负载均衡。
安全认证:对用户请求进行身份验证并仅允许可信客户端访问 API,并且还能够使用类似 RBAC 等方式来授权。
参数校验:支持参数映射与校验逻辑。
日志记录:记录所有请求的行为日志供后续使用。
监控告警:从业务指标、机器指标、JVM 指标等方面进行监控并提供配套的告警机制。
流量控制:对请求的流量进行控制,也就是限制某一时刻内的请求数。
熔断降级:实时监控请求的统计信息,达到配置的失败阈值后,自动熔断,返回默认值。
响应缓存:当用户请求获取的是一些静态的或更新不频繁的数据时,一段时间内多次请求获取到的数据很可能是一样的。
对于这种情况可以将响应缓存起来。这样用户请求可以直接在网关层得到响应数据,无需再去访问业务服务,减轻业务服务的负担。
对于这种情况可以将响应缓存起来。这样用户请求可以直接在网关层得到响应数据,无需再去访问业务服务,减轻业务服务的负担。
响应聚合:某些情况下用户请求要获取的响应内容可能会来自于多个业务服务。
网关作为业务服务的调用方,可以把多个服务的响应整合起来,再一并返回给用户。
网关作为业务服务的调用方,可以把多个服务的响应整合起来,再一并返回给用户。
灰度发布:将请求动态分流到不同的服务版本(最基本的一种灰度发布)。
异常处理:对于业务服务返回的异常响应,可以在网关层在返回给用户之前做转换处理。
这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。
这样可以把一些业务侧返回的异常细节隐藏,转换成用户友好的错误提示返回。
API 文档: 如果计划将 API 暴露给组织以外的开发人员,那么必须考虑使用 API 文档,例如 Swagger 或 OpenAPI。
协议转换:通过协议转换整合后台基于 REST、AMQP、Dubbo 等不同风格和实现技术的微服务,面向 Web Mobile、开放平台等特定客户端提供统一服务。
证书管理:将 SSL 证书部署到 API 网关,由一个统一的入口管理接口,降低了证书更换时的复杂度。
网关系统
Netflix Zuul
Zuul 是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务,基于 Java 技术栈开发,可以和 Eureka、Ribbon、Hystrix 等组件配合使用。
Zuul 主要通过过滤器(类似于 AOP)来过滤请求,从而实现网关必备的各种功能。
Zuul 1.x 基于同步 IO,性能较差。Zuul 2.x 基于 Netty 实现了异步 IO,性能得到了大幅改进。
Spring Cloud Gateway
SpringCloud Gateway 属于 Spring Cloud 生态系统中的网关,其诞生的目标是为了替代老牌网关 Zuul。
原理
为了提升网关的性能,SpringCloud Gateway 基于 Spring WebFlux 。
Spring WebFlux 使用 Reactor 库来实现响应式编程模型,底层基于 Netty 实现同步非阻塞的 I/O。
工作流程
路由判断:客户端的请求到达网关后,先经过 Gateway Handler Mapping 处理,
这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。
这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。
请求过滤:然后请求到达 Gateway Web Handler,这里面有很多过滤器,组成过滤器链(Filter Chain),
这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。
然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在...之前”。
这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。
然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在...之前”。
服务处理:后端服务会对请求进行处理。
响应过滤:后端处理完结果后,返回给 Gateway 的过滤器再次做处理,逻辑上可以称作 Post-Filters,Post 可以理解为“在...之后”。
响应返回:响应经过过滤处理后,返回给客户端。
总结
客户端的请求先通过匹配规则找到合适的路由,就能映射到具体的服务。
然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。
然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。
断言
一种编程术语,说白了它就是对一个表达式进行 if 判断,结果为真或假,如果为真则做这件事,否则做那件事。
在 Gateway 中,如果客户端发送的请求满足了断言的条件,则映射到指定的路由器,就能转发到指定的服务上进行处理。
常见的路由断言配置规则
路由和断言关系
一对多:一个路由规则可以包含多个断言。
同时满足:如果一个路由规则中有多个断言,则需要同时满足才能匹配。
第一个匹配成功:如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由。
过滤器分类
按请求和响应
Pre 类型:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
Post 类型:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。
按照作用范围
GatewayFilter:局部过滤器,应用在单个路由或一组路由上的过滤器。标红色表示比较常用的过滤器。
GlobalFilter:全局过滤器,应用在所有路由上的过滤器。
限流过滤器
对应的接口是 RateLimiter,RateLimiter 接口只有一个实现类 RedisRateLimiter (基于 Redis + Lua 实现的限流),提供的限流功能比较简易且不易使用。
自定义全局异常处理
SpringBoot 项目
只需要在项目中配置 @RestControllerAdvice和 @ExceptionHandler就可以了。
Spring Cloud Gateway
提供了多种全局处理的方式,比较常用的一种是实现 ErrorWebExceptionHandler 并重写其中的 handle 方法。
功能
不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,限流。
与 Zuul 2.x
差别不大,也是通过过滤器来处理请求。不过,目前更加推荐使用 Spring Cloud Gateway 而非 Zuul,Spring Cloud 生态对其支持更加友好。
优点
简单易用、成熟稳定、与 Spring Cloud 生态系统兼容、Spring 社区成熟等等。
缺点
一般还需要结合其他网关一起使用比如 OpenResty。并且,其性能相比较于 Kong 和 APISIX,还是差一些。
动态路由实现
基于 Nacos 注册中心。(推荐)
OpenResty
一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。
作用
用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
原理
基于 Nginx,主要还是看中了其优秀的高并发能力。
自定义
需要编写 C 语言的模块,并重新编译 Nginx。
在 Nginx 内部里嵌入 Lua 脚本,使得可以通过简单的 Lua 语言来扩展网关的功能,比如实现自定义的路由规则、过滤器、缓存策略等。
Lua
Lua 是一种非常快速的动态脚本语言,它的运行速度接近于 C 语言。
LuaJIT 是 Lua 的一个即时编译器,它可以显著提高 Lua 代码的执行效率。
LuaJIT 将一些常用的 Lua 函数和工具库预编译并缓存,这样在下次调用时就可以直接使用缓存的字节码,从而大大加快了执行速度。
Kong
一款基于 OpenResty (Nginx + Lua)的高性能、云原生、可扩展、生态丰富的网关系统。
组成
Kong Server:基于 Nginx 的服务器,用来接收 API 请求。
Apache Cassandra/PostgreSQL:用来存储操作数据。
Kong Dashboard:官方推荐 UI 管理工具,当然,也可以使用 RESTful 方式 管理 Admin api。
默认使用 Apache Cassandra/PostgreSQL 存储数据,Kong 的整个架构比较臃肿,并且会带来高可用的问题。
Kong 提供了插件机制来扩展其功能,插件在 API 请求响应循环的生命周期中被执行。
Kong 本身就是一个 Lua 应用程序,并且是在 Openresty 的基础之上做了一层封装的应用。
归根结底就是利用 Lua 嵌入 Nginx 的方式,赋予了 Nginx 可编程的能力,这样以插件的形式在 Nginx 这一层能够做到无限想象的事情。
归根结底就是利用 Lua 嵌入 Nginx 的方式,赋予了 Nginx 可编程的能力,这样以插件的形式在 Nginx 这一层能够做到无限想象的事情。
APISIX
定义
APISIX 是一款基于 OpenResty 和 etcd 的高性能、云原生、可扩展的网关系统。
etcd 是使用 Go 语言开发的一个开源的、高可用的分布式 key-value 存储系统,使用 Raft 协议做分布式共识。
与传统 API 网关相比,APISIX 具有动态路由和插件热加载,特别适合微服务系统下的 API 管理。
APISIX 与 SkyWalking(分布式链路追踪系统)、Zipkin(分布式链路追踪系统)、Prometheus(监控系统) 等 DevOps 生态工具对接都十分方便。
作为 Nginx 和 Kong 的替代项目,APISIX 目前已经是 Apache 顶级开源项目,并且是最快毕业的国产开源项目。
国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。
国内目前已经有很多知名企业(比如金山、有赞、爱奇艺、腾讯、贝壳)使用 APISIX 处理核心的业务流量。
APISIX 同样支持定制化的插件开发。开发者除了能够使用 Lua 语言开发插件,还能通过其它方式开发来避开 Lua 语言的学习成本。
避开 Lua
通过 Plugin Runner 来支持更多的主流编程语言(比如 Java、Python、Go 等等)。
通过这样的方式,可以让后端工程师通过本地 RPC 通信,使用熟悉的编程语言开发 APISIX 的插件。
这样做的好处是减少了开发成本,提高了开发效率,但是在性能上会有一些损失。
通过这样的方式,可以让后端工程师通过本地 RPC 通信,使用熟悉的编程语言开发 APISIX 的插件。
这样做的好处是减少了开发成本,提高了开发效率,但是在性能上会有一些损失。
使用 Wasm(WebAssembly) 开发插件。Wasm 被嵌入到了 APISIX 中,用户可以使用 Wasm 去编译成 Wasm 的字节码在 APISIX 中运行。
Wasm
基于堆栈的虚拟机的二进制指令格式,一种低级汇编语言,旨在非常接近已编译的机器代码,并且非常接近本机性能。
Wasm 最初是为浏览器构建的,但是随着技术的成熟,在服务器端看到了越来越多的用例。
Wasm 最初是为浏览器构建的,但是随着技术的成熟,在服务器端看到了越来越多的用例。
Shenyu
一款基于 WebFlux 的可扩展、高性能、响应式网关,Apache 顶级开源项目。
Shenyu 通过插件扩展功能,插件是 ShenYu 的灵魂,并且插件也是可扩展和热插拔的。
不同的插件实现不同的功能。Shenyu 自带了诸如限流、熔断、转发、重写、重定向、和路由监控等插件。
不同的插件实现不同的功能。Shenyu 自带了诸如限流、熔断、转发、重写、重定向、和路由监控等插件。
选择
最常用的是 Spring Cloud Gateway、Kong、APISIX 这三个。
对于公司业务以 Java 为主要开发语言的情况下,Spring Cloud Gateway 通常是个不错的选择。
Kong 和 APISIX
APISIX 基于 etcd 来做配置中心,不存在单点问题,云原生友好;
而 Kong 基于 Apache Cassandra/PostgreSQL ,存在单点风险,需要额外的基础设施保障做高可用。
而 Kong 基于 Apache Cassandra/PostgreSQL ,存在单点风险,需要额外的基础设施保障做高可用。
APISIX 支持热更新,并且实现了毫秒级别的热更新响应;而 Kong 不支持热更新。
APISIX 的性能要优于 Kong 。
APISIX 支持的插件更多,功能更丰富。
分布式ID
分布式 ID 是分布式系统下的 ID。
要求
全局唯一:ID 的全局唯一性肯定是首先要满足的!
高性能:分布式 ID 的生成速度要快,对本地资源消耗要小。
高可用:生成分布式 ID 的服务要保证可用性无限接近于 100%。
方便易用:拿来即用,使用方便,快速接入!
安全:ID 中不包含敏感信息。
有序递增:如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,还很有可能会直接通过 ID 来进行排序。
有具体的业务含义:生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
独立部署:也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。
不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。
不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。
方案
数据库
数据库主键自增
通过关系型数据库的自增主键产生来唯一的 ID。
优点
实现起来比较简单、ID 有序递增、存储消耗空间小
缺点
支持的并发量不大。
存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)。
没有具体业务含义。
安全问题(比如根据订单 ID 的递增规律就能推算出每天的订单量,商业机密啊! )。
每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)。
数据库号段模式
基于数据库的号段模式来生成分布式 ID。
优点
ID 有序递增、存储消耗空间小,数据库的访问次数更少,数据库压力更小。
缺点
和数据库主键自增方案的缺点类似。
NoSQL
一般情况下,NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 incr 命令即可实现对 id 原子顺序递增。
优点
性能不错并且生成的 ID 是有序递增的。
缺点
和数据库主键自增方案的缺点类似。
除了 Redis 之外,MongoDB ObjectId 经常也会被拿来当做分布式 ID 的解决方案。
算法
UUID
UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。
版本
版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成。
版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成。
版本 3、版本 5 : 版本 5 - 确定性 UUID 通过散列(hashing)名字空间(namespace)标识符和名称生成。
版本 4 : UUID 使用随机性或伪随机性生成。
优点
生成速度比较快、简单易用。
缺点
存储消耗空间大(32 个字符串,128 位)。
不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)。
无序(非自增)。
没有具体业务含义。
需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)。
Snowflake(雪花算法)
Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成。
组成
sign(1bit):符号位(标识正负),始终为 0,代表生成的 ID 为正数。
timestamp (41 bits):一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41 毫秒(约 69 年)。
datacenter id + worker id (10 bits):一般来说,前 5 位表示机房 ID,后 5 位表示机器 ID。这样就可以区分不同集群/机房的节点。
sequence (12 bits):一共 12 位,用来表示序列号。 序列号为自增值,代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096)。
优点
生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)。
缺点
需要解决重复 ID 问题(ID 生成依赖时间,在获取时间的时候,可能会出现时间回拨的问题)。
依赖机器 ID 对分布式环境不友好(当需要自动启停或增减机器时,固定的机器 ID 可能不够灵活)。
开源框架
UidGenerator
UidGenerator 是百度开源的一款基于 Snowflake(雪花算法)的唯一 ID 生成器。
组成
sign(1bit):符号位(标识正负),始终为 0,代表生成的 ID 为正数。
delta seconds (28 bits):当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约 8.7 年。
worker id (22 bits):机器 id,最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
sequence (13 bits):每秒下的并发序列,13 bits 可支持每秒 8192 个并发。
Leaf
Leaf 是美团开源的一个分布式 ID 解决方案 。
Leaf 提供了 号段模式 和 Snowflake(雪花算法) 这两种模式来生成分布式 ID。
支持双号段,还解决了雪花 ID 系统时钟回拨问题。不过,时钟问题的解决需要弱依赖于 Zookeeper。
性能
在 4C8G VM 基础上,通过公司 RPC 方式调用,QPS 压测结果近 5w/s,TP999 1ms。
Tinyid
Tinyid 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。
优化
双号段缓存:为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。
Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
增加多 db 支持:支持多个 DB,并且,每个 DB 都能生成唯一 ID,提高了可用性。
增加 tinyid-client:纯本地操作,无 HTTP 请求消耗,性能和可用性都有很大提升。
IdGenerator
和 UidGenerator、Leaf 一样,IdGenerator 也是一款基于 Snowflake(雪花算法)的唯一 ID 生成器。
特点
生成的唯一 ID 更短。
兼容所有雪花算法(号段模式或经典模式,大厂或小厂)。
原生支持 C#/Java/Go/C/Rust/Python/Node.js/PHP(C 扩展)/SQL等语言,并提供多线程安全调用动态库(FFI)。
解决了时间回拨问题,支持手工插入新 ID(当业务需要在历史时间生成新 ID 时,用本算法的预留位能生成 5000 个每秒)。
不依赖外部存储系统。
默认配置下,ID 可用 71000 年不重复。
分布式锁
当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
条件
基本
互斥:任意一个时刻,锁只能被一个线程持有。
高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。
可重入:一个节点获取了锁之后,还可以再次获取锁。
优秀
高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。
实现
基于关系型数据库比如 MySQL 实现分布式锁。
基于分布式协调服务 ZooKeeper 实现分布式锁。
基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
Redis
在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中的 setIfAbsent 方法)。
优点
实现方式比较简单,性能也很高效。
缺点
释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
锁释放/续期
过期时间
问题:过期时间具体数值较难界定。
Redisson
一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。
Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),
如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
可重入锁
在一个线程中可以多次获取同一把锁。Java 中的 synchronized 和 ReentrantLock 都属于可重入锁。
不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。
原理
线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。
推荐 Redisson ,其内置了多种类型的锁,
比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。
比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。
Redlock 算法
让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,
如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。
Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。
Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。(不推荐)
ZooKeeper
ZooKeeper 分布式锁是基于 临时顺序节点 和 Watcher(事件监听器) 实现的。
获取锁
首先我们要有一个持久节点/locks,客户端获取锁就是在locks下创建临时顺序节点。
假设客户端 1 创建了/locks/lock1节点,创建成功之后,会判断 lock1是否是 /locks 下最小的子节点。
如果 lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
如果获取锁失败,则说明有其他的客户端已经成功获取锁。
客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。
这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。
客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。
这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。
释放锁
成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
Curator
Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架。
相比于 ZooKeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。
实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。
主要实现了四种锁
InterProcessMutex:分布式可重入排它锁。
InterProcessSemaphoreMutex:分布式不可重入排它锁。
InterProcessReadWriteLock:分布式读写锁。
InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
原理
整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。
znode
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。
分类
持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
临时(EPHEMERAL)节点:临时节点的生命周期是与客户端会话(session)绑定的,会话消失则节点消失 。临时节点只能做叶子节点 ,不能创建子节点。
持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。
临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。
这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。
这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。
使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。
Watcher(事件监听器)
ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,
并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),
通知获取锁失败的客户端(唤醒等待的线程,Java 中的 wait/notifyAll ),让它尝试去获取锁,然后就成功获取锁了。
通知获取锁失败的客户端(唤醒等待的线程,Java 中的 wait/notifyAll ),让它尝试去获取锁,然后就成功获取锁了。
选择
如果对性能要求比较高的话,建议使用 Redis 实现分布式锁(优先选择 Redisson 提供的现成的分布式锁,而不是自己实现)。
如果对可靠性要求比较高的话,建议使用 ZooKeeper 实现分布式锁(推荐基于 Curator 框架实现)。但 ZooKeeper 使用率低,单因分布式锁,不可取。
分布式事务
定义
微服务架构下,系统被拆分为多个微服务,而每个微服务都可能有单独的机器或数据库,这时,⼀组操作可能会涉及到多个微服务以及多个数据库。
分布式事务的终极⽬标就是保证系统中多个相关联的数据库中的数据的⼀致性!
理论
CAP 理论和 BASE 理论
业界⽐较推崇是 最终⼀致性,但是某些对数据⼀致要求⼗分严格的场景⽐如银⾏转账还是要保证强⼀致性。
柔性事务
原理
BASE 理论 + 业务实践,追求最终⼀致性。
目标
根据⾃身业务特性,通过适当的⽅式来保证系统数据的最终⼀致性。 像 TCC、Saga、MQ 事务 、本地消息表 就属于柔性事务。
刚性事务
刚性事务追求的是强⼀致性。像2PC 、3PC 就属于刚性事务。
方案
2PC
定义
2PC(Two-Phase Commit)两阶段提交协议
2 -> 指代事务提交的 2 个阶段。
P-> Prepare (准备阶段)。
C ->Commit(提交阶段)。
阶段
准备阶段(Prepare)
准备阶段的核⼼是“询问”事务参与者执⾏本地数据库事务操作是否成功。
目的
测试 事务参与者 能否执⾏ 本地数据库事务 操作(!!!注意:这⼀步并不会提交事务)。
过程
事务协调者/管理者 向所有参与者发送消息询问:“你是否可以执⾏事务操作呢?”,并等待其答复。
事务参与者 接收到消息之后,开始执⾏本地数据库事务预操作⽐如写 redolog/undo log ⽇志。但是 ,此时并不会提交事务!
事务参与者 如果执⾏本地数据库事务操作成功,那就回复:“就绪”,否则就回复:“未就绪”。
提交阶段(Commit)
提交阶段的核⼼是“询问”事务参与者提交事务是否成功。
目的
事务协调者/管理者 会根据 准备阶段 中 事务参与者 的消息来决定是执⾏事务提交还是回滚操作。
就绪
事务协调者/管理者 向所有参与者发送消息:“你们可以提交事务啦!”(commit 消息)
事务参与者 接收到 commit 消息 后执⾏ 提交本地数据库事务 操作,执⾏完成之后 释放整个事务期间所占⽤的资源。
事务参与者 回复:“事务已经提交” (ack 消息)。
事务协调者/管理者 收到所有 事务参与者 的 ack 消息 之后,整个分布式事务过程正式结束。
未就绪
事务协调者/管理者 向所有参与者发送消息:“你们可以执⾏回滚操作了!”(rollback 消息)。
事务参与者 接收到 rollback 消息 后执⾏ 本地数据库事务回滚 执⾏完成之后 释放整个事务期间所占⽤的资源。
事务参与者 回复:“事务已经回滚” (ack 消息)。
事务协调者/管理者 收到所有 事务参与者 的 ack 消息 之后,取消事务。
提交阶段 之后⼀定会结束当前的分布式事务。
优点
实现起来⾮常简单,各⼤主流数据库⽐如 MySQL、Oracle 都有⾃⼰实现。
针对的是数据强⼀致性。不过,仍然可能存在数据不⼀致的情况。
缺点
同步阻塞 :事务参与者会在正式提交事务之前会⼀直占⽤相关的资源。
数据不⼀致 :由于⽹络问题或者事务协调者/管理者宕机都有可能会造成数据不⼀致的情况。
单点问题 : 事务协调者/管理者在其中也是⼀个很重要的⻆⾊,如果在准备(Prepare)阶段完成之后挂掉,事务参与者就会⼀直卡在提交(Commit)阶段。
3PC
3PC 是⼈们在 2PC 的基础上做了⼀些优化。把 2PC 中的 准备阶段(Prepare) 做了进⼀步细化。
细化内容
询问阶段(CanCommit) :这⼀步 不会执⾏事务操作,只会询问事务参与者能否执⾏本地数据库事操作。
准备阶段(PreCommit) :当所有事物参与者都返回“可执⾏”之后, 事务参与者才会执⾏本地数据库事务预操作⽐如写 redo log/undo log ⽇志。
除此之外,3PC 还引⼊了 超时机制 来避免事务参与者⼀直阻塞占⽤资源。
TCC
TCC 是 Try、Confirm、Cancel 三个词的缩写,属于⽬前⽐较⽕的⼀种柔性事务解决⽅案。
阶段
Try(尝试)阶段 : 尝试执⾏。完成业务检查,并预留好必需的业务资源。
Confirm(确认)阶段 :确认执⾏。
当所有事务参与者的 Try 阶段执⾏成功就会执⾏ Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执⾏ Cancel 。
当所有事务参与者的 Try 阶段执⾏成功就会执⾏ Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执⾏ Cancel 。
Cancel(取消)阶段 :取消执⾏,释放 Try 阶段预留的业务资源。
TCC 模式不需要依赖于底层数据资源的事务⽀持,但是需要我们⼿动实现更多的代码,属于 侵⼊业务代码 的⼀种分布式解决⽅案。
开源框架
ByteTCC : ByteTCC 是基于 Try-Confirm-Cancel(TCC)机制的分布式事务管理器的实现。
Seata : Seata 是⼀款开源的分布式事务解决⽅案,致⼒于在微服务架构下提供⾼性能和简单易⽤的分布式事务服务。
Hmily : ⾦融级分布式事务解决⽅案。
MQ 事务
RocketMQ 、 Kafka、Pulsar 、QMQ都提供了事务相关的功能。事务允许事件流应⽤将消费,处理,⽣产消息整个过程定义为⼀个原⼦操作。
过程
MQ 发送⽅(⽐如物流服务)在消息队列上开启⼀个事务,然后发送⼀个“半消息”给 MQ Server/Broker。
事务提交之前,半消息对于 MQ 订阅⽅/消费者(⽐如第三⽅通知服务)不可⻅。
事务提交之前,半消息对于 MQ 订阅⽅/消费者(⽐如第三⽅通知服务)不可⻅。
“半消息”发送成功的话,MQ 发送⽅就开始执⾏本地事务。
MQ 发送⽅的本地事务执⾏成功的话,“半消息”变成正常消息,可以正常被消费。MQ 发送⽅的本地事务执⾏失败的话,会直接回滚。
MQ 的事务消息使⽤的是两阶段提交(2PC),简单来说就是咱先发送半消息,等本地事务执⾏成功之后,半消息才变为正常消息。
RocketMQ 中的 Broker 会定期去 MQ 发送⽅上反查这个事务的本地事务的执⾏情况,并根据反查结果决定提交或者回滚这个事务。
消息消费失败的话,RocketMQ 会⾃动进⾏消费重试。
如果超过最⼤重试次数这个消息还是没有正确消费,RocketMQ 就会认为这个消息有问题,然后将其放到死信队列。
如果超过最⼤重试次数这个消息还是没有正确消费,RocketMQ 就会认为这个消息有问题,然后将其放到死信队列。
Saga
Saga 属于⻓事务解决⽅案,其核⼼思想是将⻓事务拆分为多个本地短事务(本地短事务序列)。
⻓事务 —> T1,T2 ~ Tn 个本地短事务。
每个短事务都有⼀个补偿动作 —> C1,C2 ~ Cn。
如果 T1,T2 ~ Tn 这些短事务都能顺利完成的话,整个事务也就顺利结束,否则,将采取恢复模式。
反向恢复
简介:如果 Ti 短事务提交失败,则补偿所有已完成的事务(⼀直执⾏ Ci 对 Ti 进⾏补偿)。
执⾏顺序:T1,T2,…,Ti(失败),Ci(补偿),…,C2,C1。
正向恢复
简介:如果 Ti 短事务提交失败,则⼀直对 Ti 进⾏重试,直⾄成功为⽌。
执⾏顺序:T1,T2,…,Ti(失败),Ti(重试)…,Ti+1,…,Tn。
与 TCC
TCC 类似,Saga 正向操作与补偿操作都需要业务开发者⾃⼰实现,因此也属于 侵⼊业务代码 的⼀种分布式解决⽅案。
和 TCC 很⼤的⼀点不同是 Saga 没有“Try” 动作,它的本地事务 Ti 直接被提交。因此,性能⾮常⾼!
缺点
因为 Saga 没有进⾏“Try” 动作预留资源,所以不能保证隔离性。
理论上,补偿操作⼀定能够执⾏成功。不过,当⽹络出现问题或者服务器宕机的话,补偿操作也会执⾏失败。这种情况下,往往需要我们进⾏⼈⼯⼲预。
开源框架
ServiceComb Pack :微服务应⽤的数据最终⼀致性解决⽅案。
Seata :Seata 是⼀款开源的分布式事务解决⽅案,致⼒于在微服务架构下提供⾼性能和简单易⽤的分布式事务服务。
分布式配置
配置中心
Spring Cloud Config
属于 Spring Cloud ⽣态组件,可以和 Spring Cloud 体系⽆缝整合。由于基于 Git 存储配置,因此 Spring Cloud Config 的整体设计很简单。
Nacos
Nacos 阿里开源,社区活跃,使⽤起来⽐较简单,并且还可以直接⽤来做服务发现及管理。
Apollo
Apollo 携程开源,社区活跃,只能⽤来做配置管理,使⽤相对复杂⼀些。
K8s ConfigMap
技术选型是 Kubernetes 的话,可以考虑使⽤ K8s ConfigMap 来作为配置中⼼。
Disconf 和 Qconf
已经没有维护,⽣态也并不活跃,并不建议使⽤,在做配置中⼼技术选型的时候可以跳过。
功能需求
权限控制 :配置的修改、发布等操作需要严格的权限控制。
⽇志记录 : 配置的修改、发布等操需要记录完整的⽇志,便于后期排查问题。
配置推送 : 推送模式通常为推、拉、推拉结合。
灰度发布 :⽀持配置只推给部分应⽤。
易操作 : 提供 Web 界⾯⽅便配置修改和发布。
版本跟踪 :所有的配置发布都有版本概念,从⽽可以⽅便的⽀持配置的回滚。
⽀持配置回滚 : 我们⼀键回滚配置到指定的位置,这个需要和版本跟踪结合使⽤。
RPC
定义
RPC(Remote Procedure Call) 即远程过程调用,通过名字我们就能看出 RPC 关注的是远程调用而非本地调用。
通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。
组成
客户端(服务消费端):调用远程方法的一端。
客户端 Stub(桩):这其实就是一代理类。代理类主要做的事情很简单,就是把你调用方法、类、方法参数等信息传递到服务端。
网络传输:网络传输就是要把调用的方法的信息传输到服务端,然后服务端执行完之后再把返回结果通过网络传输传输回来。
网络传输的实现方式有很多种,比如最近基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。
网络传输的实现方式有很多种,比如最近基本的 Socket 或者性能以及封装更加优秀的 Netty(推荐)。
服务端 Stub(桩):这个桩就不是代理类了。
这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去执行对应的方法然后返回结果给客户端的类。
这里的服务端 Stub 实际指的就是接收到客户端执行方法的请求后,去执行对应的方法然后返回结果给客户端的类。
服务端(服务提供端):提供远程方法的一端。
过程
服务消费端(client)以本地调用的方式调用远程服务。
客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):RpcRequest。
客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端。
服务端 Stub(桩)收到消息将消息反序列化为 Java 对象: RpcRequest。
服务端 Stub(桩)根据RpcRequest中的类、方法、方法参数等信息调用本地的方法。
服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:RpcResponse(序列化)发送至消费方。
客户端 Stub(client stub)接收到消息并将消息反序列化为 Java 对象:RpcResponse ,这样也就得到了最终结果。
框架
Dubbo
定义
Apache Dubbo 是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案,涵盖 Java、Golang 等多种语言。
Dubbo 提供了从服务定义、服务发现、服务通信到流量管控等几乎所有的服务治理能力,
支持 Triple 协议(基于 HTTP/2 之上定义的下一代 RPC 通信协议)、应用级服务发现、Dubbo Mesh (Dubbo3 赋予了很多云原生友好的新特性)等特性。
支持 Triple 协议(基于 HTTP/2 之上定义的下一代 RPC 通信协议)、应用级服务发现、Dubbo Mesh (Dubbo3 赋予了很多云原生友好的新特性)等特性。
Dubbo 是由阿里开源,后来加入了 Apache 。正是由于 Dubbo 的出现,才使得越来越多的公司开始使用以及接受分布式架构。
Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。
但由于 Spring Cloud 在微服务中应用更加广泛,所以,一般提 Dubbo,大部分是分布式系统的情况。
但由于 Spring Cloud 在微服务中应用更加广泛,所以,一般提 Dubbo,大部分是分布式系统的情况。
核心能力
面向接口代理的高性能 RPC 调用。
智能容错和负载均衡。
服务自动注册和发现。
高度可扩展能力。
运行期流量调度。
可视化的服务治理与运维。
总结
Dubbo 不光可以调用远程服务,还提供了一些其他开箱即用的功能,比如智能负载均衡。
分布式
把整个系统拆分成不同的服务然后将这些服务放在不同的服务器上减轻单体服务的压力提高并发量和性能。
核心角色
Container: 服务运行容器,负责加载、运行服务提供者。必须。
Provider: 暴露服务的服务提供方,会向注册中心注册自己提供的服务。必须。
Consumer: 调用远程服务的服务消费方,会向注册中心订阅自己所需的服务。必须。
Registry: 服务注册与发现的注册中心。注册中心会返回服务提供者地址列表给消费者。非必须。
Monitor: 统计服务的调用次数和调用时间的监控中心。服务消费者和提供者会定时发送统计数据到监控中心。 非必须。
Invoker
Invoker 是 Dubbo 领域模型中非常重要的一个概念,就是 Dubbo 对远程调用的抽象。
分类
服务提供 Invoker
服务消费 Invoker
整体设计分层
config 配置层:Dubbo 相关的配置。支持代码配置,同时也支持基于 Spring 来做配置,以 ServiceConfig, ReferenceConfig 为中心。
proxy 服务代理层:调用远程方法像调用本地的方法一样简单的一个关键,真实调用过程依赖代理类,以 ServiceProxy 为中心。
registry 注册中心层:封装服务地址的注册与发现。
cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心。
monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心。
protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心。
exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心。
transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心。
serialize 数据序列化层:对需要在网络传输的数据进行序列化。
SPI
SPI(Service Provider Interface) 机制被大量用在开源项目中,它可以帮助我们动态寻找服务/功能(比如负载均衡策略)的实现。
原理
将接口的实现类放在配置文件中,在程序运行过程中读取配置文件,通过反射加载实现类。这样可以在运行时,动态替换接口的实现类。
Java 本身就提供了 SPI 机制的实现。不过,Dubbo 没有直接用,而是对 Java 原生的 SPI 机制进行了增强,以便更好满足自己的需求。
自定义负载均衡策略
创建对应的实现类 XxxLoadBalance 实现 LoadBalance 接口或者 AbstractLoadBalance 类。
将这个实现类的路径写入到 resources 目录下的 META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance 文件中即可。
微内核架构
定义
微内核架构模式(有时被称为插件架构模式)是实现基于产品应用程序的一种自然模式。
可让用户添加额外的应用如插件,到核心应用,继而提供了可扩展性和功能分离的用法。
可让用户添加额外的应用如插件,到核心应用,继而提供了可扩展性和功能分离的用法。
微内核架构包含两类组件:核心系统(core system) 和 插件模块(plug-in modules)。
核心系统提供系统所需核心能力,插件模块可以扩展系统的功能。因此, 基于微内核架构的系统,非常易于扩展功能。
我们常见的一些 IDE,都可以看作是基于微内核架构设计的。绝大多数 IDE 比如 IDEA、VSCode 都提供了插件来丰富自己的功能。
Dubbo 采用 微内核(Microkernel) + 插件(Plugin) 模式,简单来说就是微内核架构。微内核只负责组装插件。
通常情况下,微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。Dubbo 不想依赖 Spring 等 IoC 容器,
也不想自己造一个小的 IoC 容器(过度设计),因此采用了一种最简单的 Factory 方式管理插件:JDK 标准的 SPI 扩展机制 (java.util.ServiceLoader)。
也不想自己造一个小的 IoC 容器(过度设计),因此采用了一种最简单的 Factory 方式管理插件:JDK 标准的 SPI 扩展机制 (java.util.ServiceLoader)。
负载均衡策略
负载均衡
旨在优化资源使用,最大化吞吐量,最小化响应时间,并避免任何单个资源的过载。
策略
在集群负载均衡时,Dubbo 提供了多种均衡策略,默认为 random 随机调用。
在 Dubbo 中,所有负载均衡实现类均继承自 AbstractLoadBalance,该类实现了 LoadBalance 接口,并封装了一些公共的逻辑。
策略
RandomLoadBalance
根据权重随机选择(对加权随机算法的实现)。这是 Dubbo 默认采用的一种负载均衡策略。
原理
假如有两个提供相同服务的服务器 S1 和 S2,S1 权重为 7,S2 权重为 3。把这些权重值分布在坐标区间会得到:S1->[0, 7) ,S2->[7, 10)。
生成[0, 10) 之间的随机数,随机数落到对应的区间,我们就选择对应的服务器来处理请求。
生成[0, 10) 之间的随机数,随机数落到对应的区间,我们就选择对应的服务器来处理请求。
LeastActiveLoadBalance
最小活跃数负载均衡
原理
Dubbo 就认为谁的活跃数越少,谁的处理速度就越快,性能也越好,这样就优先把请求给活跃数少的服务提供者处理。
如果有多个服务提供者的活跃数相等,那就再走一遍 RandomLoadBalance 。
ConsistentHashLoadBalance
一致性 Hash 负载均衡策略
原理
没有权重的概念,具体是哪个服务提供者处理请求是由你的请求的参数决定的,也就是说相同参数的请求总是发到同一个服务提供者。
为避免数据倾斜问题(节点不够分散,大量请求落到同一节点),还引入虚拟节点的概念。通过虚拟节点可以让节点更加分散,有效均衡各个节点的请求量。
RoundRobinLoadBalance
加权轮询负载均衡
原理
轮询就是把请求依次分配给每个服务提供者。加权轮询就是在轮询的基础上,让更多的请求落到权重更大的服务提供者上。
序列化协议
Dubbo 支持多种序列化方式:JDK 自带的序列化、hessian2(默认)、JSON、Kryo、FST、Protostuff,ProtoBuf 等等。
Motan
Motan 是新浪微博开源的一款 RPC 框架,据说在新浪微博正支撑着千亿次调用。不过很少看到有公司使用,而且网上的资料也比较少。
Motan 更像是一个精简版的 Dubbo,可能是借鉴了 Dubbo 的思想,Motan 的设计更加精简,功能更加纯粹。
gRPC
gRPC 是 Google 开源的一个高性能、通用的开源 RPC 框架。
面向移动应用开发并基于 HTTP/2 协议标准而设计(支持双向流、消息头压缩等功能,更加节省带宽),基于 ProtoBuf 序列化协议,支持众多开发语言。
ProtoBuf( Protocol Buffer)
一种更加灵活、高效的数据格式,可用于通讯协议、数据存储等领域,基本支持所有主流编程语言且与平台无关。
不过,通过 ProtoBuf 定义接口和数据类型还挺繁琐的,这是一个小问题。
gRPC 的通信层的设计还是非常优秀的,Dubbo-go 3.0 的通信层改进主要借鉴了 gRPC。
gRPC 的设计导致其几乎没有服务治理能力。如果要解决这个问题,就需要依赖其他组件,比如腾讯的 PolarisMesh(北极星)。
Thrift
Apache Thrift 是 Facebook 开源的跨语言的 RPC 通信框架,目前已经捐献给 Apache 基金会管理。
由于其跨语言特性和出色的性能,在很多互联网公司应用,有能力的公司甚至会基于 thrift 研发一套分布式服务框架,增加诸如服务注册、服务发现等功能。
Thrift 支持多种不同的编程语言,包括C++、Java、Python、PHP、Ruby等(相比于 gRPC 支持的语言更多 )。
ZooKeeper
定义
ZooKeeper 是一个开源的分布式协调服务。
设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
原语
操作系统或计算机网络用语范畴。由若干条指令组成,用于完成一定功能的一个过程。
具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。
具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。
ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案。
ZooKeeper 将数据保存在内存中,性能是不错的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。
功能
数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
这些功能的实现主要依赖于 ZooKeeper 提供的 数据存储+事件监听 功能。
特点
顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的。
单一系统映像: 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
实时性: 每个客户端的系统视图都是最新的。
重要概念
Data model(数据模型)
采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二进制序列。
每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。每个 znode 都有一个唯一的路径标识。
注意
ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,每个节点的数据大小上限是 1M 。
znode(数据节点)
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。每个 znode 都有一个唯一的路径标识。
分类
持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
临时(EPHEMERAL)节点:临时节点的生命周期是与客户端会话(session)绑定的,会话消失则节点消失 。临时节点只能做叶子节点 ,不能创建子节点。
持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。
临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
组成
stat:状态信息。
data:节点存放的数据的具体内容。(包括版本信息)
版本(version)
dataVersion:当前 znode 节点的版本号
cversion:当前 znode 子节点的版本
aclVersion:当前 znode 的 ACL 的版本。
ACL(权限控制)
ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。
znode 操作权限
CREATE : 能创建子节点。
READ:能获取节点数据和列出其子节点。
WRITE : 能设置/更新节点数据。
DELETE : 能删除子节点。
ADMIN : 能设置节点 ACL 的权限。
注意
CREATE 和 DELETE 这两种权限都是针对 子节点 的权限控制。
身份认证
world:默认方式,所有用户都可无条件访问。
auth:不使用任何 id,代表任何已认证的用户。
digest:用户名/密码认证方式:username:password 。
ip : 对指定 ip 进行限制。
Watcher(事件监听器)
Watcher(事件监听器)是 ZooKeeper 中的一个很重要的特性。
ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,
ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,
ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
会话(Session)
Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接。
通过连接,客户端能够通过心跳检测与服务器保持有效的会话,也能向服务器发送请求并接受响应,还能通过该连接接收来自服务器的 Watcher 事件通知。
通过连接,客户端能够通过心跳检测与服务器保持有效的会话,也能向服务器发送请求并接受响应,还能通过该连接接收来自服务器的 Watcher 事件通知。
在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID。
由于 sessionID是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,
因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。
由于 sessionID是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,
因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。
集群
定义
为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么仍然是可用的。
集群间通过 ZAB 协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。
最典型集群模式:Master/Slave 模式(主备模式)。
通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
集群角色
在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。
Leader
为客户端提供读和写的服务,负责投票的发起和决议,更新系统状态。
Follower
为客户端提供读服务,如果是写服务则转发给 Leader。参与选举过程中的投票。
Observer
为客户端提供读服务,如果是写服务则转发给 Leader。
不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于 ZooKeeper3.3 系列新增的角色。
不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于 ZooKeeper3.3 系列新增的角色。
选举过程
触发
当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入 Leader 选举过程,这个过程会选举产生新的 Leader 服务器。
Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。
Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。
Synchronization(同步阶段):主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后准 leader 才会成为真正的 leader。
Broadcast(广播阶段):这个阶段才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。
服务器状态
LOOKING:寻找 Leader。
LEADING:Leader 状态,对应的节点为 Leader。
FOLLOWING:Follower 状态,对应的节点为 Follower。
OBSERVING:Observer 状态,对应节点为 Observer,该节点不参与 Leader 选举。
奇数台
ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用。
假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。
脑裂
保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群,这时候子集群各自选主导致“脑裂”的情况。
防止
ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂。
ZAB协议
定义
Paxos 算法是 ZooKeeper 的灵魂。但 ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。
ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。
ZooKeeper 主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
模式
崩溃恢复:当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,
ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。
当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。
其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。
ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。
当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。
其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。
消息广播:当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。
当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,
那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。
当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,
那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。
应用
Kafka : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。
不过,在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构。
不过,在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构。
Hbase : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。
Hadoop : ZooKeeper 为 Namenode 提供高可用支持。
命名服务:可以通过 ZooKeeper 的顺序节点生成全局唯一 ID。
数据发布/订阅:通过 Watcher 机制 可以很方便地实现数据发布/订阅。
当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。
当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。
分布式锁:通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。分布式锁的实现也需要用到 Watcher 机制。
注意
这些功能的实现基本都得益于 ZooKeeper 可以保存数据的功能,但是 ZooKeeper 不适合保存大量数据,这一点需要注意。
高性能
CDN
定义
CDN 全称是 Content Delivery Network/Content Distribution Network,翻译过的意思是 内容分发网络 。
内容:指的是静态资源比如图片、视频、文档、JS、CSS、HTML。
分发网络:指的是将这些静态资源分发到位于多个不同的地理位置机房中的服务器上,这样,就可以实现静态资源的就近访问。
CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。
全站加速(腾讯云 ECDN、阿里云 DCDN)既可以加速静态资源又可以加速动态资源,内容分发网络(CDN)主要针对的是 静态资源 。
原理
静态资源缓存到 CDN
预热
将源站的资源同步到 CDN 的节点中。
减少了对源站的访问压力,提高了访问速度。
回源
访问的资源可能不在 CDN 节点中,这时 CDN 节点将请求源站获取资源。
此时比未接入 CDN 访问慢。
刷新
如果资源有更新,删除 CDN 节点上缓存的旧资源,并强制 CDN 节点回源站获取最新资源。
指标
命中率 和 回源率 是衡量 CDN 服务质量两个重要指标。命中率越高越好,回源率越低越好。
最合适的 CDN 节点
GSLB (Global Server Load Balance,全局负载均衡)是 CDN 的大脑,负责多个 CDN 节点之间相互协作,最常用的是基于 DNS 的 GSLB。
CDN 会通过 GSLB 找到最合适的 CDN 节点。
寻找过程
浏览器向 DNS 服务器发送域名请求。
DNS 服务器向根据 CNAME( Canonical Name ) 别名记录向 GSLB 发送请求。
GSLB 返回性能最好(通常距离请求地址最近)的 CDN 节点(边缘服务器,真正缓存内容的地方)的地址给浏览器。
浏览器直接访问指定的 CDN 节点。
寻找标准
GSLB 会根据请求的 IP 地址、CDN 节点状态(比如负载情况、性能、响应时间、带宽)等指标来综合判断具体返回哪一个 CDN 节点的地址。
防止资源被盗刷
Referer 防盗链
根据 HTTP 请求的头信息里面的 Referer 字段对请求进行限制。
时间戳防盗链
时间戳防盗链的 URL 通常会有两个参数一个是签名字符串,一个是过期时间。
签名字符串一般是通过对用户设定的加密字符串、请求路径、过期时间通过 MD5 哈希算法取哈希的方式获得。
签名字符串一般是通过对用户设定的加密字符串、请求路径、过期时间通过 MD5 哈希算法取哈希的方式获得。
时间戳防盗链的实现也比较简单,并且可靠性较高,推荐使用。并且,绝大部分 CDN 服务提供商都提供了开箱即用的时间戳防盗链机制。
还可以 Referer 防盗链 + 时间戳防盗链 组合使用,同时还可以 IP 黑白名单配置、IP 访问限频配置等机制来防盗刷。
选择
基于成本、稳定性和易用性考虑,建议选择专业的云厂商(如阿里云、腾讯云、华为云)或者 CDN 厂商(比如网宿、蓝汛)提供的开箱即用的 CDN 服务。
负载均衡
定义
将用户请求分摊到不同的服务器上处理,以提高系统整体的并发处理能力以及可靠性。
负载均衡是一种比较常用且实施起来较为简单的提高系统并发能力和可靠性的手段,不论是单体架构的系统还是微服务架构的系统几乎都会用到。
分类
服务端负载均衡
应用
主要应用在 系统外部请求 和 网关层 之间,可以使用 软件 或者 硬件 实现。
硬件负载均衡
通过专门的硬件设备(比如 F5、A10、Array )实现负载均衡功能。
优缺点
优势是性能很强且稳定,缺点就是实在是太贵了。像基础款的 F5 最低也要 20 多万。。。
软件负载均衡
通过软件(比如 LVS、Nginx、HAproxy )实现负载均衡功能。
优缺点
性能虽然差一些,但价格便宜。像基础款的 Linux 服务器也就几千,性能好一点的 2~3 万的就很不错了。
根据 OSI 模型划分
二层负载均衡
非重点
三层负载均衡
非重点
四层负载均衡
工作在 OSI 模型第四层,也就是传输层,这一层的主要协议是 TCP/UDP。
负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址,会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。
也就是说,四层负载均衡的核心就是 IP+端口层面的负载均衡,不涉及具体的报文内容。
负载均衡器在这一层能够看到数据包里的源端口地址以及目的端口地址,会基于这些信息通过一定的负载均衡算法将数据包转发到后端真实服务器。
也就是说,四层负载均衡的核心就是 IP+端口层面的负载均衡,不涉及具体的报文内容。
七层负载均衡
工作在 OSI 模型第七层,也就是应用层,这一层的主要协议是 HTTP 。
这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂,它会读取报文的数据部分(比如说我们的 HTTP 部分的报文),
然后根据读取到的数据内容(如 URL、Cookie)做出负载均衡决策。
也就是说,七层负载均衡器的核心是报文内容(如 URL、Cookie)层面的负载均衡,执行第七层负载均衡的设备通常被称为 反向代理服务器 。
这一层的负载均衡比四层负载均衡路由网络请求的方式更加复杂,它会读取报文的数据部分(比如说我们的 HTTP 部分的报文),
然后根据读取到的数据内容(如 URL、Cookie)做出负载均衡决策。
也就是说,七层负载均衡器的核心是报文内容(如 URL、Cookie)层面的负载均衡,执行第七层负载均衡的设备通常被称为 反向代理服务器 。
总结
四层负载均衡性能很强,七层负载均衡功能更强。但对于绝大部分业务场景来说,两者差异基本可以忽略不计。
实际工作中,通常会使用 Nginx 来做七层负载均衡,LVS(Linux Virtual Server 虚拟服务器, Linux 内核的 4 层负载均衡)来做四层负载均衡。
不过,LVS 这个绝大部分公司真用不上,像阿里、百度、腾讯、eBay 等大厂才会使用到,用的最多的还是 Nginx。
客户端负载均衡
应用
主要应用于系统内部的不同的服务之间,可以使用现成的负载均衡组件来实现。
原理
在客户端负载均衡中,客户端会自己维护一份服务器的地址列表,发送请求之前,客户端会根据对应的负载均衡算法来选择具体某一台服务器处理请求。
框架
Java 领域主流的微服务框架 Dubbo、Spring Cloud 等都内置了开箱即用的客户端负载均衡实现。
Dubbo 属于是默认自带了负载均衡功能。
Spring Cloud 是通过组件的形式实现的负载均衡,属于可选项。
比较常用的是 Spring Cloud Load Balancer(官方,推荐) 和 Ribbon(Netflix,已被启用)。
算法
随机法
最简单粗暴的负载均衡算法。
权重
未加权重
所有的服务器被访问到的概率都是相同的。
适合于服务器性能相近的集群,其中每个服务器承载相同的负载。
加权重
权重越高的服务器被访问的概率就越大。
适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
缺陷
部分机器在一段时间之内无法被随机到,毕竟是概率算法,就算是大家权重一样, 也可能会出现这种情况。
轮询法
挨个轮询服务器处理,也可以设置权重。
权重
未加权重
每个请求按时间顺序逐一分配到不同的服务器处理。
适合于服务器性能相近的集群,其中每个服务器承载相同的负载。
加权重
权重越高的服务器被访问的次数就越多。
适合于服务器性能不等的集群,权重的存在可以使请求分配更加合理化。
改进
在加权轮询的基础上,还有进一步改进得到的负载均衡算法,比如平滑的加权轮训算法。
两次随机法
在随机法的基础上多增加了一次随机,多选出一个服务器。随后再根据两台服务器的负载等情况,从其中选择出一个最合适的服务器。
优点
两次随机法的好处是可以动态地调节后端节点的负载,使其更加均衡。
哈希法
将请求的参数信息通过哈希函数转换成一个哈希值,然后根据哈希值来决定请求被哪一台服务器处理。
在服务器数量不变的情况下,相同参数的请求总是发到同一台服务器处理,比如同个 IP 的请求、同一个用户的请求。
一致性 Hash 法
和哈希法类似,一致性 Hash 法也可以让相同参数的请求总是发到同一台服务器处理。不过,它解决了哈希法存在的一些问题。
常规哈希法在服务器数量变化时,哈希值会重新落在不同的服务器上,这明显违背了使用哈希法的本意。
核心
将数据和节点都映射到一个哈希环上,然后根据哈希值的顺序来确定数据属于哪个节点。
当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。
当服务器增加或删除时,只影响该服务器的哈希,而不会导致整个服务集群的哈希键值重新分布。
最小连接法
当有新的请求出现时,遍历服务器节点列表并选取其中连接数最小的一台服务器来响应当前请求。相同连接的情况下,可以进行加权随机。
最少连接数基于一个服务器连接数越多,负载就越高这一理想假设。
然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。
然而, 实际情况是连接数并不能代表服务器的实际负载,有些连接耗费系统资源更多,有些连接不怎么耗费系统资源。
最少活跃法
最少活跃法和最小连接法类似,但要更科学一些。
以活动连接数为标准,活动连接数可以理解为当前正在处理的请求数。
活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。
活跃数越低,说明处理能力越强,这样就可以使处理能力强的服务器处理更多请求。相同活跃数的情况下,可以进行加权随机。
最快响应时间法
不同于最小连接法和最少活跃法,最快响应时间法以响应时间为标准来选择具体是哪一台服务器处理。
客户端会维持每个服务器的响应时间,每次请求挑选响应时间最短的。相同响应时间的情况下,可以进行加权随机。
这种算法可以使得请求被更快处理,但可能会造成流量过于集中于高性能服务器的问题。
七层负载均衡实现
DNS 解析
DNS 解析是比较早期的七层负载均衡实现方式,非常简单。
原理
在 DNS 服务器中为同一个主机记录配置多个 IP 地址,这些 IP 地址对应不同的服务器。
当用户请求域名的时候,DNS 服务器采用轮询算法返回 IP 地址,这样就实现了轮询版负载均衡。
当用户请求域名的时候,DNS 服务器采用轮询算法返回 IP 地址,这样就实现了轮询版负载均衡。
反向代理
客户端将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器,获取数据后再返回给客户端。
对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。
反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。
对外暴露的是反向代理服务器地址,隐藏了真实服务器 IP 地址。
反向代理“代理”的是目标服务器,这一个过程对于客户端而言是透明的。
Nginx 就是最常用的反向代理服务器,它可以将接收到的客户端请求以一定的规则(负载均衡策略)均匀地分配到这个服务器集群中所有的服务器上。
HTTP 重定向
不常用。
客户端负载均衡实现
客户端负载均衡可以使用现成的负载均衡组件来实现。
Netflix Ribbon
老牌负载均衡组件,由 Netflix 开发,功能比较全面,支持的负载均衡策略也比较多。
策略
RandomRule:随机策略。
RoundRobinRule(默认):轮询策略。
WeightedResponseTimeRule:权重(根据响应时间决定权重)策略。
BestAvailableRule:最小连接数策略。
RetryRule:重试策略(按照轮询策略来获取服务,
如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,
如果超过指定时间依然没获取到服务实例则返回 null)
如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试来获取服务,
如果超过指定时间依然没获取到服务实例则返回 null)
AvailabilityFilteringRule:可用敏感性策略(先过滤掉非健康的服务实例,然后再选择连接数较小的服务实例)。
ZoneAvoidanceRule:区域敏感性策略(根据服务所在区域的性能和服务的可用性来选择服务实例)。
Spring Cloud Load Balancer
Spring 官方为了取代 Ribbon 而推出的,功能相对更简单一些,支持的负载均衡也少一些。
策略
RandomLoadBalancer:随机策略。
RoundRobinLoadBalancer(默认):轮询策略。
ServiceInstanceListSupplier 的实现类同样可以让其支持类似于 Ribbon 的负载均衡策略。
建议
如非必需 Ribbon 某个特有的功能或者负载均衡策略的话,就优先选择 Spring 官方提供的 Spring Cloud Load Balancer。
不推荐 Ribbon 原因
Spring Cloud 2020.0.0 版本移除了 Netflix 除 Eureka 外的所有组件。
Netflix 公司开源的 Feign、Ribbon、Zuul、Hystrix、Eureka 等知名的微服务系统构建所必须的组件,Netflix 公司引领了 Java 技术栈下的微服务发展。
在 2018 年,Netflix 宣布其开源的核心组件 Hystrix、Ribbon、Zuul、Eureka 等进入维护状态,不再进行新特性开发,只修 BUG。
数据库优化
读写分离
为了将对数据库的读写操作分散到不同的数据库节点上。 这样就能够小幅提升写性能,大幅提升读性能。
步骤
部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制。
系统将写请求交给主数据库处理,读请求交给从数据库处理。
方式
代理方式
可以在应用和数据中间加了一个代理层。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中。
中间件
MySQL Router(官方, MySQL Proxy 的替代方案)、Atlas(基于 MySQL Proxy)、MaxScale、MyCat。
在 MySQL 8.2 的版本中,MySQL Router 能自动分辨对数据库读写/操作并把这些操作路由到正确的实例上。
组件方式
可以通过引入第三方组件来帮助我们读写请求。(推荐)
推荐使用 sharding-jdbc ,直接引入 jar 包即可使用,非常方便。同时,也节省了很多运维的成本。
主从复制原理
根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。
MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。
过程
主库将数据库中数据的变化写入到 binlog。
从库连接主库。
从库会创建一个 I/O 线程向主库请求更新的 binlog。
主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收。
从库的 I/O 线程将接收的 binlog 写入到 relay log 中。
从库的 SQL 线程读取 relay log 同步数据本地(也就是再执行一遍 SQL )。
canal
阿里开源工具,可以帮助我们实现 MySQL 和其他数据源比如 Elasticsearch 或者另外一台 MySQL 数据库之间的数据同步。
原理
模拟 MySQL 主从复制的过程,解析 binlog 将数据同步到其他的数据源。
主从同步延迟
原因
根本
从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度,导致从库 relay log 的数据滞后于主库 binlog 的数据。
从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度,导致从库的数据滞后于从库 relay log 的数据。
场景
从库机器性能比主库差
解决方法是选择与主库一样规格或更高规格的机器作为从库,或者对从库进行性能优化,比如调整参数、增加缓存、使用 SSD 等。
从库处理的读请求过多
解决方法是引入缓存(推荐)、使用一主多从的架构,将读请求分散到不同的从库,
或者使用其他系统来提供查询的能力,比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。
或者使用其他系统来提供查询的能力,比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。
大事务
解决办法是避免大批量修改数据,尽量分批进行。类似的情况还有执行时间较长的慢 SQL ,实际项目遇到慢 SQL 应该进行优化。
从库太多
解决方案是减少从库的数量,或者将从库分为不同的层级,让上层的从库再同步给下层的从库,减少主库的压力。
网络延迟
解决方法是优化网络环境,比如提升带宽、降低延迟、增加稳定性等。
单线程复制
MySQL5.5 及之前,只支持单线程复制。为了优化复制性能,MySQL 5.6 引入了 多线程复制,MySQL 5.7 还进一步完善了多线程复制。
复制模式
MySQL 默认的复制是异步的,必然会存在延迟问题。
全同步复制不存在延迟问题,但性能太差了。
半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。
MySQL 5.5 开始,MySQL 以插件的形式支持 semi-sync 半同步复制。并且,MySQL 5.7 引入了 增强半同步复制 。
全同步复制不存在延迟问题,但性能太差了。
半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。
MySQL 5.5 开始,MySQL 以插件的形式支持 semi-sync 半同步复制。并且,MySQL 5.7 引入了 增强半同步复制 。
方案
强制将读请求路由到主库处理
可以将那些必须获取最新数据的读请求都交给主库处理。(最常用)
延迟读取
对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。
分库分表
定义
分库
将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。
垂直分库:把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。
水平分库:把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。
分表
对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。
垂直分表:对数据表列的拆分,把一张列比较多的表拆分为多张表。
水平分表:对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。
水平拆分只能解决单表数据量大的问题,为了提升性能,通常会选择将拆分后的多张表放在不同的数据库中。也就是说,水平分表通常和水平分库同时出现。
场景
单表的数据达到千万级别以上,数据库读写速度比较缓慢。
数据库中的数据占用的空间越来越大,备份时间越来越长。
应用的并发量太大。
分片算法
主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。
哈希分片:求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。比较适合随机读写的场景,不太适合经常需要范围查询的场景。
范围分片:按照特性的范围区间(比如时间区间、ID 区间)来分配数据。
适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。
地理位置分片:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
融合算法:灵活组合多种分片算法,比如将哈希分片和范围分片组合。
问题
join 操作
同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。
建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。
解决
可以采用多次查询业务层进行数据组装的方法。但需要考虑业务上多次查询的事务性的容忍度。
事务问题
同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。
解决
需要引入分布式事务。
分布式 ID
分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。
解决
需要引入分布式 ID。
跨库聚合查询问题
分库分表会导致常规聚合查询操作,如 group by,order by 等变得异常复杂。
引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。
方案
Apache ShardingSphere
一款分布式的数据库生态系统, 可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。
ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。
首选!ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理、影子库、数据加密和脱敏等功能。
现在很多公司都是用的类似于 TiDB 这种分布式关系型数据库,不需要手动进行分库分表(数据库层面已经帮我们做了),也不需要解决手动分库分表引入的各种问题,直接一步到位,内置很多实用的功能(如无感扩容和缩容、冷热存储分离)!如果公司条件允许的话,也是比较推荐这种方式!
数据迁移
停机迁移
简单高效,推荐。
双写方案
针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。
原理
对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中,从而保证新库里的数据是最新的。
在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。
如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
重复上一步的操作,直到老库和新库的数据一致为止。
在项目中实施双写还是比较麻烦的,很容易出问题。可以借助数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。
数据冷热分离
定义
指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。
热数据:指经常被访问和修改且需要快速访问的数据。
冷数据:指不经常访问,对当前项目价值较低,但需要长期保存的数据。
冷热数据区分
时间维度区分:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。
访问评率区分:将高频访问的数据视为热数据,低频访问的数据视为冷数据。
优点
热数据的查询性能得到优化(用户的绝大部分操作体验会更好)。
节约成本(可以冷热数据的不同存储需求,选择对应的数据库类型和硬件配置,比如将热数据放在 SSD 上,将冷数据放在 HDD 上)。
缺点
系统复杂性和风险增加(需要分离冷热数据,数据错误的风险增加)。
统计效率低(统计的时候可能需要用到冷库的数据)。
冷数据迁移
业务层代码实现:当有对数据进行写操作时,触发冷热分离的逻辑,判断数据是冷数据还是热数据,冷数据就入冷库,热数据就入热库。
这种方案会影响性能且冷热数据的判断逻辑不太好确定,还需要修改业务层代码,因此一般不会使用。
这种方案会影响性能且冷热数据的判断逻辑不太好确定,还需要修改业务层代码,因此一般不会使用。
任务调度:利用 xxl-job 或者其他分布式任务调度平台定时去扫描数据库,找出满足冷数据条件的数据,然后批量地将其复制到冷库中,并从热库中删除。
这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。
这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。
监听数据库的变更日志 binlog :将满足冷数据条件的数据从 binlog 中提取出来,然后复制到冷库中,并从热库中删除。
这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。
这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。
冷数据存储
冷数据的存储要求主要是容量大,成本低,可靠性高,访问速度可以适当牺牲。
方案
中小厂:直接使用 MySQL/PostgreSQL 即可(不改变数据库选型和项目当前使用的数据库保持一致)。
大厂:Hbase(常用)、RocksDB、Doris、Cassandra。
如果公司成本预算足的话,也可以直接上 TiDB 这种分布式关系型数据库,直接一步到位。
TiDB 6.0 正式支持数据冷热存储分离,可以降低 SSD 使用成本。
使用 TiDB 6.0 的数据放置功能,可以在同一个集群实现海量数据的冷热存储,将新的热数据存入 SSD,历史冷数据存入 HDD。
TiDB 6.0 正式支持数据冷热存储分离,可以降低 SSD 使用成本。
使用 TiDB 6.0 的数据放置功能,可以在同一个集群实现海量数据的冷热存储,将新的热数据存入 SSD,历史冷数据存入 HDD。
深度分页
查询偏移量过大的场景我们称为深度分页,这会导致查询性能较低。
优化
范围查询
当可以保证 ID 的连续性时,根据 ID 范围进行分页是比较好的解决方案。
这种优化方式限制比较大,且一般项目的 ID 也没办法保证完全连续。
子查询
先查询出 limit 第一个参数对应的主键值,再根据这个主键值再去过滤并 limit,这样效率会更快一些。
不过,子查询的结果会产生一张新表,会影响性能,应该尽量避免大量使用子查询。
并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。
当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。
并且,这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。
当然,我们也可以利用子查询先去获取目标分页的 ID 集合,然后再根据 ID 集合获取内容,但这种写法非常繁琐,不如使用 INNER JOIN 延迟关联。
延迟关联
跟子查询的优化思路一样:都是把条件转移到主键索引树,减少回表的次数。不同点是,延迟关联使用了 INNER JOIN(内连接) 包含子查询。
覆盖索引
索引中已经包含了所有需要获取的字段的查询方式称为覆盖索引。
优点
避免 InnoDB 表进行索引的二次查询,也就是回表操作。
可以把随机 IO 变成顺序 IO 加快查询效率。
不过,当查询的结果集占表的总行数的很大一部分时,可能就不会走索引了,自动转换为全表扫描。
当然了,也可以通过 FORCE INDEX 来强制查询优化器走索引,但这种提升效果一般不明显。
当然了,也可以通过 FORCE INDEX 来强制查询优化器走索引,但这种提升效果一般不明显。
消息队列
定义
一个存放消息的容器,当我们需要使用消息的时候,直接从容器中取出消息供自己使用即可。
由于队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。
参与消息传递的双方称为 生产者 和 消费者 ,生产者负责发送消息,消费者负责处理消息。
中间件
中间件(Middleware),又译中间件、中介层,一类为应用软件服务的软件,应用软件是为用户服务的,用户不会接触或者使用到中间件。
作用
通过异步处理提高系统性能(减少响应所需时间)
削峰/限流
先将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再慢慢根据自己的能力去消费这些消息,这样就避免直接把后端服务打垮掉。
降低系统耦合性
对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。
实现分布式事务
RocketMQ、 Kafka、Pulsar、QMQ 都提供了事务相关的功能。事务允许事件流应用将消费,处理,生产消息整个过程定义为一个原子操作。
问题
系统可用性降低: 系统可用性在某种程度上降低。
系统复杂性提高: 加入 MQ 之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
一致性问题: 消息的真正消费者并没有正确消费消息,这样就会导致数据不一致的情况。
JMS
定义
JMS(JAVA Message Service)Java 的消息服务,JMS 的客户端之间可以通过 JMS 服务进行异步的消息传输。
JMS API 是一个消息服务的标准或者说是规范,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。
JMS API 使分布式通信耦合度更低,消息服务更加可靠以及异步性。
ActiveMQ(已被淘汰) 就是基于 JMS 规范实现的。
消息格式
StreamMessage:Java 原始值的数据流。
MapMessage:一套名称-值对。
TextMessage:一个字符串对象。
ObjectMessage:一个序列化的 Java 对象。
BytesMessage:一个字节的数据流。
消息模型
点到点(P2P)模型
使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。
发布/订阅(Pub/Sub)模型
使用主题(Topic)作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者。
AMQP
定义
AMQP(Advanced Message Queuing Protocol)高级消息队列协议,应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。
基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。
RabbitMQ 就是基于 AMQP 协议实现的。
JMS 和 AMQP
AMQP 为消息定义了线路层(wire-level protocol)的协议,而 JMS 所定义的是 API 规范。
在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。
在 Java 体系中,多个 client 均可以通过 JMS 进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而 AMQP 天然具有跨平台、跨语言特性。
JMS 支持 TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。
由于 Exchange 提供的路由算法,AMQP 可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。
RPC 和 消息队列
从用途来看:RPC 主要用来解决两个服务的远程通信问题,不需要了解底层网络的通信机制。
通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。
消息队列主要用来降低系统耦合性、实现任务异步、有效地进行流量削峰。
通过 RPC 可以帮助我们调用远程计算机上某个服务的方法,这个过程就像调用本地方法一样简单。
消息队列主要用来降低系统耦合性、实现任务异步、有效地进行流量削峰。
从通信方式来看:RPC 是双向直接网络通讯,消息队列是单向引入中间载体的网络通讯。
从架构上来看:消息队列需要把消息存储起来,RPC 则没有这个要求,因为前面也说了 RPC 是双向直接网络通讯。
从请求处理的时效性来看:通过 RPC 发出的调用一般会立即被处理,存放在消息队列中的消息并不一定会立即被处理。
RPC 和 消息队列 本质上是网络通讯的两种不同的实现机制,两者的用途不同,万不可将两者混为一谈。
消息模型
队列模型
使用队列(Queue)作为消息通信载体,满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。
缺点
将生产者产生的消息分发给多个消费者,不好解决。
发布-订阅模型
发布订阅模型/主题模型(Pub-Sub) 使用主题(Topic) 作为消息通信载体,类似于广播模式。
消息的生产者称为 发布者(Publisher) ,消息的消费者称为 订阅者(Subscriber) ,存放消息的容器称为 主题(Topic) 。
发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。
发布-订阅模型主要是为了解决队列模型存在的问题。
在发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样的了。所以,发布 - 订阅模型在功能层面上是可以兼容队列模型的。
Kafka、RocketMQ、RabbitMQ 采用的都是发布 - 订阅模型。但底层设计不一样,如 Kafka 的 分区 ,RocketMQ 的 队列 ,RabbitMQ 的 Exchange 。
技术选型
Kafka
定义
Kafka 是 LinkedIn 开源的一个分布式流式处理平台,Apache 顶级项目,早期用于处理海量的日志,后面才慢慢发展成一款功能全面的高性能消息队列。
Kafka 是一个分布式系统,由通过高性能 TCP 网络协议进行通信的服务器和客户端组成,可以部署在在本地和云环境中的裸机硬件、虚拟机和容器上。
在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper 做元数据管理和集群的高可用。
在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。
在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的方式来使用 Kafka。
如果要使用 KRaft 模式,建议选择较高版本的 Kafka(Kafka 3.3.1 及以上),低版本此功能还不完善。
关键功能
消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。
容错的持久方式存储记录消息流:Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。
流式处理平台: 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。
核心概念
Producer(生产者) : 产生消息的一方。
Consumer(消费者) : 消费消息的一方。
Broker(代理) : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。
Topic(主题) : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic(主题) 来消费消息。
Partition(分区) : Partition 属于 Topic 的一部分。一个 Topic 可以有多个 Partition ,
并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。
并且同一 Topic 下的 Partition 可以分布在不同的 Broker 上,这也就表明一个 Topic 可以横跨多个 Broker 。
Kafka 中的 Partition(分区) 实际上可以对应成为消息队列中的队列。
多副本机制
Kafka 为分区(Partition)引入了多副本(Replica)机制。
分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。
发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。
分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。
发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。
生产者和消费者只与 leader 副本交互。可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。
当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。
当 leader 副本发生故障时会从 follower 中选举出一个 leader,但是 follower 中如果有和 leader 同步程度达不到要求的参加不了 leader 的竞选。
优点
Kafka 通过给特定 Topic 指定多个 Partition,而各个 Partition 可以分布在不同的 Broker 上,这样便能提供比较好的并发能力(负载均衡)。
Partition 可以指定对应的 Replica 数,这也极大地提高了消息存储的安全性,提高了容灾能力,不过也相应的增加了所需要的存储空间。
Zookeeper 和 Kafka
ZooKeeper 主要为 Kafka 提供元数据的管理的功能。
Broker 注册:在 Zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,
即到 /brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去。
即到 /brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去。
Topic 注册:同一个Topic 的消息会被分成多个分区并将其分布在多个 Broker 上,这些分区信息及与 Broker 的对应关系也都是由 Zookeeper 在维护。
比如创建一个名为 my-topic 的主题并且它有两个分区,zookeeper 创建文件夹:/brokers/topics/my-topic/Partitions/0、/brokers/topics/my-topic/Partitions/1。
比如创建一个名为 my-topic 的主题并且它有两个分区,zookeeper 创建文件夹:/brokers/topics/my-topic/Partitions/0、/brokers/topics/my-topic/Partitions/1。
负载均衡:Kafka 通过给特定 Topic 指定多个 Partition,而各个 Partition 可以分布在不同的 Broker 上,这样便能提供比较好的并发能力。
对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。
当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。
当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。
当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。
当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。
消息模型
发布 - 订阅模型。
底层:分区。
消息安全
消费顺序
原理
每次添加消息到 Partition(分区) 的时候都会采用尾加法。 Kafka 只能为我们保证 Partition(分区) 中的消息有序。
消息在被追加到 Partition(分区) 的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。
方法
1 个 Topic 只对应一个 Partition。
(推荐)发送消息的时候指定 key/Partition。
消息不丢失
生产者丢失消息
生产者(Producer) 调用 send 方法(异步)发送消息之后,消息可能因为网络问题并没有发送过去。
方法
异步调用 send 后,通过 get() 方法获取调用结果,但会变为同步操作,不推荐。
异步调用 send 后,添加回调函数形式获取结果。若失败,检查原因后重新发送即可。推荐设定一个 retries(重试次数)。
消费者丢失消息
当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。
方法
手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。
消息理论上会被消费两次。
Kafka 弄丢了消息
假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,
但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。
但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。
方法
设置 acks = all。
acks 是 Kafka 生产者(Producer) 很重要的一个参数,默认为 1。
当 acks = 1 时,代表我们的消息被 leader 副本接收之后就算被成功发送。
当 acks = all 时,表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应。最高级别且最安全的,但延迟会很高。
acks 是 Kafka 生产者(Producer) 很重要的一个参数,默认为 1。
当 acks = 1 时,代表我们的消息被 leader 副本接收之后就算被成功发送。
当 acks = all 时,表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应。最高级别且最安全的,但延迟会很高。
设置 replication.factor >= 3。
为了保证 leader 副本能有 follower 副本能同步消息,一般会为 topic 设置 replication.factor >= 3。
这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。
为了保证 leader 副本能有 follower 副本能同步消息,一般会为 topic 设置 replication.factor >= 3。
这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。
设置 min.insync.replicas > 1。
一般情况下我们还需要设置 min.insync.replicas > 1 ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。
min.insync.replicas 默认值为 1 ,在实际生产中应尽量避免默认值 1。
为保证整个 Kafka 服务的高可用性,需确保 replication.factor > min.insync.replicas 。一般推荐设置成 replication.factor = min.insync.replicas + 1。
一般情况下我们还需要设置 min.insync.replicas > 1 ,这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。
min.insync.replicas 默认值为 1 ,在实际生产中应尽量避免默认值 1。
为保证整个 Kafka 服务的高可用性,需确保 replication.factor > min.insync.replicas 。一般推荐设置成 replication.factor = min.insync.replicas + 1。
设置 unclean.leader.election.enable = false。
Kafka 0.11.0.0 版本开始 unclean.leader.election.enable 参数的默认值由原来的 true 改为 false
当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
Kafka 0.11.0.0 版本开始 unclean.leader.election.enable 参数的默认值由原来的 true 改为 false
当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
消息不重复消费
原因
服务端侧已经消费的数据没有成功提交 offset(根本原因)。
Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死,触发了分区 rebalance。
方法
消费消息服务做幂等校验,比如 Redis 的 set、MySQL 的主键等天然的幂等功能。这种方法最有效。
将 enable.auto.commit 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。
手动提交 offset 时机
处理完消息再提交:依旧有消息重复消费的风险,和自动提交一样。
拉取到消息即提交:会有消息丢失的风险。允许消息延时的场景,一般会采用这种方式。然后,通过定时任务在业务不繁忙(比如凌晨)的时候做数据兜底。
重试机制
消费失败
在默认配置下,当消费异常会进行重试,重试多次后会跳过当前消息,继续进行后续消息的消费,不会一直卡在当前消息。
重试规则
默认配置下会进行最多 10 次 的重试,每次重试的时间间隔为 0,即立即进行重试。
如果在 10 次重试后仍然无法成功消费消息,则不再进行重试,消息将被视为消费失败。
如果在 10 次重试后仍然无法成功消费消息,则不再进行重试,消息将被视为消费失败。
自定义重试规则
只需要在 DefaultErrorHandler 初始化的时候传入自定义的 FixedBackOff 即可。
重新实现一个 KafkaListenerContainerFactory ,调用 setCommonErrorHandler 设置新的自定义的错误处理器就可以实现。
重新实现一个 KafkaListenerContainerFactory ,调用 setCommonErrorHandler 设置新的自定义的错误处理器就可以实现。
重试失败后告警
自定义重试失败后逻辑,需要手动实现,重写 DefaultErrorHandler 的 handleRemaining 函数,加上自定义的告警等操作。
DefaultErrorHandler 只是默认的一个错误处理器,Spring Kafka 还提供了 CommonErrorHandler 接口。
手动实现 CommonErrorHandler 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。
手动实现 CommonErrorHandler 就可以实现更多的自定义操作,有很高的灵活性。例如根据不同的错误类型,实现不同的重试逻辑以及业务逻辑等。
重试失败后再处理
死信队列(Dead Letter Queue,简称 DLQ) 是消息中间件中的一种特殊队列。
它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。
它主要用于处理无法被消费者正确处理的消息,通常是因为消息格式错误、处理失败、消费超时等情况导致的消息被"丢弃"或"死亡"的情况。
@RetryableTopic 是 Spring Kafka 中的一个注解,它用于配置某个 Topic 支持消息重试,更推荐使用这个注解来完成重试。
当达到最大重试次数后,如果仍然无法成功处理消息,消息会被发送到对应的死信队列中。
对于死信队列的处理,既可以用 @DltHandler 处理,也可以使用 @KafkaListener 重新消费。
对于死信队列的处理,既可以用 @DltHandler 处理,也可以使用 @KafkaListener 重新消费。
应用场景
消息队列:建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
数据处理: 构建实时的流数据处理程序来转换或处理数据流。
优势
极致的性能:基于 Scala 和 Java 语言开发,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。
生态系统兼容性无可匹敌:Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域。
RocketMQ
定义
RocketMQ 是一个 队列模型 的消息中间件,具有高性能、高可靠、高实时、分布式的特点。采用 Java 语言开发的分布式的消息系统。
阿里开源的一款云原生“消息、事件、流”实时数据处理平台,借鉴了 Kafka,已经成为 Apache 顶级项目。
核心特性
云原生:生与云,长与云,无限弹性扩缩,K8s 友好。
高吞吐:万亿级吞吐保证,同时满足微服务与大数据场景。
流处理:提供轻量、高扩展、高性能和丰富功能的流计算引擎。
金融级:金融级的稳定性,广泛用于交易核心链路。
架构极简:零外部依赖,Shared-nothing 架构。
生态友好:无缝对接微服务、实时计算、数据湖等周边生态。
消息模型
发布 - 订阅模型。
底层:队列。
RocketMQ 通过使用在一个 Topic 中配置多个队列并且每个队列维护每个消费者组的消费位置 实现了 主题模式/发布订阅模式 。
角色
Producer Group 生产者组:代表某一类的生产者,比如有多个秒杀系统作为生产者,合在一起就是一个 Producer Group 生产者组,一般生产相同的消息。
Consumer Group 消费者组:代表某一类的消费者,比如有多个短信系统作为消费者,合在一起就是一个 Consumer Group 消费者组,一般消费相同的消息。
Topic 主题:代表一类消息,比如订单消息,物流消息等等。
角色
Broker:主要负责消息的存储、投递和查询以及服务高可用保证。就是消息队列服务器,生产者生产消息到 Broker ,消费者从 Broker 拉取消息并消费。
一个 Topic 分布在多个 Broker上,一个 Broker 可以配置多个 Topic ,它们是多对多的关系。
一个 Topic 分布在多个 Broker上,一个 Broker 可以配置多个 Topic ,它们是多对多的关系。
NameServer:注册中心 ,主要提供 Broker 管理 和 路由信息管理。就是 Broker 注册后,消费者和生产者从中获取信息来与 Broker 通信。
Producer:消息发布的角色,支持分布式集群方式部署。就是生产者。
Consumer:消息消费的角色,支持分布式集群方式部署。
支持以 push 推,pull 拉 两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。
支持以 push 推,pull 拉 两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制。说白了就是消费者。
消息类型
普通消息
一般应用于微服务解耦、事件驱动、数据集成等场景,这些大多数要求数据传输通道具有可靠传输的能力,且对消息的处理时机、处理顺序没有特别要求。
生命周期
初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。
消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。
消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。
消息删除:RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
定时消息
可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。
优势
精度高、开发门槛低:基于消息通知方式不存在定时阶梯间隔。可以轻松实现任意精度事件触发,无需业务去重。
高性能可扩展:传统的数据库扫描方式较为复杂,需要频繁调用接口扫描,容易产生性能瓶颈。RocketMQ 的定时消息具有高并发和水平扩展的能力。
生命周期
初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息单独存储在定时存储系统中,等待定时时刻到达。
待消费:定时时刻到达后,服务端将消息重新写入普通存储引擎,对下游消费者可见,等待消费者消费的状态。
消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。
消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。
消息删除:Apache RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
顺序消息
仅支持使用 MessageType 为 FIFO 的主题,即顺序消息只能发送至类型为顺序消息的主题中,发送的消息的类型必须和主题的类型一致。
和普通消息发送相比,顺序消息发送必须要设置消息组。(推荐实现 MessageQueueSelector 的方式)。要保证消息的顺序性需要单一生产者串行发送。
单线程使用 MessageListenerConcurrently 可以顺序消费,多线程环境下使用 MessageListenerOrderly 才能顺序消费。
事务消息
高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。
发送消息
不建议单一进程创建大量生产者
Apache RocketMQ 的生产者和主题是多对多的关系,支持同一个生产者向多个主题发送消息。
对于生产者的创建和初始化,建议遵循够用即可、最大化复用原则,如果有需要发送消息到多个主题的场景,无需为每个主题都创建一个生产者。
不建议频繁创建和销毁生产者
Apache RocketMQ 的生产者是可以重复利用的底层资源,类似数据库的连接池。
不需要在每次发送消息时动态创建生产者,且在发送结束后销毁生产者。这样频繁的创建销毁会在服务端产生大量短连接请求,严重影响系统性能。
消费者分类
PushConsumer
高度封装的消费者类型,消费消息仅仅通过消费监听器监听并返回结果。消息的获取、消费状态提交以及消费重试都通过 RocketMQ 的客户端 SDK 完成。
监听执行结果
返回消费成功:以 Java SDK 为例,返回ConsumeResult.SUCCESS,表示该消息处理成功,服务端按照消费结果更新消费进度。
返回消费失败:以 Java SDK 为例,返回ConsumeResult.FAILURE,表示该消息处理失败,需要根据消费重试逻辑判断是否进行重试消费。
出现非预期失败:例如抛异常等行为,该结果按照消费失败处理,需要根据消费重试逻辑判断是否进行重试消费。
错误方式
消息还未处理完成,就提前返回消费成功结果。此时如果消息消费失败,RocketMQ 服务端是无法感知的,因此不会进行消费重试。
在消费监听器内将消息再次分发到自定义的其他线程,消费监听器提前返回消费结果。
此时如果消息消费失败,RocketMQ 服务端同样无法感知,因此也不会进行消费重试。
此时如果消息消费失败,RocketMQ 服务端同样无法感知,因此也不会进行消费重试。
PushConsumer 严格限制了消息同步处理及每条消息的处理超时时间,适用于以下场景:
消息处理时间可预估:如果不确定消息处理耗时,经常有预期之外的耗时的消息,PushConsumer 的可靠性保证会频繁触发消息重试机制造成大量重复消息。
无异步化、高级定制场景:PushConsumer 限制了消费逻辑的线程模型,由客户端 SDK 内部按最大吞吐量触发消息处理。该模型开发逻辑简单,但是不允许使用异步化和自定义处理流程。
消息处理时间可预估:如果不确定消息处理耗时,经常有预期之外的耗时的消息,PushConsumer 的可靠性保证会频繁触发消息重试机制造成大量重复消息。
无异步化、高级定制场景:PushConsumer 限制了消费逻辑的线程模型,由客户端 SDK 内部按最大吞吐量触发消息处理。该模型开发逻辑简单,但是不允许使用异步化和自定义处理流程。
SimpleConsumer
一种接口原子型的消费者类型,消息的获取、消费状态提交以及消费重试都是通过消费者业务逻辑主动发起调用完成。
应用
消息处理时长不可控:如果消息处理时长无法预估,经常有长时间耗时的消息处理情况。
建议使用 SimpleConsumer 消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改。
建议使用 SimpleConsumer 消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改。
需要异步化、批量消费等高级定制场景:SimpleConsumer 在 SDK 内部没有复杂的线程封装,完全由业务逻辑自由定制,可以实现异步分发、批量消费等高级定制场景。
需要自定义消费速率:SimpleConsumer 是由业务逻辑主动调用接口获取消息,因此可以自由调整获取消息的频率,自定义控制消费速率。
PullConsumer
主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。
一旦获取了批量消息,应用就会启动消费过程。Pull指的是客户端主动向服务端请求,拉取数据。
一旦获取了批量消息,应用就会启动消费过程。Pull指的是客户端主动向服务端请求,拉取数据。
分组
生产者分组
RocketMQ 服务端 5.x 版本开始,生产者是匿名的,无需管理生产者分组(ProducerGroup);
对于历史版本服务端 3.x 和 4.x 版本,已经使用的生产者分组可以废弃无需再设置,且不会对当前业务产生影响。
对于历史版本服务端 3.x 和 4.x 版本,已经使用的生产者分组可以废弃无需再设置,且不会对当前业务产生影响。
消费者分组
多个消费行为一致的消费者的负载均衡分组。消费者分组不是具体实体而是一个逻辑资源。通过消费者分组实现消费性能的水平扩展以及高可用容灾。
策略
消费者分组中的订阅关系、投递顺序性、消费重试策略是一致的。
订阅关系:Apache RocketMQ 以消费者分组的粒度管理订阅关系,实现订阅关系的管理和追溯。
投递顺序性:Apache RocketMQ 的服务端将消息投递给消费者消费时,支持顺序投递和并发投递,投递方式在消费者分组中统一配置。
消费重试策略: 消费者消费消息失败时的重试策略,包括重试次数、死信队列设置等。
RocketMQ 服务端 5.x 版本:消费者的消费行为从关联的消费者分组中统一获取,因此,同一分组内所有消费者的消费行为必然是一致的,客户端无需关注。
RocketMQ 服务端 3.x/4.x 历史版本:消费逻辑由消费者客户端接口定义,因此,需要在消费者客户端设置时保证同一分组下的消费者的消费行为一致。
RocketMQ 服务端 3.x/4.x 历史版本:消费逻辑由消费者客户端接口定义,因此,需要在消费者客户端设置时保证同一分组下的消费者的消费行为一致。
消费安全
顺序消费
RocketMQ 在主题上是无序的、它只有在队列层面才是保证有序的。
普通顺序
消费者通过同一个消费队列收到的消息是有顺序的 ,不同消息队列收到的消息则可能是无顺序的。
普通顺序消息在 Broker 重启情况下不会保证消息顺序性 (短暂时间) 。
严格顺序
消费者收到的 所有消息 均是有顺序的。严格顺序消息 即使在异常情况下也会保证消息的顺序性 。
如果你使用严格顺序模式,Broker 集群中只要有一台机器不可用,则整个集群都不可用。
MQ 都是能容忍短暂的乱序,所以推荐使用普通顺序模式。
可以使用 Hash 取模法 来保证将同一语义下的消息放入同一个队列。
队列选择算法
轮询算法
轮询算法就是向消息指定的 topic 所在队列中依次发送消息,保证消息均匀分布
RocketMQ 默认队列选择算法。
最小投递延迟算法
每次消息投递的时候统计消息投递的延迟,选择队列时优先选择消息延时小的队列,导致消息分布不均匀。
解决消息分布不均匀
按照 producer.setSendLatencyFaultEnable(true); 设置即可。
特殊情况处理
发送异常
选择队列后会与 Broker 建立连接,通过网络请求将消息发送到 Broker 上,如果 Broker 挂了或者网络波动发送消息超时此时 RocketMQ 会进行重试。
重新选择其他 Broker 中的消息队列进行发送,默认重试两次,可以手动设置。
producer.setRetryTimesWhenSendFailed(5);
消息过大
消息超过 4k 时 RocketMQ 会将消息压缩后在发送到 Broker 上,减少网络资源的占用。
重复消费
幂等
其任意多次执行所产生的影响均与一次执行的影响相同。
实现
这个还是需要结合具体的业务的。
可以使用 写入 Redis 来保证,因为 Redis 的 key 和 value 就是天然支持幂等的。
使用 数据库插入法 ,基于数据库的唯一键来保证重复数据不会被插入多条。
分布式事务
在 RocketMQ 中使用的是 事务消息加上事务反查机制 来解决分布式事务问题的。
消息队列中的分布式事务是本地事务和存储消息到消息队列才是同一个事务。
这样也就产生了事务的最终一致性,因为整个过程是异步的,每个系统只要保证它自己那一部分的事务就行了。
这样也就产生了事务的最终一致性,因为整个过程是异步的,每个系统只要保证它自己那一部分的事务就行了。
消息堆积
当流量到峰值的时候是因为生产者生产太快,可以使用一些 限流降级 的方法。
增加多个消费者实例去水平扩展增加消费能力来匹配生产的激增,不过 同时你还需要增加每个主题的队列数量 。(最快速)
如果消费者消费过慢的话,可以先检查消费者否是出现了大量的消费错误 ,或打印一下日志查看是否是哪一个线程卡死,出现了锁资源不释放等等的问题。
回溯消费
Consumer 已经消费成功的消息,由于业务上需求需要重新消费,在RocketMQ 中,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留 。
高性能读写
传统 IO 方式
传统的 IO 读写其实就是 read + write 的操作。
过程
用户调用 read()方法,开始读取数据,此时发生一次上下文从用户态到内核态的切换。
将磁盘数据通过 DMA 拷贝到内核缓存区。
将内核缓存区的数据拷贝到用户缓冲区,这样用户,也就是我们写的代码就能拿到文件的数据。
read()方法返回,此时就会从内核态切换到用户态。
当我们拿到数据之后,就可以调用 write()方法,此时上下文会从用户态切换到内核态。
CPU 将用户缓冲区的数据拷贝到 Socket 缓冲区。
将 Socket 缓冲区数据拷贝至网卡。
write()方法返回,上下文重新从内核态切换到用户态。
整个过程发生了 4 次上下文切换和 4 次数据的拷贝,这在高并发场景下肯定会严重影响读写性能,故引入了零拷贝技术。
零拷贝技术
mmap
一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
简单地说就是内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次 CPU 拷贝。
基于 mmap IO 读写其实就变成 mmap + write 的操作,也就是用 mmap 替代传统 IO 中的 read 操作。
过程
当用户发起 mmap 调用的时候会发生上下文切换 1,进行内存映射,然后数据被拷贝到内核缓冲区,mmap 返回,发生上下文切换 2;
随后用户调用 write,发生上下文切换 3,将内核缓冲区的数据拷贝到 Socket 缓冲区,write 返回,发生上下文切换 4。
随后用户调用 write,发生上下文切换 3,将内核缓冲区的数据拷贝到 Socket 缓冲区,write 返回,发生上下文切换 4。
发生 4 次上下文切换和 3 次 IO 拷贝操作。
sendfile
sendfile()跟 mmap()一样,也会减少一次 CPU 拷贝,但是它同时也会减少两次上下文切换。
过程
用户在发起 sendfile()调用时会发生切换 1,之后数据通过 DMA 拷贝到内核缓冲区,
之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换。
之后再将内核缓冲区的数据 CPU 拷贝到 Socket 缓冲区,最后拷贝到网卡,sendfile()返回,发生切换 2。发生了 3 次拷贝和两次切换。
sendfile 并没有文件的读写操作,而是直接将文件的数据传输到 target 目标缓冲区,也就是说,sendfile 是无法知道文件的具体的数据的;
但是 mmap 不一样,他是可以修改内核缓冲区的数据的。假设如果需要对文件的内容进行修改之后再传输,只有 mmap 可以满足。
但是 mmap 不一样,他是可以修改内核缓冲区的数据的。假设如果需要对文件的内容进行修改之后再传输,只有 mmap 可以满足。
基于零拷贝技术,可以减少 CPU 的拷贝次数和上下文切换次数,从而可以实现文件高效的读写操作。
RocketMQ 内部主要是使用基于 mmap 实现的零拷贝,用来读写文件,这也是 RocketMQ 为什么快的一个很重要原因。
刷盘机制
刷盘
同步刷盘
在同步刷盘中需要等待一个刷盘成功的 ACK ,对 MQ 消息可靠性来说是一种不错的保障,但性能上会有较大影响 ,一般地适用于金融等特定业务场景。
异步刷盘
异步刷盘是开启一个线程去异步地执行刷盘操作。
消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了 MQ 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。
消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了 MQ 的性能和吞吐量,一般适用于如发验证码等对于消息保证要求不太高的业务场景。
异步刷盘只有在 Broker 意外宕机的时候会丢失部分数据,可以设置 Broker 的参数 FlushDiskType 来调整刷盘策略(ASYNC_FLUSH 或者 SYNC_FLUSH)。
复制
同步刷盘和异步刷盘是在单个结点层面的,而同步复制和异步复制主要是指的 Borker 主从模式下,主节点返回消息给客户端的时候是否需要同步从节点。
同步复制
也叫 “同步双写”,也就是说,只有消息同步双写到主从节点上时才返回写入成功 。
异步复制
消息写入主节点之后就直接返回写入成功 。
问题
复制方式无法保证 严格顺序。
解决:采用 Dledger,要求写入消息时,要求至少消息复制到半数以上的节点之后,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。
解决:采用 Dledger,要求写入消息时,要求至少消息复制到半数以上的节点之后,才给客⼾端返回写⼊成功,并且它是⽀持通过选举来动态切换主节点的。
对于消息可靠性是通过不同的刷盘策略保证的,而像异步同步复制策略仅仅是影响到了可用性 。
主要原因是 RocketMQ 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了。
主要原因是 RocketMQ 是不支持自动主从切换的,当主节点挂掉之后,生产者就不能再给这个主节点生产消息了。
存储机制
存储架构角色
CommitLog
消息主体以及元数据的存储主体,存储 Producer 端写入的消息主体内容,消息内容不是定长的。
单个文件大小默认 1G ,文件名长度为 20 位,左边补零,剩余为起始偏移量。
消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
ConsumeQueue
消息消费队列,引入的目的主要是提高消息消费的性能。
RocketMQ 是基于主题 Topic 的订阅模式,消息消费是针对主题进行的,如果要遍历 commitlog 文件中根据 Topic 检索消息是非常低效的。
Consumer 即可根据 ConsumeQueue 来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,
保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset ,消息大小 size 和消息 Tag 的 HashCode 值。
保存了指定 Topic 下的队列消息在 CommitLog 中的起始物理偏移量 offset ,消息大小 size 和消息 Tag 的 HashCode 值。
consumequeue 文件可以看成是基于 topic 的 commitlog 索引文件,
故 consumequeue 文件夹的组织方式如下:topic/queue/file 三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。
故 consumequeue 文件夹的组织方式如下:topic/queue/file 三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。
IndexFile
IndexFile(索引文件)提供了一种可以通过 key 或时间区间来查询消息的方法。
整个消息存储的结构,最主要的就是 CommitLoq 和 ConsumeQueue 。而 ConsumeQueue 你可以大概理解为 Topic 中的队列。
RocketMQ 采用的是 混合型的存储结构 ,即为 Broker 单个实例下所有的队列共用一个日志数据文件来存储消息。
在同样高并发的 Kafka 中会为每个 Topic 分配一个存储文件。
在同样高并发的 Kafka 中会为每个 Topic 分配一个存储文件。
应用
大规模消息流处理:RocketMQ 能够处理大量的消息流,并提供高吞吐量和低延迟的消息传递能力。
因此,它适用于需要处理大规模消息流的场景,如实时日志处理、实时数据分析和监控系统等。
因此,它适用于需要处理大规模消息流的场景,如实时日志处理、实时数据分析和监控系统等。
异步通信:RocketMQ 的发布-订阅模式和队列模式可以实现异步通信,将消息发送方和接收方解耦,提高系统的可伸缩性和弹性。
它适用于需要异步通信的场景,如异步任务处理、解耦系统组件和微服务架构等。
它适用于需要异步通信的场景,如异步任务处理、解耦系统组件和微服务架构等。
高可靠性和顺序性要求:RocketMQ 提供了可靠性投递和消息顺序保证的特性,
适用于对消息的可靠性和顺序性要求较高的场景,如金融交易系统、订单处理和支付系统等。
适用于对消息的可靠性和顺序性要求较高的场景,如金融交易系统、订单处理和支付系统等。
数据集成和异构系统集成:RocketMQ 可以作为数据集成和异构系统集成的中间件,将不同系统之间的数据进行传递和转换。
它适用于需要数据集成和系统间通信的场景,如数据同步、消息驱动的架构和异构系统集成等。
它适用于需要数据集成和系统间通信的场景,如数据同步、消息驱动的架构和异构系统集成等。
RabbitMQ
定义
采用 Erlang 语言实现 AMQP 的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。
在易用性、扩展性、可靠性和高可用性等方面有着卓著表现。
特点
可靠性: RabbitMQ 使用一些机制来保证消息的可靠性,如持久化、传输确认及发布确认等。
灵活的路由: 在消息进入队列之前,通过交换器来路由消息。
对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。
针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
对于典型的路由功能,RabbitMQ 己经提供了一些内置的交换器来实现。
针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
扩展性: 多个 RabbitMQ 节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
高可用性: 队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。
支持多种协议: RabbitMQ 除了原生支持 AMQP 协议,还支持 STOMP、MQTT 等多种消息中间件协议。
多语言客户端: RabbitMQ 几乎支持所有常用语言,比如 Java、Python、Ruby、PHP、C#、JavaScript 等。
易用的管理界面: RabbitMQ 提供了一个易用的用户界面,使用户可以监控和管理消息、集群中的节点等。
插件机制: RabbitMQ 提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。感觉这个有点类似 Dubbo 的 SPI 机制。
消息模型
发布 - 订阅模型。
底层:Exchange。
核心概念
RabbitMQ 整体上是一个生产者与消费者模型,主要负责接收、存储和转发消息。
Producer(生产者)
生产消息的一方(邮件投递者)。
消息
消息头(标签 Label)
由可选属性组成,这些属性包括 routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
消息体
消息体也可以称为 payLoad,消息体是不透明的。
生产者把消息交由 RabbitMQ 后,RabbitMQ 会根据消息头把消息发送给感兴趣的 Consumer(消费者)。
Consumer(消费者)
消费消息的一方(邮件收件人)。消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
Exchange
生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。
在 RabbitMQ 中,消息并不是直接被投递到 Queue(消息队列) 中的,
中间还必须经过 Exchange(交换器) 这一层,Exchange(交换器) 会把我们的消息分配到对应的 Queue(消息队列) 中。
中间还必须经过 Exchange(交换器) 这一层,Exchange(交换器) 会把我们的消息分配到对应的 Queue(消息队列) 中。
Exchange(交换器) 用来接收生产者发送的消息并将这些消息路由给服务器中的队列中,
如果路由不到,或许会返回给 Producer(生产者) ,或许会被直接丢弃掉。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。
如果路由不到,或许会返回给 Producer(生产者) ,或许会被直接丢弃掉。这里可以将 RabbitMQ 中的交换器看作一个简单的实体。
生产者将消息发给交换器的时候,一般会指定一个 RoutingKey(路由键),
用来指定这个消息的路由规则,而这个 RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
用来指定这个消息的路由规则,而这个 RoutingKey 需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。
RabbitMQ 中通过 Binding(绑定) 将 Exchange(交换器) 与 Queue(消息队列) 关联起来,在绑定的时候一般会指定一个 BindingKey(绑定建),
这样 RabbitMQ 就知道如何正确将消息路由到队列了。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,
所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
这样 RabbitMQ 就知道如何正确将消息路由到队列了。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,
所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和 Queue 的绑定可以是多对多的关系。
生产者将消息发送给交换器时,需要一个 RoutingKey,当 BindingKey 和 RoutingKey 相匹配时,消息会被路由到对应的队列中。
在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。
BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的 BindingKey。
BindingKey 并不是在所有的情况下都生效,它依赖于交换器类型,比如 fanout 类型的交换器就会无视,而是将消息路由到所有绑定到该交换器的队列中。
类型
fanout
fanout 类型的 Exchange 路由规则非常简单,它会把所有发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中,
不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。
不需要做任何判断操作,所以 fanout 类型是所有的交换机类型里面速度最快的。fanout 类型常用来广播消息。
direct
direct 类型的 Exchange 路由规则也很简单,它会把消息路由到那些 Bindingkey 与 RoutingKey 完全匹配的 Queue 中。
如果发送消息的时候设置路由键为“warning”,那么消息会路由到 Queue1 和 Queue2。
如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到 Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。
如果在发送消息的时候设置路由键为"Info”或者"debug”,消息只会路由到 Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。
direct 类型常用在处理有优先级的任务,根据任务的优先级把消息发送到对应的队列,这样可以指派更多的资源去处理高优先级的队列。
topic
topic 类型的交换器在匹配规则上进行了扩展,它与 direct 类型的交换器相似,
也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同。
也是将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,但这里的匹配规则有些不同。
约定
RoutingKey 为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),
如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”。
如 “com.rabbitmq.client”、“java.util.concurrent”、“com.hidden.client”。
BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串。
BindingKey 中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。
示例
路由键为 “com.rabbitmq.client” 的消息会同时路由到 Queue1 和 Queue2。
路由键为 “com.hidden.client” 的消息只会路由到 Queue2 中。
路由键为 “com.hidden.demo” 的消息只会路由到 Queue2 中。
路由键为 “java.rabbitmq.demo” 的消息只会路由到 Queue1 中。
路由键为 “java.util.concurrent” 的消息将会被丢弃或者返回给生产者(需要设置 mandatory 参数),因为它没有匹配任何路由键。
headers(不推荐)
headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。
在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),
对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。
headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
在绑定队列和交换器时指定一组键值对,当发送消息到交换器时,RabbitMQ 会获取到该消息的 headers(也是一个键值对的形式),
对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。
headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。
Queue
RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
Queue(消息队列) 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。
一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
RabbitMQ 中消息只能存储在 队列 中,这一点和 Kafka 这种消息中间件相反。
Kafka 将消息存储在 topic(主题) 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。
RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
Kafka 将消息存储在 topic(主题) 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。
RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。
多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,
而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。
而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。
RabbitMQ 不支持队列层面的广播消费,如果有广播消费的需求,需要在其上进行二次开发,这样会很麻烦,不建议这样做。
Broker
对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。
大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。
可以看做 RabbitMQ 的服务节点。一般情况下一个 Broker 可以看做一个 RabbitMQ 服务器。
AMQP
定义
RabbitMQ 就是 AMQP 协议的 Erlang 的实现(当然 RabbitMQ 还支持 STOMP2、 MQTT3 等协议 )。
AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。
RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相 应的概念。目前 RabbitMQ 最新版本默认支持的是 AMQP 0-9-1。
协议层
Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。
Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。
TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。
组件
交换器 (Exchange):消息代理服务器中用于把消息路由到队列的组件。
队列 (Queue):用来存储消息的数据结构,位于硬盘或内存中。
绑定 (Binding):一套规则,告知交换器消息应该将消息投递给哪个队列。
队列类型
死信队列
DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。
当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。
原因
消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
消息 TTL 过期。
队列满了,无法再添加。
延迟队列
延迟队列指的是存储对应的延迟消息,消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
AMQP 协议以及 RabbitMQ 本身没有直接支持延迟队列的功能,但是可以通过 TTL 和 DLX 模拟出延迟队列的功能。
实现方式
通过 RabbitMQ 本身队列的特性来实现,需要使用 RabbitMQ 的死信交换机(Exchange)和消息的存活时间 TTL(Time To Live)。
在 RabbitMQ 3.5.7 及以上的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延迟队列功能。插件依赖 Erlang/OPT 18.0 及以上。
优先级队列
RabbitMQ 自 V3.5.0 有优先级队列实现,优先级高的队列会先被消费。
可以通过x-max-priority参数来实现优先级队列。不过,当消费速度大于生产速度且 Broker 没有堆积的情况下,优先级显得没有意义。
工作模式
简单模式
work 工作模式
pub/sub 发布订阅模式
Routing 路由模式
Topic 主题模式
消息
传输
由于 TCP 链接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈,所以 RabbitMQ 使用信道的方式来传输数据。
信道(Channel)是生产者、消费者与 RabbitMQ 通信的渠道,信道是建立在 TCP 链接上的虚拟链接,且每条 TCP 链接上的信道数量没有限制。
就是说 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理,
这个 TCP 被多个线程共享,每个信道在 RabbitMQ 都有唯一的 ID,保证了信道私有性,每个信道对应一个线程使用。
信道(Channel)是生产者、消费者与 RabbitMQ 通信的渠道,信道是建立在 TCP 链接上的虚拟链接,且每条 TCP 链接上的信道数量没有限制。
就是说 RabbitMQ 在一条 TCP 链接上建立成百上千个信道来达到多个线程处理,
这个 TCP 被多个线程共享,每个信道在 RabbitMQ 都有唯一的 ID,保证了信道私有性,每个信道对应一个线程使用。
安全
可靠性
消息到 MQ 的过程中搞丢,MQ 自己搞丢,MQ 到消费过程中搞丢。
方式
生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。
RabbitMQ 自身:持久化、集群、普通模式、镜像模式。
RabbitMQ 到消费者:basicAck 机制、死信队列、消息补偿机制。
顺序性
拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;
或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
高可用
RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的。
模式
单机模式
Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式。
普通集群模式
在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。
镜像集群模式
RabbitMQ 的高可用模式。
跟普通集群模式不一样,在镜像集群模式下,创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,
就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。
然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,
也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。
然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,
也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
优点
任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。
缺点
这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重。
过期失效
RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。
解决
批量重导,即手动补回来。
Disruptor
定义
Disruptor 是一个开源的高性能内存队列,诞生初衷是为了解决内存队列的性能和内存安全问题,由英国外汇交易公司 LMAX 开发。
基于 Disruptor 开发的系统 LMAX(新的零售金融交易平台),单线程就能支撑每秒 600 万订单。
Disruptor 提供的功能优点类似于 Kafka、RocketMQ 这类分布式队列,不过,其作为范围是 JVM(内存)。
Disruptor 主要解决了 JDK 内置线程安全队列的性能和内存安全问题。
Kafka 和 Disruptor
Kafka:分布式消息队列,一般用在系统或者服务之间的消息传递,还可以被用作流式处理平台。
Disruptor:内存级别的消息队列,一般用在系统内部中线程间的消息传递。
应用
Log4j2:Log4j2 是一款常用的日志框架,它基于 Disruptor 来实现异步日志。
SOFATracer:SOFATracer 是蚂蚁金服开源的分布式应用链路追踪工具,它基于 Disruptor 来实现异步日志。
Storm : Storm 是一个开源的分布式实时计算系统,它基于 Disruptor 来实现工作进程内发生的消息传递(同一 Storm 节点上的线程间,无需网络通信)。
HBase:HBase 是一个分布式列存储数据库系统,它基于 Disruptor 来提高写并发性能。
核心概念
Event:你可以把 Event 理解为存放在队列中等待消费的消息对象。
EventFactory:事件工厂用于生产事件,我们在初始化 Disruptor 类的时候需要用到。
EventHandler:Event 在对应的 Handler 中被处理,你可以将其理解为生产消费者模型中的消费者。
EventProcessor:EventProcessor 持有特定消费者(Consumer)的 Sequence,并提供用于调用事件处理实现的事件循环(Event Loop)。
Disruptor:事件的生产和消费需要用到 Disruptor 对象。
RingBuffer:RingBuffer(环形数组)用于保存事件。
WaitStrategy:等待策略。决定了没有事件可以消费的时候,事件消费者如何等待新事件的到来。
Producer:生产者,只是泛指调用 Disruptor 对象发布事件的用户代码,Disruptor 没有定义特定接口或类型。
ProducerType:指定是单个事件发布者模式还是多个事件发布者模式(发布者和生产者的意思类似,我个人比较喜欢用发布者)。
Sequencer:Sequencer 是 Disruptor 的真正核心。
此接口有两个实现类 SingleProducerSequencer、MultiProducerSequencer ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。
此接口有两个实现类 SingleProducerSequencer、MultiProducerSequencer ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。
等待策略
BlockingWaitStrategy:基于 ReentrantLock+Condition 来实现等待和唤醒操作,实现代码非常简单,是 Disruptor 默认的等待策略。
虽然最慢,但也是 CPU 使用率最低和最稳定的选项生产环境推荐使用。
虽然最慢,但也是 CPU 使用率最低和最稳定的选项生产环境推荐使用。
BusySpinWaitStrategy:性能很好,存在持续自旋的风险,使用不当会造成 CPU 负载 100%,慎用。
LiteBlockingWaitStrategy:基于 BlockingWaitStrategy 的轻量级等待策略,在没有锁竞争的时候会省去唤醒操作,但是作者说测试不充分,因此不建议使用。
TimeoutBlockingWaitStrategy:带超时的等待策略,超时后会执行业务指定的处理逻辑。
LiteTimeoutBlockingWaitStrategy:基于TimeoutBlockingWaitStrategy的策略,当没有锁竞争的时候会省去唤醒操作。
SleepingWaitStrategy:三段式策略,第一阶段自旋,第二阶段执行 Thread.yield 让出 CPU,第三阶段睡眠执行时间,反复的睡眠。
YieldingWaitStrategy:二段式策略,第一阶段自旋,第二阶段执行 Thread.yield 交出 CPU。
PhasedBackoffWaitStrategy:四段式策略,第一阶段自旋指定次数,第二阶段自旋指定时间,第三阶段执行 Thread.yield 交出 CPU,
第四阶段调用成员变量的waitFor方法,该成员变量可以被设置为BlockingWaitStrategy、LiteBlockingWaitStrategy、SleepingWaitStrategy三个中的一个。
第四阶段调用成员变量的waitFor方法,该成员变量可以被设置为BlockingWaitStrategy、LiteBlockingWaitStrategy、SleepingWaitStrategy三个中的一个。
快的原因
RingBuffer(环形数组) : Disruptor 内部的 RingBuffer 是通过数组实现的。
由于这个数组中的所有元素在初始化时一次性全部创建,因此这些元素的内存地址一般来说是连续的。
由于这个数组中的所有元素在初始化时一次性全部创建,因此这些元素的内存地址一般来说是连续的。
避免了伪共享问题:CPU 缓存内部是按照 Cache Line(缓存行)管理的,一般的 Cache Line 大小在 64 字节左右。
为了确保目标字段独占一个 Cache Line,会在目标字段前后增加了 64 个字节的填充(前 56 后 8 ),可以避免 伪共享(False Sharing)问题。
为了确保目标字段独占一个 Cache Line,会在目标字段前后增加了 64 个字节的填充(前 56 后 8 ),可以避免 伪共享(False Sharing)问题。
无锁设计:Disruptor 采用无锁设计,避免了传统锁机制带来的竞争和延迟。
Disruptor 的无锁实现起来比较复杂,主要是基于 CAS、内存屏障(Memory Barrier)、RingBuffer 等技术实现的。
Disruptor 的无锁实现起来比较复杂,主要是基于 CAS、内存屏障(Memory Barrier)、RingBuffer 等技术实现的。
总结
Disruptor 之所以能够如此快,是基于一系列优化策略的综合作用,既充分利用了现代 CPU 缓存结构的特点,又避免了常见的并发问题和性能瓶颈。
Pulsar
定义
下一代云原生分布式消息流平台,最初由 Yahoo 开发 ,已经成为 Apache 顶级项目。
Pulsar 集消息、存储、轻量化函数式计算为一体,采用计算与存储分离架构设计,支持多租户、持久化存储、多机房跨区域数据复制,
具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。
具有强一致性、高吞吐、低延时及高可扩展性等流数据存储特性,被看作是云原生时代实时消息流传输、存储和计算最佳解决方案。
特性
是下一代云原生分布式消息流平台。
Pulsar 的单个实例原生支持多个集群,可跨机房在集群间无缝地完成消息复制。
极低的发布延迟和端到端延迟。
可无缝扩展到超过一百万个 topic。
简单的客户端 API,支持 Java、Go、Python 和 C++。
主题的多种订阅模式(独占、共享和故障转移)。
通过 Apache BookKeeper 提供的持久化消息存储机制保证消息传递 。
由轻量级的 serverless 计算框架 Pulsar Functions 实现流原生的数据处理。
基于 Pulsar Functions 的 serverless connector 框架 Pulsar IO 使得数据更易移入、移出 Apache Pulsar。
分层式存储可在数据陈旧时,将数据从热存储卸载到冷/长期存储(如 S3、GCS)中。
ActiveMQ
目前已经被淘汰,不推荐使用,不建议学习。
选择
ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用,已经被淘汰了。
RabbitMQ 在吞吐量方面虽然稍逊于 Kafka、RocketMQ 和 Pulsar,但是由于它基于 Erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。
但也因为基于 Erlang 开发,国内很难进行源码级的研究和定制,如果业务场景对并发量要求不高(十万级、百万级),RabbitMQ 是首选。
但也因为基于 Erlang 开发,国内很难进行源码级的研究和定制,如果业务场景对并发量要求不高(十万级、百万级),RabbitMQ 是首选。
RocketMQ 和 Pulsar 支持强一致性,对消息一致性要求比较高的场景可以使用。
RocketMQ 阿里出品,Java 系开源项目,源代码可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。
Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。
同时 Kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。
Kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略。
天然适合大数据实时计算以及日志收集。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,几乎是全世界这个领域的事实性规范。
同时 Kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。
Kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略。
天然适合大数据实时计算以及日志收集。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,几乎是全世界这个领域的事实性规范。
高可用
高可用系统
高可用描述的是一个系统在大部分时间都是可用的,可以为我们提供服务的。高可用代表系统即使在发生硬件故障或者系统升级的时候,服务仍然是可用的。
不可用情况
黑客攻击。
硬件故障,比如服务器坏掉。
并发量/用户请求量激增导致整个服务宕掉或者部分服务不可用。
代码中的坏味道导致内存泄漏或者其他问题导致程序挂掉。
网站架构某个重要的角色比如 Nginx 或者数据库突然不可用。
自然灾害或者人为破坏。
提高可用性方法
注重代码质量,测试严格把关
最重要。
比较实际可用的就是 CodeReview,不要在乎每天多花的那 1 个小时左右的时间,作用可大着呢!
神器
Sonarqube
Alibaba 开源的 Java 诊断工具 Arthas
阿里巴巴 Java 代码规范(Alibaba Java Code Guidelines)
IDEA 自带的代码分析等工具
使用集群,减少单点故障
限流
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,
当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
超时和重试机制设置
一旦用户请求超过某个时间的得不到响应,就抛出异常。
熔断机制
超时和重试机制设置之外,熔断机制也是很重要的。
熔断机制说的是系统自动收集所依赖服务的资源使用情况和性能指标,
当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。
当所依赖的服务恶化或者调用失败次数达到某个阈值的时候就迅速失败,让当前系统立即切换依赖其他备用服务。
比较常用的流量控制和熔断降级框架是 Netflix 的 Hystrix 和 alibaba 的 Sentinel。
异步调用
异步调用的话我们不需要关心最后的结果,这样我们就可以用户请求完成之后就立即返回结果,具体处理我们可以后续再做,秒杀场景用这个还是蛮多的。
使用缓存
如果我们的系统属于并发量比较高的话,如果我们单纯使用数据库的话,当大量请求直接落到数据库可能数据库就会直接挂掉。
灰度发布
定义
灰度发布(⼜名⾦丝雀发布) 是⼀种平滑发布新版本系统的⽅式。
是先分配⼀⼩部分请求流量到新版本,看看有没有问题,没问题的话,再⼀点点地增加流量,最终让所有流量都切换到新版本。
为了直观对新版本服务的稳定性进⾏观测,灰度发布的正确完成还需要依赖可靠的 监控系统 。
方式
AB 测试
是把⽤户分成两组,⼀组⽤户使⽤ A ⽅案(新版本),⼀组⽤户使⽤ B ⽅案(⽼版本)。
方案
基于 Nginx+OpenResty+Redis+Lua 实现流量动态分流来实现灰度发布,新浪的 ABTestingGateway 就是这种基于这种⽅案的⼀个开源项⽬。
使⽤ Jenkins + Nginx 实现灰度发布策,这种⽅案的原理和第⼀种类似,都是通过对 Nginx ⽂件的修改来实现流量的定向分流。
类似地,如果你⽤到了其他⽹关⽐如 Spring Cloud Gateway 的话,思路也是⼀样的。
另外, Spring Cloud Gateway 配合 Spring Cloud LoadBalancer(官⽅推荐)/Ribbon 也可以实现简单的灰度发布。
类似地,如果你⽤到了其他⽹关⽐如 Spring Cloud Gateway 的话,思路也是⼀样的。
另外, Spring Cloud Gateway 配合 Spring Cloud LoadBalancer(官⽅推荐)/Ribbon 也可以实现简单的灰度发布。
基于 Apollo 动态更新配置加上其⾃带的灰度发布策略来实现灰度发布。
是通过修改灰度发布配置的⽅式来实现灰度发布,如果灰度的配置测试没问题的话,再全量发布配置。
是通过修改灰度发布配置的⽅式来实现灰度发布,如果灰度的配置测试没问题的话,再全量发布配置。
通过⼀些现成的⼯具来做,⽐如说 Rainbond(云原⽣应⽤管理平台)就⾃带了灰度发布解决⽅案并且还⽀持滚动发布和蓝绿发布。
Flagger 是⼀种渐进式交付⼯具,可⾃动控制 Kubernetes 上应⽤程序的发布过程。
通过指标监控和运⾏⼀致性测试,将流量逐渐切换到新版本,降低在⽣产环境中发布新软件版本导致的⻛险。
Flagger 可以使⽤ Service Mesh(App Mesh,Istio,Linkerd)或 Ingress Controller(Contour,Gloo,Nginx)来实现多种部署策略(⾦丝雀发布,A/B
测试,蓝绿发布)。
通过指标监控和运⾏⼀致性测试,将流量逐渐切换到新版本,降低在⽣产环境中发布新软件版本导致的⻛险。
Flagger 可以使⽤ Service Mesh(App Mesh,Istio,Linkerd)或 Ingress Controller(Contour,Gloo,Nginx)来实现多种部署策略(⾦丝雀发布,A/B
测试,蓝绿发布)。
其他
核心应用和服务优先使用更好的硬件。
监控系统资源使用情况增加报警设置。
注意备份,必要时候回滚。
定期检查/更换硬件: 如果不是购买的云服务的话,定期还是需要对硬件进行一波检查的,对于一些需要更换或者升级的硬件,要及时更换或者升级。
冗余设计
定义
冗余设计是保证系统和数据高可用的最常的手段。
对于服务来说,冗余的思想就是相同的服务部署多份,
如果正在使用的服务突然挂掉的话,系统可以很快切换到备份服务上,大大减少系统的不可用时间,提高系统的可用性。
如果正在使用的服务突然挂掉的话,系统可以很快切换到备份服务上,大大减少系统的不可用时间,提高系统的可用性。
对于数据来说,冗余的思想就是相同的数据备份多份,这样就可以很简单地提高数据的安全性。
应用
高可用集群
高可用集群(High Availability Cluster,简称 HA Cluster)
同一份服务部署两份或者多份,当正在使用的服务突然挂掉的话,可以切换到另外一台服务,从而保证服务的高可用。
同城灾备
一整个集群可以部署在同一个机房,而同城灾备中相同服务部署在同一个城市的不同机房中。
并且,备用服务不处理请求。这样可以避免机房出现意外情况比如停电、火灾。
并且,备用服务不处理请求。这样可以避免机房出现意外情况比如停电、火灾。
异地灾备
类似于同城灾备,不同的是,相同服务部署在异地(通常距离较远,甚至是在不同的城市或者国家)的不同机房中。
同城多活
类似于同城灾备,但备用服务可以处理请求,这样可以充分利用系统资源,提高系统的并发。
异地多活
将服务部署在异地的不同机房中,并且,它们可以同时对外提供服务。
故障转移
简单来说就是实现不可用服务快速且自动地切换到可用服务,整个过程不需要人为干涉。
服务限流
定义
针对软件系统来说,限流就是对请求的速率进行限制,避免瞬时的大量请求击垮软件系统。
毕竟,软件系统的处理能力是有限的。如果说超过了其处理能力的范围,软件系统可能直接就挂掉了。
毕竟,软件系统的处理能力是有限的。如果说超过了其处理能力的范围,软件系统可能直接就挂掉了。
限流可能会导致用户的请求无法被正确处理,不过,这往往也是权衡了软件系统的稳定性之后得到的最优解。
算法
固定窗口计数器算法
固定窗口其实就是时间窗口。固定窗口计数器算法 规定了我们单位时间处理的请求数量。
滑动窗口计数器算法
滑动窗口计数器算法 算的上是固定窗口计数器算法的升级版。
滑动窗口计数器算法相比于固定窗口计数器算法的优化在于:它把时间以一定比例分片 。
当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。
漏桶算法
定义
我们可以把发请求的动作比作成注水到桶中,我们处理请求的过程可以比喻为漏桶漏水。
我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
我们往桶中以任意速率流入水,以一定速率流出水。当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
如果想要实现这个算法的话也很简单,准备一个队列用来保存请求,然后我们定期从队列中拿请求来执行就好了(和消息队列削峰/限流的思想是一样的)。
漏桶算法可以控制限流速率,避免网络拥塞和系统过载。但漏桶算法无法应对突然激增的流量,因为只能以固定的速率处理请求,对系统资源利用不够友好。
实际业务场景中,基本不会使用漏桶算法。
令牌桶算法
令牌桶算法也比较简单。和漏桶算法算法一样,我们的主角还是桶(这限流算法和桶过不去啊)。
不过现在桶里装的是令牌了,请求在被处理之前需要拿到一个令牌,请求处理完毕之后将这个令牌丢弃(删除)。
我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了。
不过现在桶里装的是令牌了,请求在被处理之前需要拿到一个令牌,请求处理完毕之后将这个令牌丢弃(删除)。
我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了。
令牌桶算法可以限制平均速率和应对突然激增的流量,还可以动态调整生成令牌的速率。
不过,如果令牌产生速率和桶的容量设置不合理,可能会出现问题比如大量的请求被丢弃、系统过载。
不过,如果令牌产生速率和桶的容量设置不合理,可能会出现问题比如大量的请求被丢弃、系统过载。
限流对象
IP :针对 IP 进行限流,适用面较广,简单粗暴。
业务 ID:挑选唯一的业务 ID 以实现更针对性地限流。例如,基于用户 ID 进行限流。
个性化:根据用户的属性或行为,进行不同的限流策略。
例如, VIP 用户不限流,而普通用户限流。
根据系统的运行指标(如 QPS、并发调用数、系统负载等),动态调整限流策略。例如,当系统负载较高的时候,控制每秒通过的请求减少。
例如, VIP 用户不限流,而普通用户限流。
根据系统的运行指标(如 QPS、并发调用数、系统负载等),动态调整限流策略。例如,当系统负载较高的时候,控制每秒通过的请求减少。
阿里的 Sentinel 还支持 基于调用关系的限流(包括基于调用方限流、基于调用链入口限流、关联流量限流等)
以及更细维度的 热点参数限流(实时的统计热点参数并针对热点参数的资源调用进行流量控制)。
以及更细维度的 热点参数限流(实时的统计热点参数并针对热点参数的资源调用进行流量控制)。
单机限流
单机限流针对的是单体架构应用。
RateLimiter
单机限流可以直接使用 Google Guava 自带的限流工具类 RateLimiter 。 RateLimiter 基于令牌桶算法,可以应对突发流量。
除了最基本的令牌桶算法(平滑突发限流)实现之外,Guava 的 RateLimiter 还提供了 平滑预热限流 的算法实现。
平滑突发限流就是按照指定的速率放令牌到桶里,而平滑预热限流会有一段预热时间,预热时间之内,速率会逐渐提升到配置的速率。
Bucket4j
一个非常不错的基于令牌/漏桶算法的限流库。
相对于,Guava 的限流工具类来说,Bucket4j 提供的限流功能更加全面。
不仅支持单机限流和分布式限流,还可以集成监控,搭配 Prometheus 和 Grafana 使用。
不仅支持单机限流和分布式限流,还可以集成监控,搭配 Prometheus 和 Grafana 使用。
Spring Cloud Gateway 中自带的单机限流的早期版本就是基于 Bucket4j 实现的。后来,替换成了 Resilience4j。
Resilience4j
一个轻量级的容错组件,其灵感来自于 Hystrix。
一般情况下,为了保证系统的高可用,项目的限流和熔断都是要一起做的。
Resilience4j 不仅提供限流,还提供了熔断、负载保护、自动重试等保障系统高可用开箱即用的功能。
并且,Resilience4j 的生态也更好,很多网关都使用 Resilience4j 来做限流熔断的。
并且,Resilience4j 的生态也更好,很多网关都使用 Resilience4j 来做限流熔断的。
因此,在绝大部分场景下 Resilience4j 或许会是更好的选择。如果是一些比较简单的限流场景的话,Guava 或者 Bucket4j 也是不错的选择。
分布式限流
分布式限流针对的分布式/微服务应用架构应用,在这种架构下,单机限流就不适用了,因为会存在多种服务,并且一种服务也可能会被部署多份。
方案
借助中间件架限流:可以借助 Sentinel 或者使用 Redis 来自己实现对应的限流逻辑。
网关层限流:比较常用的一种方案,直接在网关层把限流给安排上了。不过,通常网关层限流通常也需要借助到中间件/框架。
就比如 Spring Cloud Gateway 的分布式限流实现 RedisRateLimiter 就是基于 Redis+Lua 来实现的,
再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。
就比如 Spring Cloud Gateway 的分布式限流实现 RedisRateLimiter 就是基于 Redis+Lua 来实现的,
再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。
如果你要基于 Redis 来手动实现限流逻辑的话,建议配合 Lua 脚本来做。
减少了网络开销
原子性
如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码。
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,
比如 Java 中常用的数据结构实现、分布式锁、延迟队列等等。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
比如 Java 中常用的数据结构实现、分布式锁、延迟队列等等。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
RRateLimiter 的使用方式非常简单。我们首先需要获取一个RRateLimiter对象,直接通过 Redisson 客户端获取即可。然后,设置限流规则就好。
降级&熔断
降级
定义
降级是从系统功能优先级的⻆度考虑如何应对系统故障。
服务降级指的是当服务器压⼒剧增的情况下,根据当前业务情况及流量对⼀些服务和⻚⾯有策略的降级,以此释放服务器资源以保证核⼼任务的正常运⾏。
特征
原因:整体负荷超出整体负载承受能⼒。
⽬的:保证重要或基本服务正常运⾏,⾮重要服务延迟使⽤或暂停使⽤。
⼤⼩:降低服务粒度,要考虑整体模块粒度的⼤⼩,将粒度控制在合适的范围内。
可控性:在服务粒度⼤⼩的基础上增加服务的可控性,后台服务开关的功能是⼀项必要配置(单机可配置⽂件),可分为⼿动控制和⾃动控制。
次序:⼀般从外围延伸服务开始降级,需要有⼀定的配置项,重要性低的优先降级,如分组设置等级 1-10,当服务需降级到某⼀个级别时,进⾏相关配置。
方式
延迟服务:⽐如发表了评论,重要服务,⽐如在⽂章中显示正常,但是延迟给⽤户增加积分,只是放到⼀个缓存中,等服务平稳之后再执⾏。
在粒度范围内关闭服务(⽚段降级或服务功能降级):⽐如关闭相关⽂章的推荐,直接关闭推荐区。
⻚⾯异步请求降级:⽐如商品详情⻚上有推荐信息/配送⾄等异步加载的请求,如果这些信息响应慢或者后端服务有问题,可以进⾏降级。
⻚⾯跳转(⻚⾯降级):⽐如可以有相关⽂章推荐,但是更多的⻚⾯则直接跳转到某⼀个地址。
写降级:⽐如秒杀抢购,我们可以只进⾏ Cache 的更新,然后异步同步扣减库存到 DB,保证最终⼀致性即可,此时可以将 DB 降级为 Cache。
读降级:⽐如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种⽅式适⽤于对读⼀致性要求不⾼的场景。
分类
⾃动开关降级
超时降级:主要配置好超时时间和超时重试次数和机制,并使⽤异步机制探测回复情况。
失败次数降级:主要是⼀些不稳定的 api,当失败调⽤次数达到⼀定阀值⾃动降级,同样要使⽤异步机制探测回复情况。
故障降级:⽐如要调⽤的远程服务挂掉了(⽹络故障、DNS 故障、http 服务返回错误的状态码、rpc 服务抛出异常),则可以直接降级。
降级后处理⽅案:默认值(库存服务挂了,返回默认现货)、兜底数据(⼴告挂了,返回提前准备好的⼀些静态⻚⾯)、缓存(之前暂存的⼀些缓存数据)。
降级后处理⽅案:默认值(库存服务挂了,返回默认现货)、兜底数据(⼴告挂了,返回提前准备好的⼀些静态⻚⾯)、缓存(之前暂存的⼀些缓存数据)。
限流降级:当去秒杀或者抢购⼀些限购商品时,可能会因为访问量太⼤⽽导致系统崩溃,此时开发者会使⽤限流来进⾏限制访问量,当达到限流阀值,后续请求会被降级;降级后处理⽅案:排队⻚⾯(将⽤户导流到排队⻚⾯等⼀会重试)、⽆货(直接告知⽤户没货了)、错误⻚(如活动太⽕爆了,稍后重试)。
⼈⼯开关降级(秒杀、电商⼤促等)
分布式系统
需要技术和产品提前对业务和系统进⾏梳理,根据梳理结果确定哪些服务可以降级,哪些服务不可以降级,降级策略是什么,降级顺序怎么样。
熔断
定义
熔断是应对微服务雪崩效应的⼀种链路保护机制,类似股市、保险丝。
降级和熔断
降级的⽬的在于应对系统⾃身的故障,⽽熔断的⽬的在于应对当前系统依赖的外部系统或者第三⽅系统的故障。
组件
Hystrix
Hystrix 是 Netflix 开源的熔断降级组件。
Sentinel
Sentinel 是阿⾥中间件团队开源的⼀款不光具有熔断降级功能,同时还⽀持系统负载保护的组件。
Spring Retry
Resilience4J
超时&重试
定义
由于网络问题、系统或者服务内部的 Bug、服务器宕机、操作系统崩溃等问题的不确定性,系统或者服务永远不可能保证时刻都是可用的状态。
为了最大限度的减小系统或者服务出现故障之后带来的影响,我们需要用到的 超时(Timeout) 和 重试(Retry) 机制。
超时机制
当一个请求超过指定的时间(比如 1s)还没有被处理的话,这个请求就会直接被取消并抛出指定的异常或者错误(比如 504 Gateway Timeout)。
分类
连接超时(ConnectTimeout):客户端与服务端建立连接的最长等待时间。
读取超时(ReadTimeout):客户端和服务端已经建立连接,客户端等待服务端处理完请求的最长时间。实际项目中,关注比较多的还是读取超时。
如果没有设置超时的话,就可能会导致服务端连接数爆炸和大量请求堆积的问题。
这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。
这些堆积的连接和请求会消耗系统资源,影响新收到的请求的处理。严重的情况下,甚至会拖垮整个系统或者服务。
设置
超时到底设置多长时间是一个难题!超时值设置太高或者太低都有风险。
太高
会降低超时机制的有效性,比如你设置超时为 10s 的话,那设置超时就没啥意义了,系统依然可能会出现大量慢请求堆积的问题。
太低
可能会导致在系统或者服务在某些处理请求速度变慢的情况下(比如请求突然增多),
大量请求重试(超时通常会结合重试)继续加重系统或者服务的压力,进而导致整个系统或者服务被拖垮的问题。
大量请求重试(超时通常会结合重试)继续加重系统或者服务的压力,进而导致整个系统或者服务被拖垮的问题。
重试机制
重试机制一般配合超时机制一起使用,指的是多次发送相同的请求来避免瞬态故障和偶然性故障。
瞬态故障可以简单理解为某一瞬间系统偶然出现的故障,并不会持久。偶然性故障可以理解为哪些在某些情况下偶尔出现的故障,频率通常较低。
重试的核心思想是通过消耗服务器的资源来尽可能获得请求更大概率被成功处理。
由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。
由于瞬态故障和偶然性故障是很少发生的,因此,重试对于服务器的资源消耗几乎是可以被忽略的。
策略
固定间隔时间重试
每次重试之间都使用相同的时间间隔,比如每隔 1.5 秒进行一次重试。
优点
实现起来比较简单,不需要考虑重试次数和时间的关系,也不需要维护额外的状态信息。
缺点
可能会导致重试过于频繁或过于稀疏,从而影响系统的性能和效率。
如果重试间隔太短,可能会对目标系统造成过大的压力,导致雪崩效应;如果重试间隔太长,可能会导致用户等待时间过长,影响用户体验。
应用
适用于目标系统恢复时间比较稳定和可预测的场景,比如网络波动或服务重启。
梯度间隔重试
根据重试次数的增加去延长下次重试时间,比如第一次重试间隔为 1 秒,第二次为 2 秒,第三次为 4 秒,以此类推。
优点
能够有效提高重试成功的几率,也能通过柔性化的重试避免对下游系统造成更大压力。
缺点
实现起来比较复杂,需要考虑重试次数和时间的关系,以及设置合理的上限和下限值。
可能会导致用户等待时间过长,影响用户体验。
应用
适用于目标系统恢复时间比较长或不可预测的场景,比如网络故障和服务故障。
重试次数
重试的次数不宜过多,否则依然会对系统负载造成比较大的压力。
重试的次数通常建议设为 3 次。大部分情况下,我们还是更建议使用梯度间隔重试策略。
重试幂等
超时和重试机制在实际项目中使用的话,需要注意保证同一个请求没有被多次执行。
触发
客户端等待服务端完成请求完成超时但此时服务端已经执行了请求,只是由于短暂的网络波动导致响应在发送给客户端的过程中延迟了。
实现
如果要手动编写代码实现重试逻辑的话,可以通过循环(例如 while 或 for 循环)或者递归实现。
第三方开源库提供了更完善的重试机制实现,例如 Spring Retry、Resilience4j、Guava Retrying。(推荐)
性能测试
不同角色看网站性能
用户
根据响应速度的快慢来评判网站的性能。
开发人员
项目架构是分布式的吗?
用到了缓存和消息队列没有?
高并发的业务有没有特殊处理?
数据库设计是否合理?
系统用到的算法是否还需要优化?
系统是否存在内存泄露的问题?
项目使用的 Redis 缓存多大?服务器性能如何?用的是机械硬盘还是固态硬盘?
测试人员
响应时间、请求成功率、吞吐量。
运维人员
根据基础设施和资源的利用率来判断网站的性能。
注意
性能测试之前更需要你了解当前的系统的业务场景。
历史数据非常有用。
指标
响应时间
响应时间就是用户发出请求到用户收到系统处理结果所需要的时间。
2-5-8 原则:通常来说,2 到 5 秒 页面体验会比较好,5 到 8 秒还可以接受,8 秒以上基本就很难接受了。据统计当网站慢一秒就会流失十分之一的客户。
并发数
并发数是系统能同时处理请求的数目即同时提交请求的用户数目。
吞吐量
吞吐量指的是系统单位时间内系统处理的请求数量。衡量吞吐量有几个重要的参数:QPS(TPS)、并发数、响应时间。
QPS(Query Per Second):服务器每秒可以执行的查询次数。
TPS(Transaction Per Second):服务器每秒处理的事务数(这里的一个事务可以理解为客户发出请求到收到服务器的过程)。
并发数;系统能同时处理请求的数目即同时提交请求的用户数目。
响应时间:一般取多次请求的平均响应时间。
QPS(TPS) = 并发数/平均响应时间;并发数 = QPS*平均响应时间;
性能计数器
性能计数器是描述服务器或者操作系统的一些数据指标如内存使用、CPU 使用、磁盘与网络 I/O 等情况。
分类
性能测试
通过测试工具模拟用户请求系统,目的主要是为了测试系统的性能是否满足要求。
负载测试
对被测试的系统继续加大请求压力,直到服务器的某个资源已经达到饱和了。
压力测试
不去管系统资源的使用情况,对系统继续加大请求压力,直到服务器崩溃无法再继续提供服务。
稳定性测试
模拟真实场景,给系统一定压力,看看业务是否能稳定运行。
工具
后端
Jmeter:Apache JMeter 是 JAVA 开发的性能测试工具。
LoadRunner:一款商业的性能测试工具。(收费)
Galtling:一款基于 Scala 开发的高性能服务器性能测试工具。
ab:全称为 Apache Bench 。Apache 旗下的一款测试工具,非常实用。
前端
Fiddler:抓包工具,它可以修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是 Web 调试的利器。
HttpWatch: 可用于录制 HTTP 请求信息的工具。
优化策略
系统是否需要缓存?
系统架构本身是不是就有问题?
系统是否存在死锁的地方?
系统是否存在内存泄漏?(Java 的自动回收内存虽然很方便,但是,有时候代码写的不好真的会造成内存泄漏)
数据库索引使用是否合理?
0 条评论
下一页