导图社区 大数据面试(每周更新)
大数据面试指南,包括常用大数据组件(Hadoop、Kafka、Zookeeper、Flink、Spark)、Java、网络、数据结构、操作系统、数据库等知识,更新中!
编辑于2020-12-10 16:52:09大数据面试
Java
Java 基础
数据类型
基本类型
整数
byte
short
int
long
小数
float
double
boolean
char
引用数据类型
String
数组
自定义的类或接口
枚举类型
enum
== VS equals
==:基本数据类型、引用数据类型
equals:重写、没有重写
Object类有哪些方法?(10个)
反射:getClass
HashMap:hashCode、equals
重写euqals方法
if(this == obj)
if(!(obj instanceOf Student))
Student stu = (Students)obj;
GC:finalize
浅拷贝:clone
wait/notify
toString
String
String、StringBuffer、StringBuilder的区别?
是否可变
是否线程安全
String不变性
final修饰的字符串数组
不可变的优势
线程安全
hash值不需要重复计算
可以使用String Pool
String Pool的优点
减少内存开销
提升性能
创建字符串的两种方式
字面值方式
new关键字创建字符串对象
Java 容器
Collection
Map
HashMap
实现
采用数组加链表的方式实现
特点
线程不安全
key、value 允许为 null
优点
采用数组加链表的方式实现
在查询和修改方面继承了数组的线性查找和链表的寻址修改
非 synchronized,
所以 HashMap 效率很高
常用参数
capacity
当前数组容量,默认为16,可扩容
只能成倍扩容,所以该值为2^n
loadFactor
负载因子默认为 0.75f
threshold
扩容的阈值,等于 capacity * loadFactor
Map 中存储的数据量超过此阈值,会触发扩容
重要方法
put
get
扩容
HashMap为什么需要扩容?
HashMap 的扩容是加长 table 的长度,目的是减少 Hash 冲突发生的概率
hash冲突越多 -> 有某个hash槽下的元素数量过多 -> 链表过长 -> 查找效率低
触发扩容的两个条件
HashMap.size >= Capacity * LoadFactor
当前加入的数据发生了hash冲突
扩容过程
rehash
复制数据
扩容非常消耗性能
JDK 1.8 的优化
加入红黑树
哈希碰撞后链表上达到8个节点后会将链表重构为红黑树
查询的时间复杂度为 O(lgn)
子主题
问题
并发问题?
HashMap 在并发执行put操作时可能会引起 Entry 链表成环,导致查找陷入死循环
HashMap 和 HashTable 的区别?
HashMap
线程不安全
key、value 允许为 null
HashTable的所有操作都是被synchronized锁保护的
HashTable
线程安全
key、value 不允许为 null
HashMap 中 hash 函数怎么是实现的?
高16 bit 不变,低16 bit 和高16 bit 做异或,
让低16位保留高16位的特性,增加随机性
HashMap 是不是有序的?
不是
影响HashMap性能的因素?
负载因子
哈希值是否足够随机
理想情况下是均匀散列到各个桶中
手写 HashMap
ConcurrentHashMap
特点
线程安全
key、value 不允许为 null
使用加锁实现同步
JDK 1.7
JDK 1.8
有序的Map
TreeMap
如何保证有序?
TreeMap 实现了SortedMap接口
保证了键的有序性
时间复杂度
底层基于红黑树,时间复杂度为O(lgn)
LinkedHashMap
如何保证有序?
维护一个双向链表
保证了键值有序
保证了插入顺序
取出顺序和 put 的顺序一致
保证了访问顺序
把访问过的放到底部
Collections
Java 并发
线程安全
概念
线程安全就是要确保多个线程能够正确的访问共享数据
实现线程安全的方式(两种)
避免数据共享
使用 ThreadLocal 变量
使用不可变变量
通过锁机制来保证对共享数据的有序访问
Java 内存模型
CPU、高速缓存、主存(硬件)
为什么引入Cache?
解决速率不匹配
缓存一致性问题
每个CPU都有自己的高速缓存
缓存一致性协议
MESI协议
MESI协议保证了每个缓存中的共享变量副本都是一致的
核心思想
缓存行置为无效状态
Java线程、工作内存(高速缓存)、主存
每个线程都有独立的工作内存
工作内存与主存的交互操作
lock、unlock
lock(锁定)
作用于主内存的变量
unlock(解锁)
作用于主内存的变量
read、load
顺序执行
read(读取)
作用于主内存的变量
load(载入)
作用于工作内存的变量
use、assign
use(使用)
作用于工作内存的变量
assign(赋值)
作用于工作内存的变量
store、write
顺序执行
store(存储)
作用于工作内存的变量
write(写入)
作用于主内存的变量
volatile关键字
作用
可见性
工作内存共享变量置为无效状态
禁止指令重排
Java内存模型允许编译器和处理器对指令进行重排序
volatile关键字会禁止指令重排
volatile修饰的变量之前的指令不准出现在其之后,在其之后的指令不准出现在其之前
原理
加了volatile关键字的汇编代码会多出一个lock前缀指令
lock前缀指令相当于一个内存屏障,内存屏障提供两个功能
强制更新到主存;无效化工作内存
禁止指令重排
使用
状态量标记(可见性)
单例模式双重检查(禁止指令重排)
happens-before原则
happens-before是什么?
Java内存模型具备的先天的“有序性”
不需要通过任何手段就能够得到保证的有序性
分类(八种)
<程序锁定是为了传递>
程序次序规则
前面的先于后面的
锁定规则
unlock先于lock
volatile变量规则
写操作先于读操作
传递规则
A、B、C
<现象>
线程启动规则
线程中断规则
线程终结规则
对象终结规则
Java 线程
线程池
为什么要使用线程池?
创建大量线程降低系统性能
线程复用
ThreadPoolExecutor构造器核心参数(7个)
corePoolSize
核心池大小
maximumPoolSize
缓存队列已满时使用
keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止
池中线程数大于corePoolSize时,才会起作用
unit
keepAliveTime的时间单位(7种)
纳秒、毫秒、微妙
秒、分、时
天
workQueue
一个阻塞队列,用来存储等待执行的任务
分类
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
threadFactory
线程工厂,主要用来创建线程
handler
拒绝策略
分类<AD(2)C>
ThreadPoolExecutor.AbortPolicy
丢弃任务并抛出异常
ThreadPoolExecutor.DiscardPolicy
丢弃任务但不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列最前面的任务
ThreadPoolExecutor.CallerRunsPolicy
由调用线程处理该任务
<记忆>
两个Size相关
两个存活时间相关
一个阻塞队列
拒绝或接受(创建线程)
线程池分类(Executors类创建线程池)
Executors.newSingleThreadExecutor();
创建一个单线程的线程池
Executors.newFixedThreadPool(int);
创建固定容量大小的线程池
Executors.newScheduledThreadPool(int);
创建固定容量大小的线程池,支持定时和周期性任务执行
Executors.newCachedThreadPool();
创建一个可缓存线程池,它是一个可以无限扩大的线程池
可灵活回收空闲线程;若无可回收,且有新任务,则新建线程
<记忆>
一个单个大小
两个固定大小
一个可缓存
Java 锁机制
线程要不要锁住同步资源?
锁住
悲观锁
悲观锁认为,对于同一个数据的并发操作,自己在使用数据的时候一定有别的线程来修改数据
因此悲观锁在处理数据前,先对数据进行加锁,直到完成修改操作(commit)才释放锁
悲观锁具有强烈的独占和排他特性
实现
synchronized 关键字和 Lock 的实现类都是悲观锁
基于操作系统底层加锁机制
应用场景
悲观锁适合写入操作比较频繁的场景
如果有大量读操作,每次读都进行加锁的话会产生大量的锁的开销
悲观锁的调用方式
// synchronized 方式 public synchronized void testMethod() { // 操作同步资源 } // ReentrantLock 方式 private ReentrantLock lock = new ReentrantLock(); public void modifyPublicResources() { lock.lock(); // 操作同步资源 // lock.unlock(); }
不锁住
乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,
只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
实现
使用无锁编程来实现
最常采用的是 CAS 算法
全称
CompareAndSwap
三个操作数
内存地址值 V
旧的预期值 A
即将要更新的目标值 B
更新过程
CAS 指令执行时,当且仅当内存地址 V 的值与预期值 A 相等时,
将内存地址 V 的值修改为 B,否则就什么都不做。
整个比较并替换的操作是一个原子操作
问题
ABA 问题
如果刚开始读取到的地址值是 A,
然后被其他线程连续修改两次,最终结果还是 A
那么 CAS 很可能识别不到数据发生了改变
这种情况对程序造成了极大的安全隐患
解决
通过添加版本号标志位来解决该问题
循环开销大
CAS 通常是配合无限循环一起使用的,如果 CAS 失败,会一直进行尝试
如果 CAS 长时间一直不成功,会给 CPU 带来很大的开销
解决
可以使用自适应自旋锁解决这个问题
只能保证一个变量的原子操作
比如 AtomicInteger 都是每次只能对一个变量进行原子性控制
解决
使用互斥锁来保证原子性
将多个变量封装成对象,通过 AtomicReference 来保证原子性
实现
Java 原子类中的递增操作
自旋锁
应用场景
乐观锁比较适合读操作比较频繁的场景
如果出现大量的写入操作,数据发生冲突的可能性就会增大
乐观锁的调用方式
private AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); // 执行自增1
锁住同步资源失败,线程要不要阻塞?
阻塞
阻塞或唤醒 Java 线程需要操作系统切换 CPU 的状态来完成
这种状态转换需要耗费处理器时间
如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
不阻塞
自旋锁
不断循环尝试获取锁
通过自旋操作减少 CPU 切换以及恢复现场导致的消耗
存在的问题
如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
自旋必须设置等待时间,超过限定次数没有成功获得锁,就应当挂起线程
自适应自旋锁
自适应意味着自旋的时间(次数)不再固定
而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
如果在同一锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,就可以认为这次自旋很有可能再次成功,进而运行自旋等待更长的时间
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
图片说明
竞争同步资源的流程细节区别
无锁
不锁住资源,同一时刻多个线程中只有一个能修改资源成功,其他线程会重试
CAS 原理及应用即是无锁的实现
偏向锁
同一线程执行同步资源时自动获取锁,
以此降低获取锁的代价
大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争
是为了在只有一个线程执行同步代码块时能够提高性能
轻量级锁
多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放
重量级锁
多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒
Java 对象头
主要包括两部分数据:
Mark Word
标记字段
默认存储对象的 HashCode,分代年龄和锁标志位信息
被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据
运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化
Klass Pointer
类型指针
对象指向它的类元数据的指针
虚拟机通过该指针来确定该对象是哪个类的实例
Monitor
synchronized 通过 Monitor 来实现线程同步,
Monitor 是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的线程同步
这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”
这就是 JDK 6 之前 synchronized 效率低的原因
JDK 6 中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”
图片说明
多线程竞争锁时要不要排队?
公平锁
排队
优点
等待锁的线程不会饿死
缺点
整体吞吐效率相对非公平锁要低
等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大
非公平锁
先尝试插队,插队失败再排队
多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待
优点
插队成功的线程直接获取了锁,没有进入等待队列,也不会阻塞
CPU 不需要增加唤醒工作,吞吐量高
缺点
整体吞吐效率相对非公平锁要低
等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大
一个线程中的多个流程能不能获取同一把锁?
可重入锁
又名递归锁
子主题
不可重入锁
多个线程能不能共享同一把锁
共享锁
排它锁
图片说明
参考
https://blog.csdn.net/yetaoii/article/details/104818237
临时
synchronized关键字
使用方法
修饰成员方法
修饰静态方法
修饰代码块
底层原理
monitor
monitorenter/monitorexit
特点
可重入
早期重量级
两个monitorexit
sleep VS wait[类依赖、锁唤醒]
来自不同的类
是否释放锁
是否依赖synchronized
是否需要唤醒
notify VS notifyAll
两个概念
锁池
等待池
等待池进入锁池
notify随机选一个
notifyAll全部
JVM
JVM的主要组件包括类加载器,运行时数据区域和执行引擎
类加载器
运行时数据区
线程共有
堆
存放对象
方法区
存放被JVM加载的类信息、常量、静态变量、即时编译期编译后的代码
线程私有
随线程而生,随线程而灭
程序计数器
当前线程执行字节码的行号指示器
虚拟机栈
用于 JVM 执行 Java 方法
每个 Java 方法执行时都会创建一个栈帧
每个方法从开始调用到结束都对应一个栈帧在虚拟机中从入栈到出栈的过程
本地方法栈
用于 JVM 执行本地方法
HotSpot 虚拟机把本地方法栈和虚拟机栈合二为一
直接内存(Direct Memory)
jdk 1.4 的 NIO, 可以使用 native 函数直接分配堆外内存
执行引擎
GC
哪些内存需要被垃圾收集器回收?
需要
Java 堆和方法区
这部分内存的分配和回收都是动态的
不需要
程序计数器、虚拟机栈、本地方法栈
随线程而生,随线程而灭,其内存的分配和回收都具备确定性
怎么判断对象是否死亡?
判断死亡
引用计数法
给对象中添加一个引用计数器
有一个地方引用该对象,计数器就加1
引用失效(超出作用范围),计数器就减1
当计数器为0时,该对象被认为死亡
缺点
无法解决对象之间相互引用的问题
可达性分析
以一系列GC Roots对象作为起点,从这些节点向下搜索,
搜索走过的路径称为引用链,
当一个对象到GC Roots没有任何引用链相连时,就证明该对象不可达
GC Roots 种类
方法区中常量引用的对象
方法区中静态变量引用的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中 JNI (Native 本地方法)引用的对象
引用
目的
为了让垃圾回收更加具有弹性
强引用
类似 Object obj = new Object() 这类的引用,
垃圾收集器永远不会回收掉被强引用引用的对象。
软引用
用来描述有用但并非必须的对象
对于软引用关联的对象,
在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行回收
弱引用
也是用来描述有用但并非必须的对象
被弱引用关联的对象只能生存到下一次垃圾收集之前
虚引用
设置虚引用关联的唯一目的是能够在该对象被垃圾收集器回收的时候收到一个系统通知
<强软弱虚>
finalize()
作用
finalize()方法是对象逃脱死亡命运的最后一次机会
如果对象要在finalize()中拯救自己,只需要与引用链上任何一个对象建立关联即可
如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,
则在对象回收之前进行一次筛选
筛选
对象是否覆盖finalize()方法
是
JVM 是否 已经执行过该对象的 finalize() 方法
是
JVM执行 finalize 方法(只执行一次)
否
JVM回收对象
否
JVM回收对象
垃圾收集算法
标记-清除算法(Mark-Sweep)
先标记需要回收的对象,再统一回收所有被标记的对象
缺点
产生大量不连续的内存碎片
可能导致大对象无法分配内存而不得不提前触发另一次垃圾收集动作
复制算法(Copying)
将内存按容量划分为大小相等的两块,每次只使用其中的一块
当这一块用完了,就将存活的对象复制到另一块上面,
然后再把已使用过的内存空间一次清理掉
优点
不会产生内存碎片
缺点
浪费了一半的空间
标记整理算法(Mark-Compact)
先标记需要回收的对象,
然后让所有的存活对象都向一端移动,再清理掉边界以外的内存区域
优点
不会产生内存碎片
避免了浪费了一半的空间
缺点
内存变动频繁,需要整理所有存活对象的引用地址,效率低
分代收集算法(Generational Collection)
分代
新生代
复制算法
在新生代中,每次垃圾收集都会有大批对象死去,只有少量存活
Eden 区
Minor GC
当Eden区中没有足够空间进行分配时,虚拟机会发起一次Minor GC
Minor GC 后存活的对象进入 Survivor区
Surviror 区
from
to
老年代
标记清除或标记整理算法
老年代中因为对象存活率高,没有额外空间对它进行分配担保
Old 区
只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”
什么对象会进入老年代?
大对象
为避免大对象在新生代频繁复制
大对象直接进入老年代
长期存活对象
虚拟机会给每个对象定义一个对象年龄(Age)
对象在Survivor区中每经历一次Minor GC,年龄就会增加一岁
当增加到15岁时就会被转移到老年代
动态对象年龄
如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,
则年龄大于该年龄的对象就可以直接进入老年代
HotSpot 虚拟机
HotSpot 算法的实现
枚举根节点
可达性分析必须在一个能确保一致性的快照中进行
“一致性”指的是不可以出现分析过程中对象引用关系不断变化的情况
这点是导致 GC 进行时必须停顿所有 Java 执行线程(STW)的一个重要原因
主流Java虚拟机都是使用准确式 GC
也就是虚拟机可以直接得到哪些地方存在着对象引用
HotSpot 使用 OopMap 来达到这个目的
OopMap 会把对象内什么偏移量上是什么类型的数据计算出来
也会在特定位置下记录栈和寄存器中哪些位置是引用
这样 GC 直接扫描 OopMap 就可以得知这些信息了
安全点(SafePoint)
在 OopMap 协助下
HotSpot 可以快速且准确地完成 GC Roots 的枚举
但是可能导致引用变化,或者说 OopMap 内容变化的指令非常多
如果为每一条指令都生成对应的 OopMap,
那么将会需要大量的额外空间,这样GC的空间成本将会变得很高
事实上,HotSpot 只在安全点(SafePoint)记录这些信息,
也就是说程序执行时只有在到达安全点时才能停顿下来开始 GC
安全点的选取
选取原则
既不能太少以至于让 GC 等待时间太长
也不能太频繁以至于过分增大运行时负载
选取标准
以程序“是否具有让程序长时间执行的特征”为标准选定的
“长时间执行”最明显的特征就是指令复用
例如方法调用、循环跳转、异常跳转等
如何在GC发生时让线程都“跑”到最近的安全点再停顿下来?
抢先式中断
GC发生时,首先中断所有线程,如果发现有线程不在安全点,就让它“跑”到安全点
主动式中断
设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志就自己中断挂起
安全区域
概念
安全区域是指在一段代码片段中,引用关系不会发生变化,
在这个区域中的任意地方开始GC都是安全的
HotSpot 中的垃圾收集器(7个)
新生代
Serial 收集器
Serial 收集器是一种单线程收集器
它进行垃圾回收时,必须暂停所有工作线程
Serial 是 Client 模式下的默认新生代收集器,适用于单CPU环境
ParNew 收集器
ParNew 收集器是 Serial 收集器的多线程版本
ParNew 是运行在 Server 模式下的虚拟机中首选的新生代收集器,
因为目前除了 Serial 之外,只有它能与 CMS 收集器配合工作
Parallel Scavenge 收集器
该收集器的目的是达到一个可控的吞吐量
所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值
Parallel Scavenge 收集器适用于后台运算而不需要太多交互的任务
老年代
Serial Old 收集器
Serial Old 是 Serial 的老年代版本,也是主要给 Client 模式下的虚拟机使用
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本
在注重吞吐量以及 CPU 敏感的场合,
都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器
CMS 收集器
以获取最短回收停顿时间为目标
适用于重视服务器的响应速度的应用
步骤
初始标记
标记 GC Roots 能直接关联到的对象
并发标记
进行 GC Roots Tracing 的过程
重新标记
修正并发标记期间因为用户程序继续运行而导致标记产生变化的那一部分对象的标记记录
并发清除
垃圾回收
步骤总结
CMS 整个过程中耗时最长的并发标记和并发清除过程都可以和用户线程一起工作
所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的
缺点
CMS 收集器对 CPU 资源非常敏感
CMS 默认启动的回收线程数是(CPU + 3) / 4
所以 CPU 数量少会导致用户程序执行速度降低较多
并发垃圾回收线程的占比随着 CPU 数量的增加而降低
CMS 收集器无法处理浮动垃圾
CMS 并发清除阶段用户线程产生的垃圾需要等到下次 GC 来收集
浮动垃圾会造成的问题
版本区别
jdk1.5
默认 CMS 收集器当老年代使用了68%的空间时被激活
jdk1.6
默认 CMS 收集器当老年代使用了92%的空间时被激活
如果 CMS 运行期间预留的内存无法满足程序的需要,就会出现 Concurrent Mode Failure
然后降级临时启用 Serial Old 收集器进行老年代的垃圾收集
这样停顿时间就会很长
所以 -XX:CMSInitialOccupancyFraction 不能设置太高
CMS 是基于“标记-清除”算法实现的,所以会产生大量的内存碎片
解决
CMS 提供了 -XX:UseCMSCompactAtFullCollection 参数用于开启内存碎片的合并整理,
由于内存整理是无法并行的,所以停顿时间会变长
G1
全称
Garbage-First
G1 是面向服务端应用的垃圾收集器
可预测的 GC 暂停时间
和 CMS 一样追求低停顿
同时实现高吞吐量
G1 的使命是替换掉 JDK1.5 发布的 CMS
重要概念
Region
将整个堆分成若干相同大小的 Region
每个分区可能是 Eden、Survivor、Old
Eden、Survivor、Old 成了逻辑上的概念
G1 优先回收垃圾对象特别多的 Region
花费较少的时间来回收较多的垃圾
G1 名字的由来
分区的好处
垃圾回收时间可控
RSet
Remembered Sets
已记忆集合
将对象引用跟踪到给定区域中
表示“谁引用了我的对象”
RSet 的价值是垃圾收集器不需要扫描整个堆来找谁引用了当前分区中的对象,
只要通过 RSet 就可以获取
CSet
Collection Sets
收集集合
一组可以被回收的分区的集合
在 CSet 中存活的数据会在 GC 过程中被移动到另一个可用分区
局部压缩
G1 是一种带压缩的收集器,
在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区
G1 堆结构
传统堆结构都是连续分配的
G1 的堆结构就是把一整块内存区域切分成多个固定大小的小块(Region)
一般切分为2000个小 Region
Region 的大小在 JVM 启动时决定
G1 内存分配
Region 被分别标记为 Eden,Survivor 和 Old
Eden,Survivor 和 Old 的内存不再连续,大小不再固定
增加了第四种类型
Humongous
类型主要是用来存储那些比标准 Region 大50%,或更大的那些对象
存储为一组连续区域
Humongous 的区域就是堆里没有被使用的区域
垃圾收集过程
新生代 GC
新生代 GC 主要是回收 Eden 区和 Survivor 区
一旦 Eden 区被占满,新生代 GC 就会启动
新生代 GC 之后
Eden 区都会被清空,Survivor 区会被收集一部分数据,老年代增多
并发标记周期
步骤
初始标记
STW
标记从GC Root 直接可达的对象
会伴随一次新生代GC,并且产生全局停顿
根区域扫描
将扫描由 Survivor 区直接可达的老年代区域,并标记为可达对象
并发标记
扫描堆内的所有存活对象,并做标记
重新标记
STW
修正并发标记期间因为用户程序继续运行而导致标记产生变化的那一部分对象的标记记录
独占清理
STW
计算各个区域的存活对象和 GC 回收比例并进行排序,识别可提供混合回收的区域
并发清理
识别并清理完全空闲的区域
图片说明
并发标记周期的结果
收集完成之后还会存在 Eden 区
因为过程中有用户线程的执行
会产生一些 G 区域
这些区域用于后面的混合收集
混合收集
Eden 区会被清空
G 区域较高的会被收集
G 区域是标记为垃圾的老年代区域
可能会进行 Full GC
在内存不充足的情况下会产生
图片说明
G1 VS CMS
与应用线程同时工作,几乎不需要 STW
与 CMS 类似
基于复制算法,不产生内存碎片
CMS 是基于“标记-清除”算法实现的,所以会产生大量的内存碎片
整理碎片需要长时间 STW
GC 停顿时间可控,且不牺牲系统吞吐量
GC 不需要额外的内存空间
CMS 需要预留空间存储浮动垃圾
G1 可以在 Young GC 中使用,CMS只能在 Old 区使用
查看 GC 日志
JIT 编译器
图片说明
Java IO
Java NIO
概念
Java 中的 BIO、NIO 和 AIO 是 Java 语言对操作系统的各种 IO 模型的封装!
同步与异步
同步
同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回
异步
异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,
但是被调用者并没有返回结果,此时调用方可以处理其他的请求
被调用者通常依靠事件,回调等机制来通知调用者其返回结果
最大区别是是否需要等待处理结果
阻塞和非阻塞
阻塞
阻塞就是发起一个请求,调用者一直等待请求结果返回,无法从事其他任务,
只有当条件就绪才能继续
非阻塞
非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情
最大区别是结果没出来之前是否可以干其他的事情
同步阻塞、同步非阻塞和异步非阻塞
同步阻塞
烧水等水开,不干其他事一直等
同步非阻塞
烧水等水开,空隙去干其他事,时不时看看水是否烧开了
异步非阻塞
电水壶,水开了会响
传统的 BIO
Blocking I/O
BIO 是一种同步阻塞的 IO 模型
NIO 与传统 IO(BIO) 的区别?
IO 流是阻塞的,NIO 流是不阻塞的
IO
当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入
线程在此期间不能再干任何事情了
NIO
单线程中从通道读取数据到 buffer,同时可以继续做别的事情,
当数据读取到 buffer 中后,线程再继续处理数据
写数据也一样
IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)
NIO
Java 1.4 引入
New IO (Java中的称法)
Non-Blocking I/O(操作系统中的称法)
NIO 是一种同步非阻塞的 I/O 模型,NIO 采用的是多路复用技术
NIO 已经被越来越多地应用到大型应用服务器,是解决高并发、I/O处理问题的有效方式
AIO
Asynchronous I/O
异步非阻塞的 IO 模型
AIO 是 Java7 引入的 也就是 NIO2
AIO 的应用还不是很广泛
核心部分
Channel
功能
数据可以从 Channel 读到 Buffer 中
也可以从 Buffer 写到 Channel 中
Java NIO 的通道类似流,但又有些不同
既可以从通道中读取数据,又可以写数据到通道
但流的读写通常是单向的
通道可以异步地读写
通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入
类型/实现
FileChannel
文件 IO
从文件中读写数据
DatagramChannel
收发 UDP 包的通道
能通过UDP读写网络中的数据
SocketChannel
能通过TCP读写网络中的数据
ServerSocketChannel
可以监听新进来的 TCP 连接
对每一个新进来的连接都会创建一个 SocketChannel
TCP
通道之间的数据传输
Java NIO中,如果两个通道中有一个是 FileChannel,
那么可以直接将数据从一个 Channel 传输到另外一个 Channel
transferFrom()
FileChannel 的 transferFrom() 方法可以将数据从源通道传输到 FileChannel 中
transferTo()
FileChannel 的 transferTo() 方法可以将数据从 FileChannel 传输到其他 Channel 中
示例
使用 FileChannel 读取数据到 Buffer 中
RandomAccessFile aFile = new RandomAccessFile("nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); // 返回与此文件关联的唯一的FileChannel对象 ByteBuffer buf = ByteBuffer.allocate(4); // 分配一个新的字节缓冲区 int bytesRead = inChannel.read(buf); // 从该通道读取到给定缓冲区的字节序列 while (bytesRead != -1) { System.out.println("Read " + bytesRead); buf.flip(); // 翻转这个缓冲区 while(buf.hasRemaining()) { System.out.print((char) buf.get()); } System.out.println(); buf.clear(); // 清除此缓冲区 bytesRead = inChannel.read(buf); } aFile.close();
Buffer
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存
这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存
类型/实现
CharBuffer
ByteBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
MappedByteBuffer
重要属性
不管 Buffer 处在什么模式,含义不变
capacity
表示 Buffer 的容量(固定),即有 capacity 个单元
含义取决于 Buffer 处于读模式还是写模式
position
写
当向 Buffer 中写数据时,position 表示当前的位置
读
当将 Buffer 从写模式切换到读模式,position 会被重置为0
当从 Buffer 的 position 处读取数据时,position 向前移动到下一个可读的位置。
limit
写
limit 等于 Buffer 的 capacity
limit 表示最多能往 Buffer 里写多少数据
读
当将 Buffer 从写模式切换到读模式,limit 表示最多能从 Buffer 读到多少数据
limit 会被设置成写模式下的 position 值
重要方法
Buffer 的分配
allocate()
ByteBuffer buf = ByteBuffer.allocate(48);
向 Buffer 中写入数据(两种方式)
从 Channel 写到 Buffer
int bytesRead = inChannel.read(buf);
通过 Buffer 的 put() 方法写到 Buffer里
buf.put(127);
将 Buffer 从写模式切换到读模式
flip() 方法
调用 flip() 方法会将 position 设置为0,
并将 limit 设置为写模式下的 position值
从 Buffer 中读取数据(两种方式)
从 Buffer 读取数据到 Channel
int bytesWritten = inChannel.write(buf);
使用 Buffer 的 get() 方法从 Buffer中读取数据
byte aByte = buf.get();
rewind()
将 position 设回0
可以重读 Buffer 中的所有数据
clear() && compact()
clear()
调用 clear() 方法,position 被设置为 0,limit 被设置成 capacity 的值
整个 Buffer 被清空了,但是 Buffer 中的数据并没有被清除
compact
未读的数据都被移到缓冲区的起始处
然后将 position 设置为最后一个未读元素的位置加1
limit 被设置成 capacity 的值
mark() && reset()
mark()
标记 Buffer 中的一个特定 position
reset()
恢复到 mark() 标记的 position 处
equals() && compareTo()
equals()
满足以下条件,两个 Buffer 相等
有相同的类型
两个对象都剩余同样数量的元素
独立于其起始位置的剩余元素的两个序列是相等的
compareTo()
满足以下条件,第一个 Buffer 小于第二个 Buffer
第一个不相等的元素小于另一个Buffer中对应的元素
元素都相等的情况下,第一个 Buffer 的元素个数比另一个少
Buffer 使用步骤(四步)
1. 写入数据到 Buffer
2. 调用 flip() 方法
flip() 方法将 Buffer 从写模式切换到读模式
3. 从 Buffer 中读取数据
4. 调用 clear() 方法或者 compact() 方法
clear()
清空整个缓冲区
compact()
只会清除已经读过的数据
未读的数据都被移到缓冲区的起始处
新写入的数据将放到缓冲区未读数据的后面
Selector
功能
Selector 是 Java NIO 中能够检测一到多个 NIO 通道,
并能够知晓通道是否为诸如读写事件做好准备的组件
这样,一个单独的线程可以管理多个 Channel
Selector 的创建
Selector selector = Selector.open();
通过调用 Selector.open() 方法创建一个 Selector
向 Selector 注册通道
channel.configureBlocking(false); SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
与 Selector 一起使用时,Channel 必须处于非阻塞模式下
FileChannel 不能与 Selector 一起使用
因为 FileChannel 不能切换到非阻塞模式
register() 的第二个参数是一个“interest集合”
意思是在通过 Selector 监听 Channel 时对什么事件感兴趣
四个事件
Connect
连接就绪
SelectionKey.OP_CONNECT
某个 Channel 成功连接到另一个服务器
Accept
接收就绪
SelectionKey.OP_ACCEPT
一个 Server Socket Channel 准备好接收新进入的连接
Read
读就绪
SelectionKey.OP_READ
有数据可读的通道
Write
写就绪
SelectionKey.OP_WRITE
等待写数据的通道
SelectionKey 包括
interest 集合
int interestSet = selectionKey.interestOps(); // 用“位与”操作 interest 集合和给定的 SelectionKey 常量, // 可以确定某个确定的事件是否在interest 集合中 boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
ready 集合
int readySet = selectionKey.readyOps();
ready 集合是通道已经准备就绪的操作的集合
检查 Channel 中什么事件或操作已经就绪
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
Channel
// 从 SelectionKey 访问 Channel Channel channel = selectionKey.channel();
Selector
// 从 SelectionKey 访问 Selector Selector selector = selectionKey.selector();
附加的对象
可以附加 与通道一起使用的 Buffer
或是包含聚集数据的某个对象
通过 Selector 选择通道
select 方法
int select()
阻塞到至少有一个通道在注册的事件上就绪了
int select(long timeout)
和 select() 一样
除了最长会阻塞 timeout 毫秒
int selectNow()
不会阻塞,不管什么通道就绪都立刻返回
如果没有通道变成可选择的,则此方法直接返回零
返回的 int 值表示有多少通道已经就绪
selectedKeys()
Set selectedKeys = selector.selectedKeys();
通过调用 selector 的 selectedKeys() 方法,访问“已选择键集(selected key set)”中的就绪通道
wakeUp()
某个线程调用 select() 方法后阻塞了,即使没有通道已经就绪,也有办法让其从 select() 方法返回
close()
关闭 Selector
图片说明
Scatter/Gather
作用
Scatter/Gather 用于描述从 Channel 中读取或者写入到Channel的操作
功能
Scatter(分散) 从 Channel 中读取是指在读操作时将读取的数据写入多个 Buffer 中
Gather(聚集) 是指在写操作时将多个 Buffer 的数据写入同一个 Channel
使用场景
Scatter/Gather 经常用于需要将传输的数据分开处理的场合
使用示例
Scattering Reads
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); ByteBuffer[] bufferArray = { header, body }; channel.read(bufferArray);
Buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数
read() 方法按照 Buffer 在数组中的顺序将从 Channel 中读取的数据写入到 Buffer,
当一个 Buffer 被写满后,Channel 紧接着向另一个 Buffer 中写
Scattering Reads 要求消息大小固定,不适用于动态消息
Gathering Writes
ByteBuffer header = ByteBuffer.allocate(128); ByteBuffer body = ByteBuffer.allocate(1024); //write data into buffers ByteBuffer[] bufferArray = { header, body }; channel.write(bufferArray);
Buffer 数组是 write() 方法的入参,write() 方法会按照 Buffer 在数组中的顺序,将数据写入到 Channel,
注意只有 position 和 limit 之间的数据才会被写入
Gathering Writes 能较好的处理动态消息
NIO 存在的问题
NIO 跨平台和兼容性问题
NIO 的实现依赖于操作系统针对 IO 操作的APIs.
所以 JAVA 能在所有操作系统上实现统一的接口,并用一致的行为来操作 IO 是很伟大的
但是会出现在 Linux 上正常运行,但在 Windows 上就出现问题的情况
所以编写 NIO 程序需要在程序支持的所有操作系统上进行功能测试,
否则程序运行中可能遇到意想不到的问题
NIO 对缓冲区的聚合和分散操作可能会导致内存泄露
很多 Channel 的实现支持 Gather 和 Scatter,
该功能允许从从多个 ByteBuffer 中读入或写入
但是 Gather/Scatter 功能会导致内存泄露
Java 1.7 才解决 Gather/Scatter 内存泄露的问题
注意版本
选择器 epoll bug
epoll-bug 也可能会导致无效的状态选择和 100% 的CPU利用率
Netty 解决了很多 Java NIO 存在的问题
https://ifeve.com/file-channel/
Java8
Stream API
特点
不是数据结构,
不会保存数据
不会修改原来的数据源,
它会将操作后的数据保存到另外一个对象中
惰性求值
流在中间处理过程中,只是对操作进行了记录,并不会立即执行
需要等到执行终止操作的时候才会进行实际的计算
Stream 操作分类
中间操作
无状态操作
元素的处理不受之前元素的影响
map 类
map
map
mapToInt
mapToLong
mapToDouble
flatmap
flatmap
flatmapToInt
flatmapToLong
flatmapToDouble
filter
peek
unordered
有状态操作
元素的处理受到之前元素的影响
distinct
sorted
limit
skip
结束操作
非短路操作
必须处理所有元素才能得到最终结果
forEach 类
forEach
forEachOrdered
常见类
reduce
collect
max,min
count
toArray
短路操作
遇到某些符合条件的元素就可以得到最终结果
Match 类
anyMatch
allMatch
noneMatch
find 类
findFirst
findAny
图片说明
https://blog.csdn.net/y_k_y/article/details/84633001
Lambda 表达式
特点
函数式编程
允许把函数当做参数来使用
代码很简洁
语法
(paramters) -> expression;
(paramters) -> {statements;}
示例
匿名内部类
内部类写法
//匿名内部类写法 new Thread(new Runnable() { @Override public void run() { System.out.println("内部类写法"); } }).start();
Lambda 写法
//lambda 写法 new Thread(() -> System.out.println("lambda写法")).start();
特征
可选类型声明
不需要声明参数类型,编译器可以统一识别参数值
可选的参数圆括号
一个参数无需定义圆括号,但多个参数需要定义圆括号
可选的大括号
如果主体只包含一个语句,就不需要使用大括号
可选的返回关键字
如果主体只有一个表达式返回值则编译器会自动返回值
:: 关键字
引用静态方法
类::静态方法
引用特定对象的实例方法
对象::实例方法
引用特定类型的任意对象的实例方法
类::方法
引用构造函数
类<构造方法参数>::new
HashSet<Person>::new
设计模式
设计模式的目的<四可一高低>
可重用
代码复用
可维护/可扩展
增加新功能方便
可靠性
增加新功能对原功能没有影响
可读性
方便他人阅读和理解
高内聚、低耦合
设计模式六大原则<李一凯和姐弟>
开闭原则
对扩展开放,对修改关闭
里氏代换原则
任何基类可以出现的地方,子类一定可以出现
依赖倒转原则
依赖于抽象而不依赖于具体
接口隔离原则
使用多个隔离的接口,比使用单个接口要好
迪米特法则(最少知道原则)
一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立
合成复用原则
当要扩展类的功能时,优先考虑使用组合,而不是继承
三大类设计模式
创建型(5种)
单例模式
工厂模式
结构型
代理模式
行为型
计算机基础
计算机网络
计算机网络分类
OSI模型
特点
法律上的国际标准
过于复杂,没有广泛使用
分层(7层)
应用层
表示层
会话层
运输层
网络层
数据链路层
物理层
计算机网络学习(5层)
应用层
两个应用进程之间的通信
运输层
为应用进程之间的通信提供数据传输服务
网络层
为数据传输选择路由
数据链路层
负责两个相邻节点之间的数据传输
物理层
实现相邻节点之间比特流的透明传输
TCP/IP模型
特点
事实上的国际标准
广泛使用
分层(4层)
应用层
运输层
网络层IP
网际接口层
TCP VS UDP
区别
TCP 协议是面向连接的,保证可靠传输
UDP 协议是面向无连接的,尽最大努力交付
TCP 只支持一对一通信
UDP 支持一对一,一对多,多对一和多对多交互通信
TCP 面向字节流
UDP 面向报文
TCP 首部最小20个字节,最大60个字节
UDP首部开销小,仅占8个字节
优缺点
TCP
优点
可靠、稳定
缺点
耗时、耗资源
容易被攻击
UDP
优点
速度快
不容易被攻击
缺点
UDP传输容易丢包
使用场景
TCP
适合于对通信质量有要求的应用
使用TCP的应用
HTTP、FTP等文件传输协议
SMTP、POP等邮件传输协议
QQ在线文件传输
UDP
适合于对网络通信质量要求不高,但是要求网络通讯速度尽可能快的应用
使用UDP的应用
DNS、TFTP等
视频会议、网络直播
TCP流量控制
作用
流量控制就是让发送方的发送速率不要太快,要让接收方来的及接收。
滑动窗口
TCP通过滑动窗口提供了流量控制机制
TCP包状态(四种)
已发送已确认
不在窗口内
已发送未确定
在窗口内
待发送未确认
在窗口内
未发送未确认
不在窗口内
工作流程
接收方提出窗口
受到接收缓冲区的影响
发送方可用窗口
等于提出窗口 - 已发送未确定数据包大小
零窗口
发送方启动零窗口探测
TCP流量控制 VS 拥塞控制
联系
二者都是通过限制发送方发送的数据包个数来解决问题
区别
流量控制是接收端对发送端的控制
拥塞控制是为了缓解发送端到接收端路径上的拥堵问题
超时重传
每发送一个报文段,就启动一个定时器
TCP拥塞控制
什么是拥塞控制?
传输路径上出现拥塞,路由器丢包
解决办法是:当出现拥塞现象时立即减少发送端发送的数据量
发送窗口 = min(拥塞窗口,提出窗口)
慢开始
思路
探测网络拥塞程度,逐渐增大拥塞窗口
乘法增长
当收到单个确认但此确认多个数据报的时候就加相应的数值
慢开始门限
拥塞窗口 < 慢开始门限
使用慢开始算法
拥塞窗口 > 慢开始门限
使用拥塞避免算法
拥塞窗口 = 慢开始门限
都可以
拥塞避免
思路
让拥塞窗口缓慢增长
加法增长
每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1
快重传
如果接收方接收到一个不按顺序的数据段,它会立即给发送方发送一个重复确认
快恢复
如果发送方接收到三个重复确认,它就会立即重传这些丢失的数据段
TCP三次握手
第一次握手
客户端发送请求连接报文段
SYN标志位设置为1,Sequence Number设置为x
进入SYN_SENT状态
第二次握手
服务器接收到SYN报文段之后进行确认
ACK标志位设置为1,Acknowledgment Number为x+1
SYN标志位设置为1,Sequence Number设置为y
进入SYN_RCVD状态
第三次握手
客户端收到服务器的SYN+ACK报文段
将ACK标志位设置为1,Acknowledgment Number为y+1
进入ESTABLISHED状态,客户端连接建立
服务器端接收到该报文段后也进入ESTABLISHED状态,三次握手结束
为什么需要三次握手?为什么两次握手不行?
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
所以主要目的防止server端一直等待,浪费资源。
TCP四次挥手
第一次挥手
第二次挥手
第三次挥手
第四次挥手
为什么需要四次挥手?
从用户输入域名到浏览器显示出页面的工作过程
操作系统
进程和线程的区别是什么?
基本区别
进程是CPU分配资源的最小单元
线程是CPU调度的基本单元
内存资源
进程有独立的内存资源
同一个进程里的多个线程共享进程的内存资源
系统开销
创建、撤销进程时系统开销大
进程切换时需要对当前CPU环境的保存以及对新调度的CPU环境的设置
创建、撤销线程时系统开销小
线程切换时只需对少量的寄存器进行设置
进程间通信的方式有什么?
管道
子主题
子主题
零拷贝技术
Linux 文件 I/O
读写要在内核态缓存和用户态缓存之间互相拷贝
zero-copy
主要的任务就是避免 CPU 做大量的数据拷贝任务
减少文件在内核空间和用户空间的来回拷贝
常见零拷贝技术
mmap
磁盘数据通过DMA被拷贝到内核缓冲区 ->
操作系统把这段内核缓冲区与应用程序共享
这样就不需要把内核缓冲区的内容往用户空间拷贝
存在的问题
map 的文件被另一个进程截断(truncate)时,
write 系统调用会因为访问非法地址而被 SIGBUS 信号终止
sendfile
sendfile 只能将数据从文件传递到套接字上,反之则不行
sendfile 不仅减少了数据拷贝的次数,还减少了上下文切换
I/O控制方式
目标是尽量减少 CPU 对 I/O 控制的干预,把 CPU 从繁杂的 I/O 控制事务中解脱出来,
以便更多地进行数据处理,提高计算机效率和资源的利用率
直接程序控制方式
又称为询问方式
或忙/等待方式
程序直接控制主机和外部设备之间的 I/O 操作,程序必须不停探测 I/O 端口的状态
CPU 与 I/O 串行工作
中断驱动控制方式
CPU 处理自己的事情,收到中断请求之后,转过去处理中断,
结束之后回到原来的位置继续处理自己的事情
CPU 与 I/O 并行工作(宏观上)
以字符为单位交换数据
直接存储器访问(DMA)控制方式
在 I/O 设备和主存之间开辟直接数据交换通道,
可以成批地交换数据,而不必让 CPU 干预
以数据块为单位交换数据
并行
通道控制方式
通道是一种特殊的处理机,它本身没有内存,与 CPU 共享系统的主存
通道只是在 I/O 操作的起始和结束时向 CPU 发出 I/O 中断申请
与 DMA 不同的是一个通道可以控制多个 I/O 设备
并行
死锁
什么是死锁?
多个并发进程因争夺系统资源而产生相互等待的现象
死锁产生的原因?
竞争资源
进程间推进顺序不当
死锁产生的必要条件?
互斥条件
某种资源一次只允许一个进程访问
互斥条件是无法破坏的
请求和保持条件
当进程因请求资源而阻塞时,对已获得的资源保持不放
不可剥夺条件
进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
循环等待条件
存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源
避免死锁的方法
预防死锁
破坏产生死锁的四个必要条件,确保系统永远不会进入死锁状态
资源一次性分配
一次性分配所有资源,只要有一个资源得不到分配,也不给这个进程分配其他的资源
破坏请求和保持条件
可剥夺资源
当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,
它必须释放已经保持的所有资源
待以后需要使用的时候再重新申请
破坏不可剥夺条件
资源有序分配
系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反
破坏循环等待条件
避免死锁
指进程在每次申请资源时判断这些操作是否安全
银行家算法
检测死锁
判断系统是否处于死锁状态
资源分配图简化
解除死锁
资源剥夺法
剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除
进程撤销法(两种)
撤销陷入死锁的所有进程,解除死锁,继续运行
逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除
数据库
存储引擎
MySQL 常见存储引擎
MyISAM
InnoDB
MyISAM 与 InnoDB 的区别?
MyISAM 不支持事务,InnoDB 支持事务
MyISAM 不支持行级锁(只支持表级锁),InnoDB 支持行级锁
MyISAM 不支持外键,InnoDB 支持外键
MyISAM 存储记录行数,InnoDB 不存储记录行数
MyISAM 支持 FULLTEXT 类型的索引,InnoDB 不支持 FULLTEXT 类型的索引
InnoDB 是 MySQL5.5 之后的默认引擎
<三否两是一默认>
MyISAM 与 InnoDB 如何选择?
MyISAM 适合查询以及插入为主的应用,
InnoDB 适合频繁修改以及涉及到安全性较高的应用
如果要使用事务功能、外键、行级锁,则必须要选择 INNODB 引擎
目前一般都是选用 innodb
事务
概念
一组被事务管理的操作,要么同时成功,要么同时失败
事务的基本操作
开启事务:start transaction
回滚:rollback
提交事务:commit
MySQL 数据库中事务默认自动提交和回滚
事务的四大特征(ACID)
原子性
不可分割,要么同时成功,要么同时失败
持久性
事务提交或回滚后,数据库会持久化的保存数据
隔离性
多个事务之间相互独立
一致性
事务操作前后数据总量不变
事务的隔离级别
多个事务操作同一批数据,会引发一些问题,需要设置不同的隔离级别来解决这些问题
问题
脏读
一个事务读到另一个事务中没有提交的数据
不可重复读(虚读)
同一个事务中两次读取的数据不一样
幻读
一个事务操作数据表中所有的记录,另一个事务添加了一条数据,
则操作第一个事务的用户发现表中还存在没有修改的行,就好像发生了幻觉一样
隔离级别
读未提交
可能会产生:脏读,不可重复读,幻读
读已提交
可能会产生:不可重复读,幻读
可重复读
可能会产生:幻读
MySQL 默认级别
可串行化
没有问题,性能低
隔离级别从小到达,安全性越来越高,效率越来越低
查询
查询语句书写先后顺序
select
from
where
groupby
having
orderby
select 和 from 必须,其他可选
查询语句执行先后顺序
from
表示从哪个数据表检索数据
where
表示过滤条件
group by
对数据进行分组
having
对分组后的数据进行过滤
select
表示查看结果集中的哪些列,或列的计算结果
order by
表示按照什么样的顺序来查看返回的数据
范式
第一范式
列不可分
第二范式
消除了非主属性对码的部分依赖
第三范式
消除了非主属性对码的传递依赖
索引
分类
索引列数
单列索引
一个索引只包含单个列
组合索引(复合索引)
一个索引包含多个列
是否显式创建
直接创建索引
CREATE INDEX indexName ON table_name (column_name)
间接创建索引
定义主键约束或唯一性键约束,可以间接创建索引
列数据是否唯一
普通索引
唯一索引
保证在索引列的数据是唯一的
聚簇索引
物理索引,与基表的物理顺序相同,数据值的顺序总是按照顺序排列
非聚簇索引
https://www.cnblogs.com/jiawen010/p/11805241.html
目的
加快查找速度
缺点
创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加
索引需要占物理空间
对表中数据进行增删改时,索引也要动态的维护,降低了更新表的速度
<时间空间更新>
MySQL 高并发环境的解决方案
SQL 语句优化
索引优化
分库分表
读写分离
MySQL 存储引擎选择
读多用 MyISAM
写多用 InnoDB
结构化、半结构化、非结构化
结构化
数据结构字段含义确定,清晰
关系型数据库中的表结构
半结构化
具有一定结构,但语义不够确定
日志、XML、JSON
非结构化
WORD、PDF、PPT、图片、视频
数据结构
排序算法
内部排序
交换排序
冒泡排序
快速排序
插入排序
简单插入排序
希尔排序
选择排序
简单选择排序
堆排序
归并排序
外部排序
二路归并排序
多路归并排序
大数据
分布式系统原理
概念
模型
节点
一个节点往往对应一个进程
异常
机器宕机
大型集群中每日宕机发生的概率为千分之一左右
一般需要人工介入重启机器
网络异常
网络分化
存储数据丢失
从其他节点读取、恢复存储的状态
异常处理原则
任何在设计阶段考虑到的异常情况一定会在系统实际运行中发生
但在系统实际运行遇到的异常却很有可能在设计时未能考虑
所以,除非需求指标允许,在系统设计时不能放过任何异常情况
分布式三态
A 向 B 发送消息,B 返回给 A 一个确认
成功
失败
超时(未知)
副本
指在分布式系统中为数据或服务提供的冗余
副本一致性(5种)
强一致性
任何时刻任何用户(节点)都可以读到最近一次成功更新的副本数据
单调一致性
任何时刻,任何用户一旦读到某个数据在某次更新后的值,
这个用户不会再读到比这个值更旧的值
会话一致性
任何用户在某一次会话内一旦读到某个数据在某次更新后的值,
这个用户在这次会话过程中不会再读到比这个值更旧的值
最终一致性
最终一致性要求一旦更新成功,各个副本上的数据最终将达到完全一致的状态,
但达到完全一致状态所需要的时间不能保障
弱一致性
一旦某个更新成功,用户无法在一个确定时间内读到这次更新的值,
且即使在某个副本上读到了新的值,也不能保证在其他副本上可以读到新的值
强一致性很难实现,弱一致性很难使用
<单调、会话、最终>
衡量分布式系统的指标
性能
吞吐量
响应时间
系统并发能力
QPS(query per second)
可用性
在面对各种异常时系统可以正确提供服务的能力
可用性=系统无故障运行时间/(系统无故障运行时间+系统故障维护时间)
可扩展性
通过扩展集群机器规模提高系统性能、存储容量等
线性扩展性
系统的某一指标可以随着集群中的机器数量线性增长
一致性
副本一致性
CAP 理论
Consistency
Availiablity
Tolerance to the partition of network(Partition tolerance)
无法设计出一种分布式协议,同时具备CAP三种属性
该协议下的副本始终是强一致性的
服务始终可用
协议可以容忍任何网络分区异常
大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区(partition)
CAP 理论中,P 无法避免,C 和 A 无法同时做到,
因此只能尽量地在 C 和 A 之间寻求平衡
意义
不要去妄图设计一种对 CAP 三大属性都完全拥有的完美系统
热力学第二定律、永动机
副本一致性协议
Lease机制
Quorum 机制
CAP
有一定的C,有较好的A,也有较好的P
比较平衡
WARO
Write All Read One
所有副本都写入成功才算更新成功
问题
写操作很脆弱
只要有一个副本更新失败,此次写操作就视为失败了
读操作很简单
读取任一副本都可以
Quorum 原理
Quorum 在读和写之间做一个折中
N个副本
写操作
在 W 个副本中更新成功之后,才认为此次更新成功
读操作
至少需要读取 R 个副本才能读到此次更新的数据
要求 W 和 R 有重叠
W + R > N
一般 W + R = N + 1
Quorum 机制分析
Quorum无法保证强一致性
如何读取最新数据?
在已经知道最近成功提交的数据版本号的前提下,
最多读R个副本就可以读到最新的数据了
如何确定最高版本号的数据是一个成功提交的数据?
继续读取其他副本,直到读到最高版本号副本出现了W次
应用
基于 Quorum 机制选择 primary
中心节点(服务器)读取 R 个副本,
选择 R 个副本中版本号最高的副本作为新的 primary
新选出的 primary 不能立即提供服务,
还需要与至少 W 个副本完成同步后,才能提供服务
应用案例
HDFS 高可用中的 Quorum
Zookeeper 的 Quorum
Zookeeper 集群要求
集群的节点数目必须是奇数
比如集群3个节点,Quorums = 2,也就是说集群可以容忍1个节点失效,这时候还可以选举出1个leader,集群还可用
节点数配置成奇数的集群的容忍度更高
比如集群4个节点,Quorums = 3,相当于集群的容忍度还是1,如果2个节点失效,整个集群还是无效的
集群中必须超过半数节点(Majority)可用,整个集群才能对外可用
防止脑裂
脑裂(Split Brain)
如果两个控制器之间的网络通路出现了问题,此时两个控制器其实都是正常工作的,
但是两者都检测不到对方的存在,所以两者都会尝试接管所有总线,这就是脑裂。
如果网络又恢复了的话,会出现两个Brain的情况,整个集群的行为不一致了
两阶段提交协议
Paxos 协议
Flink
实时大数据处理架构
Lambda架构
Storm创始人提出
组件
Batch Layer
Speed Layer
Serving Layer
不足
一个业务维护两套代码
Kappa架构
LinkedIn前首席工程师杰伊·克雷普斯提出
组件
Real-Time Layer
Serving Layer
改进
在Lambda 的基础上进行了优化,删除了 Batch Layer 的架构
Kappa架构中只有流计算
核心
避免维护batch和speed层两份独立的代码
将实时和离线代码统一起来
应用场景
事件驱动型应用
定义
事件驱动型应用是一类具有状态的应用
该应用会根据事件流中的事件触发计算、更新状态或进行外部操作
场景
(社交)微博关注动作
关注者的关注数增加
被关注者的粉丝数增加
(电商实时推荐)浏览商品动作
猜你喜欢
(金融反欺诈)异常交易动作
给用户发送短信提醒
限制交易金额
(工业能耗控制)钢铁冶炼的能耗控制
指标
质量
能耗
采集设备温度/炉压/含氧量等指标,进行计算,进而自动控制设备的煤烟阀开度/喷氧阀开度,达到高质量低能耗的企业要求
业务本质
每条数据(事件)触发变化
数据分析型应用
定义
数据分析型应用是从原始数据中提取出有价值的信息和指标
业务本质
对数据集进行操作,进行操作
数据管道型应用(ETL)
ETL
Extract-Transform-Load
从数据源提取/转换/加载数据到目标端的过程
在数据仓库领域应用广泛
为什么需要ETL?
企业数据各自独立
客户数据
销售数据
采购数据
消除数据孤岛,实现企业数据的全局观
ETL的特点
多数据源
数据库
文件
音视频
T是最耗时的部分
ETL要解决的问题
噪音
数据完整性
字段缺失
格式不统一
比如日期格式
错误数据
重复数据
ETL同步方式
增量同步
全量同步
多针对文件系统,无法识别增量数据
实时同步
触发器机制
架构
API 层次
组件
Client
Flink Master
Dispatcher
Jobmanager
ResourceManager
TaskManager
https://zhuanlan.zhihu.com/p/197438282?utm_source=wechat_session
任务执行
Transformation 算子
作用
一个或多个 DataStream 转换为新的 DataStream
分类
基本转换算子
DataStream -> DataStream
map
输入一个元素,输出一个元素
flatMap
输入一个元素,输出零个或多个元素
filter
过滤筛选,将符合判断条件的结果集输出
键控流转换算子
DataStream -> KeyedStream
keyBy
将 DataStream 根据指定的 Key 进行分区
根据key的Hash值进行分区
聚合算子(aggregations)
KeyedStream -> DataStream,
WindowedStream -> DataStream,
AllWindowedStream -> DataStream
sum
min,max
fold
子主题
多流转换算子
unoin
DataStream -> DataStream
可以将多个流合并到一个流中
要求多个数据流的数据类型必须相同
connect
DataStream -> ConnectedStreams
和union类似,但只能连接两个流,两个流的数据类型可以不同,
会对两个流中的数据应用不同的处理方法
split
DataStream -> SplitStream
根据规则把一个数据流切分成多个流
窗口转换算子
KeyedStream -> WindowedStream,
DataStream -> WindowedStream
window
按照规则对 KeyedStream 进行分组
windowAll
对常规数据流(DataStream)进行分组
因为是在非分区数据流上运行,所以是非并行数据转换
并行度始终为1
reduce
KeyedStream -> DataStream,
WindowedStream -> DataStream,
AllWindowedStream -> DataStream
归并操作,返回单个的结果值
常用聚合操作例如 min、max 等都可使用 reduce 方法实现
Join
task
Slot
四层执行图
StreamGraph
Stream API 编写的代码生成的最初的图
用来表示程序的拓扑结构
JobGraph
StreamGraph 经过优化后生成了 JobGraph,
JobGraph 是提交给 JobManager 的数据结构。
优化为:将多个符合条件的节点 chain 在一起作为一个节点
从而减少数据 Shuffle
ExecutionGraph
JobManager 根据 JobGraph 生成 ExecutionGraph
ExecutionGraph 是 JobGraph 的并行化版本,是调度层最核心的数据结构
物理执行图
在各个 TaskManager 上部署 Task 后形成的“图”,并不是一个具体的数据结构
图片说明
分区方式
Flink 基础知识
Flink 分区策略
继承关系
StreamPartitioner 是所有所有流分区器的基类
StreamPartitioner 实现了 ChannelSelector 接口
ChannelSelector 接口
作用
决定将记录写入到哪个 Channel
主要方法
void setup(int numberOfChannels)
初始化输出 Channel 的数量
int selectChannel(T record)
根据当前记录以及 Channel 总数,决定将记录写入下游哪个 Channel
boolean isBroadcast()
是否是广播模式,决定了是否将记录写入下游所有 Channel
八大分区策略
GlobalPartitioner
将记录输出到下游 Operator 的第一个实例
ShufflePartitioner
将记录随机输出到下游Operator的每个实例
RebalancePartitioner
将记录以循环的方式输出到下游Operator的每个实例
RescalePartitioner
上游2并行度对下游4并行度,则一对二;上游4并行度对下游2并行度,则二对一
BroadcastPartitioner
广播分区将上游数据集输出到下游 Operator 的每个实例中
ForwardPartitioner
将记录输出到本地下游 Operator 实例
ForwardPartitioner 要求上下游算子的并行度一样,上下游同属一个 SubTasks
只有 ForwardPartitioner 的上下游算子可以 chain 到一起
KeyGroupStreamPartitioner(Hash分区)
按 key 的 Hash 值输出到下游 Operator 实例
CustomPartitionerWrapper
自定义分区器
Flink 并行度
概念
Flink 中的任务被分成多个并行任务来执行,其中每个并行的实例处理一部分数据,
这些并行实例的数量被称为并行度
设置(优先级由高到低)
操作算子层面(Operator Level)
执行环境层面(Execution Environment Level)
客户端层面(Client Level)
系统层面(System Level)(配置文件层面)
Flink 中的并行度是由什么来决定的?
Flink 中的并行度由最大的算子并行度决定
Flink 中的 Slot 和 Parallelism 有什么区别?
Slot 是 表示集群的并发执行能力
Parallelism 是实际使用的并发能力
Flink 重启策略
分类
固定延迟重启(Fixed Delay Restart)
参数
restart-strategy.fixed-delay.attempts
作业宣告失败之前 Flink 重试执行的最大次数
restart-strategy.fixed-delay.delay
连续两次重启尝试之间的延时
故障率重启(Failure Rate Restart)
故障率重启策略在 Job 失败后重启,但是超过失败率后,Job 会最终被认定失败
在两个连续的重启尝试之间,重启策略会等待一个固定的时间
参数
restart-strategy.failure-rate.max-failures-per-interval
单个时间间隔内允许的最大重启次数
restart-strategy.failure-rate.failure-rate-interval
测量故障率的时间间隔
restart-strategy.failure-rate.delay
连续两次重启尝试之间的延时
没有重启策略(No Restart)
作业直接失败,不尝试重启
是否启用了 Checkpoint
是
默认使用固定延迟重启策略
否
使用无重启策略
配置
可以在 flink-conf.yaml 中配置,表示全局配置
也可以在代码中动态指定,会覆盖全局配置
流处理概念
Bounded & Unbounded Data
时间语义
Event Time
Ingestion Time
Processing Time
Window
为什么需要 Window?
Flink是天然支持无限流数据处理的分布式计算框架
在Flink中可以通过Window将无限流切分成有限流
Window是处理有限流的核心组件
Window 分类
时间驱动(Time Window)
Tumbling Window
Sliding Window
Session Window (gap)
如果到来的元素之间的时间间隔均不大于gap,这些元素会被合并到一个 Window 中
如果两个元素的时间间隔大于gap,则后面的元素会进入一个新的window中
<不大于gap,大于gap>
事件驱动(Count Window)
Tumbling Window
Sliding Window
Window 三大核心组件
WindowAssigner
负责将传入的元素分配给一个或多个窗口
Trigger
每个WindowAssigner都自带一个默认的Trigger
用来判断一个窗口是否需要被触发
方法<四on一销毁>
onElement()
每次往window增加一个元素的时候都触发
onEventTime()
当event-time timer被触发的时候会调用
onProcessingTimer()
当processing-time timer被触发的时候会调用
onMerge()
对两个trigger的state进行merge操作
clear()
window销毁的时候被调用
TriggerResult
onElement()、onEventTime()、onProcessingTimer() 会返回一个 TriggerResult
结果分类
CONTINUE
不做任何事
FIRE
触发窗口
PURGE
清空整个window的元素并销毁窗口
FIRE_AND_PURGE
触发窗口,然后销毁窗口
Evictor
作用
剔除掉不应该在此时间窗口内的数据
或者用户想要剔除的其它数据
分类
evictorBefore
后续处理逻辑之前调用evictorBefore
evictorAfter
后续处理逻辑之后调用evictorAfter
Window 的生命周期
创建
当属于该窗口的第一个元素到达时就会创建该窗口
销毁
当时间超过窗口的结束时间戳 + 用户指定的延迟时延时(allowedLateness(<time>))
窗口将被移除
Watermark
乱序问题
事件产生的时间顺序和事件被处理的顺序不一致
Flink 中处理乱序的三种方法
window + trigger + watermark
全局乱序处理
allowedLateness
局部乱序处理(对窗口计算的修正)
allowedLateness会再次触发窗口的计算,而之前触发的数据会buffer起来
watermark超过end-of-window + allowedLateness()的时间,窗口才会被真正销毁
SideOutput
侧输出流针对迟到很久的数据进行额外的处理
不会再次触发原窗口的计算,需要用户另外指定处理逻辑
Watermark 是什么?
Watermark是
一个时间戳
它标识了小于这个时间戳的事件已经都到达了
Watermark和StreamRecord一样继承了StreamElement,
和普通数据一起在算子之间传递
目的是
通过超时等待来解决数据乱序问题
本质是
对数据延迟和正确性的平衡
watermark设置太大,收到结果的速度就会很慢,处理延迟就会很高
如果太小,则计算的正确性就不能保证
<是,继承,目的是,本质是>
Watermark的设置
Watermark的特性
必须单调递增
单输入取最大
最大watermark的出现表明了此watermark之前的数据都已经出现
自然也包括更小的watermark
多输入取最小
木桶效应
代表当前事件时间
如果程序收到一个Long.MAX_VALUE数值的watermark,
就表示对应的那一条流不会再有数据发过来了
Watermark的传递
Watermark通过广播的方式传播到下游
对于一个算子,它会维护所有分区发来的watermark,
然后在最小的watermark更新之后,把更新的值作为当前事件时间时钟并广播到下游
Flink Connector
File System Connector
JDBC Connector
Kafka Connector
Elasticsearch Connector
HBase Connector
Hive Connector
状态管理
状态后端
MemoryStateBackend
FsStateBackend
RocksDBStateBackend
一般在生产情况下使用
状态
状态管理方式
Managed State
Flink Runtime 管理
自动存储,自动恢复
Raw State
用户自己管理
不常用
子主题
Keyed State(LVM)
特点
只能用在 KeyedStream 上的算子中
每个 Key 对应一个 State
并发改变,State 随着 Key 在实例间迁移
使用
通过 getRuntimeContext() 访问
Rich Function
分类/继承关系
State
Value State
存储数据类型
单个值
Map State
存储数据类型
Map
AppendingState
MergingState
List State
存储数据类型
List
ReducingState
存储数据类型
单个值
求平均值
只存和,不存计数
AggregatingState
存储数据类型
单个值
求平均值
只存和,也存计数
图片说明
<LVRAM>
Operator State<BLU(E)>
特点
可以用于所有算子
常用于 Source
一个 Operator 实例对应一个 State
并发改变,有多种重新分配方式可选
均匀分配
合并后每个得到全量
使用
实现 CheckpointedFunction 或 ListCheckpointed 接口
List State
Union List State
Broadcast State
容错
Checkpointing
Flink Checkpoint 机制
主要工作是持久化备份 state ,做一个全局的分布式快照
4步
发起者Checkpoint Coordinator
JobManager中的 Checkpoint Coordinator 是整个 Checkpoint 的发起者
1. trigger Checkpoint
Checkpoint Coordinator 向所有的Source节点 trigger Checkpoint
Source节点向下游广播barrier,
并将自己的状态(异步)写入到持久化存储(Persistent Storage)中
2. barrier对齐
对于下游task,只有收到所有input的barrier后
也就是完成barrier对齐之后
才会执行相应的CheckPoint
也就是将自己的状态写入到持久化存储中
3. 汇报 state handle
当task完成state备份后,
会将备份数据的地址(state handle)通知给Checkpoint Coordinator
4. 全局 Checkpoint 完成
当 Checkpoint Coordinator 收集齐所有 task 的 state handle 之后
就认为这一次Checkpoint全局完成了,
就会向持久化存储中再备份一个 Checkpoint meta 文件
<tb汇完>
Barrier 对齐
begin aligning
当第一个 barrier 到达时开始对齐
aligning
checkpoinging
Exactly Once
Checkpoint 保证了内部算子的 Exactly Once
Checkpoint + 两阶段提交协议 共同保证了端到端的 Exactly-Once
Asynchronous State Snapshots
Chandy-Lamport算法
Chandy-Lamport 算法是一种分布式快照(Distributed Snapshot)算法
分布式快照算法
用来在缺乏类似全局时钟或者全局时钟不可靠的分布式系统中来确定一种全局状态
Flink 使用的是 Chandy-Lamport 的改进算法
核心思想是在 input source 端插入 barrier
来替代 Chandy-Lamport 算法中的 Marker
通过控制 barrier 的同步来实现 snapshot 的备份和 exactly-once 语义
Global Snapshot
可以理解为 Global State
将分布式系统简化成
有限个进程(节点)
和进程之间的 channel 组成(边)
input channel
output channel
的有向图
局部快照
每个进程的 local state
它的 input channel 中有序的 message
将所有的进程的局部快照合并起来就可以得到全局快照
算法步骤
Initiating a snapshot
创建快照
可以由系统中的任意一个进程发起
Propagating a snapshot
传播创建快照事件
系统中其他进程开始逐个创建 snapshot
Terminating a snapshot
算法结束条件
举例说明
P1 P2 两个进程各有三个变量
P1 发起全局 Snapshot,P1 先记录本身的进程状态,然后向 P2 发送 marker 信息
在 marker 信息到达 P2 之前,P2 向 P1 发送 message: M
P2 收到 P1 发送过来的 marker 信息之后,记录自己的状态
P1 收到 P2 之前发送过来的 message: M,P1 需要记录 message M
增量检查点
不用对齐的 Checkpoint
Flink 1.11 Unaligned Checkpoint
子主题
Savepoints
Flink 内存管理
JVM分代内存管理(Hotspot)
GC策略
Young Gen
Eden
Survivor
From
To
Old Gen
从新生代存活的对象、大对象
Permanent Gen
类定义、字节码、常量
GC类型
Minor GC
触发条件
新生代区域满
回收区域
新生代
Major GC(Full GC)
触发条件
老年代或持久代区域满
回收区域
新生代/老年代/持久代
JVM存在的问题
Java对象存储密度低
一个只包含boolean属性的对象占16个字节
Java GC触发频繁
处理大量数据时产生大量对象
OOM问题影响稳定性
JVM中所有对象大小超过分配给JVM的内存大小时
Flink改进
将对象序列化到预分配的MemorySegment(32KB)上
Flink堆内存划分
Network Buffers
用于数据的网络传输
2048个32KB大小的buffer(MemorySegment)
taskmanager.network.numberOfBuffers设置
Memory Manager Pool
由众多MemorySegment组成的超大集合
Flink 中的算法(sort/shuffle/join)会向这个内存池申请 MemorySegment
占了堆内存的 70%
Remaining (Free) Heap
留给用户代码以及 TaskManager 的数据结构使用
Flink序列化框架
序列化/反序列化
为了能正确反序列化,序列化时存储辅助信息
Java序列化的问题
序列化时记录了过多的类信息,辅助信息过大
Flink实现了自己的序列化框架
数据流通常是一种类型,所以可以只保存一份对象Schema信息
TypeSerializer
针对 Flink 支持的六大类数据类型,Flink都可以自动生成对应的 TypeSerializer
能非常高效的对数据集进行序列化和反序列化
使用堆外内存
Java支持任意类型的Java和Scala类型
BasicTypeInfo
Java基本类型或String类型
BasicArrayTypeInfo
Java基本类型数组或 String 数组
CaseClassTypeInfo
Scala CaseClass(包括 Scala tuples)
PojoTypeInfo
任意的 POJO (Java or Scala)
TupleTypeInfo
任意 Flink Tuple 类型(Tuple1 到 Tuple25)
WriteableTypeInfo
任意 Hadoop Writeable 接口的实现类
GenericTypeInfo
任意其他类型
Flink 网络流控与反压
为什么需要网络流控?
Producer 发送速率大于 Consumer 接收速率
问题
接收 buffer 有界
consumer 丢弃新到的数据
接收 buffer 无界
buffer 持续扩张,耗尽 consumer 内存
实现
静态限速
问题
通常无法事先预估 consumer 端的最大速率
consumer 承受能力通常会动态波动
动态反馈/自动反压
正反馈
接收速率大于发送速率时发生
负反馈
接收速率小于发送速率时发生
反压
狭义
负反馈
真正的反压机制
正负反馈都包含
什么是反压?
产生
短时负载高峰导致系统接收数据的速率远高于它处理数据的速率
反压机制
系统能够自动检测到被阻塞的 Operator
然后系统自适应地降低源头或者上游的发送速率
反压产生的具体场景?
大促或秒杀活动导致流量陡增
垃圾回收停顿(Stop-The-World)可能会导致流入的数据快速堆积
反压问题不解决会怎么样?
会导致资源耗尽甚至系统崩溃
Flink 网络传输三级 Buffer
Flink Network Buffer
Netty ChannelOutboundBuffer / ChannelInboundBuffer
Socket Buffer
图片说明
Flink 反压机制
Flink V1.5 之前
基于 TCP 滑动窗口机制实现反压
由于 TCP 天然具备 feedback 流控机制,所以 Flink 基于 TCP 实现了反压的效果
Flink 没有专门的反压机制
反压传播两个阶段
跨 Taskmanager
反压如何从 IC(InputChannel) 传播到 RS(ResultSubPartition)
反压传播路径(前5点)
InputChannel 满了
InputChannel LocalBufferPool
LocalBufferPool 满了,向 NetworkBufferPool 申请空间,
但是能够申请到的空间是有限的
Flink NetworkBufferPool
NetworkBufferPool 未必用完了
Netty
Netty buffer 满了(Netty buffer 其实是无界的,通过高水位控制)
disable netty autoRead
Netty 不再从 Socket 读取数据
Socket buffer 变满
向发送端 Socket 发送 ACK window = 0
发送端 Socket 收到 ACK window = 0 之后会停止数据发送
发送方 Socket buffer 逐渐变满
发送方 Netty 逐渐变满,Netty 不可写
发送方 ResultSubPartition 不可写
最终,发送方算子不可写
图片说明
Taskmanager 内部
反压如何从 RS 传播到 IC
反压传播路径
图片说明
反压路径
Flink 应用层的 LocalBufferPool,NetWorkBufferPool
到 Netty 的 Buffer
到 底层Socket的Buffer
TCP-Based 反压的弊端
单个 task 导致的反压,会阻断整个 TM-TM 之间的 socket,导致 barrier 也发不出去
反压传播链路过长,导致反压生效时间过长
Flink V1.5 之后
Credit-Based反压机制
在 Flink 层面实现了类似 TCP 滑动窗口的机制来实现流量控制
backlog
credit 表示接收端允许发送的数据量
解决的问题
压力不会传导到底层 Socket,也就不会阻塞 Socket
传播链路减少了两层,反压响应时间更快了
反压不一定会触发
外部数据存储到 Sink 的反压源头是否会触发?
Kafka 可以,ES 不可以
所以反压不是万能的,静态限流也非常重要
如何查看 Flink 背压?
Flink WebUI中有 Back Pressure 选项
可以查看各个算子的背压状况
也可以查看某个算子各个分区的背压状况
Sampling in progress 参数
表示 JobManager 正在对运行的任务触发堆栈跟踪采样
背压状态
OK
0 <= Ratio <= 0.10
LOW
0.10 <Ratio <= 0.5
HIGH
0.5 <Ratio <= 1
常用类
ParameterTool
org.apache.flink.api.java.utils.ParameterTool
这个类提供了从不同来源读取和解析程序参数的简单实用方法
重要方法
fromArgs
从命令行参数解析程序参数
fromPropertiesFile
从配置文件解析程序参数
Spark
Hadoop
Hadoop
Hadoop 高可用
1.x 版本
只有一个 Namenode
所有元数据由惟一的 Namenode 负责管理
2.x 版本
引入双NameNode架构
同时借助共享存储系统来进行元数据的同步
元数据存储形式
内存镜像
Namenode 启动时,会加载磁盘镜像到内存中以进行元数据的管理,
存储在 NameNode 内存
磁盘镜像(FSImage)
磁盘镜像是某一时刻 HDFS 的元数据信息的快照
包含所有相关 Datanode 节点文件块映射关系和命名空间(Namespace)信息,
存储在 NameNode 本地文件系统
状态
checkpoint
表示合并中的fsimage
finalized
表示已经持久化磁盘的文件
日志(EditLog)
记录 Client 发起的每一次操作信息
用于定期和磁盘镜像合并成最新镜像,保证 NameNode 元数据信息的完整,
存储在 NameNode 本地和共享存储系统(QJM)中。
状态
inprocess
表示正在写的日志文件
finalized
表示已经写完的日志文件
HDFS
HDFS读写流程
block、packet、chunk
block
block是文件分块的基本单位
分块大小
Hadoop 1.x默认的block大小是64MB
Hadoop 2.x默认的block大小是128MB
如果文件大小小于分块大小,则按实际文件大小分配,而非一个块大小
packet
packet是数据传输的基本单位
传输数据组件
Client端和DataNode
DataNode的PipLine之间
默认大小为64KB
chunk
chunk是进行数据校验的基本单位
默认大小为512Byte
再加4Byte的校验位
三层buffer
写流程(6)<CNCNCC>
Client
向NameNode发出写文件请求
NameNode
检查
文件名是否已经存在
用户是否有写入权限
若检查通过
则返回输出流对象
Client
按照 BLOCKSIZE 对文件分块
按照分块逐个向 NameNode 提出写入申请
两个参数
BLOCKSIZE
REPLICATION FACTOR
NameNode
对可写入DataNode进行排序
返回前 REPLICATION FACTOR 个 DataNode 组成的列表
Client
Client
将NameNode返回的DataNode列表和块数据一同发送给列表中的第一个DataNode
以packet为单位进行发送
DataNode
每向第一个 DataNode 写入一个packet,这个packet就会在 DataNode 组成的pipeline 中进行复制
DataNode块数据写入完成之后会给NameNode发送完成信号
Client/NameNode
整个文件写入完成之后Client会请求关闭输出流
NameNode会持久化元数据
读流程(4)<CNCC>
Client
向NameNode提出读文件请求
NameNode
发送给Client
此文件的所有数据块组成的列表
每个数据块对应的DataNode列表
Client
逐个下载数据块
Client
关闭输入流
三类故障
节点故障
NameNode
有备份,则启动备份
有备份,则集群挂掉
DataNode
故障检测
DataNode
每3秒向NameNode发送一个心跳信号
NameNode
十分钟内没有收到心跳信号,则任务DataNode挂掉了
DataNode可能只是出现了网络故障
网络故障
每当发送数据时,接受方会回复应答信号
多次发送尝试后还是没有收到应答信号,则认为发生网络故障
数据故障
DataNode
DataNode定时给NameNode发送数据块报告
在发送数据块报告前会检测校验和是否正常
DataNode不会发送损坏的数据块
NameNode
NameNode根据数据块报告就知道哪个数据块损坏了
MapReduce
map 数量和 reduce 数量
map 数量
由输入切片的数量决定
128MB为一个切片,有多少切片就有多少个 map task
reduce 数量
由用户配置
Yarn
Hive
Hive 是什么?
由 Facebook 开源的用于解决海量结构化日志的数据统计
Hive 是基于 Hadoop 的数据仓库工具
可以将结构化的数据文件映射为一张表,并提供类 SQL 查询功能
本质是将 HQL 转化成 MapReduce 程序
图标是一个象头蜜蜂
原理
Hive 处理的数据存储在 HDFS
Hive 分析数据底层的默认实现是 MapReduce
执行程序运行在 Yarn 上
架构
元数据
元数据中记录着表对应文件的 Path
元数据默认存放在 Derby 数据库中
只能允许一个会话连接
一般使用 MySQL 作为元数据库
为了支持多用户多会话
Hive Client
用户接口
CLI (Hive shell)
JDBC/ODBC(Java 访问 Hive)
WebUI(浏览器访问 Hive)
解析器
检查基本语法
编译期
将任务翻译成 MR
优化器
执行器
MapReduce
HDFS
特点
与数据库对比
Hive 处理的数据量大
Hive 读多写少(数据仓库)
Hive 不能建立索引
优缺点
优点
简单
适用于处理大数据且对实时性要求不高的场景
缺点
无法表达迭代算法
效率低,调优困难
使用
加载数据
insert
不常用
从文件系统加载数据
load
本地加载
load data local inpath '路径' into table student;
HDFS 加载
load data inpath '路径' into table student;
put
hadoop fs -put stu1.txt /user/hive/warehouse/stu
HBase
HBase 是什么?
HBase 是 Hadoop Database 的简称
是建立在 HDFS 之上的面向列的分布式数据库
提供快速随机访问海量结构化数据
HDFS 为 HBase 提供可靠的底层数据存储服务
MapReduce 为 HBase 提供高性能的计算能力
Zookeeper 为 HBase 提供稳定服务和 Failover 机制
HBase 是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案
HBase 和 HDFS
HDFS
适用于存储大容量文件的分布式文件系统
不支持快速单独记录查找
提供了高延迟批量处理
提供的数据只能够顺序访问
HBASE
HBase 是建立在 HDFS 之上的数据库
提供在较大的表快速查找
HBase 存储机制
HBase 是一个面向列的数据库
表是行的集合
行是列族的集合
列族是列的集合
列是键值对的集合
Hbase 架构
依赖
依赖 HDFS 做底层的数据存储
依赖 MapReduce 做数据计算
依赖 ZooKeeper 做服务协调和 Failover 机制
Client
HBase 两张特殊表
.META.
记录用户所有表拆分出来的的 Region 映射信息
一个 .META. 可以包含多个 Regoin
-ROOT-
记录了 .META. 表的 Region 信息
一个 -ROOT- 只有一个 Region
数据访问过程
Client 访问用户数据前需要首先访问 ZooKeeper,
找到 -ROOT- 表的 Region 所在的位置
然后访问 -ROOT- 表,
接着访问 .META. 表
最后才能找到用户数据的位置去访问
寻址
HMaster
主服务器
功能
为 RegionServer 分配 Region
负责 RegionServer 的负载均衡
发现失效的 RegionServer 并重新分配其上的 Region
HDFS 上的垃圾文件(HBase)回收
处理 Schema 更新请求
表的创建,删除,修改,列簇的增加等等
HRegionServer
区域服务器
功能
负责维护 Master 分配给它的 Region,处理对这些 Region 的 IO 请求
负责 Split 在运行过程中变得过大的 Region,负责 Compact 操作
图片说明
参考
https://www.cnblogs.com/frankdeng/p/9310278.html
名词概念
Rowkey
和 MySQL 中的主键概念是完全一样的
Hbase 使用 Rowkey 来唯一的区分某一行的数据
HBase 会对表中的数据按照 rowkey 排序
Hbase 只支持3种查询方式
基于 Rowkey 的单行查询
基于 Rowkey 的范围扫描
全表扫描
Column
可以理解成 MySQL 的列
ColumnFamily
Hbase 通过列族划分数据的存储
列族下面可以包含任意多的列
在表创建的时候就必须指定列族
TimeStamp
时间戳是实现 Hbase 多版本的关键
使用不同的 TimeStamp 来标识相同 Rowkey 行对应的不同版本的数据
为了避免数据存在过多版本造成的的管理(包括存贮和索引)负担
hbase 提供了两种数据版本回收方式
保存数据的最后 n 个版本
保存最近一段时间内的版本
Cell
通过 rowkey 和 columns 确定的为一个存储单元称为 cell
每个 cell 都保存着同一份数据的多个版本
Region
类似关系数据库分区或分片的概念
Hbase 会将一个大表的数据基于 Rowkey 的不同范围分配到不同的 Region 中,
每个 Region 负责一定范围的数据访问和存储
区域是表被拆分,并分布在区域服务器中
由于大表数据被切分到不通的 Region,所以访问起来的时延很低
HBase 注意点
介于 NoSQL 和 RDBMS 之间,
仅能通过主键(rowkey)和主键的 range 来检索数据
HBase 查询数据功能很简单,不支持 join 等复杂操作
可通过 hive 支持来实现多表 join 等复杂操作
HBase 中支持的数据类型是 byte[]
主要用来存储结构化和半结构化的松散数据
HBase 特点
海量存储
适合存储 PB 级别的海量数据
列式存储
列式存储其实说的是列族存储
列族下面可以有非常多的列
列族在创建表的时候就必须指定
易扩展(两个方面)
一个是基于上层处理能力(RegionServer)的扩展
RegionServer 的作用是管理 Region、承接业务的访问
通过横向添加 RegionSever 的机器,进行水平扩展,
提升Hbase上层的处理能力,提升Hbsae服务更多Region的能力
一个是基于存储的扩展(HDFS)
通过横向添加 Datanode 的机器,进行存储层扩容
提升 Hbase 的数据存储能力和提升后端存储的读写能力
高并发
稀疏
列族中,你可以指定任意多的列
在列数据为空的情况下,不会占用存储空间
HBase 中的表特点
大
一个表可以有上十亿行,上百万列
面向列
面向列(簇)的存储和权限控制,列(簇)独立检索
稀疏
对于为空(null)的列,并不占用存储空间,
因此,表可以设计的非常稀疏
无模式
同一张表中不同的行可以有截然不同的列
Hbase 为什么查询速度快?
OLTP && OLAP
OLTP
联机事务处理
OLTP 是传统的关系型数据库的主要应用,主要是基本的、日常的事务处理,例如银行交易
采用面向行的数据库
数据库被设计为小数目的行和列
面向交易的处理系统
OLAP
联机分析处理
是数据仓库系统最主要的应用
OLAP 是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果
采用面向列的数据库
设计为巨大表
面向决策的处理系统
使用场景
HBase 适用于海量数据存储和准实时查询
只有当数据量非常大的时候,才能发挥其良好的性能
在上百亿行、上百万列的数据中,实现百毫秒级别的查询
查询简单、不涉及复杂关联的环境
交通(红绿灯信息采集)
交易记录(长久保存)
数据库历史数据
Kafka
Kafka 是什么?
Kafka是一种高吞吐量的分布式发布订阅消息系统
基本概念
Topic
一个Topic对应一个消息队列
Partition
一个Topic分成多个Partition
偏移量
Partition中会为消息保存一个Partition内唯一的ID,一般称为偏移量
Partition中被消费的消息是何时删除的?
无论消息是否被消费,除非消息到期Partition从不删除消息
Broker
每个Partition最终都要存储在物理机器上,这样的物理机器称为Broker
Partition冗余,同一个Partition有多个副本
Producer应该写入到哪一个副本上呢?Consumer又应该从哪个副本上读取呢?
Kafka的各个Broker需要Zookeeper进行通信
每个Partition的多个副本之间通过Zookeeper的Leader选举机制选出主副本
所有该Partition上的读写都通过这个主副本进行
其他冗余副本会从主副本上同步新的消息,就像其他的Consumer一样
Producer
消息生产者
Consumer
消息消费者
Consumer Group
消费模型
点对点模型(队列模型)
多个消费者共同消费一个队列,每条消息只发送给一个消费者
效率高
发布/订阅模型
多个消费者订阅Topic,每个消息会发布给所有的消费者
支持冗余的消费
组内点对点、组间发布/订阅
消费者组内以点对点模式工作,组内一条消息只被消费一次
消费者组以发布/订阅模式工作,组间一条消息被消费多次
Kafka 的优点
高吞吐
Kafka 每秒可以处理几十万条消息
低延迟
毫秒级延迟
容错性
n 个节点的集群运行最多 n-1 个节点出现故障
持久性,
消息被持久化到本地磁盘
可靠性
支持数据备份防止数据丢失
可扩展
<高低容持可可>
Kafka 为什么快?Kafka 如何保证高吞吐量?
利用 partition 实现并行处理
Kafka的Topic中都包含一个或多个partition,
不同partition可位于不同节点并行处理,所以速度更快
顺序写磁盘
追加写相比于随机写要减少了磁盘寻址时间,速度更快
Kafka中每个分区是一个有序的,不可变的消息序列,
新的消息不断追加到partition的末尾
充分利用 Page Cache
引入 Cache 的目的是为了提高 Linux 操作系统对磁盘访问的性能
Broker 收到数据后,写磁盘时只是将数据写入 Page Cache,
并不保证数据一定完全写入磁盘。
零拷贝技术(Zero Copy)
减少了文件在内核空间和用户空间的来回拷贝
减少了上下文切换
Producer 生产的数据持久化到 Broker
采用 mmap 文件映射
Consumer 从 Broker 读取数据
采用 sendfile 实现快速读取
批量压缩
很多情况下,系统的瓶颈不是CPU或磁盘,而是网络IO
批量数据的传输
减少了网络往返的开销,同时使用更大的数据包可以提升带宽利用率
message 支持压缩
进一步提高了网络传输数据的效率
Kafka 负载均衡策略
Kafka Exactly Once 语义
Kafka 通过幂等性和事务来实现 Exactly-Once
幂等
可以实现 Producer 的 exactly-once 语义
如果一条消息被 Producer 发送多次,在 Broker 端这条消息只会被写入日志一次
原理
发送到broker端的每批消息都会被赋予一个序列号,Kafka 会把序列号保存在底层日志中,若发送消息的序列号小于或等于 broker 端保存的序列号,就说明该消息已经被写入 broker,那么 broker 就会拒绝这条消息的写入操作
事务
可以实现 Producer 和 Consumer 的 Exactly-Once
通过事务可以将一组消息放入一个原子性单元中统一处理,Kafka 为实现事务要求应用程序必须提供一个唯一的 id 来表征事务,这个 id 被称为事务 id 。
Producer 写入数据的时候会写入控制消息,控制消息共有两类(COMMIT 和 ABORT),分别表示事务的提交和事务终止。将控制消息保存到 Kafka 日志中的目的就是让 Consumer 能够识别事务边界,从而整体读取某个事务下的所有消息。
两者关系
幂等性是可以独立使用的,不需要依赖事务属性
事务属性不能独立使用,必须依赖于幂等性
通过幂等实现了 Producer 的 Exactly Once
通过控制消息实现了 Consumer 的 Exactly Once
Zookeeper
Zookeeper 是什么?
开发工具
Git
Git是什么?
Git是一个开源的分布式版本控制系统
Git是Linus Torvalds为了帮助管理Linux内核而开发的
Git工作流程(6步)
克隆 Git 资源作为工作目录
在克隆的资源上添加或修改文件
如果其他人修改了,你可以更新资源
在提交前查看修改
提交修改
在修改完成后,如果发现错误,可以撤回提交并再次修改并提交
基本概念
工作区(working copy)
工作目录
暂存区(index)
在.git目录下的index文件中
也叫索引
版本库(head)
Head
当前分支最近的一个提交
创建仓库
git init
初始化一个仓库
会生成一个.git目录
git add README
git commit -m '初始化项目版本'
提交文件
命令
git add
针对修改的工作区文件
暂存区目录树被更新
git commit
暂存区的目录树写到版本库(对象库)中
master分支会做相应的更新
-m '注释'
git push origin 远程分支名
将当前分支推送到远程仓库
git reset
作用
重置HEAD(当前分支的版本顶端)到另外一个commit
git reset HEAD~n
将HEAD从顶端的commit往下移动n个更早的commit
参数
soft
仅重置head
mixed(default)
重置head、index
hard
重置head、index、working copying
会造成数据丢失,比较危险
git status
查看哪些文件被修改
git diff
查看文件具体被修改的地方
Maven
算法题
剑指Offer
JZ29 最小的k个数×
智力题
逻辑类
5L 的杯子和 3L 的杯子,怎么得到 4L 水?
5L 瓶子加满水,再倒入 3L 瓶子倒满,此时 5L 瓶子有 2L 水
3L 瓶子清空,5L 瓶子的 2L 水倒入 3L 瓶子,此时 3L 瓶子有 2L 水
5L 瓶子加满水,倒满 3L 瓶子,此时 5L 瓶子有 4L 水
5L 和 6L 的瓶子,如何得到 3L 的水?
6L瓶子加满水,再倒入5L瓶子倒满,此时6L瓶子有1升水
5升瓶子清空,6升瓶子的1升水倒入5L瓶子,此时5L瓶子有1升水
6L瓶子加满水,倒满5升瓶子,此时6L瓶子有2L水
5升瓶子清空,6升瓶子的2升水倒入5L瓶子,此时5L瓶子有2升水
6L瓶子加满水,倒满5升瓶子,此时6L瓶子有3L水
<6加满5清空,6加满5清空,6加满>
脑筋急转弯
住十楼的男人,电梯有人、下雨天直接坐到十楼;否则坐到七楼,再走三层是为什么?
男人是个侏儒
十瓶可乐,三个空瓶可以换一瓶,请问最多喝几瓶?
15瓶,最后剩两个空瓶,先借一瓶再还给店家三个空瓶
子主题
面经(3)
https://www.nowcoder.com/discuss/109518
https://blog.csdn.net/a934079371/article/details/109792573
https://blog.csdn.net/a934079371/article/details/100010565