java面试技术基础
2024-06-18 14:03:15 0 举报
AI智能生成
java面试技术基础
作者其他创作
大纲/内容
应用(Java)
Java
基础
抽象/接口
组合
接口
新类可以组合多个接口
抽象类
只能继承单一抽象类
状态
接口
不能包含属性(除了静态属性,不支持对象状态)
抽象类
可以包含属性,非抽象方法可能引用这些属性
默认方法 和 抽象方法
接口
不需要在子类中实现默认方法。默认方法可以引用其他接口的方法
抽象类
必须在子类中实现抽象方法
构造器
接口
没有构造器
抽象类
可以有构造器
可见性
接口
隐式 public
抽象类
可以是 protected 或 “friendly”
接口
字段
自动是 static 和 final
内部类
定义
一个定义在另一个类中的类,叫作内部类。
使用理由
实现了某类型的接口,可以创建并返回对其的引用
要解决一个复杂的问题,想创建一个类来辅助解决方案,但是又不希望这个类是公共可用的
最吸引人的地方
每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类有效地实现了“多重继承”
不能被重写
匿名内部类
要使用一个外部环境(在本匿名内部类之外定义)对象,那么编译器会要求其(该对象)参数引用是 final 或者是 “effectively final”(也就是说,该参数在初始化后不能被重新赋值,所以可以当作 final)的
局部内部类
不是外部类的一部分,所以不能有访问说明符
可以访问当前代码块内的常量,以及此外部类的所有成员
闭包
定义
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域
回调
定义
对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象
标识符
非匿名
A$a.class
匿名
A$数字.class
初始化
构造器
initialize()
重载
无参构造
有参构造
不采用返回值区分构造,因为无明确类型引用的情况下不能区分
全局变量
先被初始化为默认值,然后被初始化为构造器指定的值
隐式static
如果子类重写了父类的方法,而父类的构造器中引用了该方法,则并不会初始化子类的数据
对象初始化顺序
通常
Java解释器定位到对应class
首次加载完class之后,初始化所有静态初始化
new 开始创建对象,分配堆内存
将分配的对象内存清零,为属性赋默认值
执行所有出现在字段定义处的初始化动作
执行构造器
static加载顺序
所有的 static 对象和 static 代码块在加载时按照文本的顺序
构造器加载顺序
顶级父类会被最先构造,然后是它的派生类
按声明顺序初始化成员
调用派生类构造器的方法体
静态初始化
直接引用静态变量或方法的情况下,构造器不执行
类
类的代码在首次使用时加载
static
概念
由于方法或字段的使用,通常需要通过new分配内存之后才可以被使用,因此在 1. 只想为一个域(字段或方法)分配一个共享内存,不考虑创建对象;2. c创建一个与对象无关的方法(任何情况下都可以调用) 场景下new关键字不再适用。基于特定,在一个类中创建一个带有static的变量,即使通过new创建多个对象,被static修饰的字段内存都是同一个
作用域
代码块
方法
变量
final
数据
一个被 static 和 final 同时修饰的属性只会占用一段不能改变的存储空间
场景
一个永不改变的编译时常量
一个在运行时初始化就不会改变的值
类型
基本类型
数值恒定不变
对象引用
不能改为指向其他对象,对象本身是可以修改的,Java 没有提供将任意对象设为常量的方法
空白 final
编译器确保空白 final 在使用前必须被初始化
初始化赋值
构造器赋值
final 参数
在方法中不能改变参数指向的对象或基本变量
方法
原因
给方法上锁,防止子类通过覆写改变方法的行为
效率
在早期的 Java 实现中,如果将一个方法指明为 final,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 final 方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用
final 和 private
类中所有的 private 方法都隐式地指定为 final。因为不能访问 private 方法,所以不能覆写它
类
不能被继承,之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类
this
如果在一个类中有相同的方法或字段,使用this会生成一个当前类的引用对象,但只能在非static方法中调用
String
概念
String 对象是不可变的,String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象,以包含修改后的字符串内容。而最初的 String 对象则丝毫未动。
意外递归
重写toString()方法中使用了 字符串 + this,而this并不是字符串于是,默认调用 this.toString(),于是形成递归。需要使用ssuper.toString(),从而调用Object的方法
紧凑字符串
在Java11中使用了紧凑字符串,因为通常来说堆中最常见的就是字符串,因此在Java11优化字符串后是Java8的75%左右的使用率
去重
通过G1收集器自动去重(默认关闭)
方法
通过G1在垃圾收集过程中找到重复字符串,然后去重,将所有引用指向单一副本
默认关闭的原因
需要在G1 GC的新生代回收和混合回收阶段进行额外处理,会延长垃圾收集时间
需要一个额外线程与应用线程同时运行,可能会暂用程序线程的CPU周期
如果重复字符串非常少,那么应用程序的内存使用量会更高,因为额外占用的内存来自于跟踪字符串以寻找重复而产生的簿记工作
通过String.intern()方法
保留字符串在特殊的哈希表中,后者在原生内存中(而字符串本身在堆中)
自定义方法
可以自定义方法去重,但GC可能会面临较大压力,需要谨慎测试
连接
+
字节码底层依旧会使用StringBuilder,但是会重复创建StringBuilder对象
JDK11中优化了部分数据类型的连接,单行连接会变快
StringBuilder
只会创建一次对象,且线程安全
JVM
类型信息
怎么知道类型
“传统的” RTTI:假定我们在编译时已经知道了所有的类型;
“反射”机制:允许我们在运行时发现和使用类的信息。
Class 对象
Class对象在运行时表示了与类有关的信息,编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用”类加载器”子系统把这个类加载到内存中。
类加载器
类加载器子系统可能包含一条类加载器链,但有且只有一个原生类加载器,它是 JVM 实现的一部分。原生类加载器加载的是“可信类”(包括 Java API 类)。如果有特殊需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。
类都是第一次使用时动态加载到 JVM 中(当程序创建第一个对类的静态成员的引用时,就会加载这个类)(其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用。)
加载顺序
普通加载 与 类.class 加载(延迟初始化)
只引用 非final/非常量
加载
首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码),并从这些字节码中创建一个 Class 对象。
链接
类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码。为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。
初始化
如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象
只引用 static final 常量
加载
链接
Class.forName()加载(立刻初始化)
加载
链接
初始化
CDS(class data sharing)类数据共享
概念
是JVM之间共享类元数据的一种机制。通常每个JVM都有自己的类元数据,会占用一些物理内存。如果进行CDS,那么只需要在内存中保留一份副本。虽然共享所有的类元数据会占用更多的元空间内存,但是相对与普通的堆内存来说还是非常小的,通常应用程序的对象占用更多。即使如此,还是能带来很大的性能提升,在多个JVM情况下,还是省了内存。
Java8之前是分类型工作的,Java11之后会合并
类型
常规的CDS(共享默认的JDK类)
应用程序类数据共享
类型转换
强制转换
cast()
比较
==
比较实际的Class 对象
instanceof
说的是“你是这个类,还是从这个类派生的类?”
动态代理
定义
一个对象封装真实对象,代替其提供其他或不同的操作
使用场景
将额外的操作与“真实对象”做分离
实现方法
不仅动态创建代理对象而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个调用处理程序,该处理程序负责发现调用的内容并决定如何处理
Proxy.newProxyInstance()
类加载器
代理实现的接口列表(不是类或抽象类)
InvocationHandler 的一个实现
反射
不会发生真实修改final字段的值
性能优化手段
写出更好的算法,使用时间复杂度或空间复杂度更好的算法,提高程序性能
写更少的代码,过量的代码可能会导致更多的内存分配或者更复杂的结构,增加了复杂度,可能会增加
不要过早优化,防止出现更复杂的代码并没有带来性能提升,反而出现不稳定的因素
数据库或其他中间件通常是也是瓶颈,不要单方面从Java层次优化
性能测试方法
微基准测试
概念
通过测试一小部分代码的性能来确定多种实现中的哪个好
宏基准测试
概念
要测试一个系统的性能,最好的对象就是应用程序本身,加上他使用的任何外部资源
介基准测试
概念
位于微基准与宏基准之间,通常用于开发人员测试某个单一场景下的性能测试
性能指标
吞吐量
概念
所有客户端完成的操作数之和,通常以秒锁单位
类型
每秒事务数(TPS)
每秒请求数(RPS)
每秒操作数(OPS)
批处理时间
一个任务的运行时间
响应时间
客户端发送到收到请求的时间
监控工具
操作系统工具
CPU利用率
Linux 命令
vmstat
空闲情况
应用程序阻塞在原语同步上,直到锁释放后才继续执行
应用程序在等待某些东西,例如数据库返回
应用程序无事可做
磁盘使用率
Linux 命令
iostat
w_await
每次I/O写操作的时间
si
换入
so
换出
网络使用率
Linux 命令
netstat
Java工具
jcmd
打印java进程中的类、线程和JVM信息
jconsole
提供JVM活动的图形化视图,包括线程的使用、类的使用和GC活动
jmap
提供堆转储和其他JVM内存使用的信息
jinfo
查看JVM系统属性,允许设置一些系统属性
jstack
转储Java进程的栈信息
jstat
提供GC和类加载活动的信息
编译
即时编译
语言类型
编译型语言
程序编写完成后,由静态编译器生成二进制文件。受限于CPU的汇编语言指令集
解释型语言
只要机器上有合适的解释器,在这些程序代码运行时,解释器将每一行代码都翻译成二进制代码
概念
在程序执行时,编译成基于JVM的字节码,这使得Java具有解释型语言的平台独立性,同时运行的更快
缺点
这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间
会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度
字节码(.class)
原理
当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。
热点编译
Java在执行时并不是编译所有代码,将会对调用或循环等次数多的代码进行编译,相对于只执行一次哦的代码,解释执行Java代码会更快一点
惰性评估
意味着即时编译器只有在必要的时候才编译代码,从未被执行的代码也许就压根不会被 JIT 编译,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快
分层编译
C1编译器
编译器1,client编译器
C1比C2更早的开始编译,C1编译器的速度更快,与C2相比,编译的代码更多
C2编译器
编译器2,server编译器
C2编译器在等待时获得了信息,这些信息让C2能够对边以后的代码进行更好的优化
GraalVM
概念
一种新的虚拟机,可以运行Java、JS、Pyhtion、Ruby、R和其他JVM语言(Scala、Kotlin)等
性能方面贡献
插件技术允许GraalVM生成完全原生的二进制文件
用一个基于Java语言编写的编译器,用来支持常规的JVM运行
预编译
提前编译
概念
提前编译运行在运行应用程序之前先编译应用程序的一部分(或全部),编译后的代码会形成一个共享苦难,供JVM启动应用程序时使用
问题
目前,对于很小很简单的程序来说,可能并没有帮助,启动程序甚至会运行的更慢
GraalVM原生编译
概念
GraalVM生成的二进制文件启动速度很快,特别是对比JVM中运行的程序
问题
在GraalVM这种模式下,优化代码并没有很C2激进,所以程序运行的足够久,最终JVM可能会胜出
GC
分代
老年代
空间被填满时,会触发Full GC,如果有足够的CPU可以选择并发收集器来避免Full GC停顿时间过长
新生代
Eden空间
Yong GC 后整个空间的的对象要么移动(Survivors/老年代)要么被丢弃,可能会产生STW停顿
Survivor空间
这个空间的目的是为了让对象在新生代停留多个周期,为了避免所有对象都在老年代才能被释放的可能性。但同时要注意是否会内存溢出,最好适当增加堆的大小,否则因为内存原因,也会让新生代对象很快进入老年代
类型
Serial
概念
运行在32位JVM的windows机器或者单处理的机器
Throughput
概念
有两个及以上CPU的64位机器的默认收集器,在CPU密集型场景下,如果Full GC较少或老年代一般是满的则相比G1收集器更有优势
操作
Minor GC
标记、释放和压缩年轻代
Full GC
标记、释放和压缩老年代
优化
动态优化
设置GC(合理)的停顿时间,Throughput收集器会调整堆的大小来满足设置的停顿时间。如果设置时间过短,可能会产生频繁Full GC,影响JVM性能
静态优化
设置堆(合理)的大小,如果设置过小,同样会频繁Full GC,如果设置过大,GC时间可能会变成
G1
概念
有两个及以上CPU的64位机器的JDK11之后默认收集器,使用并发回收策略来以最小停顿回收。G1将堆划分为多个Region,用region分别组成新生代和老年代,新生代回收还是可能会触发STW。在老年代回收时,是利用后台线程进行回收,当然着牺牲了CPU的周期,同时因为被划分多个区域,可以利用区域复制并清理的操作来回收
核心
区域数量
默认2048个区域
区域大小
1M-32M
垃圾优先
专注于回收大部分是垃圾的区域
并发回收
标记老年代中不使用的对象线程与应用程序线程同时发生,但G1并不是完全并发,因为新生代的标记和压缩仍需要短暂停顿应用程序线程
操作
新生代回收
后台并发标记周期
如果发生新生代回收,并发标记会被中断,回收后完毕后会重新标记并清理,这两个阶段会产生STW
混合回收
Mixed混合回收包括执行新生代回收和后台标记,回收部分老年代
必要的Full GC
问题
并发模式失败
增加堆大小,后台需要处理的更快,或者优化标记周期
晋升失败
混合回收需要更快的执行,或处理更多老年代的垃圾
巨型对象分配失败
元数据GC阈值
JDK8需要触发Full GC进行回收,JDK11可以被回收,不需要Full GC
频繁Full GC
增加老年代大小或整个堆的大小,如果大小合理,适当调整分配比例
如果是多核CPU,可以增加后台线程数
更加频繁的执行G1 GC后台活动
增加Mixed GC周期工作量
STW超过200ms
会自动进行弥补
CMS(不推荐使用)
概念
并发回收,但是在Young GC时会停止所有应用程序线程,而且不能处理内存碎片。与G1不同的是,CMS的目标是尽可能不发生Full GC,意味着如果CMS经过精心调教,是不会发生内存调整的情况,所以通常来说,设置更大的堆和元空间是好选择
操作
回收新生代(暂停所有应用程序线程)
运行并发周期来清理老年代的数据
如有必要,执行Full GC来压缩老年代
问题
并发失败
增加堆大小,更频繁的使用后台线程,增加更多的线程
晋升失败
增加堆大小,更频繁的使用后台线程,增加更多的线程
ZGC/Shenandoah(实验性)
影响
堆不需要再分代
分代的核心目的是为了更快(停顿短/回收少)回收
停顿会减少
相比G1,不再有Full GC的长时间停顿,也不再有Young GC的短暂停顿,但是仍有非常短的(10ms左右)标记线程停顿
堆大小(默认)
最大值(Xmx)
物理(Docker)内存的1/4,如果遇到内存很少的机器,JVM会将堆的最大值限制在96M或更少
最小值(Xms)
物理(Docker)内存1/64
新生代
堆的1/3(33%)
元空间(默认)
概念
存储类信息相关的数据
最小值
20.75M
问题
调整元空间大小会导致Full GC ,可以调整服务器元空间大小至128M/192M或更大进行改善
元空间也可能会因为不断定义新的加载器且加载旧类,导致内存溢出。通常在元空间内存溢出前,堆可能会先溢出,需要结合jmap/jstat命令排查
GC日志
PrintGCDetails
分配大对象
大 是一个相对术语,取决于JVM中特定缓冲区的大小。这个缓冲区称为 线程本地分配缓冲区(thread-local allocation buffer,TLAB)。
TLAB(线程本地分配缓冲区)
Eden空间分配速度很快是因为存在TLAB,当对象直接分配在像Eden空间这样的共享空间时,需要使用同步机制来管理该空间内的空闲空间指针。通过给每个线程设置自己的专用分配区域,线程在分配对象时候就不需要进行任何同步了
如果对一个对象,已使用的TLAB无法再为其分配空间,那么通常有两种方式,一是清退当前的TLAB重新分配,二是直接分配到堆上
大对象必须直接分配在堆上,因为TLAB需要与堆进行同步,会消耗额外时间
在代码无法修改的情况下,花大量时间在堆上直接分配不如将分配移至TLAB上,这时候可以调整TLAB的大小,而TLAB空间基于Eden空间,可以增加Eden空间来同步增加TLAB空间
PLAB(晋升本地分配缓冲区)
每个线程都会将晋升的对象放到特定的PLAB中
巨型对象
巨型对象通常无法放入TLAB上,会直接在Eden中分配,最坏的情况是如果当前Eden空间无法分配将会直接分配到老年代中,这回扰乱该对象GC的正常生命周期
在G1中设定大于区域一半大小的对象(512K,G1区域最小的为1M)就是巨型对象,如果对象大于一个区域的大小,G1会寻找连续的区域为对象进行分配,且区域中空闲部分不再分配,好处是在G1的并发标记阶段,对于巨型对象就会进行回收
STW停顿(stop-the-world pause)
调整内存后会需要修改应用程序中的内存指针,这些修改所花费的时间是停顿的关键
要点
对象可能不被垃圾回收
如果 Java 虚拟机(JVM)并未面临内存耗尽的情形,它可能不会浪费时间执行垃圾回收以恢复内存
垃圾回收不等同于析构
在 C++ 中,对象总是被销毁的,在销毁对象时会调用这个函数
垃圾回收只与内存有关
回收程序不再使用的内存
方式
引用计数
原理
简单但速度很慢。每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。
引用计数常用来说明垃圾回收的工作方式,但似乎从未被应用于任何一种 Java 虚拟机实现中
缺点
很难确定循环引用的对象,因为其计数器均不唯一
解决方案
“活”的定义:一定能最终追溯到其存活在栈或静态存储区中的引用
这个引用链条可能会穿过数个对象层次,由此,如果从栈或静态存储区出发,遍历所有的引用,你将会发现所有”活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是该对象包含的所有引用,如此反复进行,直到访问完”根源于栈或静态存储区的引用”所形成的整个网络。
“自适应”
要点
如果所有对象都很稳定,垃圾回收的效率降低的话,就切换到”标记-清扫”方式。
,如果堆空间出现很多碎片,就会切换回”停止-复制”方式。
方式
停止-复制(stop-and-copy)
原理
先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的
当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了
当对象从一处复制到另一处,所有指向它的引用都必须修正。位于栈或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到
如果块在某处被引用,其年代数加 1,垃圾回收器会对上次回收动作之后新分配的块进行整理,大型对象仍然不会复制(只是年代数会增加)
缺点
效率低下
空间浪费
得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间
解决方案
某些 Java 虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间
复制本身
一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。
解决方案
一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即”自适应”)。这种模式称为标记-清扫(mark-and-sweep),对一般用途而言,”标记-清扫”方式速度相当慢,但是当你知道程序只会产生少量垃圾甚至不产生垃圾时,它的速度就很快了
垃圾回收动作发生的同时,程序将会暂停
标记-清扫(mark-and-sweep)
原理
从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。”标记-清扫”后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。
内存
类型
寄存器(Registers)
由于非常稀少,是直接提供CPU使用,根据CPU的使用情况来分配寄存器内存,且,程序基本无法找到直接操作寄存器的操作方法,因此不在寄存器进行分配内存
栈内存(Stack)
分配在RAM(random access memory),栈指针向下分配内存,向上回收内存,非常快速有效。因此,存入栈内存的对象需要准确知道位置,限制了灵活性。只存放对象引用等
堆内存(Heap)
同样处于RAM,但是编译器和程序不需要关系堆内存对象的位置及生命周期,因此对象实现存放于此,虽然灵活性得到了释放,但是管理对象生命周期变得复杂,且分配和释放需要更长的时间。不过,Java已经解决了很大的效率问题,因此不用过于关心
常量存储
常量通常只存放于代码中,且永远不会改变,因此只会存储在ROM(Read Only Memory)中
非RAM存储
数据存在于程序之外,不在程序管控的生命周期内。例如:1.序列化对象,将对象序列化为二进制字节流发送到另外的机器;2. 持久化对象,对象被c存放到磁盘上,不受程序启动和终止的影响。这些通常用于将对象发送到其他介质中,并且在需要时恢复到常规的、基于RAM的对象。例如JDBC等
堆内存
堆溢出
类型
原生空间不足
元空间不足
堆内存溢出
达到GC开销限制
优化
减少对象大小,提高GC效率
及早清理,可以在某些场景下将引用设置为null,协助释放内存
使用标准的或不可变的对象,减少对象的创建
对象重用
对象池
GC影响
对象重用会导致大量对象活跃,影响GC的效率,需要慎重使用。如果数量极少,影响就很少,注意对象池的数量
同步
需要同步,如果还存在删除和替换,必然还存在大量竞争,结果访问对象可能比创建对象还慢
限流
如果一直给对象池增加超过能力的负载,CPU和内存都会不堪重负,导致性能下降
线程局部变量
生命周期
线程内部
基数性
通常与线程数量一一对应
同步
由于在局部,不需要同步
软引用、弱引用和其他引用
概念
不确定的引用,会额外分配引用内存,并且至少需要两个GC周期才会回收
软引用
概念
本质上是一个大型的、最近最少使用的(LRU)对象池,关键是要确保它被及时清理
GC时机
没有其他强引用所引用,当软引用最近没有被访问时,就会在下个周期被释放
弱引用
概念
GC只会根据是否有强引用来引用来回收的引用
GC时机
跟正常的对象一样有完整的GC生命周期,一旦引用弱引用的引用被回收,那么弱引用在下一次GC时候会被立刻回收
虚引用
概念
对象不再被强引用所引用时,会比较快的被回收
终结器/最终引用(不推荐使用)
概念
需要调用方法Object.finalizer(),然后通过终结器队列进行清理
问题
如果对象是一个弱引用或软引用对象,然而同时调用了Finalizer,此时对象会再产生一个终结器队列的强引用,导致默认的软/弱引用的GC机制失效
解决方案
如果一定要使用该方法,则最好使用虚引用或者其他不确定引用重写终结队列
普通对象指针(oop)
概念
在64位机器上的引用(8bit)比在32位机器上的(4bit)oop多了一倍,在大量对象的情况下,引用会占据更多堆内存,导致更多的GC周期
压缩
JVM使用了压缩到35bit引用的办法,将oop尽可能的压缩,方法是 在64位寄存器时候向左移3位末位补0,当从32位寄存器取出时 向右移动3位高位补0
原生内存
JVM使用场景
解压缩器
原生NIO缓冲区
类信息
代码缓存
JVM运行内存
共享库
第三方扩展库
页
概念
操作系统用来管理物理内存的内存单位,是操作系统分配的最小单元
Linux
巨页
默认分配是2M,不能被交换,操作系统预留
透明巨页
可以与磁盘互相交换,操作系统按需分配
Windows
大页
并发
核心矛盾
矛盾
CPU、内存、I/O设备的速度差异
改进
CPU增加了缓存,以均衡与内存的速度差异
操作系统增加了进程、线程,以分时复用CPU,进而均衡CPU与I/O设备的速度差异
编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
改进引发的问题
CPU利用率
对IO密集型并无作用
竞争问题
共享内存的使用失误
上下文切换成本
在同一线程操作多种事务,可能会导致频繁的上下文切换增加额外开销
增加系统复杂度
在无需提速的场景下,使用并发并无效果,可能会更慢。在大规模使用并发编程,会使得系统变得更加复杂和不可控,因为很难进行对并发的测试,编写的代码仅仅是适用于设计者的设计之中
跨平台的不一致
某些计算机上很快出现的竞争状况,而在其他计算机上却没有
并发定义
传统定义
定义
并发
同时完成多任务。无需等待当前任务完成即可执行其他任务。“并发”解决了程序因外部控制而无法进一步执行的阻塞问题。最常见的例子就是 I/O 操作,任务必须等待数据输入(在一些例子中也称阻塞)。这个问题常见于 I/O 密集型任务。
并行
同时在多个位置完成多任务。这解决了所谓的 CPU 密集型问题:将程序分为多部分,在多个处理器上同时处理不同部分来加快程序执行效率。
更细的粒度去进行定义
纯并发
仍然在单个 CPU 上运行任务。纯并发系统比顺序系统更快地产生结果,但是它的运行速度不会因为处理器的增加而变得更快。
并发-并行
使用并发技术,结果程序可以利用更多处理器更快地产生结果。
并行-并发
使用并行编程技术编写,如果只有一个处理器,结果程序仍然可以运行(Java 8 Streams 就是一个很好的例子)。
纯并行
除非有多个处理器,否则不会运行。
抽象泄露问题
支持并发性的语言和库似乎是抽象泄露(Leaky Abstraction) 一词的完美候选。抽象的目标是“抽象”掉那些对手头的想法不重要的部分,以屏蔽不必要的细节所带来的影响。如果抽象是有漏洞的,那么即使废很大功夫去隐藏它们,这些细枝末节也总会不断凸显出自己是重要的,。
于是我开始怀疑是否真的有高度地抽象。因为当编写这类程序时,底层的系统、工具,甚至是关于 CPU 缓存如何工作的细节,都永远不会被屏蔽。最后,即使你已非常谨慎,你开发的程序也不一定在所有情况下运行正常。有时是因为两台机器的配置不同,有时是程序的预计负载不同。这不是 Java 特有的 - 这是并发和并行编程的本质。
你可能会认为纯函数式 语言没有这些限制。实际上,纯函数式语言的确解决了大量并发问题。如果你正在解决一个困难的并发问题,可以考虑用纯函数语言编写这个部分。但是,如果你编写一个使用队列的系统,例如,如果该系统没有被正确地调整,并且输入速率也没有被正确地估计或限制(在不同的情况下,限制意味着具有不同的影响的不同东西),该队列要么被填满并阻塞,要么溢出。最后,你必须了解所有细节,任何问题都可能会破坏你的系统。这是一种非常不同的编程方式。
新的定义
并发性是一系列性能技术,专注于减少等待
这是一个集合
包含许多不同的方法来解决这个问题。这是使定义并发性如此具有挑战性的问题之一,因为技术差异很大。
这些是性能技术
并发的关键点在于让你的程序运行得更快。使用最简单的方法产生你需要的性能,因为并发很快变得无法管理。
“减少等待”
无论(例如)你运行多少个处理器,你只能在等待发生时产生效益。并发的唯一机会是如果程序的某些部分被迫等待。
并发为速度而生
问题源头
源头之一:缓存导致的可见性问题
可见性定义
一个线程对共享变量的修改,另外一个线程能够立刻看到
如何解决可见性和有序性问题
volital(禁用缓存以及编译优化)
场景
字分裂
Java 数据类型足够大(在 Java 中 long 和 double 类型都是 64 位),写入变量的过程分两步进行,就会发生 Word tearing (字分裂)情况。 JVM 被允许将64位数量的读写作为两个单独的32位操作执行^3,这增加了在读写过程中发生上下文切换的可能性,因此其他任务会看到不正确的结果。这被称为 Word tearing (字分裂),因为你可能只看到其中一部分修改后的值。
可见性
缓存一致性问题
缓存的主要目的是避免从主内存中读取数据
当并发时,有时不清楚 Java 什么时候应该将值从主内存刷新到本地缓存
解决方法
将字段定义为 volatile 可以防止这些编译器优化,这样读写就可以直接进入内存,而不会被缓存。如果一个 volatile 字段刚好存储在本地缓存,则会立即将其写入主内存,并且该字段的任何读取都始终发生在主内存中
何时修饰变量
该变量同时被多个任务访问
这些访问中至少有一个是写操作
尝试避免同步
Happen-Before原则
含义
前面一个操作的结果对后续操作是可见的
原则
1. 程序的顺序性规则
指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作
2. volatile变量规则
这条规则是指对一个volatile变量的写操作, Happens-Before 于后续对这个volatile变量的读操作。
volatile 关键字可以阻止重排 volatile 变量周围的读写指令,volatile 变量读写之前发生的指令先于自身的读写指令。这种 volatile (易变性)操作通常称为 memory barrier (内存屏障)。happens before 担保原则确保 volatile 变量的读写指令不能跨过内存屏障进行重排
当线程向一个 volatile 变量写入时,在线程写入之前的其他所有变量(包括非 volatile 变量)也会刷新到主内存。当线程读取一个 volatile 变量时,它也会读取其他所有变量(包括非 volatile 变量)与 volatile 变量一起刷新到主内存。
3. 传递性
这条规则是指如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。
4. 管程中锁的规则
这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
管程定义
管程 是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现。
5. 线程 start() 规则
指主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B前的操作。
换句话说就是,如果线程A调用线程B的 start() 方法(即在线程A中启动线程B),那么该start()操作 Happens-Before 于线程B中的任意操作。
换句话说就是,如果线程A调用线程B的 start() 方法(即在线程A中启动线程B),那么该start()操作 Happens-Before 于线程B中的任意操作。
6. 线程 join() 规则
指主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法实现),当子线程B完成后(主线程A中join()方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对 共享变量 的操作。
换句话说就是,如果在线程A中,调用线程B的 join() 并成功返回,那么线程B中的任意操作Happens-Before 于该 join() 操作的返回。具体可参考下面示例代码。
换句话说就是,如果在线程A中,调用线程B的 join() 并成功返回,那么线程B中的任意操作Happens-Before 于该 join() 操作的返回。具体可参考下面示例代码。
Happens-Before语义的理解
Happens-Before的语义是一种因果关系
在现实世界里,如果A事件是导致B事件的起因,那么A事件一定是先于(Happens-Before)B事件发生的
final
初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。只要我们提供正确构造函数没有“逸出”,就不会出问题了。
源头之二:线程切换带来的原子性问题
定义
时间片定义
操作系统允许某个进程执行一小段时间,例如50毫秒,过了50毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个50毫秒称为“ 时间片”。
线程切换定义
早期的操作系统基于进程来调度CPU,不同进程间是不共享内存空间的,所以进程要做任务切换就要切换内存映射地址,而一个进程创建的所有线程,都是共享一个内存空间的,所以线程做任务切换成本就很低了。
原子性定义
我们把一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性。一个 原子操作 是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程执行)执行完毕。
“原子性”的本质
其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求, 操作的中间状态对外不可见
线程之间切换上下文的代价
存储被挂起线程的当前状态,并检索另一个线程的当前状态,以便从它进入挂起的位置继续执行。
解决原子性问题
管程
定义
指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。翻译为Java领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。
模型
Hasen模型
要求notify()放在代码的最后,这样T2通知完T1后,T2就结束了,然后T1再执行,这样就能保证同一时刻只有一个线程执行。
Hoare模型
T2通知完T1后,T2阻塞,T1马上执行;等T1执行完,再唤醒T2,也能保证同一时刻只有一个线程执行。但是相比Hasen模型,T2多了一次阻塞唤醒操作。
MESA模型(Java使用的)
T2通知完T1后,T2还是会接着执行,T1并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。
副作用
就是当T1再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。
并发编程领域两大核心问题
互斥
定义
即同一时刻只允许一个线程访问共享资源
解决互斥问题
将共享变量及其对共享变量的操作统一封装起来。线程不安全的操作封装起来,对外提供线程安全的操作方法
同步
定义
即线程之间如何通信、协作
解决同步问题
引入定义
条件变量
每个条件变量都对应有一个等待队列
条件变量等待队列
阻塞队列和等待队列是不同的
使用notify()的条件
所有等待线程拥有相同的等待条件
所有等待线程被唤醒后,执行相同的操作
只需要唤醒一个线程
Java内置
互斥锁概念
同一时刻只有一个线程执行
简易锁模型
lock,临界区,unlock
引发的问题
粗略的加锁,引来疑问,我们锁的是什么?我们保护的又是什么?
改进后的锁模型
以资源为保护的核心,创建需保护的资源(R)的锁LR,lock(LR),临界区(操作R),unlock(LR)
锁和受保护资源的关系
问题
多个锁保护同一个资源可能导致加锁对象不一致,不能打到保护的目的
合理的关系
受保护资源和锁之间的关联关系是N:1的关系
方法
举例:A向B转账,B向C转账
错误方法
使用对象锁synchronized(this)
正确方法
使用共享锁,在构造方法传入统一创建的需要被锁的对象
问题
在复杂系统中,在任意地方传入同一对象非常困难
改进方法
使用类锁synchronized(class),使用该类的方法都上锁
问题
这样会导致所有对于该类的操作都是串行,不符合并发性质,影响实际性能
改进方法
同时锁住被保护的对象,A和B或C
细粒度锁
用不同的锁对受保护资源进行精细化管理,能够提升性能
缺点
死锁
定义
一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象
描述
某些任务必须去等待 - 阻塞来获得其他任务的结果。被阻止的任务有可能等待另一个被阻止的任务,另一个被阻止的任务也在等待其他任务,等等。如果被阻止的任务链循环到第一个,没有人可以取得任何进展,你就会陷入死锁。
必要条件
互斥条件,任务使用的资源中至少有一个不能共享的
如何解决
不可解决,锁本身就互斥
占有且等待,至少有一个任务它必须持有一个资源且正在等待获取一个被当前别的任务持有的资源
如何解决
可以一次性申请所有的资源,通过创造单例的资源分配者,由它来分配和释放资源
不可抢占, 任务必须把资源释放当作普通事件
如何解决
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。synchronized解决不了,可以通过java.util.concurrent内的方法
循环等待,一个任务等待其它任务所持有的资源, 后者又在等待第一个任务所持有的资源
如何解决
可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的
缺点
在等待资源释放的过程中,可能会采用while(true)的方式,导致CPU压力过大
更好的解决方案
等待-通知机制
原理
线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁
等待队列
等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列
前提
进入synchrozied的临界区时是已经获取了相应的互斥锁
方式
wait()
线程在进入等待队列的同时, 会释放持有的互斥锁
notify()
会通知等待队列( 互斥锁的等待队列)中的线程,告诉它 条件曾经满足过。notify()只能保证在通知时间点,条件是满足的。而被通知线程的 执行时间点和通知的时间点 基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)
随机地通知等待队列中的一个线程
风险
可能导致某些线程永远不会被通知到,所以推荐使用notifyAll()
notifyAll()
通知等待队列中的所有线程
四个要素
互斥锁
线程要求的条件
何时等待
线程要求的条件不满足就等待
何时通知
当有线程释放账户时就通知
经典问题
“哲学家进餐”问题
活锁
有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况
如何解决
尝试等待一个随机的时间
饥饿
指的是线程因无法访问所需资源而无法执行下去的情况。“不患寡,而患不均”,如果线程优先级“不均”,在CPU繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。
如何解决
保证资源充足
公平地分配资源
公平锁
避免持有锁的线程长时间执行
synchronized
概念
所有对象都自动包含独立的锁(也称为 monitor,即监视器),当一个线程首次获得锁时,计数变为 1 ,每当线程离开 synchronized 方法时,计数递减,直到计数变为 0 ,完全释放锁以给其他线程使用
临界区(同步控制块)
在进入此段代码前,必须得到对象锁。如果一些其他任务已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。当发生这种情况时,尝试获取该锁的任务就会挂起。线程调度会定期回来并检查锁是否已经释放;如果释放了锁则唤醒任务。
优点
相比同步整个方法,同步控制块减小了同步的范围,可能会提高性能
风险
在同步块之外的代码要是线程安全的
使用场景
如果你正在写一个变量,它可能接下来被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。
加锁方式
静态方法
锁定的是当前类的Class对象
非静态方法
锁定的是当前实例对象this
并发工具包 J.U.C
概念
为什么需要
原始的synchronized无法破坏死锁中的“不可抢占”问题,synchronized在获取不到锁的情况下会直接被阻塞
优点
更细粒度的加锁和解锁控制
缺点
编码复杂度变高
总体方案
能够响应中断
synchronized的问题是,持有锁A后,如果尝试获取锁B失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁A。这样就破坏了不可抢占条件了。
支持超时
如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
非阻塞地获取锁
如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
锁的类型
可重入锁/不可重入锁
可重入指的是线程可以重复获取同一把锁,不可重入则会直接阻塞
公平锁/非公平锁/偏向锁
公平指的是谁等待时间长就唤醒谁,非公平则随机唤醒,偏向锁是如果线程最近使用了某个锁之后再执行头一个锁保护的代码时会优先获得锁
悲观锁/乐观锁(无锁,非互斥)
悲观锁写操作都会获取锁,乐观锁是无锁机制
自旋锁(无锁,非互斥)
ReentrantLock(可重入锁/互斥锁/公平锁/悲观锁)
Lock(解决互斥问题)
核心方法
(支持中断的API)void lockInterruptibly() throws InterruptedException;
(支持超时的API)boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
(支持非阻塞获取锁的API)boolean tryLock();
如何保证可见性
利用了volatile相关的Happens-Before规则
顺序性规则
volatile变量规则
传递性规则
Condition(解决同步问题,实现了管程模型里面的条件变量)
同步与异步
同步
调用方需要等待结果
异步
调用方不需等待结果
实现方式
异步调用
调用方创建一个子线程,在子线程中执行方法调用
异步方法
被调用方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接return
ReadWriteLock(ReentrantReadWriteLock)(读写锁/互斥锁/公平锁/悲观锁)
为什么需要
分场景优化性能,提升易用性
与互斥锁区别
读写锁允许多个线程同时读共享变量
原则
允许多个线程同时读共享变量
只允许一个线程写共享变量
如果一个写线程正在执行写操作,此时禁止读线程读共享变量
升级与降级
读锁不能升级为写锁
读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。
写锁能降级为读锁
StampedLock(读写锁/互斥锁/悲观锁/读乐观锁)
允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。写锁和悲观读锁加锁成功之后,都会返回一个stamp,然后解锁的时候,需要传入这个stamp。
锁模式
写锁
允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。
读锁
悲观读锁
加锁后阻塞
乐观读(无锁)
因此最后读完之后,还需要再次验证一下是否存在写操作,这个验证操作是通过调用validate(stamp)来实现的。如果存在写操作,则需要升级为悲观读锁,进行数据操作后释放悲观读锁
升级降级
升级
tryConvertToWriteLock()
降级
tryConvertToReadLock()
注意事项
StampedLock的功能仅仅是ReadWriteLock的子集
StampedLock不支持重入
悲观读锁、写锁都不支持条件变量
使用StampedLock一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁readLockInterruptibly()和写锁writeLockInterruptibly()
Semaphore(信号量/互斥锁/公平锁/悲观锁)(PV原语)
为什么需要信号量
与Lock不同的是,Semaphore可以允许多个线程访问一个临界区,例如连接池、对象池、线程池等等。
模型
一个计数器(外部不可访问,需要通过方法)
一个等待队列(外部不可访问,需要通过方法)
三个方法
init()
设置计数器的初始值
down()
(与acquire()对应)计数器的值减1;如果此时计数器的值小于0,则当前线程将被阻塞,否则当前线程可以继续执行
up()
(与release()对应)计数器的值加1;如果此时计数器的值小于或者等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除
CountDownLatch/CyclicBarrier(让线程步调保持一致)
方案
使用new Thread(),让每个线程都执行join(),等待所有线程执行完毕
使用CountDownLatch并初始化计数器并在主线程调用await(),每个子线程执行完成主体方法再调用countdown(),等待计数器变为0,await()会自动通过,继续执行主线程方法
在一定场景中,需要循环初始化计数器并在计数器为0后执行特定方法,则选用CyclicBarrier
并发容器
复制策略
使用“复制”策略,修改是在数据结构一部分的单独副本(或有时是整个数据的副本)上进行的,并且在整个修改过程期间这个副本是不可见的。仅当修改完成时,修改后的结构才与“主”数据结构安全地交换,然后读取者才会看到修改。
加锁策略
使用synchronized
类型
List
CopyOnWriteArrayList
概念
写入操作会复制整个底层数组,保留原来的数组,以便在修改复制的数组时可以线程安全地进行读取。当修改完成后,原子操作会将其交换到新数组中,以便新的读取操作能够看到新数组内容。当多个迭代器遍历和修改列表时,它不会抛出 ConcurrentModificationException 异常,因此你不用就像过去必须做的那样,编写特殊的代码来防止此类异常。
注意事项
应用场景
仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致
Map
ConcurrentHashMap
key是无序的,只复制和修改集合的一部分,而不是整个集合。读取者仍然不会看到任何不完整的修改。ConcurrentHashMap 不会抛出concurrentmodificationexception异常。
key和value都不能为空
ConcurrentSkipListMap
key是有序的
key和value都不能为空
跳表
插入、删除、查询操作平均的时间复杂度是 O(log n)
Set
CopyOnWriteArraySet
参考CopyOnWriteArrayList
ConcurrentSkipListSet
参考ConcurrentSkipListMap
Queue
维度
阻塞与非阻塞
阻塞
当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞
非阻塞
当队列不满时,入队操作不阻塞;当队列不为空时,出队操作不阻塞
单端与双端
单端
队尾入队,队首出队
双端
队尾入队,队首队尾皆可入队出队
类型
单端阻塞队列
ArrayBlockingQueue
内部持有数组队列,且有界
LinkedBlockingQueue
内部持有链表队列,且有界
SynchronousQueue
内部不持有队列,此时生产者线程的入队操作必须等待消费者线程的出队操作
LinkedTransferQueue
融合LinkedBlockingQueue和SynchronousQueue的功能,性能比LinkedBlockingQueue更好
PriorityBlockingQueue
支持按照优先级出队
DelayQueue
支持延时出,这是一个无界阻塞队列 ( BlockingQueue ),用于放置实现了 Delayed 接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,因此队首对象的延迟到期的时间最长。
双端阻塞队列
LinkedBlockingDeque
单端非阻塞队列
ConcurrentLinkedQueue
双端非阻塞队列
ConcurrentLinkedDeque
注意事项
阻塞队列都用Blocking关键字标识,单端队列使用Queue标识,双端队列使用Deque标识
只有ArrayBlockingQueue和LinkedBlockingQueue是支持有界的,所以 在使用其他无界队列时,一定要充分考虑是否存在导致OOM的隐患
CAS(Compare And Swap“比较并交换”/自旋锁)
概念
从内存中获取一个值,并在计算新值时保留原始值。然后使用 CAS 指令,它将原始值与当前内存中的值进行比较,如果这两个值是相等的,则将内存中的旧值替换为计算新值的结果,所有操作都在一个原子操作中完成。如果原始值比较失败,则不会进行交换,因为这意味着另一个线程同时修改了内存。
CPU为了解决并发问题,提供了CAS指令,作为一条CPU指令,CAS指令本身是原子性的
优点
如果内存仅轻量竞争,CAS操作几乎总是在没有重复尝试的情况下完成,因此它非常快。相反,synchronized 操作需要考虑每次获取和释放锁的成本,这要昂贵得多,而且没有额外的好处。
缺点
随着内存竞争的增加,使用 CAS 的操作会变慢,因为它必须更频繁地重复自己的操作,但这是对更多资源竞争的动态响应。
只针对一个变量进行操作,如果是多个变量或复制更新,依旧需要选择互斥锁方案
核心参数
共享变量的内存地址A
用于比较的值B
共享变量的新值C
执行方式
只有当内存中地址A处的值等于B时,才能将内存中地址A处的值更新为新值C
ABA问题
概念
预期值从A被其他线程改为B之后,再由另一个线程改回A
解决方案
每次执行CAS操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便A变成B之后再变回A,版本号也不会变回来(版本号递增的)
类型
基本数据类型
AtomicBoolean
AtomicInteger
AtomicLong
对象引用类型
AtomicReference
AtomicStampedReference
增加了int类型版本号参数
AtomicMarkableReference
增加了boolean类型版本号参数
数组
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
对象属性更新器(利用反射机制实现)
创建更新器
newUpdater(Class<U> tclass,String fieldName)
实现
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
注意实现
对象属性必须是volatile类型的,只有这样才能保证可见性
累加器
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
线程池
概念
为什么需要线程池
线程是一个重量级的对象,应该避免频繁创建和销毁
模式
生产者-消费者模式,线程池的使用方是生产者,线程池本身是消费者
ThreadPoolExecutor
参数
int corePoolSize
表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留corePoolSize个人坚守阵地。
int maximumPoolSize
表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到maximumPoolSize个人。当项目闲下来时,就要撤人了,最多能撤到corePoolSize个人。
long keepAliveTime & TimeUnit unit
面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了 keepAliveTime & unit 这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
BlockingQueue<Runnable> workQueue
工作队列
ThreadFactory threadFactory
通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
RejectedExecutionHandler handler
概念
通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过handler这个参数来指定。ThreadPoolExecutor已经提供了以下4种策略。
拒绝策略
AbortPolicy
默认的拒绝策略,会throws RejectedExecutionException。
CallerRunsPolicy
提交任务的线程自己去执行该任务。
DiscardPolicy
直接丢弃任务,没有任何异常抛出。
DiscardOldestPolicy
丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
方法
execute(Runnable task);(提交Runnable任务无返回值)
Future<?> submit(Runnable task);(提交Runnable任务)
这个方法的参数是一个Runnable接口,Runnable接口的run()方法是没有返回值的,所以 submit(Runnable task) 这个方法返回的Future仅可以用来断言任务已经结束了,类似于Thread.join()。
<T> Future<T> submit(Callable<T> task); (提交Callable任务)
这个方法的参数是一个Callable接口,它只有一个call()方法,并且这个方法是有返回值的,所以这个方法返回的Future对象可以通过调用其get()方法来获取任务的执行结果。
<T> Future<T> submit(Runnable task, T result);(提交Runnable任务及结果引用)
get()的返回值就是传给submit()方法的参数result,result相当于主线程和子线程之间的桥梁,通过它主子线程可以共享数据
Future
方法
boolean cancel(boolean mayInterruptIfRunning);(取消任务)
boolean isCancelled();(判断任务是否已取消)
boolean isDone();(判断任务是否已结束)
get();(获得任务执行结果)
get(long timeout, TimeUnit unit);(获得任务执行结果,支持超时)
FutureTask
构造
FutureTask(Callable<V> callable);
FutureTask(Runnable runnable, V result);
优点
由于实现了Runnable接口,所以可以将FutureTask对象作为任务提交给ThreadPoolExecutor去执行,也可以直接被Thread执行
又因为实现了Future接口,所以也能用来获得任务的执行结果
注意实现
不建议使用Executors
Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue,高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,这是致命问题
RejectedExecutionException编译器并不强制catch
因此 默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
异常处理的问题
execute()方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常
ExecutorService
submit
Futures
Future
调用 get() 时,Future 会阻塞,所以它只能解决等待任务完成才暴露问题
解决方案
CompletableFuture
invokeAll
ExecutorService 允许你使用 invokeAll() 启动集合中的每个 Callable
shutdown
诉 ExecutorService 完成已经提交的任务,但不接受任何新任务,此时,这些任务仍然在运行。一旦 shutdown(),尝试提交新任务将抛出 RejectedExecutionException。
独立运行的任务不会确定性地响应信号。
shutdownNow
它除了不接受新任务外,还会尝试通过中断任务来停止任何当前正在运行的任务。
CompletableFuture
场景
可以编排任务,有聚合关系,无论是AND聚合还是OR聚合,实现了Future、CompletionStage接口
核心方法
Future
runAsync()
runAsync(Runnable runnable)
异步执行,无结果
runAsync(Runnable runnable, Executor executor)
指定线程池,异步执行,无结果
supplyAsync()
supplyAsync(Supplier<U> supplier)
异步执行,返回结果
supplyAsync(Supplier<U> supplier, Executor executor)
指定线程池,异步执行,返回结果
get()
在等待结果时阻塞调用线程。此块可以通过 InterruptedException 或 ExecutionException 中断。在这种情况下,阻止永远不会发生,因为 CompletableFuture 已经完成,所以结果立即可用。
join()
阻塞线程,等待完成
CompletionStage
描述串行关系
thenCombine()
例如 f3 = f1.thenCombine(f2, ()->{}) 能够清晰地表述“任务3要等待任务1和任务2都完成后才能开始”;
CompletionStage<R> thenApply(fn);
Function<T, R>,R apply(T t)
CompletionStage<R> thenApplyAsync(fn);
CompletionStage<Void> thenAccept(consumer);
Consumer<T>,void accept(T t)
CompletionStage<Void> thenAcceptAsync(consumer);
CompletionStage<Void> thenRun(action);
Runnable
CompletionStage<R> thenCompose(fn);
会新创建出一个子流程
CompletionStage<R> thenComposeAsync(fn);
描述AND汇聚关系
CompletionStage<R> thenCombine(other, fn);
CompletionStage<R> thenCombineAsync(other, fn);
CompletionStage<Void> thenAcceptBoth(other, consumer);
CompletionStage<Void> thenAcceptBothAsync(other, consumer);
CompletionStage<Void> runAfterBoth(other, action);
CompletionStage<Void> runAfterBothAsync(other, action);
“立即返回”的异步能力
它将你需要的操作链存储为一组回调
当操作的第一个链路(后台操作)完成并返回时,第二个链路(后台操作)必须获取生成的 Machina 并开始工作,以此类推!
但这种异步机制没有我们可以通过程序调用栈控制的普通函数调用序列,它的调用链路顺序会丢失,因此它使用一个函数地址来存储的回调来解决这个问题。
异常
没有将抛出的异常暴露给调用方
不管异常还是成功,它仍然被视为已“完成”
判断是否异常
isCompletedExceptionally
在CompletableFuture插入异常
completeExceptionally(ex)
自动响应异常,而不是使用粗糙的 try-catch
exceptionally()
优点
参数仅在出现异常时才运行。通过将一个好的对象插入到流中来恢复到一个可行的状态。
缺点
局限性在于,该函数只能返回输入类型相同的值。
handle()
优点
可以生成任何新类型,而不是像使用 exceptionally()那样简单地恢复。
缺点
一直被调用来查看是否发生异常(必须检查 fail 是否为 true)。
whenComplete()
优点
参数是一个消费者,并且不修改传递给它的结果对象。
缺点
一直被调用来查看是否发生异常(必须检查 fail 是否为 true)。
流异常
在你应用一个终端操作之前,什么都不会暴露给 Client,流绝对不会存储它的异常
检查性异常
CompletableFuture 和 parallel Stream 都不支持包含检查性异常的操作。必须在调用操作时处理检查到的异常
注意事项
默认情况下CompletableFuture会使用公共的ForkJoinPool线程池,这个线程池默认创建的线程数是CPU的核数(也可以通过JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism来设置ForkJoinPool线程池的线程数)。建议根据不同的业务类型创建不同的线程池,以避免互相干扰
CompletionService
场景
需要批量提交异步任务,内部维护了一个阻塞队列,当任务执行结束就把任务的执行结果Future加入到阻塞队列
构造
ExecutorCompletionService(Executor executor)
默认使用无界的LinkedBlockingQueue
ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)
核心方法
Future<V> submit(Callable<V> task);
Future<V> submit(Runnable task, V result);
Future<V> take() throws InterruptedException;
如果阻塞队列是空的,线程会被阻塞
Future<V> poll();
如果阻塞队列是空的,会返回 null 值
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了 timeout unit时间,阻塞队列还是空的,那么该方法会返回 null 值
Fork/Join
场景
分治,把一个复杂的问题分解成多个相似的子问题,然后再把子问题分解成更小的子问题,直到子问题简单到可以直接求解
分治任务模型(两个阶段)
任务分解(对应Fork)
将任务迭代地分解为子任务,直至子任务可以直接计算出结果
结果合并(对应Join)
逐层合并子任务的执行结果,直至获得最终结果
计算框架(两个部分)
ForkJoinPool
场景
分治任务
与ThreadPoolExecutor比较
相同点
生产者-消费者模式的实现
多个工作线程
不同点
ThreadPoolExecutor内部只有一个任务队列
ForkJoinPool内部有多个任务队列,当通过ForkJoinPool的invoke()或者submit()方法提交任务时,ForkJoinPool根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。
ForkJoinPool支持一种叫做“ 任务窃取”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务
概念
ForkJoinTask
场景
分治任务
实现
fork()
join()
子类
RecursiveAction
compute() 无返回值
RecursiveTask
compute() 有返回值
Parallel Streams(并行流)
场景
面向数据
优点
通过简单地将 parallel() 添加到表达式来并行化流。这是一种简单,强大,坦率地说是利用多处理器的惊人方式
缺陷
创建和运行任务
任务是一段可以独立运行的代码
终止长时间运行的任务
任务独立运行,因此需要一种机制来关闭它们
阻塞
Completable Futures 当你将衣服带到干洗店时,他们会给你一张收据。你继续完成其他任务,当你的衣服洗干净时你可以把它取走。收据是你与干洗店在后台执行的任务的连接。
共享一个ForkJoinPool
所有的并行流计算都共享一个ForkJoinPool,这个共享的ForkJoinPool默认的线程数是CPU的核数;如果所有的并行流计算都是CPU密集型计算的话,完全没有问题,但是如果存在I/O密集型的并行流计算,那么很可能会因为一个很慢的I/O计算而拖慢整个系统的性能
Spliterator迭代器
ThreadLocal
为什么需要
线程可以拥有自己的“局部变量”且避免与其他线程共享,因此是线程安全的
实现方式
Thread这个类内部有一个私有属性threadLocals,其类型就是ThreadLocalMap,ThreadLocalMap的Key是ThreadLocal,ThreadLocalMap内部是数组而不是Map,数组内对象是继承WeakReference
继承
子线程中是无法通过ThreadLocal来访问父线程的线程变量,t通过继承InheritableThreadLocal来实现,InheritableThreadLocal是ThreadLocal子类
内存泄露
非线程池
Thread持有ThreadLocalMap,而且ThreadLocalMap里对ThreadLocal的引用是弱引用(WeakReference),所以只要Thread对象可以被回收,那么ThreadLocalMap就能被回收
线程池
线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着Thread持有的ThreadLocalMap一直都不会被回收,再加上ThreadLocalMap中的Entry对ThreadLocal是弱引用(WeakReference),所以只要ThreadLocal结束了自己的生命周期是可以被回收掉的。但是Entry中的Value却是被Entry强引用的,所以即便Value的生命周期结束了,Value也是无法被回收的,从而导致内存泄露。
方案
使用try{}finally{},在finally中手动释放资源
弱引用
源头之三:编译优化带来的有序性问题
有序性定义
有序性指的是程序按照代码的先后顺序执行
线程
结构
程序计数器
指明要执行的下一个 JVM 字节码指令
栈
用于支持 Java 代码执行的栈
包含有关此线程已到达当时执行位置所调用方法的信息。它也包含每个正在执行的方法的所有局部变量(包括原语和堆对象的引用)。每个线程的栈通常在64K到1M之间
用于 native code(本机方法代码)执行的栈
线程本地变量
thread-local variables (线程本地变量)的存储区域
用于控制线程的状态管理变量
生命周期
通用线程的生命周期
初始状态
线程已经被创建,但是还不允许分配CPU执行,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
可运行状态
指的是线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配CPU执行。
运行状态
当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了 运行状态。
休眠状态
运行状态的线程如果调用一个阻塞的API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到 休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
终止状态
线程执行完或者出现异常就会进入 终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
Java线程
初始化状态(NEW)
可运行/运行状态(RUNNABLE)
阻塞状态(BLOCKED)
无时限等待(WAITING)
有时限等待(TIMED_WAITING)
终止状态(TERMINATED)
Java线程状态转换(JVM层面并不关心操作系统调度相关的状态)
RUNNABLE与BLOCKED的状态转换
只有一种场景会触发这种转换,就是线程等待synchronized的隐式锁。
RUNNABLE与WAITING的状态转换
获得synchronized隐式锁的线程,调用无参数的Object.wait()方法。
调用无参数的Thread.join()方法。其中的join()是一种线程同步方法,例如有一个线程对象thread A,当调用A.join()的时候,执行这条语句的线程会等待thread A执行完,而等待中的这个线程,其状态会从RUNNABLE转换到WAITING。当线程thread A执行完,原来等待它的线程又会从WAITING状态转换到RUNNABLE。
调用LockSupport.park()方法。其中的LockSupport对象,也许你有点陌生,其实Java并发包中的锁,都是基于它实现的。调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从RUNNABLE转换到WAITING。调用LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从WAITING状态转换到RUNNABLE。
调用LockSupport.park()方法。其中的LockSupport对象,也许你有点陌生,其实Java并发包中的锁,都是基于它实现的。调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从RUNNABLE转换到WAITING。调用LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从WAITING状态转换到RUNNABLE。
RUNNABLE与TIMED_WAITING的状态转换(触发条件多了 超时参数)
调用 带超时参数 的Thread.sleep(long millis)方法;
获得synchronized隐式锁的线程,调用 带超时参数 的Object.wait(long timeout)方法;
调用 带超时参数 的Thread.join(long millis)方法;
调用 带超时参数 的LockSupport.parkNanos(Object blocker, long deadline)方法;
调用 带超时参数 的LockSupport.parkUntil(long deadline)方法。
从NEW到RUNNABLE状态
创建出来的Thread对象就是NEW状态
创建方法
Thread
run()
Runnable
run(),它没有包含实际运行任务的机制
Callable
call(),返回任务结果
从RUNNABLE到TERMINATED状态
stop() 已经过时,不建议使用
会真的杀死线程,不给线程喘息的机会,例如并不会调用unlock()。(还有suspend() 和 resume()方法)
interrupt()
仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知
怎么收到通知
异常
当线程A处于WAITING、TIMED_WAITING状态时,如果其他线程调用线程A的interrupt()方法,会使线程A返回到RUNNABLE状态,同时线程A的代码会触发InterruptedException异常。
处于RUNNABLE状态
阻塞在java.nio.channels.InterruptibleChannel
如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptException这个异常
阻塞在java.nio.channels.Selector
如果其他线程调用线程A的interrupt()方法,线程A的java.nio.channels.Selector会立即返回。
主动检测
处于RUNNABLE状态,并且没有阻塞在某个I/O操作
如果其他线程调用线程A的interrupt()方法,那么线程A可以通过isInterrupted()方法,检测是不是自己被中断了。
数量分配
从JVM内存空间角度
Thread (线程)对象的大小因操作系统而异,其中最大部分用于执行方法的 Java 堆栈。因此,数量与JVM有关,可以分配直到 JVM 内存不足为止
场景
CPU密集型
理论
线程的数量=CPU核数
工程
线程的数量=CPU核数+1,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证CPU的利用率。
I/O密集型
核数
单核
最佳线程数=1 +(I/O耗时 / CPU耗时)
多核
最佳线程数=CPU核数 * [ 1 +(I/O耗时 / CPU耗时)]
处理方法
对于I/O密集型计算场景,I/O耗时和CPU耗时的比值是一个关键参数,不幸的是这个参数是未知的,而且是动态变化的,所以工程上,我们要估算这个参数,然后做各种不同场景下的压测来验证我们的估计。不过工程上,原则还是 将硬件的性能发挥到极致,所以压测时,我们需要重点关注CPU、I/O设备的利用率和性能指标(响应时间、吞吐量)之间的关系。
硬件带来的问题
定义了 “逻辑处理器” 数量的 Intel 超线程,但并没有增加计算能力,所以,线程数量与物理内核(而不是超线程)的数量匹配
非/线程安全
构造方法是非线程安全
类中存在共享资源
解决方法
使用synchronized锁住构造器共享资源
构造器设为私有(因此可以防止继承),并提供一个静态 Factory 方法来生成新对象
每个类也有一个锁(作为该类的 Class 对象的一部分),因此 synchronized 静态方法可以在类范围的基础上彼此锁定,不让同时访问静态数据。
局部变量是线程安全
定义
调用栈
通过CPU的堆栈寄存器存储调用方法的参数和返回地址
栈帧
方法在调用栈里的独立空间,栈帧与方法的声明周期一致
线程封闭
仅在单线程内访问数据。由于不存在共享,所以即便不同步也不会有并发问题
每个线程都有自己的调用栈,而局部变量存储在调用栈
异常捕获
问题
try/catch 无法捕获 execute() 执行带来的异常
解决方案
需要改变 Executor (执行器)生成线程的方式。 Thread.UncaughtExceptionHandler 是一个添加给每个 Thread 对象,用于进行异常处理的接口。当该线程即将死于未捕获的异常时,将自动调用 Thread.UncaughtExceptionHandler.uncaughtException() 方法
创建一个新的 ThreadFactory 类型来让 Thread.UncaughtExceptionHandler 对象附加到每个它所新创建的 Thread(线程)对象上
制定并发访问策略
封装共享变量
将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略
对于这些不会发生变化的共享变量,建议你用final关键字来修饰
识别共享变量间的约束条件
共享变量之间的约束条件,反映在代码里,基本上都会有if语句,所以,一定要特别注意竞态条件。
总体方案
避免共享
避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
不变模式
这个在Java领域应用的很少,但在其他领域却有着广泛的应用,例如Actor模式、CSP模式以及函数式编程的基础都是不变模式。
管程及其他同步工具
Java领域万能的解决方案是管程,但是对于很多特定场景,使用Java并发包提供的读写锁、并发容器等同步工具会更好。
宏观原则
优先使用成熟的工具类
迫不得已时才使用低级的同步原语
低级的同步原语主要指的是synchronized、Lock、Semaphore等
避免过早优化
安全第一,并发程序首先要保证安全,出现性能瓶颈后再优化。在设计期和开发期,很多人经常会情不自禁地预估性能的瓶颈,并对此实施优化,但残酷的现实却是:性能瓶颈不是你想预估就能预估的。
需要关心的问题
锁应是私有的、不可变的、不可重用的
安全问题
什么是线程安全
程序按照我们期望的执行
出现的场景
存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据
数据竞争(Data Race)
当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取防护措施,那么就会导致并发Bug
竞态条件
指的是程序的执行结果依赖线程执行的顺序
活跃性问题(参考问题源头之二)
死锁
活锁
饥饿
性能问题
有个阿姆达尔(Amdahl)定律,代表了处理器并行运算之后效率提升的能力,它正好可以解决这个问题,具体公式如下:
$S=\frac{1}{(1-p)+\frac{p}{n}}$
公式里的n可以理解为CPU的核数,p可以理解为并行百分比,那(1-p)就是串行百分比了,也就是我们假设的5%。我们再假设CPU的核数(也就是n)无穷大,那加速比S的极限就是20。也就是说,如果我们的串行率是5%,那么我们无论采用什么技术,最高也就只能提高20倍的性能。
指标
吞吐量:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。
延迟:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。
并发量:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是1000的时候,延迟是50毫秒。
使用经验
不要用它(避免使用并发)
没有什么是真的,一切可能都有问题
仅仅是它能运行,并不意味着它没有问题
你必须理解它(逃不掉并发)
线程不是典型的对象:每个线程都有其自己的执行环境,包括堆栈和其他必要的元素,使其比普通对象大得多
最佳实践
永远只在更新对象的成员变量时加锁
永远只在访问可变的成员变量时加锁
永远不在调用其他对象的方法时加锁
减少锁的持有时间
减小锁的粒度
并发设计模式
Immutability(不变性)
概念
对象一旦被创建之后,状态就不再发生变化。也就是说,变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性
方式
将一个类所有的属性都设置成final的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是 这个类本身也是final的,也就是不允许继承
缺点
因为不可变,所以需要重复创建对象,会造成内存浪费
解决方案
享元模式(对象池),但是这种类型的内部对象池可能会共用一把锁,所以不能用来做锁
举例
String和Long、Integer、Double等基础类型
Copy-on-Write(写时复制)
概念
更多的是表达了延时策略,只有在真正需要复制的时候才复制,而不是提前复制好
举例
String和Long、Integer、Double等基础类型
函数式编程领域
线程本地存储
概念
多个线程同时读写同一共享变量存在并发问题,因此,只要线程封闭,避免共享就不会有并发问题
方式
使用ThreadLocal
Guarded Suspension(保护性地暂停)
概念
本质上是一种等待唤醒机制的实现
方式
创建受保护的对象T,使用ReentrantLock和Conditiond对保护类进行加锁或唤醒,当收不保护的对象T不能通过验证,则线程进入等待,当更新T之后进行唤醒线程,直到T通过验证
Balking
概念
并发条件中,需要支持一种“快速失败”模式,在条件为false的情况下,不进行某些操作
方式
synchronized(this) 锁住条件变量更改逻辑
在原子性要求不高的场景下,可以用volatile替代
Thread-Per-Message
概念
主线程不执行具体任务逻辑,主线程会委托子线程完成,为每个任务分配一个独立的线程
方式
在Java领域由于线程本身是重量级对象,线程的操作全部委托给操作系统,因此创建线程的成本很高,在Java领域并不是一个很好的方案,会变更成以线程池为任务线程池的方案
在其他领域,r如Go语言中,“轻量级线程”应用的非常广泛,轻量级线程适用于这个模式
Worker Thread
概念
与Thread-Per-Message概念相似,同时委托任务,不同的是,在Worker Thread中是委托给线程池执行,不直接操作线程
方式
使用ThreadPoolExecutor线程池
注意事项
线程池死锁
如果主线程池与工作线程池共用一个线程池,可能会发生给无法给工作线程再分配更多线程,导致死锁。因此,在不同工作维度使用不同线程池(有界)可以避免线程池死锁问题
两段式终止线程
概念
将终止过程分成两个阶段,其中第一个阶段主要是线程T1向线程T2 发送终止指令,而第二阶段则是线程T2 响应终止指令
方式
线程
interrupt()方法发送中断指令
isInterrupted()方法判断是否线程被终端,如果run()是调用第三方,无法预判是否第三方能够进行终止,那么需要手动设置线程终端标志
线程池
shutdown()
拒绝接收新的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完之后才最终关闭线程池
shutdownNow()
拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为shutdownNow()方法的返回值返回
生产者-消费者
概念
生产者-消费者模式的核心是一个 任务队列,生产者线程生产任务,并将任务添加到任务队列中,而消费者线程从任务队列中获取任务并执行
案例分析
Guava RateLimiter
令牌桶算法
核心是要想通过限流器,必须拿到令牌
算法
令牌以固定的速率添加到令牌桶中,假设限流的速率是 r/秒,则令牌每 1/r 秒会添加一个
假设令牌桶的容量是 b ,如果令牌桶已满,则新的令牌会被丢弃(b是限流器允许的最大突发流量)
请求能够通过限流器的前提是令牌桶中有令牌
Netty
I/O模型
BIO(阻塞)
会为每个socket分配一个独立的线程,并且所有read()操作和write()操作都会阻塞当前线程
NIO(非阻塞)
非阻塞式API就能够实现一个线程处理多个连接
Reactor模式
事件循环(EventLoop)
监听网络事件并调用事件处理器进行处理,一个网络连接只会对应到一个Java线程,最大的好处就是对于一个网络连接的事件处理是单线程的,这样就 避免了各种并发问题。
方式
在Netty中,处理TCP连接请求和读写请求是通过两个不同的socket完成的,bossGroup就用来处理连接请求的,而workerGroup是用来处理读写请求的。bossGroup处理完连接请求后,会将这个连接提交给workerGroup来处理, workerGroup里面有多个EventLoop,通过轮询算法,选择一个EventLoop执行
高性能队列Disruptor
概念
高性能的有界内存队列
程序的局部性原理
概念
程序的局部性原理指的是在一段时间内程序的执行会限定在一个局部范围内
时间局部性
指的是程序中的某条指令一旦被执行,不久之后这条指令很可能再次被执行;如果某条数据被访问,不久之后这条数据很可能再次被访问
空间局部性
指某块内存一旦被访问,不久之后这块内存附近的内存也很可能被访问
CPU的缓存
CPU从内存中加载数据X时,会将数据X缓存在高速缓存Cache中,实际上CPU缓存X的同时,还缓存了X周围的数据,因为根据程序具备局部性原理,X周围的数据也很有可能被访问
方式
内存分配更加合理,使用RingBuffer数据结构,数组元素在初始化时一次性全部创建,提升缓存命中率;对象循环利用,避免频繁GC
能够避免伪共享,提升缓存利用率
采用无锁算法,避免频繁加锁、解锁的性能消耗
支持批量消费,消费者可以无锁方式消费多个消息
举例
Log4j2、Spring Messaging、HBase、Storm
核心
RingBuffer
RingBuffer是用数组实现,在初始化时是一次性创建所有元素,所以这些元素的内存地址大概率是连续的,因此,利用CPU缓存的原理,CPU缓存会加载访问元素附件的元素,提高性能
避免“伪共享”
概念
伪共享指的是由于共享缓存行导致缓存无效的场景。一般来说,如果缓存的数据会在内存中被修改,那么缓存就会被CPU重新加载,那么在被修改的数据附近的不变数据也会被重新加载,导致不变数据缓存失效
方式
缓存行填充
每个变量独占一个缓存行、不共享缓存行
缺点
这是浪费内存的空间下完成,需要较大的内存空间
无锁
在更新队列索引使用了CAS,没有使用互斥锁
高性能数据库连接池HiKariCP
执行数据库操作步骤
通过数据源获取一个数据库连接
创建Statement
执行SQL
通过ResultSet获取SQL执行结果
释放ResultSet
释放Statement
释放数据库连接
FastList解决了哪些性能问题
问题
当关闭Connection时,能够自动关闭Statement
解决方案
将创建的Statement保存在数组ArrayList里,这样当关闭Connection的时候,就可以依次将数组中的所有Statement关闭
因为ArrayList中Remove是顺序查找,而实际上需要逆序删除
解决方案
将 remove(Object element) 方法的 查找顺序变成了逆序查找
get(int index) 方法没有对index参数进行越界检查
ConcurrentBag解决了哪些性能问题
概念
并发容器
它的一个核心设计是使用ThreadLocal避免部分并发问题
核心属性
CopyOnWriteArrayList<T> sharedList;(用于存储所有的数据库连接)
ThreadLocal<List<Object>> threadList;(线程本地存储中的数据库连接)
AtomicInteger waiters;(等待数据库连接的线程数)
SynchronousQueue<T> handoffQueue;(分配数据库连接的工具)
核心类
add()
将这个连接加入到共享队列sharedList中,如果此时有线程在等待数据库连接,那么就通过handoffQueue将这个连接分配给等待的线程
borrow()
首先查看线程本地存储是否有空闲连接,如果有,则返回一个空闲的连接(线程本地存储中的连接是可以被其他线程窃取的,所以需要用CAS方法防止重复分配。在共享队列中获取空闲连接,也采用了CAS方法防止重复分配)
如果线程本地存储中无空闲连接,则从共享队列中获取
如果共享队列中也没有空闲的连接,则请求线程需要等待
模型
面向对象原生的并发模型(Actor模型)
概念
Actor模型本质上是一种计算模型,基本的计算单元称为Actor,换言之, 在Actor模型中,所有的计算都是在Actor中执行的。在面向对象编程里面,一切都是对象;在Actor模型里,一切都是Actor,并且Actor之间是完全隔离的,不会共享任何变量。
Actor中的消息机制完全是异步的。而 调用对象方法,实际上是 同步 的,对象方法return之前,调用方会一直等待
在调用对象方法中,需要持有对象的引用, 所有的对象必须在同一个进程中;而在Actor模型中,发送消息和接收消息的Actor可以不在一个进程中,也可以不在同一台机器上
能力
处理能力,处理接收到的消息
存储能力,Actor可以存储自己的内部状态,并且内部状态在不同Actor之间是绝对隔离的
通信能力,Actor可以和其他Actor之间通信
框架
Akka
软件事务内存(STM)
传统的数据库事务,支持4个特性
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
STM仅支持三个特性
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
框架
Multiverse
多版本并发控制(MVCC)
数据库事务在开启的时候,会给数据库打一个快照,以后所有的读写都是基于这个快照的。当提交事务的时候,如果所有读写过的数据在该事务执行期间没有发生过变化,那么就可以提交;如果发生了变化,说明该事务和有其他事务读写的数据冲突了,这个时候是不可以提交的。
协程
概念
一种轻量级的线程。从操作系统的角度来看,线程是在内核态中调度的,而协程是在用户态调度的,所以相对于线程来说,协程切换的成本更低。协程虽然也有自己的栈,但是相比线程栈要小得多,典型的线程栈大小差不多有1M,而协程栈的大小往往只有几K或者几十K。
I/O
类型
阻塞I/O
客户端连接和服务器线程之间有哦一一对应的关系,每个线程只处理一个连接
非阻塞I/O
概念
每个客户端的套字节都注册到了服务器的选择器,当客户端发送请求时,选择器会受到来操作系统的事件,然后通知服务器线程里的某个线程,特定客户端的I/O已经可以读取。该线程会从客户端读取数据,处理请求,发回响应,然后返回线程池等待下一个请求。
分工
选择器线程
在I/O可用时通知系统调用的线程
工作线程
选择器通知后,线程池处理时机请求和响应的线程
方式
选择起和工作线程是分开的,选择器现在所有套接字上等待通知,然后将请求移交给工作线程
当选择器收到关于IO的通知时,他会读取(或一部分)IO并确定请求的相关信息,然后根据请求的类型,将请求转发给不同的服务器线程
选择器池也可以接受ServerSocket上的新连接,在建立连接之后,所有工作由工作线程池处理、工作线程池里的线程有时候会用Selector类来等待现有连接的等待处理IO,有时会处理来自工作线程的待处理客户端IO的通知
完全不区分作为选择器的线程和请求的线程。被通知某个套接字上有待处理IO的线程,可以处理整个请求。同时,池中的其他线程也可以收到其他套接字上关于IO的通知,并处理套接字上的请求。
缓冲流问题
InputStream.read()和OutputStream.write()操作的是的单个字符,根据访问资源的情况,这些方法可能会非常慢。read()方法每次都需要进入内核以获取1字节的数据。在大多数操作系统,内核都会堆IO进行缓冲,所以这个操作不会在每次调用read()都会触发读取磁盘。但这个缓冲区是保存在内核中,而不是应用程序因此,每次读取1字节都会执行一次开销巨大的系统调用。写数据也是同理,每次发送1字节,也需要一次系统调用,将该字节发送到内核缓冲区,当流关闭时,内核会将该缓冲区的内容写到磁盘。
ByteArrayInputStream和ByteArrayOutputStream本质上只是大的内存缓冲区,在很多情况下,用缓冲过滤流包装它们,意味着数据会被复制两次:一次是被复制到过滤流的缓冲区,一次是被复制到ByteArrayInputStream的缓冲区(反之,输出流也是如此)。因此,如果在没有其他流参与的时候,应该避免缓冲IO。
对于文件和套接字,还有像压缩和字符串编码这样的内部操作,必须适当地堆IO进行缓冲。关于在什么时候需要在两个流之间使用缓冲流,没有通用的规则,着最终取决于所涉及的流类型,如果这两个流操作的是字节块而不是单字节却运行的更好,就需要使用。
持久化
JDBC
类型
1型
ODBC(Open Database Connectivity,开放数据库连接)与JDBC之间的桥梁,性能很差,只有必须使用时候才应该选择
2型
使用原生库来访问数据库。这使得Java驱动可以利用C库来工作,对于一些堆C库有投入的供应商来说,这种类型的驱动性能非常好
3型
特殊架构的驱动,通常有中间件提供中间转换过程,可以是独立程序。在这种类型有缓存的情况下性能较好,如果没有则需要负担网络请求的性能影响
4型
纯Java驱动,实现了数据库供应商位访问数据库定义的线路协议。与2型一样,性能非常好
连接池
数据库连接初始化成本很高,所有需要以池化方式就行管理。同时,不正常的池化方式会导致GC不正常。同时数据库会为每个连接的预处理语句额外分配内存,所以连接不宜太多
预处理语句和语句池
PreparedStatement
多数据情况下应该使用 PreparedStatement 而不是 Statement 进行JDBC调用,因为预处理语句允许数据库重用正在执行的SQL信息,为后续执行省去工作,数据库框架也会默认使用PreparedStatement。PrepareStatement.setFetchSize()设置JDBC一次传输的数据行。
Statement
如果语句只有一次使用机会则可以使用 Statement 执行,但是通常情况下一个应用程序不会出现只有几次数据库调用
CallableStatement
用于执行存储在数据库中的存储过程。
语句池
每个连接对象都会有自己的语句池,通常来说,如果使用了语句池除了第一个语句会相对慢一点,后面都是重用语句会变得更快,与其他池化操作一样,池的大小需要谨慎设置
ConnectionPoolDataSource.setMaxStatements() 如果设置为0则表示禁用语句池,大于0则表示缓存的数量
事务
ACID
原子性
一致性
隔离性
持久性
概念
对于JDBC,事务的开始和结束都是基于Connection对象的使用方法。在JDBC中默认通过setAutoCommit()方法设置事务的自动提交模式,如果关闭自动提交,在调用executeQuery()时会隐式的开启一个事务,直到执行commit()或rollback()方法。若开启下一个数据库调用,则会重写开启新事务。
Savepoint接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点
隔离
TRANSACTION_SERIALIZABLE
成本最高的隔离模式,在事务进行期间,事务访问的所有数据都被锁定,适用于通过主键或where来访问的模式
TRANSACTION_REPEATABLE_READ(MySQL默认事务)
在事务执行期间,被访问数据会被锁定,但是可以插入新数据,这会导致事务中第一次查询和第二次查询数据不一致,导致幻读
TRANSACTION_READ_COMMITTED(Oracle和Db2默认事务)
值锁定在事务期间被写入的数据,这会发现在事务进行期间某一个时刻的数据和另一时刻数据不同,导致不可重复读
TRANSACTION_READ_UNCOMMITTED
成本最低,因为不涉及到锁,因此一个事务可以读取另一个事务未被t提交的数据,导致脏读
TRANSACTION_NONE
这种模式严格意义上来说只存在只读场景,不是一种事务模式,在JDBC中不能对设置事务模式为None
锁
悲观锁
乐观锁
JPA
通过字节码增强技术,将优化在编译阶段就完成
写优化
限制数据库的写入调用数量中获益
语句池可以尝试在JDBC层实现
JPA的批量更新可以在persistence.xml声明,也可以调用flush()方法完成
读优化
限制单次操作中的数据读取量
不常用的大字段在JPA实体中应该是延迟加载的
JPA实体之间存在关系,关联数据可以选择立即加载或延时加载
当选择立即加载时,可以使用命名查询来发出一条使用join的sql语句
通过明明查询读取数据通常闭常规查询快,因为对于JPA实现来说,使用PreparedStatement进行命名查询更容易
缓存
概念
每个实体管理器实例都有自己的缓存,他会在本地缓存事务处理过程中检索的数据。此外,他也会在本地缓存事务中写入的数据,在事务提交时,才把数据发送回数据库
JPA缓存只对通过主键访问的实体进行操作,即通过调用find()方法检索到的数据,或者通过访问(或立即加载)相关实体检索到的数据
分类
一级缓存
实体管理器中的本地缓存,基本不需要优化,且默认开启
二级缓存
全局缓存,并不是默认开启
当实体管理器试图通过主键或关系映射查到一个对象时,会现在L2查找,如果找到就回返
通过查询检索到的数据不会保存在L2缓存中
时机
立即加载
延迟加载
Spring Data
Spring Data JDBC
是JPA的一个简单替代方案,有类似的实体映射,但没有缓存、延迟加载或脏实体跟踪的功能
Spring Data JPA
是标准JPA的包装器
Spring Data for NoSQL
Spring有各种NoSQL和类NoSQL技术的连接器,在一定程度简化了NoSQL的访问,因为存储技术是一样的,只是设置和初始化有区别
Spring Data R2DBC
允许对Postgres、H2和SQL server数据库进行异步访问,遵循典型的Spring Data编程模型,而不是直接的JDBC,与Spring Data JDBC类似,也同样没有缓存、延迟加载等JPA的特色功能
Mybatis
概念
以一个 SqlSessionFactory 的实例为核心的
核心配置类型
获取数据库连接实例的数据源
事务作用域/事务管理器
SqlSession
概念
提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句
创建方式
SqlSessionFactory
方式
预先配置的 Configuration 实例来构建出来
XML配置文件构建出来
创建方式
SqlSessionFactoryBuilder
方法
SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)
加载
首先,读取在 properties 元素体中指定的属性;
其次,读取在 properties 元素的类路径 resource 或 url 指定的属性,且会覆盖已经指定了的重复属性;
最后,读取作为方法参数传递的属性,且会覆盖已经从 properties 元素体和 resource 或 url 属性中加载了的重复属性。
方法
SqlSession openSession()
特征(默认)
事务作用域将会开启(也就是不自动提交)。
将由当前环境配置的 DataSource 实例中获取 Connection 对象。
事务隔离级别将会使用驱动或数据源的默认设置。
预处理语句不会被复用,也不会批量处理更新。
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();
方法(核心)
<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
<T> Cursor<T> selectCursor(String statement, Object parameter)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)
事务
方法(核心)
void commit()
void commit(boolean force)
void rollback()
void rollback(boolean force)
动态SQL
核心注解
@InsertProvider
@UpdateProvider
@DeleteProvider
@SelectProvider
接口
ProviderMethodResolver
SQL 语句构建器
new SQL()
配置
作用域和生命周期
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
还可以覆盖配置类,需继承配置类后覆盖其中的某个方法,再把它传递到 SqlSessionFactoryBuilder.build(myConfig) 方法即可。但这可能会极大影响 MyBatis 的行为。
最佳作用域
方法作用域(局部方法)
SqlSessionFactory
一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。多次重建 SqlSessionFactory 被视为一种代码“坏习惯”
最佳作用域
应用作用域(单例模式或静态单例)
SqlSession
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的
最近作用域
请求或方法作用域
映射器实例
映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。
最近作用域
方法作用域(局部方法)
类型处理器
基础类型扩展
BaseTypeHandler
EnumTypeHandler
处理任意继承了 Enum 的类
EnumOrdinalTypeHandler
对象工厂(objectFactory)
概念
每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。
扩展
DefaultObjectFactory
插件(plugins)
概念
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。
类型
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
扩展
实现Interceptor接口,配合使用插件类型
环境配置(environments)
概念
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中。尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
事务管理器(transactionManager)
类型
JDBC
使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
MANAGED
MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为
Spring + MyBatis
Spring 模块会使用自带的管理器来覆盖前面的配置。
实现
事务管理器实例化后,所有在 XML 中配置的属性将会被传递给 setProperties() 方法,之后再创建一个 TransactionFactory 和 Transaction 接口的实现类,就可以自定义事务
数据源(dataSource)
类型
UNPOOLED
这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。
POOLED
这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。
JNDI
这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
扩展
DataSourceFactory 接口实现
UnpooledDataSourceFactory 可被用作父类来构建新的数据源适配器
配置
在Type字段指定
数据库厂商标识(databaseIdProvider)
概念
MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。 MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。
扩展
mybatis-config.xml 文件中加入 databaseIdProvider,并实现接口 DatabaseIdProvider,来构建自定义的标识
映射器(mappers)
方式
使用相对于类路径的资源引用
使用完全限定资源定位符(URL)
使用映射器接口实现类的完全限定类名
将包内的映射器接口实现全部注册为映射器
自动映射
概念
当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。
通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true。
在提供了结果映射后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。
等级
NONE
禁用自动映射。仅对手动映射的属性进行映射。
PARTIAL(默认)
对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射。
FULL
自动映射所有属性
缓存
概念
MyBatis 内置了一个强大的事务性查询缓存机制,默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:<cache/>
缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。
本地缓存
概念
每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。
生命周期
默认情况下,本地缓存数据的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度。
如果 localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。
二级缓存
映射语句文件中的所有 select 语句的结果将会被缓存。
映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
缓存会使用最 LRU(默认)/ FIFO / SOFT / WEAK 算法来清除不需要的缓存。
缓存根据 flushInterval(毫秒) 的值指定,默认不刷新。
缓存数量根据 size 属性指定,默认 1024 。
缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。通过配置 readOnly(只读)属性可以被设置为 true 或 false 来设定缓存是只读还是可读写。
二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
多数据库支持
如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句
Spring
IoC
BeanFactory
概念
类的通用工程,可以创建并管理各种类的对象
初始化
XmlBeanFactory 通过 Resource装载 Spring 配置信息并启动 IoC 容器,BeanFactory 启动 IoC 容器时,并不会初始化定义的 Bean,而是发生着 Bean 的第一个调用时
如果一个配置错误的Bean被懒初始化了,那么在启动过程中就不会再出现故障,问题只有在Bean被初始化时才会显现出来
DefaultSingletonBeanRegistry
用于单例Bean的缓存容器,由HashMap实现,对于单例Bean的第二次getBean()则会从这里获取实例缓存
ApplicationContext
概念
由BeanFactory派生而来,提供了可以通过配置方案实现的功能
加载
类文件路径
ClassPathXmlApplication
文件系统路径
FilySystemXmlApplication
初始化
初始化应用上下文时就会实例化所有单实例的Bean,所以相比BeanFactory初始化时间会更长
注解实现类
AnnotactionConfigApplicationContext
WebApplicationContext
概念
WebApplicationContext专门为Web应用准备,它允许从相对于Web根目录的路径中装配文件
从WebApplicationContext中可以获得SerlevtContext的引用,整个Web上下文对象将作为属性放到SerlevtContext中,以便Web应用可以访问Spring应用上下文
初始化
需要处于web环境中,因此需要SerlevtContext实例。需要借助 web.xml 中的配置或 web容器监听器 SerlevtContextListener 猜能完成启动 Spring Web应用上下文工作
提供了两个用于启动的监听器
ContextLoaderServlet
ContextLoaderListhenr
资源加载
地址前缀
classpath:
相对于类的根路径的加载,可以是文件,也可以是jar或zip的类包中
file:
使用UrlResource从文件系统中加载资源,可以采用绝对或相对路径
http://
使用UrlResource从web服务器中加载资源
ftp://
使用UrlResource从ftp服务器中加载
无前缀
根据 ApplicationContext 具体实现类采用对应类型的 Resource
Bean
Bean的作用域
Web环境
request
每次http请求会创建创建新的bean
session
同一个http session共用一个Bean,不同的会创建新的bean
global session
同一个全局session共用一个Bean
Singleton
Bean以单例形式存在
prototype
每次getBean()时都是新的实例
非Web
Singleton
prototype
通过@Scope指定
Spring容器启动
前置条件
Spring框架的类包都放到了应用程序的类路径下
应用程序为Spring提供了完备的Bean配置的信息
Bean的类都已经放到了应用程序的类路径下
顺序
读取应用程序提供的Bean配置信息
根据配置信息生成Bean配置注册表(BeanDefinition)
根据注册表实例化Bean
装配好Bean之间的依赖关系
将准备就绪的Bean放到Bean缓存池中
Bean配置信息(元数据)
Bean的实现类
Bean的属性信息
如数据源的连接数、用户名、密码等
Bean的依赖关系
Spring根据依赖关系配置完成Bean之间的装配
Bean的行为配置
如生命周期范围及生命周期各过程的回调函数等
依赖注入
属性注入
Spring通过Bean提供的默认构造器(无参构造)实例化Bean,再通过反射调用set方法注入属性或依赖对象
构造函数注入
按数量匹配入参
使用属性注入方式只能人为在配置时提供保证,而无法在语法级提供保护,构造函数可以满足这一要求
使用构造函数注入的前提是Bean必须提供带参数的构造函数
按类型匹配入参
通过入参位置索引确定
联合使用烈性和索引匹配入参
通过入参类型和位置确定
通过自身类型反射匹配入参
如果Bean构造器入参的类型是可辨别的(非基础类型且各有差异),可以通过反射获取构造器入参类型完成注入
循环依赖问题
成功实现注入的前提是Bean的构造函数入参引用的对象必须已经准备就绪。使用构造器注入且Bean之间互相注入,会引发循环依赖问题。
改为属性注入方式
自动装配
方式
byName
通过名称自动匹配
byType
通过Class类型
constuctor
通过构造器
autodetect
根据Bean的自省机制决定,如果Bean提供了默认构造器的,则采用byType,否则采用constuctor
注解
@Autowired
默认按类型匹配,当有且仅有一个匹配的Bean,才会注入装配成功。require=false,表示没有找到不报异常
@Qualifier
默认按名称查找
@Resource
@Autowired+@Qualifier
自动配置
@Configuration
AOP
连接点(Joinpoint)
方法的连接点,方法调用前、方法调用后、方法抛出异常时
切点(Pointcut)
通过设置的切点表达式,Spring会解析出切点中包含的所有连接点
类型
StaticMethodMatcherPointcut(静态方法切点)
DynamicMethodMatcherPointcut(动态方法切点)
AnnotationMatcherPointcut(注解切点)
ExpressionPointcut(表达式切点)
ControlFlowPointcut(流程切点)
特殊切点,会根据程序执行的堆栈的信息查看目标方法是否由某一个方案之间或间接发起调用,以此判断是否为匹配的连接点
ComposablePointcut(符合切点)
创建多个切点而提供的操作类
增强(Advice)
在连接点上增加代码
类型
BeforeAdvice(前置增强)
AfterRetuningAdvice(后置增强)
ThrowAdvice(抛出异常增强)
MethodInterceptor(环绕(前后都)增强))
IntroductionInterceptor(引介增强)
目标对象(Target)
增强逻辑的织入目标类
引介(Introduction)
一种特殊的增强,可以为类添加一些属性和方法
织入(Weaving)
增加添加到目标类具体连接点上的过程
方式
编译期织入,要求使用特殊的Java编译器(AspectJ使用)
类加载期织入,使用特殊的类加载器(AspectJ使用)
动态代理织入,在运行期为类添加生成子类(Spring使用)
代理(Proxy)
一个类被AOP增强后,就产出了一个结果类。是一个融合了原类和增强逻辑的代理类。
切面(Aspect)
切面由切点和增强(引介)组成,即包含了横切逻辑的定义,也包含了连接点的定义
类型
Advisor(一般切面)
仅仅包含一个Advice,因为这个切面太宽泛,一般不会直接使用
PointcutAdvisor(切点切面)
包含切点的切面,只包含Advice和Pointcut
IntroductionAdvisor(引介切面)
引介切面,使用ClassFilter进行定义
Proxy
ProxyFactoryBean
每一个需要被代理的Bean都需要使用一个ProxyFactoryBean进行配置
在内部,Spring使用BeanPostProcessor自动完成这项工作
BeanPostProcessor
概念
这些基于BeanPostProcessor的自动代理创建器的实现类,将根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理实例
类型
BeanNameAutoProxyCreator
基于Bean配置名规则的自动代理创建器:允许为一组特定配置名的Bean自动创建代理实例的代理创建器
DefaultAdvisorAutoProxyCreator
基于Advisor匹配机制的自动代理创建器:它会对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中(即为目标Bean创建代理实例)
AnnotationAwareAspectJAutoProxyCreator
基于Bean中AspectJ注解标签的自动代理创建器:为包含AspectJ注解的Bean自动创建代理实例
事务管理
SPI
PlatformTransactionManager
根据TransactionDefinition提供的事务属性配置信息,创建事务
TransactionDefinition
用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性
TransactionStatus
描述这个激活事务的状态
事务同步管理(TransactionSynchronizationManager)
Spring将JDBCConnection、Hibernate的session等访问数据库的连接或绘画对象统称为资源,这些资源在同一时刻是不能多线程共享的
Spring事务管理基于TransactionSynchronizationManager进行工作,TransactionSynchronizationManager使用ThreadLocal管理为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态
事务传播行为
PROPAGATION_REQUIRED
如果没有事务就创建事务,如果存在一个事务就加入该事务
PROPAGATION_SUPPORTS
支持当前事务,如果没有事务就以非事务方式运行
PROPAGATION_MANDATORY
使用当前事务,如果当前没有事务则抛出异常
PROPAGATION_REQUIREDS_NEW
创建新事务,如果当前存在事务,就把当前事务挂起
PROPAGATION_SUPPORTED
以非事务方式运行,如果存在事务,就把当前事务挂起
PROPAGATION_NEVER
以非事务方式运行,如果存在事务就抛出异常
PROPAGATION_NESTED
如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则执行创建事务
编程式事务(TransactionTemplate)
TransactionTemplate和那些持久化模板类一样,是线程安全的,因此,可以在多个业务类中共享TransactionTemplate实例进行事务管理。TransactionTemplate拥有多个设置事务属性的方法,如setReadOnly(boolean readOnly)、setIsolationLevel(int isolationLevel)等。
void setTransactionManager(PlatformTransactionManager transactionManager):设置事务管理器。
Object execute(TransactionCallback action):在TransactionCallback回调接口中定义需要以事务方式组织的数据访问逻辑。
声明式事务(@Transactional)
事务传播行为:PROPAGATION_REQUIRED。
事务隔离级别:ISOLATION_DEFAULT。
读写事务属性:读/写事务。
超时时间:依赖于底层的事务系统的默认值。
回滚设置:任何运行期异常引发回滚,任何检查型异常不会引发回滚。
MVC
概念
MVC(Model、View、Control)
处理请求的过程
整个过程始于客户端发出一个HTTP请求,Web应用服务器接收到这个请求,如果匹配DispatcherServlet的请求映射路径(在web.xml中指定),Web容器就将该请求转交给Dispatcher Servlet处理
DispatcherServlet接收到这个请求后,将根据请求的信息(包括URL、HTTP方法、请求报文头、请求参数、Cookie等)及HandlerMapping的配置找到处理请求的处理器(Handler)
当DispatcherServlet根据HandlerMapping得到对应当前请求的Handler后,通过Handler Adapter对Handler进行封装,再以统一的适配器接口调用Handler
处理器完成业务逻辑的处理后将返回一个ModelAndView给DispatcherServlet,Model AndView包含了视图逻辑名和模型数据信息
ModelAndView中包含的是“逻辑视图名”而非真正的视图对象,DispatcherServlet借由ViewResolver完成逻辑视图名到真实视图对象的解析工作
当得到真实的视图对象View后,DispatcherServlet就使用这个View对象对ModelAndView中的模型数据进行视图渲染
最终客户端得到的响应消息可能是一个普通的HTML页面,也可能是一个XML或JSON串,甚至是一张图片或一个PDF文档等不同的媒体形式
DispatcherServlet
截获特定的URL请求
通过contextConfigLocation参数指定业务层Spring容器的配置文件(多个配置文件使用逗号分隔),ContextLoaderListener是一个ServletContextListener,它通过contextConfigLocation参数所指定的Spring配置文件启动“业务层”的Spring容器
配置名为viewspace的DispatcherServlet,它默认自动加载/WEB-INF/viewspace-servlet. xml(即<servlet-Name>-servlet.xml)的Spring配置文件,启动Web层的Spring容器
通过<servlet-mapping>指定DispatcherServlet处理所有URL以.html为后缀的HTTP请求,即所有带.html后缀的HTTP请求都会被DispatcherServlet截获并处理
调用Spring Bean
“Web层”Spring容器将作为“业务层”Spring容器的子容器,即“Web层”容器可以引用“业务层”容器的Bean,而“业务层”容器却访问不到“Web层”容器的Bean
初始化
namespace
DispatcherServlet对应的命名空间,默认为“<servlet-name>-servlet”,用以构造Spring配置文件的路径。显式指定该属性后,配置文件对应的路径为WEB-INF/<namespace>.xml,而非WEB-INF/< servlet-name>-servlet.xml
如果将namespace设置为viewspace,则对应的Spring配置文件为WEB-INF/viewspace.xml
contextConfigLocation
如果DispatcherServlet上下文对应的Spring配置文件有多个,则可使用该属性按照Spring资源路径的方式指定
如“classpath: viewspace1.xml,classpath:viewspace2.xml”,DispatcherServlet将使用类路径下的viewspace1.xml和viewspace2.xml这两个配置文件初始化WebApplicationContext
publishContext
boolean类型属性,默认值为ture。DispatcherServlet根据该属性决定是否将WebApplicationContext发布到ServletContext的属性列表中,以便调用者可借由Servlet Context找到WebApplicationContext实例,对应的属性名为DispatcherServlet#getServlet ContextAttributeName()返回的值
publishEvents
boolean类型属性。当DispatcherServlet处理完一个请求后,是否需要向容器发布一个ServletRequestHandledEvent事件,默认为true。如果容器中没有任何事件监听器,可以将此属性设置为false,以便提高运行性能
ViewResolver
核心方法
View resolveViewName(String viewName, Locale locale)
主要实现
解析为Bean的名字
BeanNameViewResolver
将逻辑视图名解析为一个Bean,Bean的ID等于逻辑视图名
XmlViewResolver
目标视图Bean是一个独立的XML文件,而非定义在DispatchServlet上下文主配置中
国际化解析
ResourceBundleViewResolver
在国际化资源文件中定义视图实现类及相关信息,使用该视图解析器可以为不同的本地化类型提供不同的解析结果
解析为URL文件
InternalResourceViewResolver
将视图名解析为一个URL文件
XsltViewResolver
将视图名解析为一个指定XSLT样式表的文件(JSP)
JasperReportsViewResolver
JasperReports是一个基于Java的开源报表工具,该解析器将视图名解析为宝宝文件对于的URL
模板文件视图
FreeMarkerViewResolver
解析为基于FreeMarker模板技术的模板文件
VeocityViewResolver / VeocityLayoutViewResolver
解析为基于Veocity模板技术的模板文件
内容协商
ContentNegotiatingViewResolver
不负责具体解析,而是作为一个中间人的角色根据请求所要求的MIME类型,从上下文中选择一个合适的视图解析器,再将解析视图的工作委托其负责
SpringBoot
关键注解
加载资源
@Configuration
@ImportResource
@Import
@ComponentScan
自动装配
@SpringBootApplication
@EnableAutoConfiguration
@ComponentScan
@SpringBootConfiguration
依赖注入
@Component
@Service
@Repository
@Controller
MVC
@RestController
@RequestMapping
启动流程
初始化使用Java的版本和进程ID
初始化profile,如果没有指定,默认为“default”
初始化Tomcat启动端口、服务和引擎
初始化Spring WebApplicationContext
启动Tomcat
Application 可用性
方法
使用Spring Boot的 “actuator” ,那么这些状态将作为健康端点组(health endpoint groups)暴露出来
liveness
概念
“Liveness” 状态告诉我们它的内部状态是否允许它正常工作,或者在当前失败的情况下自行恢复。
Spring Boot应用程序的内部状态大多由Spring ApplicationContext 表示。如果 application context 已成功启动,Spring Boot就认为应用程序处于有效状态。一旦context被刷新,应用程序就被认为是活的,
状态
broken 状态
意味着应用程序处于一个无法恢复的状态,基础设施应该重新启动应用程序。
注意事项
一般来说,"Liveness" 状态不应该基于外部检查,比如健康检查。 如果是这样,外部系统如果发生异常(数据库、Web API、缓存)将引发大规模的重启和整个平台的级联故障。
readiness
概念
“Readiness” 状态告诉平台,该应用程序是否准备好处理流量
状态
failing状态
暂时不应该将流量发送到该应用程序
注意事项
预计在启动期间运行的任务应该交由 CommandLineRunner 和 ApplicationRunner 组件执行,而不是使用Spring组件的生命周期回调,如 @PostConstruct。
扩展性
注入 ApplicationAvailability 接口
Application 事件和监听器
SpringApplicationEvent 事件发布顺序
发布 ApplicationStartingEvent
在运行开始后,但在其他处理之前(除了注册监听器和初始化器)
发布 ApplicationEnvironmentPreparedEvent
在上下文中当 Environment 被创建之后,但在创建上下文之前
发布 ApplicationContextInitializedEvent
当 ApplicationContext 和 ApplicationContextInitializers 已完成初始化,且在任何Bean定义被加载之前
发布 ApplicationPreparedEvent
在刷新开始前,但在Bean定义加载后
发布 ApplicationStartedEvent
在上下文被刷新之后,但在执行任何应用程序和命令行运行程序之前
发布 AvailabilityChangeEvent(LivenessState.CORRECT)
表明应用程序被认为是存活的。
发布 ApplicationReadyEvent
在执行 ApplicationRunner 和 CommandLineRunner 后
发布 AvailabilityChangeEvent(ReadinessState.ACCEPTING_TRAFFIC)
表明应用程序已经准备好为请求提供服务
发布 ApplicationFailedEvent
如果启动时出现异常
发布 WebServerInitializedEvent
在 ApplicationPreparedEvent 之后和 ApplicationStartedEvent 之前,并且在 WebServer 准备好后发布
实现
(Servlet)ServletWebServerInitializedEvent
(Reactive)ReactiveWebServerInitializedEvent
发布 ContextRefreshedEvent
当 ApplicationContext 被刷新时
注意事项
事件监听器不应该运行潜在耗时的任务,因为它们默认是在同一个线程中执行。 考虑使用 ApplicationRunner 和 CommandLineRunner 代替。
程序退出
概念
每个 SpringApplication 都向JVM注册了一个shutdown hook,以确保 ApplicationContext 在退出时优雅地关闭。 所有标准的Spring生命周期回调(如 DisposableBean 接口或 @PreDestroy 注解)都可以使用。
外部配置
SpringCloud
OpenFeign
概念
底层通过Ribbon来实现
优点
声明式调用
Spring MVC 风格
注解
@FeignClient
@EnableFeignClients
客户端接口的继承
标注了@FeignClient,会被OpenFeign机制扫描成OpenFeign的客户端,它所继承的方法也会被OpenFeign扫描,成为可以调用远程服务器的方法
优点
微服务的开发者就可以直接通过依赖该公共模块来获取UserFacade,把它当作本地接口,通过继承的方法直接使用它,而不再需要自定义调用方法了
从客观的角度来说,由用户微服务的开发者来维护UserFacade接口是最佳的,因为他们熟悉当中的业务和逻辑,然后再通过详尽的说明(如提供API文档)就可以大大降低其他微服务使用接口的难度了
缺点
如果公共的接口需要修改,那么所有的消费者也需要做出对应的修改,尤其是那些使用十分广泛的接口,影响就更大了
编码/解码
编码器
SpringEncoder
解码器
OptionalDecoder,这个类是一个代理,实际使用的是ResponseEntityDecoder
协议
SpringMvcContract
OpenFeign拦截器
RequestInterceptor接口
Gateway
概念
Gateway并非是使用传统的Jakarta EE的Servlet容器,它是采用响应式编程的方式进行开发的.在Gateway中,需要Spring Boot和Spring WebFlux提供的基于Netty的运行环境
路由(route)
路由网关是一个最基本的组件,它由ID、目标URI、断言集合和过滤器集合共同组成,当断言判定为true时,才会匹配到路由
断言(predicate)
它主要是判定当前请求如何匹配路由,采用的是Java 8断言。可以存在多个断言,每个断言的入参都是Spring框架的ServerWebExchange对象类型
它允许开发者匹配来自HTTP请求的任何内容,例如URL、请求头或请求参数,当这些断言都返回true时,才执行这个路由
过滤器(filter)
使用特定工厂构造的SpringFrameworkGatewayFilter实例,作用是在发送下游请求之前或者之后,修改请求和响应。和断言一样,过滤器也可以有多个
执行方式
创建一条线程,通过类似Zuul的过滤器拦截请求
对源服务器转发请求,但注意,Gateway并不会等待请求调用源服务器的过程,而是将处理线程挂起,这样便不再占用资源了
等源服务器返回消息后,再通过寻址的方式来响应之前客户端发送的请求
优/缺点
优点
仅仅是负责转发请求到源服务器,并不会等待源服务器执行完成
因此Gateway的线程活动的时间会更短,线程积压的概率更低
缺点
因为Gateway依赖WebFlux,而WebFlux和Spring Web MVC的包冲突,所以项目再引入spring- boot-starter-web就会发生异常
当前Gateway只能支持Netty容器,不支持其他容器,所以引入Tomcat或者Jetty等容器就会在运行期间出现意想不到的问题
比较
Zuul
概念
Zuul 1.x是基于传统的Jakarta EE的Servlet容器方式的,而Gateway是基于响应式方式的
Zuul会为一个请求分配一条线程,然后通过执行不同类型的过滤器来完成路由的功能。
缺点
线程会等route类型的过滤器去调用源服务器,显然这是线程执行过程中最为缓慢的一步,因为源服务器可能会因执行比较复杂的业务而响应特别慢,这样Zuul中的线程就需要执行比较长的时间,容易造成线程积压,导致性能变慢。
路由配置
RouteLocatorBuilder
断言(Predicate)
RoutePredicateFactory
Before路由断言工厂
时间断言,判断路由在什么时间之前有效
After路由断言工厂
时间断言,判断路由在什么时间点之后才有效
Between路由断言工厂
时间断言,判断时间在两个时间点之间才有效
Cookie路由断言工厂
Cookie参数断言,判定某个Cookie参数是否满足某个正则式,当满足时才路由
Header路由断言工厂
请求头参数断言,判断某个请求头参数是否匹配一个正则式,当满足时才路由
Host路由断言工厂
限制主机名称(Host)的断言,主机名是否满足一个正则,满足才路由
Method路由断言工厂
Method路由断言工厂用来判断HTTP的请求类型,如判断GET、POST、PUT等请求
Path路由断言工厂
Path路由断言工厂是通过URI路径判断是否匹配路由
Query路由断言工厂
Query路由断言工厂是对请求参数的判定,它分为两类,一类是判断是否存在某些请求参数,另一类是对请求参数值进行验证
RemoteAddr路由断言工厂
判定服务器地址的断言工厂
Weight路由断言工厂
Weight路由断言工厂是一种按照权重路由的工厂。在权重路由中,存在分组和权重的概念,分组是通过一个组名的字符串进行区分的,而权重是一个数字
一个微服务可以由多个实例构成,实例的版本可以不同。例如,当前实例中存在旧版本(v1)和新版本(v2),相对来说,旧版本比较稳定,而新版本可能不太稳定,那么可以考虑先小规模使用新版本,待实践过后,再彻底地升级为新版本。可以考虑让用户的请求80%的概率路由到旧版本,而20%的概率路由到新版本
过滤器(Filter)
GatewayFilterFactory
AddRequestHeader过滤器工厂
AddRequestHeader是一个添加请求头参数的过滤器工厂,通过它可以增加请求参数
AddRequestParameter过滤器工厂
AddRequestParameter过滤器工厂可以新增请求参数
AddResponseHeader过滤器工厂
AddResponseHeader过滤器工厂可以增加响应头参数
Retry过滤器工厂
retries
重试次数,非负整数
statuses
根据HTTP响应状态来断定是否重试。当请求返回对应的响应码时,进行重试,用枚举org.springframework.http.HttpStatus表示
methods
请求方法,如GET、POST、PUT和DELETE等。使用枚举org.springframework.http. HttpMethod表示
series
重试的状态码系列,取响应码的第一位,按HTTP响应状态码规范取值范围为1 ~ 5,其中,1代表消息,2代表成功,3代表重定向,4代表客户端错误,5代表服务端错误
exceptions
请求异常列表,默认的情况下包含IOException和TimeoutException两种异常
Hystrix过滤器工厂
Hystrix过滤器工厂提供的是熔断功能,当请求失败或者出现异常的时候,就可以进行熔断了。而一般熔断发生后,会通过降级服务来提高用户体验,所以这往往还会涉及跳转的功能
RequestRateLimiter过滤器工厂
RequestRateLimiter工厂用于限制请求流量,避免过大的流量进入系统,从而保证系统在一个安全的流量下可用。在Gateway中提供了RateLimiter<C>接口来定义限流器,该接口中唯一的非抽象实现类是RedisRateLimiter,也就是当前只能通过Redis来实现限流。使用Redis的好处是,可以通过Redis随时监控,但是始终需要通过网络连接第三方服务器,会造成一定的性能消耗,所以我并不推荐这种方式,因此这里只简单介绍限流过滤器工厂,而不深入介绍它的使用。我认为使用Resilience4j限流器可能更好,因为Resilience4j限流器是基于本地内存的,不依赖第三方服务,所以速度更快,性能更好,并且提供Spring Boot度量监控实时情况
StripPrefix过滤器工厂
区分路由的源服务器,需要在路径中加入前缀,再去除路径
RewritePath过滤器工厂
重写请求路径
SetStatus过滤器工厂
设置HTTP状态码
执行原理
配置(GatewayAutoConfiguration)
// 编号
private final String id;
// 匹配URI
private final URI uri;
// 排序
private final int order;
// 断言
private final AsyncPredicate<ServerWebExchange> predicate;
// 过滤器列表
private final List<GatewayFilter> gatewayFilters;
WebHandler
FilteringWebHandler
实际执行逻辑的处理器
DispatcherHandler
转发处理器
RoutePredicateHandlerMapping
处理器映射(HandlerMapping),RoutePredicateHandlerMapping中会通过断言(Predicate)去判断当前路由是否和请求匹配
过程
概述
请求首先是通过HandlerMapping机制找到对应的WebHandler
然后是通过各类(代理)过滤器进行处理
详细
首先它通过构造方法中loadFilters方法构建了一个全局的过滤器(GlobalFilter)列表
handle方法构建了一个过滤器的列表,整合了全局的过滤器和当前路由过滤器,最终将所有的过滤器组织成为一条责任链
执行过滤器
缓存(Redis)
数据结构
strings
SET
命令
SET key value [EX seconds|PX milliseconds|KEEPTTL] [NX|XX] [GET]
参数
EX seconds – 设置键key的过期时间,单位时秒
PX milliseconds – 设置键key的过期时间,单位时毫秒
NX – 只有键key不存在的时候才会设置key的值
XX – 只有键key存在的时候才会设置key的值
KEEPTTL -- 获取 key 的过期时间
GET -- 返回 key 存储的值,如果 key 不存在返回空
锁
命令
SET resource-name anystring NX EX max-lock-time
注意
并不推荐用来实现redis分布式锁,应该参考 the Redlock algorithm 的实现
操作
如果上述命令返回OK,那么客户端就可以获得锁(如果上述命令返回Nil,那么客户端可以在一段时间之后重新尝试),并且可以通过DEL命令来释放锁。
优化
场景
a客户端获得的锁(键key)已经由于过期时间到了被redis服务器删除,但是这个时候a客户端还去执行DEL命令。而b客户端已经在a设置的过期时间之后重新获取了这个同样key的锁,那么a执行DEL就会释放了b客户端加好的锁
操作
不要设置固定的字符串,而是设置为随机的大字符串,可以称为token。
通过脚本删除指定锁的key,而不是DEL命令。
GET
命令
GET key
操作
Redis Get 命令用于获取指定 key 的值。 返回与 key 相关联的字符串值。
注意
如果键 key 不存在, 那么返回特殊值 nil 。
如果键 key 的值不是字符串类型, 返回错误, 因为 GET 命令只能用于字符串值。
SETEX
命令
SETEX key seconds value
操作
SETEX 命令将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。如果键 key 已经存在, 那么 SETEX 命令将覆盖已有的值。SETEX是原子(atomic)操作,它可以在同一时间内完成设置值和设置过期时间这两个操作。
处理死锁
持有锁的客户端应该总是要检查是否超时,保证使用DEL释放锁之前不会过期
若客户端无法释放锁,可以使用 SET GET ,检查锁是否过期,如果过期再主动获取锁
hashes
lists
sets
sorted sets
事务
概念
Redis 事务不是严格意义上的事务,只是用于帮助用户在一个步骤中执行多个命令。单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
Redis 事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
操作
批量操作在发送 EXEC 命令前被放入队列缓存。
收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
错误
调用 EXEC 时
只要出现某个命令无法成功写入缓冲队列的情况,redis 都会进行记录,在客户端调用 EXEC 时,redis 会拒绝执行这一事务
调用 EXEC 之后
redis 不会理睬这些错误,而是继续向下执行事务中的其他命令
监视
只要还没真正触发事务,WATCH 都会尽职尽责的监视,一旦发现某个 key 被修改了,在执行 EXEC 时就会返回 nil,表示事务无法触发
备份/恢复
备份
命令
SAVE / BGSAVE
操作
将在 Redis 目录中创建 dump.rdb 文件
还原
命令
CONFIG
操作
将 Redis 备份文件(dump.rdb)移动到 Redis 目录中并启动服务器以恢复 Redis 数据。查找 Redis 的安装目录,使用 Redis 的 CONFIG 命令
持久化
RDB(Redis DataBase)
概念
在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上
操作
先将数据写入到一个临时文件中
待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件
优点
可以随时来进行备份,因为快照文件总是完整可用的
redis 会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何 IO 操作的,这样就确保了 redis 极高的性能
缺点
如果你对数据的完整性非常敏感,那么 RDB 方式就不太适合你,因为即使你每 5 分钟都持久化一次,当 redis 故障时,仍然会有近 5 分钟的数据丢失。
场景
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。
AOF(Append Only File)
概念
将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了
通过配置 redis.conf 中的 appendonly yes 就可以打开 AOF 功能。如果有写操作(如 SET 等),redis 就会被追加到 AOF 文件的末尾
操作
默认的 AOF 持久化策略是每秒钟 fsync 一次(fsync 是指把缓存中的写指令记录到磁盘中)
因为采用了追加方式,如果不做任何处理的话,AOF 文件会变得越来越大,为此,redis 提供了 AOF 文件重写(rewrite)机制,即当 AOF 文件的大小超过所设定的阈值时,redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集
优点
即使 redis 故障,也只会丢失最近 1 秒钟的数据。
如果在追加日志时,恰好遇到磁盘空间满、inode 满或断电等情况导致日志写入不完整,redis 提供了 redis-check-aof 工具,可以用来进行日志修复
在进行 AOF 重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响 AOF 文件的可用性
如果AOF 文件出现了被写坏的情况,redis 并不会贸然加载这个有问题的 AOF 文件,而是报错退出。
这时可以通过以下步骤来修复出错的文件:
1.备份被写坏的 AOF 文件\
2.运行 redis-check-aof –fix 进行修复\
3.用 diff -u 来看下两个文件的差异,确认问题点\
4.重启 redis,加载修复后的 AOF 文件
这时可以通过以下步骤来修复出错的文件:
1.备份被写坏的 AOF 文件\
2.运行 redis-check-aof –fix 进行修复\
3.用 diff -u 来看下两个文件的差异,确认问题点\
4.重启 redis,加载修复后的 AOF 文件
缺点
在同样数据规模的情况下,AOF 文件要比 RDB 文件的体积大
AOF 方式的恢复速度也要慢于 RDB 方式
重写
在重写即将开始之际,redis 会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中
与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的 AOF 文件中,这样做是保证原有的 AOF 文件的可用性,避免在重写过程中出现意外
当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新 AOF 文件中
当追加结束后,redis 就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的 AOF 文件中了
如何选择
官方的建议是两个同时使用。这样可以提供更可靠的持久化方案。
客户端连接
操作
由于 Redis 使用多路复用和非阻塞 I/O,因此客户端套接字处于非阻塞状态。
设置 TCP_NODELAY 选项是为了确保我们的连接没有延迟。
创建可读文件事件,以便一旦可以在套接字上读取新数据,Redis 就能够收集客户端查询。
最大连接
最大客户端数取决于 OS 的最大文件描述符数限制
重置命令
redis-server --maxclients
Pipelining/Scripting
Pipelining
概念
流水线操作有助于客户端向服务器发送多个请求,而无需等待回复,最后只需一步即可读取回复
优点
提高了 Redis 的性能,由于多个命令同时执行,它极大地提高了协议性能
注意
客户端在调用 write 命令之前需要 read 命令的回复
Scripting
优点
可以以最小的延迟同时读取和写入数据。它使读取,计算,写入等操作变得非常快
分区
概念
用于将 Redis 数据拆分为多个 Redis 实例,以便每个实例仅包含一部分 key
方式
范围分区
概念
通过将对象的范围映射到特定的 Redis 实例来完成
哈希分区
概念
散列分区是 Range 分区的替代方法。在散列分区中,散列函数用于将 key 转换为数字,然后将数据存储在不同的 Redis 实例中
优点
分区有助于您使用多台计算机的集体内存。例如:对于较大的数据库,您需要大量内存,因此分区可以提供来自不同计算机的内存总和。如果不进行分区,则只能使用单台计算机可以支持的有限内存量。
分区还用于将计算能力扩展到多个核心和多个计算机,以及网络带宽扩展到多个计算机和网络适配器。
缺点
分区通常不支持具有多个键的操作。例如,如果两个集合存储在映射到不同 Redis 实例的键中,则无法执行它们之间的交集。
分区不支持具有多个 key 的事务。
分区粒度是关键,因此不可能使用单个巨大的 key(如非常大的有序集)对数据集进行分片。
使用分区时,数据处理更复杂,例如,您必须处理多个 RDB / AOF 文件,并且需要从多个实例和主机聚合持久性文件来备份数据。
添加和删除容量可能很复杂。例如,Redis Cluster 支持大多数透明的数据重新平衡,能够在运行时添加和删除节点,但客户端分区和代理等其他系统不支持此功能。然而,一种称为预分片的技术在这方面有所帮助。
协议(RESP)
概念
RESP 是redis客户端和服务端之前使用的一种通讯协议
特点
实现简单、快速解析、可读性好
方式
简单字符串(Simple Strings)
以"+"开头,后面跟着字符串内容,并以"\r\n"结尾。例如,"+OK\r\n"。
错误(Errors)
以"-"开头。它用于表示错误消息。例如,"-ERR unknown command 'SETX'\r\n"。
整数(Integers)
以":"开头,后面跟着整数的ASCII表示,并以"\r\n"结尾。例如,:123\r\n
批量字符串(Bulk Strings)
以""开头,后面跟着字符串长度的ASCII表示,然后是字符串内容,最后以" \n˚"结尾。如果字符串为空,长度应为0。例如,‘"6\r\nfoobar\r\n"`。
数组(Arrays)
以"*"开头,后面跟着数组长度的ASCII表示,然后是数组中的各个元素(按照上述格式)。例如,"*2\r\n:1\r\n$3\r\nfoo\r\n"代表一个包含整数1和字符串"foo"的数组。
部署架构
单机版
优点
简单
缺点
内存容量有限
处理能力有
无法高可用
主从
概念
Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。 只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同。
复制
第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer
待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存
加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程
优点
master/slave 角色
master/slave 数据相同
降低 master 读压力在转交从库
缺点
无法保证高可用
没有解决 master 写的压力
哨兵
概念
Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移
监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。
优点
保证高可用
监控各个节点
自动故障迁移
缺点
主从模式,切换需要时间丢数据
没有解决 master 写的压力
集群(proxy 型)
概念
Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器; Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。
多个master,slave分别复制各自的master
优点
多种 hash 算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
支持失败节点自动删除
后端 Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个 Redis 一致
缺点
增加了新的 proxy,需要维护其高可用
failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预
集群(直连型)
概念
从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接
优点
无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本
实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。
缺点
资源隔离性较差,容易出现相互影响的情况。
数据通过异步复制,不保证数据的强一致性
一致性hash
概念
数据和机器分配在2^ {32}的hash环上,数据会顺时针查找最近的机器
优点
hash分配更均匀
缺点
数据量过少可能会导致hash倾斜,需要足够多的数据来让hash环分配更均匀
bitmap
优点
占用空间少
排序、查找和去重计算快
缺点
数据量过少导致空间浪费
数据需要做去重预处理,不然会导致bitmap上重复
缓存穿透
概念
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力
解决方案
对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
缓存雪崩
概念
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃
解决方案
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
内存使用
使用情况
一个空实例大约占用3M内存
1百万简单字符串键值对大约占用85M内存
1百万哈希表键值对,每个对象有5个属性,大约占用160M内存
内存不足
Redis要么被Linux内核OOM杀掉,抛出错误崩溃,要么开始变得卡顿。随着现代操作系统malloc方法通常都不返回NULL,而是服务器开始交换,因此Redis性能降低。Redis内置保护措施允许用户在配置文件中使用maxmemory选项,设置Redis最大占用内存。如果达到此限制,Redis将开始返回错误给写命令(但是将继续接受只读命令),或者当最大内存限制达到时也可以配置为键淘汰,在这种情况下Redis作为缓存使用。
有空闲内存,保持报fork()错误
echo 1 > /proc/sys/vm/overcommit_memory
Redis后台保存模式依赖现代操作系统的写时拷贝技术。Redis fork(创建一个子进程)是父进程精确拷贝。子进程存储数据到磁盘并且最终退出。从理论上讲,子进程应该和父进程使用同样多内存,作为父进程副本,但是得益于多数现代操作系统实现的写时复制技术,父进程和子进程共享内存页。内存页在父进程或子进程改变时将被复制。当子进程保存时,理论上所有页面都可能改变,Linux无法提前告知子进程需要多少内存,因此如果overcommit_memory设置为0,fork将会失败除非有足够的空闲RAM真正复制父进程内存页.结果是,如果你有3G Redis数据集,只有2G可用内存将会失败。
overcommit_memory设置为1,意味着Linux 使用更乐观方式fork。
overcommit_memory设置为1,意味着Linux 使用更乐观方式fork。
键的生命周期
主实例在第一次与从实例同步时生成RDB文件。
RDB文件不包含已经过期的键,但是已经过期的键仍然在内存中。
尽管这些键从逻辑上说已经过期失效,但是还在Redis主实例内存中,他们并不被识别为存在的,当增量或访问这些键时这些键会被回收。尽管从逻辑上说这些键不是数据集一部分,但是INFO和DBSIZE命令结果包含这些信息。
当从实例读取主实例生成的RDB文件时,过期键不会被载入。
通信(Rabbitmq)
AMQP协议
概念
全称
Advanced Message Queuing Protocol(高级消息队列协议)
设计目标
AMQP像是一个把东西连在一起的语言,而不是一个系统。其设计目标是:让服务端可通过协议编程
特征
二进制协议
多通道(multi-channel)
可协商(negotiated)
异步、安全、便携、语言中立、高效的
分层
功能层(Functional Layer)
定义了一系列的命令
传输层(Transport Layer)
携带了从应用 → 服务端的方法,用于处理多路复用、分帧、编码、心跳、data-representation、错误处理
AMQ Model
组件
exchange(交换机)
从Publisher程序中收取消息,并把这些消息根据一些规则路由到消息队列(Message Queue)中
message queue(消息队列)
存储消息,直到消息被安全的投递给了消费者
binding(绑定)
定义了 message queue 和 exchange 之间的关系,提供了消息路由的规则
特点
用户可以控制消息队列和交换器的绑定规则,而不是依赖中间件自身的代码
架构
Publisher->(Publish)->[Routing][Exchange->(Routes)->Queue]->(Consumes)->Consumer
生命周期
消息
消息由生产者产生
生产者把内容放到消息里,并设置一些属性以及消息的路由。然后生产者把消息发给服务端。
服务端收到消息,交换器(大部分情况)把消息路由到若干个该服务器上的消息队列中
如果这个消息找不到路由,则会丢弃或者退回给生产者(生产者可自行决定)。
一条消息可以存在于许多消息队列中。 服务器可以通过复制消息,引用计数等方式来实现。这不会影响互操作性。 但是,将一条消息路由到多个消息队列时,每个消息队列上的消息都是相同的。 没有可以区分各种副本的唯一标识符。
消息到达消息队列
消息队列会立即尝试通过AMQP将其传递给消费者。 如果做不到,消息队列将消息存储(按生产者的要求存储在内存中或磁盘上),并等待消费者准备就绪。 如果没有消费者,则消息队列可以通过AMQP将消息返回给生产者(同样,如果生产者要求这样做)。
当消息队列可以将消息传递给消费者时,它将消息从其内部缓冲区中删除
可以立即删除,也可以在使用者确认其已成功处理消息之后删除(ack)。 由消费者选择“确认”消息的方式和时间。消费者也可以拒绝消息(否定确认)。
生产者发消息与消费者确认,被分组成一个事务。当一个应用同时扮演多个角色时:发消息,发ack,commit或者回滚事务。消息从服务端投递给消费者这个过程不是事务的。消费者对消息进行确认就够了。
交换器
每个AMQP服务端都会自己创建一些交换器,这些不能被销毁。AMQP程序也可以创建其自己的交换器。AMQP并不使用 create 这个方法,而是使用 declare 方法来表示:如果不存在,则创建,存在了则继续。程序可以创建交换器用于私有使用,并在任务完成后销毁它们。虽然AMQP提供了销毁交换器的方法,但一般来讲程序不需要销户它。
队列
持久化消息队列
由很多消费者共享。当消费者都退出后,队列依然存在,并会继续收集消息。
临时消息队列
临时消息队列对于消费者是私有和绑定的。当消费者断开连接,则消息队列被删除。
绑定
绑定是交换器和消息队列之间的关系,告诉交换器如何路有消息。
队列类型
共享队列
私有的回复队列
一般来讲,回复队列是私有的、临时的、由服务端命名、只有一个消费者。
发布-订阅队列
概念
在传统的中间件中,术语 subscription 含糊不清。至少包含两个概念:匹配消息的条件集,和一个临时队列用于存放匹配的消息。AMQP把这两部分拆成:binding和message queus。在AMQP中,并没有一个实体叫做 subscription
模型
给一个消费者保留消息(一些场景下是多个消费者)
从多个源收集消息,比如匹配Topic、消息的字段、或者内容等方式
区别
订阅队列与命名队列或回复队列之间的关键区别在于,订阅队列名称与路由目的无关,并且路由是根据抽象的匹配条件完成的,而不是路由键字段的一对一匹配。
AMQP命令架构
概念
AMQP采用方法是基于类来建立传统API模型。类中包含方法,并定义了方法明确应该做什么。
类型
同步请求-响应
一个节点发送请求,另一个阶段发送响应。适用于性能不重要的方法。发送同步请求时,该节点直到收到回复后,才能发送下一个请求
异步通知
一个节点发送数据,但是不期待回复。一般用于性能很重要的地方。异步请求会尽可能快的发送消息,不等待确认。只在需要的时候在更上层(比如消费者层)实现限流等功能。AMQP中可以没有确认,要么成功,要么就会收到关闭Channel或者连接的异常。如果需要明确的追踪成功或者失败,那么应该使用事务。
AMQP中的类
Connection类
概念
AMQP是一个长连接协议。Connection被设计为长期使用的,可以携带多个Channel
生命周期
客户端打开到服务端的TCP/IP连接,发送协议头。这是客户端发送的数据里,唯一不能被解析为方法的数据。
服务端返回其协议版本、属性(比如支持的安全机制列表)。 the Start method
客户端选择安全机制 Start-Ok
服务端开始认证过程, 它使用SASL的质询-响应模型(challenge-response model)。它向客户端发送一个质询 Secure
客户端向服务端发送一个认证响应Secure-Ok。比如,如果使用 plain 认证机制,则响应会包含登录名和密码
客户端重复质询Secure或转到协商步骤,发送一系列参数,如最大帧大小 Tune
客户端接受,或者调低这些参数 Tune-Ok
客户端正式打开连接,并选择一个Vhost Open
服务端确认VHost有效 Open-Ok
客户端可以按照预期使用连接
当一个节点打算结束连接 Close
另一个节点需要结束握手 Close-Ok
服务端和客户端关闭Socket连接。
失败
如果在发送或者收到 Open 或者 Open-Ok 之前,某一个节点发现了一个错误,则必须直接关闭Socket,且不发送任何数据。
Channel类
概念
AMQP是一个多通道协议。Channel提供了一种方式,在比较重的TCP/IP连接上建立多个轻量级的连接。这会让协议对防火墙更加友好,因为端口使用是可预知的。它也意味着很容易支持流量调整和其他QoS特性。
Channels相互是独立的,可以同步执行不同的功能。可用带宽会在当前活动之间共享。
这里期望也鼓励多线程客户端程序应该使用 每个线程一个channel 的模型。不过,一个客户端在一个或多个AMQP服务端上打开多个连接也是可以的。
生命周期
客户端打开一个新通道 Open
服务端确认新通道准备就绪 Open-Ok
客户端和服务端按预期来使用通道.
一个节点关闭了通道 Close
另一个节点对通道关闭进行握手 Close-Ok
Exchange类
概念
Exchange类能够让应用操作服务端的交换器。这个类能够让程序自己设置路由,而不是通过某些配置
生命周期
客户端让服务端确保该exchange存在Declare。客户端可以细化为:“如果交换器不存在则进行创建” 或 “如果交换器不存在,警告我,不需要创建”
客户端向Exchange发消息
客户端也可以选择删掉Exchange Delete
Queue类
概念
该类用于让程序管理服务端上的消息队列。
持久化消息队列
生命周期
客户端断言这个消息队列存在 Declare(设置 passive 参数)
服务端确认消息队列存在 Declare-Ok
客户端消息队列中读消息
临时消息队列
生命周期
客户端创建消息队列 Declare(不提供队列名称,服务器会分配一个名称)。服务端确认 Declare-Ok
客户端在消息队列上启动一个消费者
客户端取消消费,可以是显示取消,也可以是通过关闭通道或者连接连接隐式取消的
当最后一个消费者从消息队列中消失的时候,在过了礼貌性超时后,服务端会删除消息队列
绑定
生命周期
客户端创建一个队列Declare,服务端确认Declare-Ok
客户端绑定消息队列到一个topic exchange上Bind,服务端确认Bind-Ok
客户端像之前一样使用消息队列。
Basic类
语义
从客户端→服务端发消息。异步Publish
开始或者停止消费Consume,Cancel
从服务端到客户端发消息。异步Deliver,Return
确认消息Ack,Reject
同步的从消息队列中读取消息Get
事务类
类型
自动事务
每个发布的消息和应答都处理为独立事务
服务端本地事务
服务器会缓存发布的消息和应答,并会根据需要由client来提交它们
服务端本地事务Transaction类(“tx”)
语义
应用程序要求服务端事务,在需要的每个channel里Select
应用程序做一些工作Publish, Ack
应用程序提交或回滚工作Commit,Roll-back
应用程序正常工作,循环往复
注意
事务包含发布消息和ack,不包含分发。所以,回滚并不能重入队列或者重新分发任何消息。客户端有权在事务中确认这些消息。
功能说明
消息和内容
消息会携带一些属性,以及具体内容(二进制数据)
消息是可被持久化的。持久化消息是可以安全的存在硬盘上的,即使发生了验证的网络错误、服务端崩溃溢出等情况,也可以确保被投递。
消息可以有优先级。同一个队列中,高优先级的消息会比低优先级的消息先被发送。当消息需要被丢弃时(比如服务端内存不足等),将会优先丢弃低优先级消息
服务端一定不能修改消息的内容。但服务端可能会在消息头上添加一些属性,但一定不会移除或者修改已经存在的属性
虚拟主机(VHost)
虚拟主机是服务端的一个数据分区。在多租户使用时,可以方便进行管理。
虚拟主机有自己的命名空间、交换器、消息队列等等。所有连接,只可能和一个虚拟主机建立。
交换器(Exchange)
概念
交换器是一个虚拟主机内的消息路由Agent。用于处理消息的路由信息(一般是Routing-Key),然后将其发送到消息队列或者内部服务中
交换器可能是持久化的、临时的、自动删除的。交换器把消息路由到消息队列时可以是并行的。这会创建一个消息的多个实例
类型
Fanout 交换器
一个消息队列没有使用任何参数绑定交换器
生产者向交换器发了一条消息
这个消息无条件的发送到该消息队列
Direct 交换器
一个消息队列使用RoutingKey K 绑定到交换器
生产者向交换器发送RoutingKey为R的消息
当 K=R时,消息被转发到该消息队列中
Topic 交换器
消息队列使用路由规则 P 绑定到交换器
生产者使用RoutingKey R 发送消息到交换器
如果R 能够匹配 P,则把消息发到该消息队列
Headers 交换器
消息队列使用Header的参数表来绑定。不适用RoutingKey
生产者向交换器发送消息,Header中包含了指定的键值对。如果 x-match 为all,则必须都匹配才行。如果x-match为any,则有任意一个header匹配即可。
如果匹配,则传给消息队列
AMQP的传输架构
概念
AMQP是一个二进制协议。有不同类型的帧frame 构成。帧会携带协议的方法以及其他信息。所有的帧都有相同的基本结构,即:帧头,payload,帧尾。payload格式取决于帧的类型。
我们假设使用的是面向流的可靠网络层(比如TCP/IP)。单个Socket连接上可以有多个独立的控制线程,也就是通道Channel。不同的通道共享一个连接,每个通道上的帧都是按严格的顺序排列,这样可以用一个状态机来解析协议。
传输层(wire-level)的格式被设计为扩展性强、且足够通用,可以用于任何更高层的协议(不仅仅是AMQP)。我们假设AMQP是会被扩展、优化的。
数据类型
整数(1-8个字节):表示大小,数量,范围等。全都是无符号整数
Bits:用于表示为开/关值,会被封包为字节
短字符串:用于存放短的文本属性。最多255字节,解析时不用担心缓冲区溢出
长字符串:用于存放二进制数据块
字段表(Field Table):用于存放键值对
协议协商
概念
客户端连接时,和服务端协商可接受的配置。当两个节点达成一致后,连接才能继续使用。通过协商,可以让我们断言假设和前提条件。
如果协商达成一致,双方会根据协商预分配缓冲区避免死锁。传入的帧如果满足协商条件,则认为其实安全的。如果超过了,那么另一方必须断开连接。
主要协商这几方面的信息
实现的协议和版本,服务端可能会在同一端口提供多种协议的支持
加密参数和验证
最大帧尺寸、Channel的数量、某些操作的限制
分帧方式
每个连接只发送一个帧。简单,但是慢。
在流中加入分隔符来分帧。简单,但是解析较慢(因为需要不断的读取,去寻找分隔符)
计算帧的尺寸,并在每个帧之前发送尺寸。简单且快速。也是AMQP的选择
帧
处理一个帧的步骤
读帧头,检查帧类型和Channel
根据帧类型,读取payload并处理
读帧尾校验
结构
帧头
组成
帧类型 type
通道 channel
尺寸 size
帧尾
组成
错误检测信息 frame-end
方法帧
组成
class-id
method-id
arguments
处理步骤
读取方法帧的payload
解包为结构
检查方法在当前上下文中是否允许
检查参数是否有效
执行方法
内容帧
概念
内容是端到端直接发送的应用数据。内容由一系列属性和二进制数据组成。其中一系列的属性组成了 ”内容帧的帧头“。而二进制数据,可以是任意大小,它可能被拆分成多个块发送,每个块是一个 content-body帧
组成
帧头
class-id
weight
body size
property flags
property list
内容主体帧
content-body
通道与连接的关闭
对于客户端,只要发送了 Open 就认为连接和通道是打开的。对于服务端则是Open-Ok。如果一个节点想要关闭通道和连接必须要进行握手。
如果突然或者意外关闭,没办法立刻被检测到,可能会导致丢失返回值。如果节点忽略了 Close 操作,当双方同时发送 Close 时,可能会导致死锁。
Rabbitmq
线程安全
问题
通道非线程安全
会导致错误的帧交错在网络上,或造成重复确认等问题
在共享的通道上并发执行发布会导致错误的帧交错在网络上,触发连接级别的协议异常并导致连接被代理直接关闭
反模式
为每个发布的消息开放单独的通道
解决
通过每个线程使用一个通道的方式实现并发
一个线程用于消费,另一个线程在共享通道上推送是安全的
池化通道对象
自动恢复网络故障
恢复连接
客户端和RabbitMQ节点之间的网络连接会发生失败。RabbitMQ的Java客户端支持自动恢复连接和拓扑(队列,交换机,绑定和消费者)
恢复监听
可以在可恢复的连接上注册一个或多个恢复监听
恢复拓扑
拓扑的恢复涉及到交换机、队列、绑定和消费者的恢复。自动恢复启用时拓扑恢复也会随之启用
自动恢复
应用
步骤
重连
恢复连接监听
重开通道
恢复通道监听
恢复通道的basic.qos设置,发布确认和事务设置
拓扑
概念
拓扑恢复依赖于实体的每个连接缓存(队列,交换机,绑定,消费者)。当声明一个队列的时候,此队列也会被添加到缓存中。当它被删除或者列入删除计划时(例如是一个 自动删除队列),缓存会被移除
步骤
重新声明交换机(预定义的除外)
重新声明队列
恢复所有绑定
恢复所有消费者
限制
使用factory.setAutomaticRecoveryEnabled(boolean)方法开启或停用自动连接恢复
如果因为异常导致恢复失败(比如RabbitMQ节点尚不可用),会在固定的时间间隔(默认5秒)进行重试。间隔可以进行配置:factory.setNetworkRecoveryInterval(10000);
当提供了地址列表的情况下,列表会被随机重排并逐一尝试:
连接
触发优先级
连接的I/O循环中抛出了I/O异常
套接字(socket)读操作超时
检测到服务器丢失 心跳
连接的I/O循环中抛出了其他不可预期的异常
Connection.Close
被应用使用Connection.Close方法关闭的的情况下,连接恢复不会启动
队列
循环调度
使用工作队列的一个好处就是它能够并行的处理队列。如果堆积了很多任务,我们只需要添加更多的工作者(workers)就可以了,扩展很简单。
默认来说,RabbitMQ会按顺序得把消息发送给每个消费者(consumer)。平均每个消费者都会收到同等数量得消息。这种发送消息得方式叫做——轮询(round-robin)。
消息确认
为了防止消息丢失,RabbitMQ提供了消息响应(acknowledgments)。消费者会通过一个ack(响应),告诉RabbitMQ已经收到并处理了某条消息,然后RabbitMQ就会释放并删除这条消息。
如果消费者(consumer)挂掉了,没有发送响应,RabbitMQ就会认为消息没有被完全处理,然后重新发送给其他消费者(consumer)。这样,及时工作者(workers)偶尔的挂掉,也不会丢失消息。
消息是没有超时这个概念的;当工作者与它断开连的时候,RabbitMQ会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。
消息响应默认是开启的。之前的例子中我们可以使用no_ack=True标识把它关闭。是时候移除这个标识了,当工作者(worker)完成了任务,就发送一个响应。
一个很容易犯的错误就是忘了basic_ack,后果很严重。消息在你的程序退出之后就会重新发送,如果它不能够释放没响应的消息,RabbitMQ就会占用越来越多的内存。为了排除这种错误,你可以使用rabbitmqctl命令,输出messages_unacknowledged字段:
消息持久化
为了确保信息不会丢失,有两个事情是需要注意的:我们必须把“队列”和“消息”设为持久化。不能重复定义队列,需要在第一次定义队列时候设置
将消息设为持久化并不能完全保证不会丢失。以上代码只是告诉了RabbitMq要把消息存到硬盘,但从RabbitMq收到消息到保存之间还是有一个很小的间隔时间。因为RabbitMq并不是所有的消息都使用fsync(2)——它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果你一定要保证持久化,你需要改写你的代码来支持事务(transaction)。
公平调度
我们可以使用basic.qos方法,并设置prefetch_count=1。这样是告诉RabbitMQ,再同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。这样,RabbitMQ就会把消息分发给下一个空闲的工作者(worker)。
如果所有的工作者都处理繁忙状态,你的队列就会被填满。你需要留意这个问题,要么添加更多的工作者(workers),要么使用其他策略。
临时队列
调用queue_declare方法的时候,不提供queue参数就可以获得一个随机队列名的队列
当与消费者(consumer)断开连接的时候,这个队列应当被立即删除,exclusive=True
交换机
概念
从发布者方接收消息,一边把消息推送到队列
类型
直连交换机(direct)
主题交换机(topic)
头交换机(headers)
扇型交换机(fanout)
绑定
交换器和队列之间的联系我们称之为绑定(binding)
数据库(MySQL)
体系结构
支持接口
是指不同语言中与SQL的交互
管理服务和工具
系统管理和控制工具
连接池
管理缓冲用户连接、线程处理等需要缓存的需求
SQL接口
接受用户的SQL命令,并且返回用户需要查询的结果,如select from就是调用SQL Interface
解析器
SQL命令传递到解析器的时候会被解析器验证和解析,解析器是由Lex和YACC实现的,是一个很长的脚本,其主要功能如下
将SQL语句分解成数据结构,并将这个结构传递到后续步骤,以后SQL语句的传递和处理就是基于这个结构的
如果在分解构成中遇到错误,那么就说明这个SQL语句是不合理的
查询优化器
SQL语句在查询之前会使用查询优化器对查询进行优化。它使用“选取→投影→连接”策略进行查询
缓存和缓冲池
查询缓存。如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。这个缓存机制是由一系列小缓存组成的。比如表缓存、记录缓存、Key缓存、权限缓存等
存储引擎
存储引擎是MySQL中具体的与文件打交道的子系统。也是MySQL最具有特色的一个地方。从MySQL 5.5之后,InnoDB就是MySQL的默认事务引擎
引擎
InnoDB
InnoDB就是MySQL的默认事务引擎,支持事务型的存储引擎
InnoDB支持事务安全表(ACID),也支持行锁定和外键。InnoDB为MySQL提供了具有事务(transaction)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)、多版本并发控制(multi-versioned concurrency control)的事务安全(transaction-safe(ACID compliant))型表
MyISAM
读写
写
本地事务
redo log
undo log
主从同步
binlog
锁
行锁
update
for update
表锁
insert
delete
乐观锁
版本控制
读
索引
索引树
B+tree
读
写
Btree
范围
索引扫描
全表扫描
日志
错误日志(-log-err)
错误日志功能默认状态下是开启的,并且不能被禁止
记录着MySQL服务器的启动和停止过程中的信息、服务器在运行过程中发生的故障和异常情况的相关信息、事件调度器运行一个事件时产生的信息、在从服务器上启动服务器进程时产生的信息等
错误日志记录的并非全是错误信息,如MySQL如何启动InnoDB的表空间文件、如何初始化自己的存储引擎等信息也记录在错误日志文件中
查询日志(-log)
默认情况下查询日志是关闭的。如果需要打开查询日志,可以通过修改my.ini文件来启动查询日志
查询日志记录了用户的所有操作,包括对数据库的增、删、查、改等信息
在并发操作多的环境下会产生大量的信息,从而导致不必要的磁盘IO,会影响MySQL的性能。如不是为了调试数据库的目的建议不要开启查询日志
二进制日志(-log-bin)
用来记录所有用户对数据库的操作,所以当长时间开启之后,日志文件会变得很大,占用磁盘空间
当数据库发生意外时,可以通过此文件查看在一定时间段内用户所做的操作,结合数据库备份技术,即可再现用户操作,使数据库恢复
辅助实现
恢复(Recovery)
某些数据的恢复需要二进制日志,例如,在一个数据库全备文件恢复后,用户可以通过二进制日志进行point-in-time的恢复
复制(Replication)
其原理与恢复类似,通过复制和执行二进制日志使一台远程的MySQL数据库(一般称为Slave或Standby)与一台MySQL数据库(一般称为Master或Primary)进行实时同步
审计(Audit)
用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入的攻击
还原数据库
指定恢复时间
mysqIbinIog --stop-date="2005-04-20 9:59:59" /var/Iog/mysqI/bin.123456 | mysqI -u root -pmypwd
指定恢复位置
mysqIbinIog --start-date="2005-04-20 9:55:00" --stop-date="2005-04-20 10:05:00" /var/Iog/mysqI/bin.123456 > /tmp/mysqI_restore.sqI
mysqIbinIog --stop-position="368312" /var/Iog/mysqI/bin.123456 | mysqI -u root -pmypwd
mysqIbinIog --start-position="368315" /var/Iog/mysqI/bin.123456 | mysqI -u root -pmypwd
mysqIbinIog --start-position="368315" /var/Iog/mysqI/bin.123456 | mysqI -u root -pmypwd
更新日志(-log-update)
慢查询日志(-log-slow-queries)
默认状态下,慢查询日志是关闭的。可以通过配置文件my.ini或者my.cnf来启用
根据上面对慢日志的设置,可以看到在默认文件夹下生成了一个salonshi-PC-slow.log的慢日志文件,该文件可以使用记事本打开
复制
概念
复制是从一个MySQL服务器(Master)将数据拷贝到另外一台或多台MySQL服务器(Slaves)的过程
MySQL复制是指将主数据库的DDL和DML操作通过二进制日志传到复制服务器上,然后在复制服务器上将这些日志文件重新执行,从而使复制服务器和主服务器的数据保持同步
主服务器将更新写入二进制日志文件,并维护文件的一个索引以跟踪日志循环。这些日志可以记录发送到从服务器的更新
当一个从服务器连接主服务器时,它通知主服务器、从服务器在日志中读取的最后一次成功更新的位置。从服务器接收从那时起发生的任何更新,然后封锁并等待主服务器通知新的更新
复制操作是异步进行的,即在进行复制时,所有对复制中的表的更新必须在主服务器上进行。从服务器不需要持续地保持连接来接受主服务器的数据,以避免用户对主服务器上的表进行的更新与对从服务器上的表所进行的更新之间的冲突
MySQL支持一台主服务器同时向多台从服务器进行复制操作,从服务器同时可以作为其他从服务器的主服务器,如果MySQL主服务器的访问量比较大,通过复制技术,从服务器来响应用户的查询操作,从而降低主服务器的访问压力,同时从服务器可以作为主服务器的备份服务器
集群
可用性
数据库集群系统具有多个数据库节点,在单个或多个节点出现故障的情况下,其他正常节点可以继续提供服务
性能
多个节点一般可以并行处理请求,从而避免单节点的性能瓶颈,一般至少可以提高读操作的并发性能
可扩展性
单个数据库节点的处理能力毕竟有限,增加节点数量可以显著提高整个集群系统的吞吐率
用途
一般来说都是需要通过主从复制(Master-Slave)的方式来同步数据,再通过读写分离(MySQL-Proxy)来提升数据库的并发负载能力
或者是用来作为主备机的设计,保证当主机停止响应之后在很短的时间内就可以将应用切换到备机上继续运行
优点
数据库集群系统具有多个数据库节点,在单个或多个节点出现故障的情况下,其他正常节点可以继续提供服务
如果主服务器上出现了问题可以切换到从服务器上
通过复制可以在从服务器上执行查询操作,降低主服务器的访问压力,实现数据分布和负载平衡
可以在从服务器上进行备份,以避免备份期间影响主服务器的服务
缺点
网络延迟是产生MySQL主从不同步的主要原因,通常会给程序进行读写分离带来一定的困难。为了避免这种情况,在配置服务器配置文件的时候最好使用InnoDB存储引擎的表,在主机上可以开启sync_binlog
如果主机上的max_allowed_packet比较大,但是从机上却没有手工配置该值,默认为1 MB,此时很有可能导致同步失败,建议主从两台机器上都设置为5 MB比较合适
从数据库无法同步的问题,如show slave status显示“Slave_SQL_Running为No, Seconds_Behind_Master”为NULL。原因:程序可能在从机上进行了写操作,也可能是从机机器重启后,事务回滚造成的
实现方式
DRBD(Distributed Replicated Block Device)
是一种用软件实现的、无共享的、服务器之间镜像块设备内容的存储复制解决方案
MySQL Cluster(又称MySQL簇)
MySQL Replicaion(复制)本身是一个比较简单的架构,即一台从服务器(Slave)从另一台主服务器(Master)读取二进制日志然后再解析并应用到自身
主从架构
主从架构指的是使用一台MySQL服务器作为Master,一台或多台MySQL服务器作为Slave,将Master的数据复制到Slave上
一般在这种架构下,系统的写操作都在Master中进行,而读操作则分散到各个Slave中进行
实现模式
基于SQL语句的复制(Statement-Based Replication, SBR)
基于行的复制(Row-Based Replication, RBR)
混合模式复制(Mixed-Based Replication, MBR)
步骤
Master启用二进制日志
Slave上面的I/O进程连接上Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容
Master接收到来自Slave的I/O进程的请求后,通过负责复制的I/O进程根据请求信息读取指定日志指定位置之后的日志信息,返回给Slave的I/O进程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到Master端的bin-log文件的名称以及bin-log的位置
Slave的I/O进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的bin-log的文件名和位置记录到master-info文件中
Slave的SQL进程检测到relay-log中新增加的内容后,会马上解析relay-log的内容,并在自身执行
备份/恢复
逻辑备份
概念
逻辑备份也可称为文件级备份,是将数据库中的数据备份为一个文本文件,而备份的大小取决于文件大小
方式
使用MYSQLDUMP命令生成INSERT语句备份
语句
备份所有数据库
mysqIdump -uroot -proot --aII-database > /tmp/dumpback/aIIdb.sqI
备份某些数据库
mysqIdump -uroot -proot --database sqoop hive > /tmp/dumpback/sqoop_hive.sqI
备份某数据库中的表
mysqIdump -uroot -proot sqoop tb1 > /tmp/dumpback/sqoop_tb1.sqI
问题
如何保证数据备份的一致性?
解决
同一时刻取出所有数据
对于事务支持的存储引擎,如Innodb或者BDB等,可以通过控制将整个备份过程在同一个事务中,使用“--single-transaction”选项
mysqIdump --singIe-transaction test > test_backup.sqI
数据库中的数据处于静止状态
通过锁表参数未完成
LOCK-TABLES每次锁定一个数据库的表,此参数默认为true
LOCK-ALL-TABLES一次锁定所有的表,适用于dump的表分别处于各个不同的数据库中的情况
生成特定格式的纯文本文件备份
通过SELECT ... TO OUTFILE FROM ..命令
通过Query将特定数据以指定方式输出到文本文件中
FIELDS ESCAPED BY ['name'] :在SQL语句中需要转义的字符
FIELDS TERMINATED BY:设定每两个字段之间的分隔符
FIELDS [OPTIONALLY] ENCLOSED BY 'name':包装,有OPTIONALLY数字类型不被包装,否则全包装
LINES TERMINATED BY 'name':行分隔符,即每记录结束时添加的字符
通过MYSQLDUMP工具命令导出文本
逻辑恢复
INSERT语句文件的恢复
使用MySQL命令直接恢复
连接上MySQL在命令行中执行恢复
纯文本文件的恢复
使用LOAD DATA INFILE命令
此命令是SELECT ... TO OUTFILE FROM反操作
使用mysqlimport工具恢复
此工具可用于恢复上面MYSQLDUMP生成TXT和SQL两文件,所以要保证TXT文件对应的数据库中的表存在
物理备份
冷备份
这种方式是最直接的备份方式,就是首先停掉数据库服务,然后CP数据文件,恢复时停止MySQL,先进行操作系统级别恢复数据文件,然后重启MySQL服务,使用MYSQLBINLOG工具恢复自备份以来的所有BINLOG
这种方式虽然简单,而且对各个存储引擎都支持,但是有一个非常大的弊端就是需要关闭数据库服务。在当前的大多信息系统都是不允许长时间停机的
热备份
MyISAM存储引擎
本质就是将要备份的表加读锁,然后再CP数据文件到备份目录
使用MYSQLHOTCOPY工具
手工锁表COPY
InnoDB存储引擎
iBBACKUP工具可以热备份InnoDB存储引擎类数据库,但它是收费的
开源工具Xtrabackup,Xtrabackup只能备份InnoDB和XtraDB两种数据表,而不能备份MyISAM数据表
各种备份与恢复方法的具体实现
利用SELECT INTO OUTFILE实现数据的备份与还原
把需要备份的数据备份出来,会发现是个文本文件。所以不能直接导入数据库了。需要使用LOAD DATA INFILE恢复回到MySQL服务器端
利用mysqldump工具对数据进行备份和还原,mysqldump常用来做温备,所以首先需要对想备份的数据施加读锁
施加读锁的方式
直接在备份的时候添加选项
lock-all-tables是对要备份的数据库的所有表施加读锁
lock-table仅对单张表施加读锁,即使是备份整个数据库,它也是在备份某张表的时候才对该表施加读锁,因此适用于备份单张表
在服务器端书写命令
这对于InnoDB存储引擎来讲,虽然也能够请求到读锁,但是不代表它的所有数据都已经同步到磁盘上,因此当面对InnoDB的时候,要使用“mysql→ show engine innodb status; ”看看InnoDB所有的数据都已经同步到磁盘上,才进行备份操作
备份的策略:完全备份+增量备份+二进制日志
先给数据库做完全备份
回到MySQL服务器端更新数据
先查看完全备份文件里边记录的位置
在回到服务器端
做增量备份
再回到服务器
导出这次的二进制日志
先让MySQL离线
模拟数据库损坏
开始恢复数据
策略:完全备份+二进制日志
准备
事务日志必须跟数据文件在同一个LVM上
创建lvm
修改MySQL主配置文件存放目录内的文件的权限与属主属组,并初始化MySQL
修改配置文件
启动服务
回到MySQL服务器
导出二进制文件,创建个目录单独存放
为数据所在的卷创建快照
更新数据库的数据,并删除数据目录先的数据文件,模拟数据库损坏
区别
高效性
逻辑备份是基于文件级别的备份,由于每个文件都是由不同的逻辑块组成。每一个逻辑的文件块存储在连续的物理磁盘块上,但组成一个文件的不同逻辑块极有可能存储在分散的磁盘块上。逻辑备份在对非连续存储磁盘上的文件进行备份时需要额外的查找操作。这些额外的操作增加了磁盘的开销,降低了磁盘的吞吐率。所以,与物理备份相比,备份性能较差。逻辑备份模式下,文件即使一个很小的改变,也需将整个文件备份。这样如果一个文件很大的情况下,就会大幅度降低备份效率,增加磁盘开销和备份时间
物理备份是位于文件系统之下和硬件磁盘驱动之上。增加了一个软驱动,它忽略了文件和结构,处理过程简洁,因此在执行过程中所花费在搜索操作上的开销较少,备份的性能很高。物理备份避免了当文件出现一个小的改动的时候,就需要对整个文件做备份,只是会去做改动部分的备份,有效地提高了备份效率,节省了备份时间
实时性
逻辑备份是很难做到实时备份的,因为它的每次修改都是基于文件的,而文件的哪部分被修改,系统很难实时捕获到,所以备份的时候需要把整个文件读一遍再发到备机,实时效率不是很高
物理备份可以做到高效的实时备份,因为在每次主机往磁盘写数据的时候,都需要同时将数据写入到备机,这种写入操作都是基于磁盘扇区的,所以,很快就能被识别。只有在备机完成之后,才会返回给上层的应用系统来继续下一步工作
支持度
逻辑备份是以单个文件为单位对数据进行复制,所以它受文件系统限制,仅能对部分支持的文件系统做备份,不支持RAW分区
物理备份是在文件系统之下对数据进行复制,所以它不受文件系统限制,可以支持各种文件系统包括RAW分区
目的
做灾难恢复:对损坏的数据进行恢复和还原。
需求改变:因需求改变而需要把数据还原到改变以前。
测试:测试新功能是否可用。
需要考虑的问题
可以容忍丢失多长时间的数据。
恢复数据要在多长时间内完成。
恢复的时候是否需要持续提供服务。
恢复的对象,是整个库,多个表,还是单个库,单个表。
备份类型
根据是否需要数据库离线
冷备(Cold Backup):需要关MySQL服务,读写请求均不允许状态下进行。
温备(Warm Backup):服务在线,但仅支持读请求,不允许写请求。
热备(Hot Backup):备份的同时,业务不受影响。
根据要备份的数据集合的范围
完全备份(full backup):备份全部字符集。
增量备份(incremental backup):上次完全备份或增量备份以来改变了的数据,不能单独使用,要借助完全备份,备份的频率取决于数据的更新频率。
差异备份(differential backup):上次完全备份以来改变了的数据。
恢复策略
完全+增量+二进制日志
完全+差异+二进制日志
根据备份数据或文件
物理备份:直接备份数据文件
优点
备份和恢复操作都比较简单,能够跨MySQL的版本,恢复速度快,属于文件系统级别的
建议
不要假设备份一定可用,要测试“mysql→check tables”
检测表是否可用逻辑备份:备份表中的数据和代码
优点
恢复简单,备份的结果为ASCII文件,可以编辑,与存储引擎无关,可以通过网络备份和恢复
缺点
备份或恢复都需要MySQL服务器进程参与,备份结果占据更多的空间,浮点数可能会丢失,精度还原之后缩影需要重建
备份对象
数据
配置文件
代码:存储过程、存储函数、触发器
OS相关的配置文件
复制相关的配置
二进制日志
分布式应用
数据切分
概念
通过某种特定的条件,将存放在同一个数据库中的数据分散存放到多个数据库(主机)上面,以达到分散单台设备负载的效果
优点
可以提高系统的总体可用性,因为单台设备Crash之后,只有总体数据的某部分不可用,而不是所有的数据
模式
垂直(纵向)切分
按照不同的表(或者Schema)来切分到不同的数据库(主机)之上
垂直切分的最大特点就是规则简单,实施也更为方便,尤其适合各业务之间耦合度低、相互影响小、业务逻辑非常清晰的系统
水平(横向)切分
据表中数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库(主机)上面
数据源整合
总体方式
在每个应用程序模块中配置管理自己需要的一个(或者多个)数据源,直接访问各个数据库,在模块内完成数据的整合
通过中间代理层来统一管理所有的数据源,后端数据库集群对前端应用程序透明
方案
利用MySQL Proxy实现数据切分及整合
MySQL Proxy是在客户端请求与MySQL服务器之间建立一个连接池,所有客户端请求都发送到MySQL Proxy,由MySQL Proxy进行相应的分析,判断是读操作还是写操作,然后分别发送到对应的MySQL服务器上。对于多节点Slave集群,也可以做到负载均衡的效果
利用Amoeba实现数据切分及整合
Amoeba是一个基于Java开发的、专注于解决分布式数据库数据源整合Proxy程序的开源框架,Amoeba已经具有Query路由、Query过滤、读写分离、负载均衡以及HA机制等相关内容
① 数据切分后复杂数据源整合;
② 提供数据切分规则并降低数据切分规则给数据库带来的影响;
③ 降低数据库与客户端的连接数;
④ 读写分离路由。
利用HiveDB实现数据切分及整合
HiveDB同样是一个基于Java的针对MySQL数据库提供数据切分及整合的开源框架,只是目前的HiveDB仅仅支持数据的水平切分,主要解决大数据量下数据库的扩展性及数据的高性能访问问题,同时支持数据的冗余及基本的HA机制
HiveDB的实现机制与MySQL Proxy和Amoeba有一定的差异,它并不是借助MySQL的Replication功能来实现数据的冗余,而是自行实现了数据冗余机制,而其底层主要是基于HibernateShards来实现的数据切分工作
读写分离
概念
读写分离架构是利用数据库的复制技术(详见第14章),将读和写分布在不同的处理节点上,从而达到提高可用性和扩展性的目的
数据库提供写操作,从数据库提供读操作,在很多系统中,更多地是读操作
主数据库进行写操作时,数据要同步到从数据库,这样才能有效保证数据库完整性。MySQL也有自己的同步数据技术。MySQL通过二进制日志来复制数据,主数据库同步到从数据库后,从数据库一般由多台数据库组成,这样才能达到减轻压力的目的
读操作应根据服务器的压力分配到不同服务器,而不是简单的随机分配。MySQL提供了MySQL Proxy实现读写分离操作
方式
基于程序代码内部实现
在代码中根据SELECT、INSERT进行路由分类,这类方法也是目前生产环境中应用最广泛的
基于中间代理层实现
代理位于客户端和服务器之间,代理服务器收到客户端请求后通过判断转发到后端数据库
集群
概念
MySQL Cluster技术在分布式系统中为MySQL数据提供了冗余特性,增强了安全性,使得单个MySQL服务器故障不会对系统产生巨大的负面效应,系统的稳定性得到保障
Shared-Nothing(无共享)架构
MySQL Cluster采用Shared-Nothing(无共享)架构。MySQL Cluster主要利用了NDB存储引擎来实现,NDB存储引擎是一个内存式存储引擎,要求数据必须全部加载到内存之中
数据被自动分布在集群中的不同存储节点上,每个存储节点只保存完整数据的一个分片(Fragment)。同时,用户可以设置同一份数据保存在多个不同的存储节点上,以保证单点故障不会造成数据丢失
NDB存储引擎
概念
NDB存储引擎(也称为NDB Cluster或MySQL Cluster)是MySQL的一个分布式数据库集群解决方案,它允许你将数据分散到多个物理节点上,从而提供高可用性和可扩展性
NDB Cluster是一个share-nothing的架构,其中每个节点都拥有其自己的内存和磁盘存储,并且节点之间通过网络进行通信
优点
高可用性:NDB Cluster支持数据复制和自动故障转移,以确保在节点故障时数据的完整性和服务的连续性。
可扩展性:通过添加更多的NDB数据节点(Data Nodes),可以线性地增加存储能力和处理能力。
分布式数据:数据在集群中自动分区和复制,以实现负载均衡和高性能。
实时性:NDB Cluster支持实时应用,因为数据主要存储在内存中,读取和写入操作都非常快。
在线备份:NDB Cluster提供了在线备份工具,可以在不中断服务的情况下备份数据。
事件驱动:NDB Cluster的通信是事件驱动的,这意味着只有当数据发生变化时,节点之间才会进行通信。
API支持:NDB Cluster提供了多种API,允许开发者使用多种编程语言与集群进行交互。
缺点
内存依赖:NDB存储引擎主要将数据存储在内存中,这意味着数据库的规模受到系统总内存的限制。如果运行NDB的MySQL服务器内存不足,可能会导致性能下降或无法存储更多数据。
磁盘I/O性能:虽然NDB Cluster可以配置磁盘数据节点来将数据存储到磁盘上,但这通常会降低性能,因为磁盘I/O操作的速度远慢于内存操作。因此,在需要高并发和大数据量处理的应用中,磁盘I/O可能成为性能瓶颈。
复杂性:NDB Cluster的分布式架构和集群管理功能使其相对复杂。配置、管理和维护NDB Cluster需要一定的专业知识和经验。此外,由于NDB Cluster具有多个节点和组件,因此故障排除和性能调优也可能更加困难。
网络依赖:NDB Cluster的节点之间通过网络进行通信,因此网络性能和稳定性对集群的整体性能至关重要。如果网络出现故障或延迟,可能会导致数据丢失、性能下降或集群不可用。
备份和恢复:虽然NDB Cluster提供了在线备份工具,但备份和恢复过程可能相对复杂。此外,由于数据主要存储在内存中,因此在没有备份的情况下发生节点故障可能会导致数据丢失。
数据迁移:在NDB Cluster之间进行数据迁移或升级时,可能需要额外的步骤和考虑因素。例如,在将数据从其他存储引擎迁移到NDB Cluster时,可能需要转换数据格式或进行其他处理。
不支持所有MySQL功能:虽然NDB Cluster支持许多MySQL功能,但它并不支持所有功能。例如,NDB Cluster可能不支持某些特定的SQL语法或数据类型。因此,在将应用迁移到NDB Cluster之前,需要仔细评估应用的需求和兼容性。
保证NDB Cluster数据可靠性的方法
数据复制
NDB Cluster通过数据复制机制来确保数据的冗余和容错性。在NDB Cluster中,数据节点(Data Nodes)将数据复制到其他数据节点上,通常配置为至少两份数据副本(冗余因子2)。这样,即使一个数据节点发生故障,其他节点仍然可以提供服务。
备份节点(Backup Nodes)
NDB Cluster可以配置备份节点来存储数据的物理备份。备份节点不会参与正常的读写操作,但会异步地接收数据更改,并将它们写入到磁盘上的本地文件中。这提供了一种额外的数据持久化机制,并允许在数据节点发生故障时进行恢复。
磁盘数据节点
虽然NDB Cluster的主要设计是基于内存存储的,但也可以配置磁盘数据节点(Disk Data Nodes)。这些节点使用磁盘来存储数据,从而将数据持久化到磁盘上。然而,这种配置通常会影响性能,因为磁盘I/O操作比内存操作慢得多。
在线备份
NDB Cluster提供了在线备份工具(如ndb_mgm和ndb_backup),可以在不中断服务的情况下备份数据。这些工具允许管理员定期创建数据的快照,并将其存储在磁盘或其他存储介质上。
自动节点恢复
当NDB Cluster中的一个数据节点发生故障时,其他节点可以自动检测到该故障,并重新配置集群以排除故障节点。同时,集群会尝试从其他正常运行的节点恢复丢失的数据。
事务支持
NDB Cluster支持ACID事务,这有助于确保数据的完整性和一致性。即使在发生故障的情况下,也可以通过事务的日志和回滚机制来恢复数据到一致的状态。
监控和告警
NDB Cluster提供了丰富的监控和告警功能,允许管理员实时了解集群的状态和性能。通过监控工具,管理员可以及时发现潜在的问题并采取相应的措施来确保数据的可靠性。
节点类型
SQL节点
存放表结构,可以有多个
数据节点
存放Cluster中的数据,可以有多个
管理节点
对其他节点进行管理
优点
很好地实现了数据库的负载均衡,减少了数据中心节点的压力和大数据量处理
当数据库中心节点出现故障时,集群会采用一定策略切换到其他备份节点上,有效地屏蔽了故障问题,单节点的失效不会影响整个数据库对外提供服务
主从数据库之间时刻都在进行数据的同步冗余,数据库是多点的、分布式的,良好地完成了数据库数据的备份,避免了数据损失
缓存
关键字缓存
查询缓存
概念
MySQL通过在内存中建立缓冲区(Buffer)和缓存(Cache)来提升MySQL的性能
InnoDB数据库,MySQL采用缓冲池(Buffer Pool)的方式来缓存数据和索引
MyISAM数据库,MySQL采用缓存的方式来缓存数据和索引
如果有大量返回小结果数据的查询,默认数据块大小可能会导致内存碎片,显示为大量空闲内存块。由于缺少内存,内存碎片会强制查询缓存并从缓存内存中修整(删除)查询。这时,应该减少query_cache_min_res_unit变量的值
优点
MySQL查询缓存机制(Query Cache)简单地说就是缓存SQL语句及查询结果,如果运行相同的SQL,服务器直接从缓存中取到结果,而不需要再去解析和执行SQL。而且这些缓存能被所有的会话共享,一旦某个客户端建立了查询缓存,其他发送同样SQL语句的客户端也可以使用这些缓存
对于一些不常改变数据且有大量相同SQL查询的表,查询缓存会节约很大的性能
缺点
如果表更改了,那么使用这个表的所有缓冲查询将不再有效,查询缓存值的相关条目被清空。更改指的是表中任何数据或是结构的改变,包括INSERT、UPDATE、 DELETE、TRUNCATE、ALTER TABLE、DROP TABLE或DROP DATABASE等,也包括那些映射到改变了的表的使用MERGE表的查询
显然,这对于频繁更新的表,查询缓存是不适合的
缓存不生效场景
字符的大小写也被认为是不同的
同样的查询字符串由于其他原因可能认为是不同的
使用不同的数据库、不同的协议版本或者不同默认字符集的查询被认为是不同的查询并且分别进行缓存
如果一条SQL语句是另外一条SQL语句的子串,子串不生效
如果SQL语句是存储过程、触发器或者事件内部的一条语句,同样也不会被缓存
查询缓存也受到权限的影响,对于没有权限访问数据库中数据的用户,即使输入了同样的SQL语句,缓存中的数据也是无权访问的
任何一个包含不确定函数(比如NOW()或CURRENT_DATE())的查询不会被缓存。同样地,CURRENT_USER()或CONNECTION_ID()这些由不同用户执行,将会产生不同的结果的查询也不会被缓存
查询缓存不会缓存引用了用户自定义函数、存储函数、用户自定义变量、临时表、MySQL数据库中的表、INFORMATION_SCHEMA数据库中的表、performance_schema数据库中的表或者任何一个有列级权限的表的查询
如果在事务中使用了序列化隔离级别的表达式也是不能够被缓存的
autoincrement_col是自增列,是按照ODBC工作方法工作的,MySQL对于这种情况也是不会缓存结果集的
参数解释
Have_query_cache
缓存是否支持
query_cache_limit
可缓存的最大结果集(字节数)大于此值的结果集不会被缓存,如果结果集大于此值,则MySQL增加Qcache_not_cached状态值
query_cache_min_res_unit
为了避免Qcache碎片,需要平衡query_cache_min_res_unit大小和服务器保存查询结果时分配的块的数量
如果此值较小,那么会节省内存,但是这会使系统频繁分配内存块(占有CPU),若块体积过大,则会造成较多的内存碎片(占有内存)
Qcache碎片
当查询进行的时候,MySQL把查询结果保存在查询缓存中,但如果要保存的结果比较大,超过query_cache_min_res_unit的值,这时候MySQL将一边检索结果,一边进行保存结果,所以有时候并不是把所有结果全部得到后再进行一次性保存,而是每次分配一块query_cache_min_res_unit大小的内存空间保存结果集,使用完后,接着再分配一个这样的块,如果还不不够,接着再分配一个块,依此类推
也就是说,有可能在一次查询中,MySQL要进行多次内存分配的操作。当一块分配的内存没有完全使用时,MySQL会把这块内存Trim掉,把没有使用的那部分归还以重复利用。比如,第一次分配10 KB,只用了9 KB,剩1 KB,第二次连续操作,分配10 KB,用了8 KB,剩2 KB,这两次连续操作共剩下的1 KB+2 KB=3 KB,不足以做个一个内存单元分配,这时候内存碎片便产生了,也就是Qcache碎片
query_cache_size
显示查询缓存的大小,因为查询缓存本身的数据结构要占用大约40KB(不同系统会有差异),因此其值要大于40 KB
query_cache_type
表示是否启用缓存
query_cache_wlock_invalidate
是否允许在其他连接处于lock状态时,使用缓存结果,默认为off
Qcache_free_blocks
缓存空闲的内存块
Qcache_free_memory
在query_cache_size设置的缓存中的空闲的内存
Qcache_hits
缓存的命中次数
Qcache_inserts
表示查询缓存区此前总共缓存过多少条查询命令的结果
Qcache_lowmem_prunes
表示查询缓存区已满而从其中溢出和删除的查询结果的个数
Qcache_not_cached
表示没有进入查询缓存区的查询命令个数
Qcache_queries_in_cache
查询缓存区当前缓存着多少条查询命令的结果
Qcache_total_blocks
表示缓存总的内存块
索引
优点
通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性
可以大大加快数据的检索速度,这也是创建索引的最主要原因
可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义
在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间
通过使用索引,可以在查询过程中使用优化隐藏器,提高系统的性能
缺点
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加
索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引(注:聚簇索引确定表中数据的物理顺序,类似于电话簿。由于聚簇索引规定数据在表中的物理存储顺序,因此一个表只能包含一个聚簇索引。但该索引可以包含多个列,就像电话簿按姓氏和名字进行组织一样。汉语字典也是聚簇索引的典型应用,在汉语字典里,索引项是字母+声调,字典正文也是按照先字母再声调的顺序排列。),那么需要的空间就会更大
当对表中的数据进行添加、删除和修改时,索引也要动态地维护,这样就降低了数据的维护速度
由于向有些索引的表中插入记录时,数据库系统会按照索引进行排序,这样就降低了插入记录的速度。所以,可以先删除表中的索引,插入数据完成后,再创建索引
特征
唯一性索引
唯一性索引保证在索引列中的全部数据是唯一的,不会包含冗余数据
复合索引
复合索引就是一个索引创建在两个列或者多个列上
最多可以把16个列合并成一个单独的复合索引,构成复合索引的列的总长度不能超过900字节,也就是说复合列的长度不能太长
在复合索引中,所有的列必须来自同一个表中,不能跨表建立复合列
在复合索引中,列的排列顺序是非常重要的,因此,要认真排列列的顺序,原则上,应该首先定义最唯一的列
分类
普通索引
在创建普通索引时,不附加任何限制条件。这类索引可以创建在任何数据类型中,其值是否唯一和非空由字段本身的完整性约束条件决定
唯一性索引
使用UNIQUE参数可以设置索引为唯一性索引。在创建唯一性索引时,限制该索引的值必须是唯一的
全文索引
使用FULLTEXT参数可以设置索引为全文索引。全文索引只能创建在CHAR、VAR-CHAR或TEXT类型的字段上
单列索引
在表中的单个字段上创建索引。单列索引只根据该字段进行索引。单列索引可以是普通索引,也可以是唯一性索引,还可以是全文索引。只要保证该索引只对应一个字段即可
多列索引
多列索引是在表的多个字段上创建一个索引。该索引指向创建时对应的多个字段,可以通过这几个字段进行查询。但是,只有查询条件中使用了这些字段中第一个字段时,索引才会被使用
空间索引
使用SPATIAL参数可以设置索引为空间索引。空间索引只能建立在空间数据类型上,这样可以提高系统获取空间数据的效率
设计原则
选择唯一性索引
为经常需要排序、分组和联合操作的字段建立索引
为常作为查询条件的字段建立索引
限制索引的数目
尽量使用数据量少的索引
尽量使用前缀来索引
删除不再使用或者很少使用的索引
对于那些在查询中很少使用或者参考的列不应该创建索引
对于那些只有很少数据值的列也不应该增加索引
对于那些定义为text、image和bit数据类型的列不应该增加索引
当修改性能远远大于检索性能时,不应该创建索引
分布式系统
概念
分布式系统是由多个独立计算机或服务器组成的系统,这些计算机或服务器通过网络进行通信和协作,共同完成一个或多个任务。这种系统的设计旨在提高系统的可靠性、可扩展性、性能和容错性。
CAP
概念
一致性(Consistency)
一致性是指在一个分布式系统中,所有的节点在同一时刻看到的数据是一致的。
强一致性(Strong Consistency)要求系统总是返回最新的写入值;
弱一致性(Weak Consistency)则允许系统返回稍旧的值。
弱一致性(Weak Consistency)则允许系统返回稍旧的值。
可用性(Availability)
可用性是指系统总是能够响应客户端的请求,即使某个或多个节点发生了故障。在分布式系统中,即使网络分区或节点故障发生,系统仍然需要保证用户能够访问数据。
高可用性(High Availability)通常意味着系统能够快速地处理请求,且不易出现故障。
分区容错性(Partition tolerance)
分区容忍性指的是在网络分区发生时,系统仍然能够继续运行。网络分区是指系统中的一部分节点之间无法通信,但仍然可以与系统的其他部分通信。
在分布式系统中,由于网络延迟、故障或配置错误,网络分区是一个常见的现象。因此,设计分布式系统时通常需要考虑分区容忍性。
CP(一致性+分区容忍性)
在这种情况下,系统会选择强一致性,但可能在某些网络分区的情况下牺牲可用性。例如,某些数据库系统为了保证数据的一致性,在发生网络分区时可能会拒绝服务。
AP(可用性+分区容忍性)
系统优先保证可用性和分区容忍性,但在某些情况下可能会牺牲一致性。例如,某些缓存系统或最终一致性数据库在更新数据时可能会先返回旧值,然后再逐渐更新到最新值。
CA(一致性+可用性)
理论上,如果系统没有网络分区,那么它可以同时保证一致性和可用性。但在实际中,由于网络故障和延迟是不可避免的,因此这种情况很难实现。
BASE
概念
强一致性
用户完成数据更新操作之后,任何后续线程或者其他节点都能访问到最新值。这样的设计是最友好的,即用户上一次的操作,下一次都能读到
弱一致性
当用户完成数据更新操作之后,并不能保证后续线程或者其他节点马上访问到最新值。它只能通过某种方法来保证最后的一致性
比较
在不同的线程和机器之间保持数据的一致性是十分困难的,需要使用很多协议才能保证。在保证一致性的同时,也会给系统带来复杂性和性能的丢失。在BASE理论中,一致性又分为强一致性和弱一致性
BA(Basically Available,基本可用)
在分布式系统中,最重要的需求是保证基本可用,有响应结果返回
S(Soft State,软状态)
其意义在于允许系统存在中间状态。一般来说,系统之间的数据通信都会存有副本,而这些副本都会存在一定的延迟
E(Eventual Consistency,最终一致性)
是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,以保证数据的正确性
分布式事务
强一致性事务
两阶段提交协议(XA协议)
概念
在XA协议中,首先会引入一个中间件,叫作事务管理器,它是一个协调者。独立数据库的事务管理器称为本地资源管理器
步骤
第一阶段(就绪命令)
应用系统使用事务管理器,首先将数据发送给需要操作的分布式数据库,并且给予预备命令
当分布式数据库得到数据和预备命令后,对数据进行锁定(MySQL和Oracle都会将事务隔离级别设置为Serializable——序列化),保证数据不会被其他事务干扰
在本地资源管理器做好上一步后,对事务管理器提交就绪信息
第二阶段(提交命令)
当事务管理器接收到XA协议第一阶段得到的所有数据库的就绪信息后,就会对所有数据库发送提交的命令
各个本地资源管理器接收到提交的命令时,就会将在XA协议第一阶段锁定的数据进行提交,然后释放锁定的数据
当做完上一步后,本地资源管理器就会对事务管理器发送提交成功的信息,表明数据已经操作成功了
阶段提交协议(Three-Phase Commit,3PC)
概念
两段式提交在后续没有提交事务的命令或者回滚事务的命令,数据库就会一直锁定数据,造成数据库死锁的问题。为了解决这些问题,有人在XA协议的基础上引入了三阶段提交协议
三阶段提交协议是在两阶段提交协议的基础上演变出来的,它只是增加了询问和超时的功能
询问功能是指在执行XA协议之前,对数据库连接和资源进行验证
超时功能是指数据库在执行XA协议的过程中锁定的资源,在超过一个时间戳后,会自动释放锁,避免死锁
增加询问阶段
在执行两阶段前做一些询问和验证,如验证连接数据库是否成功,可通过执行简易查询SQL是否成功进行验证,如果成功了,就继续执行,如果没成功,则中断执行,这样就能在大大提升成功率的同时,减少出错的可能性
增加超时机制
其次,在执行XA协议的过程中,如果事务执行超时,则提交事务
为什么不是回滚事务呢?那是因为互联网的大量实践经验表明,在大部分情况下,提交事务的合理性要远远超过回滚事务。这样操作的好处在于,可以防止数据库锁定资源,导致系统出现的死锁问题。
弱一致性事务
概念
弱一致性事务和强一致性事务的不同在于,强一致性事务基于数据库本身的层面,而弱一致性则基于应用的层面
强一致性使用的是数据库本身提供的协议或者机制来实现,如XA协议;而弱一致性则需要自己在应用中处理,使用一定的手段保持数据的一致性
方式
状态表
优点
可以避免强一致性事务的锁机制,使得各个系统在无锁的情况下执行
缺点
需要借用第三方,在一定程度上破坏了微服务风格要求的独立性
使用可靠消息源
优点
只是保证了消息传递的有效性,降低了不一致的可能性,从而大大降低了后续需要运维和业务人员处理的不一致数据的数量
缺点
不能保证消费类能够没有异常或者错误发生,当消费类有异常或错误发生时,数据依旧会存在不一致的情况
提高尝试次数和幂等性
在分布式系统中,我们无法保证消息能正常传递给服务提供者,如果可以尝试数次,那么消息不能传达的概率就会大大降低,从而降低数据的不一致性
实现幂等性的方法很多,加锁、防止重复表等
TCC补偿事务
概念
TCC是try(尝试)、confirm(确认)和cancel(取消)
步骤
一阶段
准备提交
二阶段
正常
提交成功
异常
“回滚”
修复数据思路
提取需要对账的数据
通常因为只提取对账周期内的数据,所以数据一般不会太多
核对数据
对账的数据往往来自各个系统,但是会以一个系统为基准,和其他系统进行比对,找出对应不上的数据
修复数据
使系统数据最终达到一致
微服务架构
优缺点
优点
独立性和灵活性
每个微服务都可以独立开发、测试、部署和扩展,这允许开发团队在不影响其他服务的情况下快速响应变化。同时,由于每个服务都可以选择最适合的技术栈,因此开发团队可以更加灵活地选择技术和工具。
故障隔离和容错性
在微服务架构中,一个服务的故障不会影响到其他服务的正常运行,从而提高了系统的容错性和可用性。此外,由于每个服务都是独立的,可以更容易地进行故障排查和修复。
可伸缩性和扩展性
微服务架构允许根据每个服务的实际需求进行独立扩展,从而提高了系统的可伸缩性和性能。例如,对于需要处理大量请求的服务,可以单独增加其服务器资源。
团队协作和效率
微服务架构使得不同的团队可以并行开发不同的服务,从而提高了开发效率。同时,由于每个服务的功能明确,团队成员可以更加专注于自己的业务领域,提高了开发质量。
缺点
复杂性
微服务架构将应用程序拆分成多个独立的服务,这增加了系统的复杂性。开发团队需要处理更多的服务间通信和协调问题,同时还需要确保服务之间的数据一致性和安全性。
运维挑战
微服务架构中,由于存在大量的服务,因此需要更多的运维工作来确保每个服务的正常运行。这包括服务的监控、日志管理、版本控制等,对运维团队提出了更高的要求。
服务间通信开销
微服务架构中的服务之间需要通过网络进行通信,这可能会增加通信开销并影响系统性能。特别是在服务数量较多时,通信开销可能成为性能瓶颈。
数据一致性问题
在微服务架构中,数据通常分散在各个服务中,这可能导致数据一致性问题。需要采用合适的数据一致性保障策略,如分布式事务、数据最终一致性等,以确保数据的准确性和一致性。
设计原则
单一职责原则
每一个微服务应该只负责一项业务功能或单一职责。这意味着服务的边界应该清晰,避免一个服务承担过多的功能。通过将功能划分到不同的服务中,可以提高服务的可维护性和可测试性。
例如,一个电商系统可以拆分为商品服务、订单服务、用户服务等,每个服务都专注于自己的业务领域。
服务自治性原则
每个微服务都应该是自治的,即它可以独立地运行、升级、扩展和容错。服务应包含自己的数据和业务逻辑,不需要依赖其他服务来执行其核心功能。
自治性使得服务团队可以独立决策,快速响应业务需求,同时也减少了服务间的耦合度。
独立性原则
每个微服务应该是独立的,即不应该共享数据库或其他资源。服务之间应通过定义好的接口进行通信,避免直接的数据交换。
独立性有助于减少服务间的依赖和耦合,使得每个服务都可以独立地进行升级和扩展,提高了系统的可维护性和可扩展性。
可替换性原则
每个微服务都应该是可替换的,即在不影响系统整体功能的前提下,可以用其他服务来替换它。
这要求服务之间的接口是标准化的,并且服务的功能是明确的。当某个服务出现问题或需要升级时,可以方便地替换为其他服务,保证了系统的稳定性和可靠性。
可观察性原则
每个微服务都应该是可观察的,即可以通过监控、日志记录和指标收集等方式追踪其性能和健康状况。
可观察性有助于及时发现服务的问题和瓶颈,为服务优化和故障排查提供依据。同时,通过收集和分析服务的数据,还可以为业务决策提供支持。
轻量级通信机制原则
微服务之间的通信应该采用轻量级的通信机制,如RESTful API、gRPC等。这些通信机制通常具有低延迟、高吞吐量的特点,适用于微服务架构中服务间的快速通信。
轻量级通信机制有助于减少服务间的通信开销和延迟,提高了系统的整体性能。
技术架构
SaaS平台架构
多租户
mybatis-plus
Sharding-JDBC
鉴权
应用权限
license
应用列表
账户权限
RBAC
资源权限
ABAC
业务网关
spring cloud gateway
配置中心
nacos
服务通信
直接
spring cloud fegin clent
异步
rabbitmq
pulsar
部署
容器
docker
配置/服务/集群管理
kubernetes
流量管理/安全
istio
应用架构(DDD)
概念
领域(Domain)
指软件所要解决的业务问题范围,包括业务的概念、规则、流程等。DDD强调对领域的深入理解,以便能够准确地构建出反映业务需求的软件。
领域模型(Domain Model)
实体(Entity)
值对象(Value Object)
服务(Service)
聚合(Aggregate)
通用语言(Ubiquitous Language)
是DDD中用来描述领域的一种共同语言,它应该是团队内部共享的,包括开发人员、业务分析师、测试人员等。通用语言有助于团队成员之间的沟通和协作,确保大家对业务逻辑有共同的理解。
有界上下文(Bounded Context)
是DDD中的一个重要概念,它定义了领域的边界。在有界上下文中,团队可以独立地进行领域建模和软件开发,而不受其他上下文的影响。这有助于控制复杂性,并确保每个上下文都能够保持内聚性和一致性。
聚合(Aggregate)
是DDD中用来表示业务上的一组相关对象的模式。聚合有一个根实体(Aggregate Root),负责协调聚合内部的状态和行为。聚合是保持数据一致性和封装业务逻辑的重要手段。
领域服务(Domain Service)
当某个操作过程或转换不属于任何一个实体或值对象的职责时,应该使用领域服务。领域服务是无状态的,并且可以通过其接口来定义领域操作。
仓库(Repository)
用于封装数据访问逻辑,提供集合式的接口来访问领域对象。它使得领域层不必依赖于特定的数据存储技术或ORM框架。
实现(COLA)
Adapter(VO)
controller
scheduler
consumer
Application(DTO)
service
executor
Domain(Entity)
gateway(repository)
model
ablity(service)
intrastructrue(POJO)
gatewayImpl(repositoryImpl)
mapper
config(remote)
持续集成/部署(CI/CD)
代码仓库
gitlab
构建
Jenkins
Maven
Node.js
代码检测
p3c/pmd
部署
容器
docker
配置/服务/集群管理
kubernetes
流量管理/安全
istio
监控日志
log4j
elastic search
收藏
收藏
0 条评论
下一页