整理
一 JavaSE
1数据类型
- 8个基本:boolean(1)、byte(8)、char(16)、short(16)、int(32)、float(32)、long(64)、double(64)
Integer缓存池范围
-128~127
String的不可变性
- 可以缓存hash值
- 线程安全(因为不可改变)
StringBuffer
是线程安全的,用了synchronized
同步
2 接口
- java8开始可以拥有默认的实现
3 抽象类
abstract
修饰,不能被实例化,包含了抽象方法
4 Object
类通用方法
1 | public final native Class<?> getClass() |
5 static
- 初始化顺序:父类静态变量、代码块 ==> 子类… ==> 父类实例变量、代码块 ==> 父类构造函数 ==> 子类… ==> 子类…
6 反射
相关类
Field
:表示类的成员变量,get set方法可以修改Method
:表示类的成员方法,invoke()
调用Constructor
:表示类的构造方法,新建对象
class获取方式
类名.class
对象.getClass()
Class.forName()
- classloader.load 类全限定名
7 异常
Error
:JVM不可捕获,程序崩溃OutOfMemoryError
、StackOverflowError
…
Exception
:RunTimeException
IOException
- 尽量使用标准的异常
- 建立异常对象是建立一个普通Object耗时的约20倍,而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍。
8 泛型
泛型数组声明
1
2List<?>[] lists = new ArrayList<?>[10]; //OK
List<String>[] lists = new ArrayList[10]; //OK,但是会有警告
9 注解
https://pdai.tech/md/java/basic/java-basic-x-annotation.html
java自带的标准注解
@Override
:标明重写某个方法1
2
3
4// 注解作用于方法
// 编译时有效
public Override {
}@Deprecated
:标明方法过时1
2
3
4
5
6// 会文档化
// 运行时有效
// 能修饰构造方法,属性,局部变量,方法,包,参数,类型
public Deprecated {
}@SuppressWarnings
:标明要忽略的警告1
2
3
4
5
6
// 类型、属性、方法、参数、构造器、局部变量
// 只能存活在源码时
public SuppressWarnings {
String[] value();
}
元注解
@Retention
:标明注解被保留的阶段TYPE
、FIELD
、METHOD
、PARAMETER
、CONSTRUCTOR
、LOCAL_VARIABLE
、ANNOTATION_TYPE
、PACKAGE
、TYPE_PARAMETER
、TYPE_USE
@Target
:标明注解使用的范围SOURCE
、CLASS
、RUNTIME
@Inherited
:标明注解可继承@Documented
:标明是否生成javadoc文档
自定义注解
10 SPI机制
整体机制图
- 当服务的提供者提供了一种接口的实现之后,需要在classpath下的
META-INF/services/
目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
- 当服务的提供者提供了一种接口的实现之后,需要在classpath下的
demo
定义接口与两个实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public interface Search {
public List<String> searchDoc(String keyword);
}
public class FileSearch implements Search{
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索 " + keyword);
return null;
}
}
public class DatabaseSearch implements Search{
public List<String> searchDoc(String keyword) {
System.out.println("数据搜索 " + keyword);
return null;
}
}接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:
com.cainiao.ys.spi.learn.Search
,里面加上我们需要用到的实现类1
com.cainiao.ys.spi.learn.FileSearch
测试
1
2
3
4
5
6
7
8
9
10
11public class TestCase {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
// 最终输出文件搜索
SPI机制的广泛应用
- JDBC DriverManager
- java中定义了
java.sql.Driver
但是没有实现(由不同的厂商实现) - mysql的jar包中可以找到
META-INF/services
目录下有一个java.sql.Driver
,文件内容是:com.mysql.cj.jdbc.Driver
就是针对Java中定义的接口实现
- java中定义了
- Spring中的SPI机制
- 自动装配中最终会加载
META-INF/spring.factories
文件
- 自动装配中最终会加载
- JDBC DriverManager
SPI机制的一般流程
- 定义标准。比如接口
java.sql.Driver
- 具体厂商或框架开发者实现。在
META-INF/services
目录下定义一个名字为接口全限名的文件,如java.sql.Driver
,文件内容是具体的实现名称,比如me.cxis.sql.MyDriver
就是对接口的实现 - 程序的使用:引用具体厂商jar包来实现功能。
- 定义标准。比如接口
缺陷
- 不能按需加载,需要遍历所有的实现并实例化
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发线程使用不安全
11 集合框架
Map
TreeMap
:base红黑树HashMap
:base哈希表,hashCode()
和equals()
决定了存放的位置put过程
不考虑内存,1000亿数据要插入到hashmap中,怎么做
- 预先申请好hashmap容量,避免频繁的扩容(注意负载因子设置为1,否则750亿左右的时候就扩容了)
- 多线程:concurrentHashMap,或者根据hashmap 的哈希函数,预先对数据进行分组,由不同的线程负责不同组,也能避免并发冲突
HashTable
:base哈希表,线程安全,但是一般使用ConcurrentHashMap
(分段锁)LinkedHashSMap
:base双向链表,顺序为LRU(最近最少使用)顺序
Collection
Set
TreeSet
:base红黑树,有序,查找时间O(logN)HashSet
:base哈希表,无序,查找O(1)LinkedHashSet
:base双向链表,有序,查找O(1)
Queue
LinkedList
:双向队列PriorityQueue
:base完全二叉树实现的小顶堆,优先级队列,需要自定义一个Comparator
比较器
完全二叉树:除了最后一层,所有层填满,且叶子节点都在左边
小顶堆:特殊的完全二叉树,父节点的值小于子节点的值
移除顶部的过程:
List
ArrayList
:base动态数组,扩容代价大(每次扩容1.5),最好在创建前指定好容量Vector
:同上,但是线程安全,但是一般用CopyOnWriteArrayList
LinkedList
:base双向链表,还可以用作栈、队列、双向队列
PriorityQueue
的使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16PriorityQueue<Integer> pq = new PriorityQueue<>(new Comparator<Integer>() {
public int compare(Integer e1, Integer e2) {
//比较方法
return e1 - e2;
}
});
// or
PriorityQueue<Integer> pq2 = new PriorityQueue<>((Integer e1, Integer e2) -> {
return e1 - e2; // 升序 1 2 3...
});
// or
PriorityQueue<Integer> pq3 = new PriorityQueue<>((a, b) -> a - b);
12 设计模式
创建模式
- 工厂模式
- 单例模式
- 建造者模式:封装一个复杂对象构造过程,并允许按步骤构造。
.build()
类似 - 原型模式
结构模式
- 适配器模式
- 装饰者模式
- 代理模式
- 门面模式
- 桥接模式
- 组合模式
关系模式
策略模式
模板模式
观察者模式
责任链模式
13 设计原则
- 单一职责原则
- 开放封闭原则: 类,模块,函数等应该是可以扩展的,但是不可以修改
- 里式替换原则:所有引用基类(父类)的地方必须能透明的使用其子类的对象
- 依赖倒置原则:高层模块(调用端)不应该依赖底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节(实现类),细节应该依赖于抽象。
- 迪米特原则:一个软件实体应当少的与其他实体发生相互作用。
- 接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上
二 JVM
1 类加载
由JVM将代码生成对应的字节码文件(.class),有javac编译器、scalac编译器、groovyc编译器、kotlinc编译器,对应不同的编程语言
- .class文件本质上是8位字节为基础的二进制流
.class文件的结构
- 魔数:头4个字节(cafe babe),作用就是确定是一个class文件(图片的格式也有对应的魔数)
- 常量池:存储变量/方法的属性、类型、名称
- 访问标志:表示类的属性和访问类型(接口/类?public?final?)
- 类索引、父索引、接口索引:用于确定类的继承关系
- 字段表属性:表示变量的属性和访问类型
- 方法表属性:表示方法的属性和访问类型
- 属性表属性:描述某些场景专有的信息
反编译字节码:
javap <options> <classes>
类生命周期
加载:
- 根据类的全限定名来获取定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为堆方法区数据的访问入口
验证:检查文件格式、字节码等
准备:在方法区为类的静态变量分配内存,并设置为默认值(0,0L,null,false)
解析: 把类中的符号引用转换为直接引用
初始化:为类的静态变量赋值
什么时候初始化类?
- new、访问静态变量/方法、反射、初始化子类,父类也会初始化、启动类
使用:类访问方法内的数据结构的接口,对象是堆的数据
虚拟机结束:执行System.exit()方法、程序结束、程序异常或错误退出、操作系统错误退出程序
类加载器(从上到下)
- 启动类加载器
Bootstrap ClassLoader
:负责加载JDK\jre\lib下的类 - 扩展类加载器
Extension ClassLoader
:负责加载JDK\jre\lib\ext下的类 - 应用程序类加载器
Application ClassLoader
:负责加载用户类路径(ClassPath)所指定的类
- 启动类加载器
类的加载方式
- 命令行启动应用的时候JVM加载
Class.forName()
:将类加载到JVM中之后还会执行static块ClassLoader.loadClass()
:只加载到JVM中,当newInstance的时候才加载static块
类加载机制
- 全盘负责、父类委托、缓存机制、双亲委派
双亲委派:
- 执行loadClass的时候会优先调用父类的
loadClass()
方法,然后再调用自己的findClass()
- 执行loadClass的时候会优先调用父类的
JVM判断两个对象是否相等的规则:
- 类的全类名是否一致
- 类的加载器是否一致(保证Java的核心API不会被篡改)
2 内存结构
线程私有:程序计数器、虚拟机栈、本地方法区
线程共享:堆、方法区、堆外内存(java7的永久代或java8的元空间)
程序计数器:用于标注当前线程执行的字节码行号(区别于操作系统的PC寄存器)
虚拟机栈:保存局部变量、结果,参与方法的返回和调用
StackOverflowError 、OutOfMemoryError
存储单位是栈帧
栈帧的内部结构
本地方法区
- 本地方法接口
- 本地方法栈
堆:存储对象实例
- 新生代:分为Eden、S1、S2(8:1:1)
- 新生代的GC叫Minor/Young GC,每次GC从Eden中找幸存者并将他们和S1或者S2的所有对象移到S2或者S1去,并回收S1/S2以及Eden中的剩余对象,多次GC(默认15次)后转移到老年代
- 老年代
- Major/Old GC
- 大对象直接进入老年代
- OOM异常
- 相关JVM参数
-Xmx
:堆起始大小,一般为电脑内存/64-Xms
:堆最大内存,一般为电脑内存/4
- 新生代:分为Eden、S1、S2(8:1:1)
方法区:存储类信息、常量池、静态变量、JIT编译后的代码等数据
- java8之前在堆中(永久代),java8移到了元空间(本地内存中)
- 相关JVM参数
-XX:PermSize
、-xx:MaxPermSize
:永久代空间-XX:MetaspaceSize
、-XX:MaxMetaspaceSize
:元空间-XX:+HeapDumpBeforeFullGC
、-XX:HeapDumpPath=/httx/logs/dump
:OOM的时候自动dump JVM
- 垃圾回收:常量池中废弃的常量和不再使用的类型
3 java内存模型
负责实现java线程间的通信(JMM):决定一个线程对共享变量的写入何时对另一个线程可见。是一个抽象的模型
重排序:导致内存可见性问题
- 编译器优化的重排序:编译器不改变单线程程序语义的前提下做的
- 指令级并行的重排序:改变语句对应机器指令的执行顺序
- 内存系统的重排序
4 垃圾回收
判断对象是否可被回收
- 引用计数算法:计算引用次数,为0就可回收(有循环,所以java不用)
- 可达性算法:通过GC Root作为起点进行搜索,不可达的就可回收
- 虚拟机栈中对象、本地方法栈对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、所有被同步锁持有的对象、JNI引用的对象
- 方法区的回收:主要是常量池和对类的卸载
引用类型
强引用:不会被回收
软引用:内存不足会被回收
1
SoftReference<Object> sf = new SoftReference<Object>(obj);
弱引用:一定会被回收,只能存活到下次垃圾回收前
1
WeakReference<Object> wf = new WeakReference<Object>(obj);
虚引用:为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
1
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
垃圾回收算法
- 标记-清除
- 标记-整理
- 复制:(Eden,S1,S2)
- 分代收集
- 新生代用复制
- 老年代用标记-清除/整理
垃圾收集器
- Serial 收集器:单线程,GC的时候用户线程停止
- ParNew 收集器:新生代多线程,老年代单线程;,GC的时候用户线程停止
- Parallel Scavenge 收集器:同上,但是其侧重点在于单次的吞吐量(即减少GC的频率)
- Serial Old 收集器:Serial 收集器的老年代版本(不懂)
- Parallel Old 收集器:两个都是多线程,Parallel Scavenge 收集器的老年代版本
- CMS(Concurrent Mark Sweep) 收集器:多线程,GC和用户线程并行
- 初始标记:用户线程暂停,标记GC Root直接关联的对象(很快)
- 并发标记:用户线程继续,进行GC Root标记
- 重新标记:用户线程暂停,修正并发时间内的变化
- 并发清除:用户线程继续
- G1 收集器:引入了分区的概念,不再使用新生老年代的划分,将堆区划分为若干等分
- 启发式算法收集高效益的分区
三色标记法:黑:自身和成员都被标记;灰:自身被标记,成员未被;白:未被标记,最终回收白色的
如何防止在并发标记的时候错删、漏删对象?漏删还好,下次回收即可,错删会引发npe
CMS:写屏障 + 增量更新
- 黑色对象建立对白色对象的引用时,把该黑色对象标记为灰色,缺点是会造成重复标记(只记录增加的引用)
G1:写屏障 + 原始快照 SATB
- 灰色对象断开对白色对象的引用时,把被删除的灰色对象到白色对象的引用记录下来,把白色对象修改为灰色。(只记录删除的引用)
内存分配和回收策略
- 回收策略
- Minor GC/Young GC:新生代
- Major GC/Old GC:老年代
- Full GC:整个java堆和方法区
- 内存分配策略
- 优先Eden
- 大对象进入老年代
- 长期存活进入老年代
- Full GC条件
- 调用
System.gc()
不建议使用 - 老年代空间不足
- 调用
- 回收策略
5 JVM参数/调优
JVM
1
2
3
4
5
6
7
8-Xms -Xmx # 堆最小/大值
-Xmn # 新生代大小,一般为堆的1/4或者1/3
-XX:NewRatio # 新生代与老年代比值
-XX:PermSize -XX:MaxPermSize # 老年代的初始值/最大值
-XX:MaxTenuringThreshold # 新生代存活次数
-XX:SurvivorRatio # Eden区与Subrvivor区大小的比值为8就是8:1:1
-XX:+HeapDumpBeforeFullGC # OOM的时候自动dump JVM
-XX:HeapDumpPath=/httx/logs/dump # dump地址垃圾回收器
1
2
3
4
5
6
7-XX:+UseSerialGC # 串行回收器
-XX:+UseParNewGC # 新生代使用并行,老年代使用串行
-XX:+UseConcMarkSweepGC # 新生代使用并行,老年代使用CMS(一般都是使用这种方式)
-XX:ParallelGCThreads # 指定并行的垃圾回收线程的数量,最好等于CPU数量
-XX:+DisableExplicitGC # 禁用System.gc(),因为它会触发Full GC,这是很浪费性能的
-XX:+PrintGCDetails # 开启详细GC日志模式,日志的格式是和所使用的算法有关
-XX:+PrintGCDateStamps # 将时间和日期也加入到GC日志中
6 分析工具
- 堆内存溢出OOM
- 添加jvm参数:
-XX:+HeapDumpOnOutOfMemoryError
:在OutOfMemoryError后获取一份HPROF二进制Heap Dump文件
- 添加jvm参数:
7 JVM远程调试remote debug
前提:远程服务器项目运行且两边代码一致
远程服务器启动时附带jvm参数
1
-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=${debug_port}
idea打开,设置启动配置,输入远程机器ip和刚刚设置的端口
- 打断点,如果断点右上角有√就代表成功了
8 linux 问题排查
- 文本
- 文本查找
grep
- 文本处理
sed
(增删改查)
- 文本查找
- 文件
tail -f filename
:循环监听- 查找文件 find
- 网络进程
- netstat
三 并发
1 理论
多线程解决的问题(根本原因速度不同:内存 > CPU > IO设备)
- 可见性:CPU增加了缓存,均衡和内存的速度差异 ==> 同时带来了可见性问题:不同线程对变量的修改不会马上被共享
- 原子性:操作系统增加了进程、线程,以分时复用IO,均衡CPU与IO设备的差异 ==> 带来了原子性问题:一个操作要么全成功/失败
- 有序性:编译器优化程序指令,使缓存可以更好的利用 ==> 带来了有序性问题
Java如何解决并发问题:
可见性:
volatile
关键字修饰的变量被所有线程可见原子性:读取和简单赋值,如果需要更大范围的原子操作可以使用
synchronized
和Lock
有序性:
volatile
禁止了JVM的指令重排,保证了有序性,synchronized
和Lock
也是
不可变对象
- final修饰的基本数据类型(如果是对象的话它的成员变量是可以变的)
- String
- 枚举
- Number的部分子类,如Long和Double等,BigInteger和BigDecimal 等
2 线程
线程状态
实现(实现接口更好)
Runnable
接口Callable
接口- 继承
Thread
类:三种方法本质都需要Thread类来启动 - 使用线程池
线程安全的方法
- 互斥同步
synchronized
(JVM级别) 和ReentrantLock
(JDK级别)。
- 非阻塞同步
- CAS算法
- 不涉及共享数据就没有线程安全问题
ThreadLocal
- 互斥同步
Executor
(看不懂)synchronized
和ReentrantLock
- 前者是JVM实现,后者是JDK实现
- 两者性能大致相同(因为新版本JVM对
synchronized
进行了优化,也支持了自旋锁等 - 前者不可中断,后者可以
- 优先使用前者,因为是JVM原生支持,且不用担心死锁问题,因为JVM会保证锁的释放
线程池
- 创建
ThreadPoolExecutor
创建(推荐)- 通过
Executor
框架的工具类Executors
来创建。
- 线程池参数:
- corePoolSize : 任务队列未达到队列容量时,最大可以同时运行的线程数量。
- maximumPoolSize : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
- keepAliveTime:当线程数大于corePoolSize 且没有新的任务提交,多余的空闲线程的等待时间
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略
ThreadPoolExecutor.AbortPolicy
: 抛出RejectedExecutionException
来拒绝新任务的处理。- 推荐使用,让业务感知异常
ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。ThreadPoolExecutor.CallerRunsPolicy
:由调用线程处理该任务。如果执行程序已关闭,则会丢弃该任务
- 参数设置原则
- 最佳corePoolSize ,N为CPU的核数
- 如果是IO密集型(CPU计算时间短,而等待IO操作(如读写文件、网络通信等)的时间长)的任务就设置为2N
- 文件处理,数据库读写,网络操作等
- 如果是CPU密集型(几乎没有IO等待)就设置为N+1
- 数值计算、图像视频处理、加密解密、模型训练等
- 还可以这样计算:
((线程等待时间+线程CPU时间) / 线程CPU时间)* CPU数目
- 最佳corePoolSize ,N为CPU的核数
- 常用四大线程池
- newCachedThreadPool——可缓存线程池
- 线程数量无上线,coresize = 0
- newFixedThreadPool——指定线程数量
- 不会释放线程
- newSingleThreadExecutor——单线程的Executor
- newScheduleThreadPool——定时线程池
- newCachedThreadPool——可缓存线程池
- 创建
线程池解决了什么问题
- 频繁申请/销毁资源和调度资源,将带来额外的消耗
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险
- 系统无法合理管理内部的资源分布,会降低系统的稳定性
- 实际使用的问题:参数不好配置
- 最大核心数设置偏小容易导致抛出拒绝异常,触发接口降级
- 队列设置过长,大量任务堆积在队列中,任务执行时间长,导致超时
3 锁
Java的锁分类
线程是否锁同步资源
- 乐观锁:采用无锁编程实现,CAS算法
- 悲观锁:
synchronized
关键字和Lock
的实现类实现ReentrantLock
、ReadLock
、WriteLock
(后两者是ReentrantReadWriteLock
内部类)
锁住同步资源失败时,线程要不要阻塞
为什么要非阻塞?阻塞线程要切换CPU的状态,耗时,自旋就是不让线程阻塞(及不放弃CPU的时间片)
- 阻塞
- 非阻塞
- 自旋锁
- 适应性自旋锁
多个线程竞争同步资源的流程细节
- 这是针对
synchronized
的优化,表示锁的四个状态 - 无锁
- 偏向锁:同一个线程执行同步资源时自动获取资源
- 轻量级锁:多个线程竞争时,没获取资源的线程自旋等待所释放
- 重量级锁:多个线程竞争时,没获取资源的线程阻塞等待被唤醒
- 这是针对
多个线程竞争锁时要不要排队
- 公平锁
- 非公平锁:先尝试插队,失败了再排队
一个线程的多个流程能不能获取同一把锁(前提是锁的是同一个对象或者class)
- 可重入锁:
ReentrantLock
(底层是AQS的state变量)和synchronized
- 不可重入锁
- 可重入锁:
多个线程能否共享锁
- 共享锁:
ReentrantReadWriteLock
,本质是里面的两把锁,读锁和写锁 - 排他锁:
synchronized
和JUC中Lock
的实现类就是互斥锁。
- 共享锁:
synchronized
原理:依赖于对象的Markword。(对象在内存的信息分为三段:对象头(包含了Markword),实例数据,对齐填充)- 偏向锁就是就是将hash值设置为线程id,是否偏向置为1,之后的时间如果这个线程再获取这个锁就不用再加锁了
- 如果出现了另一个线程需要找个锁,就会进行锁膨胀为轻量级锁,自旋等待
- 再膨胀就变为重量级锁,也就是互斥锁
死锁的四大条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻时,对已获得的资源保持不放。
- 不剥夺/非抢占条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
死锁的解决
- 破坏互斥:让资源可以同时访问(往往行不通)
- 破坏非抢占:剥夺式调度算法,但是会导致资源利用率低(不怎么用)
- 破坏请求与保持:静态分配策略:在进程执行之前先获取到他需要的所有资源,如果不满足就不开启(会导致资源利用率低,因为部分资源可能在进程后期才会用)
- 破坏循环等待:层次分配策略:将资源分层,先释放低层次的资源再获取高层次的资源
4 关键字
Synchronized
- 基于JVM,本质是根据
monitorenter
和monitorexit
指令来控制程序执行 - 两个指令是依赖于操作系统的Mutex Lock实现,而Mutex Lock要切换到内核态才能执行,代价昂贵
- 基于JVM,本质是根据
volatile
作用:防止重排、保证线程之间的可见性、保证原子性(32位系统的long、double的单次读/写)
32位系统一次读/写32位,而long和double是64位的
final
如果要扩展
final
关键字修饰的类的功能,怎么做?final
无法继承/实现,所以通过组合关于final的指令重排序(因为final字段必须在构造函数执行完成之前初始化完成)
- 对于基本数据类型
- 写:禁止final域写与构造方法重排序(禁止final域写重排序到构造方法之外,保证对象初始化完后,final已经全部初始化)
- 读:禁止初次读对象的引用与读该对象包含的final域的重排序。
- 对于引用数据类型
- 额外增加:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序
- 对于基本数据类型
5 JUC框架
即java.util.concurrent包
主要部分:Lock框架和Tools类、Collections(并发集合)、Atomic(原子类)、Executors(线程池)
原子类
Atomic
- 本质是CAS算法(乐观锁)
- 如
AtomicInteger
有add()
方法可以实现不加锁的情况下并发数据的一致性 - 还有
AtomicBoolean
、AtomicLong
、AtomicIntegerArray
、AtomicLongArray
、AtomicReferenceArray
等 - 如何解决ABA问题?
AtomicStampedReference.compareAndSet()
方法可以检查
LockSupport
:用来创建锁和其他同步类的基本线程阻塞park()
函数:阻塞线程unpark()
函数:激活线程Thread.sleep()
和LockSupport.park()
- 都阻塞当前线程,且都不会释放资源
- 前者不能通过外部唤醒,只能自己苏醒;后者可以通过另外一个线程的unpark唤醒
- 前者需要抛出InterruptedException中断异常;后者没有
- 前者本身是一个native方法;后者底层是调用Unsafe的native方法
锁的核心类AQS(
AbstractQueuedSynchronizer
)- 核心思想:如果资源空闲,如果空闲则将请求资源的线程设置为有效的工作线程并锁定资源;如果资源被占用,则将线程加入到CLH队列等待
- AQS定义的资源共享方式:
- 独占:只有一个线程能获取资源,如
ReentrantLock
,分为公平锁/非公平锁 - 共享:多个线程可以同时执行,如
Semaphore/CountDownLatch
、Semaphore
、CountDownLatCh
、CyclicBarrier
、ReadWriteLock
- 独占:只有一个线程能获取资源,如
ReentrantLock
:独占锁- 里面有三个内部类
Sync extends AQS
及其两个子类:NonfairSync
、FairSync
,分别实现公平/非公平策略 - 构造函数默认使用的是
NonfairSync
- 对其操作都转化为了对
Sync
对象的操作,进而转换为对AQS
的操作
- 里面有三个内部类
ReentrantReadWriteLock
:读写锁(满足多读的场景)- 除了
ReentrantLock
里面的三个内部类外,还有ReadLock
和WriteLock
均是Lock
的实现
- 除了
ConcurrentHashMap
- 保存了一个
Segment
数组,将hash表划分为多段来实现分段锁。每个segment
通过ReentrantLock
- 每个分段里面是数组+链表+红黑树(jdk1.8引进)的方式
- 扩容:segment不能扩容(初始默认值是16,也就是16个并发量),扩容是segment里面的数组扩容
- 保存了一个
CopyOnWriteArrayList
- 属性中有一个
ReentrantLock
可重入锁,保证线程访问的安全 - 写操作的时候,拷贝数组,可能会导致young gc或者full gc
- 属性中有一个
ConcurrentLinkedQueue
线程池
ThreadPoolExecutor
本质是维护一个线程集合和一个阻塞工作队列
参数:见 2 线程部分
- 三种类型
newFixedThreadPool
- 线程池数达到corePoolSize后,即使没有可执行任务也不会释放线程 —
maximumPoolSize
、keepAliveTime
参数无效 - 工作队列为无界队列 — 饱和策略参数无效
- 线程池数达到corePoolSize后,即使没有可执行任务也不会释放线程 —
newSingleThreadExecutor
- 初始化的线程池只有一个线程,如果该线程异常结束,则会创建一个新的
- 工作队列为无界队列 — 饱和策略参数无效
newCachedThreadPool
- 线程池数量可达到
Integer.MAX_VALUE
(2147483647) - 正常理解的线程池
- 线程池数量可达到
CountDownLatch
- 目的是实现不同之间的线程同步
- 内部类
Sync extends AbstractQueuedSynchronizer
ThreadLocal
线程安全:互斥同步(synchronized 和 ReentrantLock)、非阻塞同步(CAS, AtomicXXXX)、无同步本地存储(ThreadLocal)
内部类
ThreadLocalMap
没有实现Map接口
没有public方法
ThreadLocalMap
的Entry
实现继承了WeakReference<ThreadLocal<?>>
该方法仅仅用了一个
Entry
数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry;
内存泄漏问题:使用线程池的时候可能会出现
ThreadLocalMap
中使用的 key 为ThreadLocal
的弱引用,而 value 是强引用。所以,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。- 如果
ThreadLocal
没有外部强引用,那么在发生垃圾回收的时候,ThreadLocal
就必定会被回收,而ThreadLocal
又作为Map中的key,ThreadLocal
被回收就会导致一个key为null的entry,外部就无法通过key来访问这个entry,垃圾回收也无法回收,这就造成了内存泄漏 - 如何防止:
ThreadLocal
的remove()
方法- 在get和set的方法中可能会调用这个remove方法
- ThreadLocal虽然提供了避免内存泄露的方法,但是ThreadLocal不会主动去执行这些方法,需要我们在使用完ThreadLocal对象中保存的数据后,在finally{}代码块中调用ThreadLocal的remove()方法,加快GC自动垃圾回收,避免内存泄露。
四 IO
1 JavaIO类
javaIO从传输方式来说主要分为两大类:字节流(InputStream/OutputStream)和字符流(Reader/Writer)
从数据操作来说:
- 文件:FileInputStream/OutputStream、FileReader/Writer
- 数组:ByteArrayInputStream/OutputStream、CharArrayReader/Writer
- 管道:PipedInputStream/OutputStream、PipedReader/Writer
- 基本数据类型:DataInputStream/OutputStream、DataReader/Writer
- 缓冲操作:BufferInputStream/OutputStream、BufferReader/Writer
- 打印:PrintStream/Writer
- 对象序列化反序列化:ObjectInputStream/OutputStream
- 字节字符流转换:InputStreamReader/OutputStreamWriter
2 JavaIO类设计模式—装饰者
装饰者模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45public interface CompInterface { // 顶层
void action();
}
public class Comp1 implements CompInterface { // 组件1
public void action() {
// ...行为1
}
}
public abstract class CompDecorator implements CompInterface { // 顶层抽象装饰器
protected CompInterface comp;
}
public class Decorator1 extends CompDecorator { // 装饰器1
public Decorator1(CompInterface comp) {
this.comp = comp;
}
public void action() {
comp.action();
// 自己的动作
}
}
public class Decorator2 extends CompDecorator { // 装饰器2
public Decorator2(CompInterface comp) {
this.comp = comp;
}
public void action() {
comp.action();
// 自己的动作
}
}
Comp1 comp = new Comp1(); // 声明组件1
comp = new Decorator1(comp); // 装饰上Decorator1
comp = new Decorator2(comp); // 装饰上Decorator2
comp.action; // 执行Comp1、Decorator1、Decorator2的action在IO流相关类的表现:
FilterInputStream是一个抽象组件,类似于上文的CompDecorator,使用Buffer对FileInputStream进行增加,实现缓冲
1
2FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
3 IO中常见类的使用
磁盘操作:File
字节操作:Input/OutputStream
字符操作:Reader/Writer
对象操作:
- Serializable(一个空接口,只是一个标准,不实现它就进行序列化会抛异常)
- transient关键字修饰的对象/属性不会被序列化
网络操作:
InetAddress:用来表示IP地址
URL:统一资源定位符
Sockets:使用 TCP 协议实现网络通信
Datagram:使用 UDP 协议实现网络通信
4 Unix IO模型
一个输入操作通常包括:1. 等待数据准备好 2. 从内核向进程复制数据
- 阻塞式I/O:应用进程(不是整个操作系统)被阻塞,直到数据复制到应用进程缓冲区中才返回,执行效率会比较高。
- 非阻塞式I/O:应用进程执行系统调用后,内核返回一个错误码,应用进程继续执行,但不断的执行系统调用(轮询polling)来获取IO是否完成。对CPU的利用率较低(因为CPU要不断的处理系统调用)
I/O复用:应用进程使用select或者poll等待数据并阻塞,当一个套接字变为可读时再调用recvfrom复制数据
- 单个线程处理多个套接字,即多个IO事件
信号驱动I/O:应用进程使用sigaction系统调用,内核立即返回信息,应用进程继续执行,数据准备完毕后内核向应用程序发送SIGIO信号,之后应用进程调用recvfrom获取数据
- 异步I/O:应用进程使用aio_read系统调用后立即返回,内核准备完数据后向应用进程发送信号
和信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
同步IO(前4个)和异步IO区别:同步IO应用进程在调用 recvfrom 操作时会阻塞,
4个同步IO的区别主要在于执行recvfrom之前的第一阶段不一样
select/poll/epoll
- select:将socket放到一个文件描述符集合(固定长度1024的 BitsMap)。需要2次遍历文件描述符集合,2次拷贝文件描述符集合
poll:bitsMap变为了链表
epoll:
文件描述符使用红黑树O(logn),只在内核维护,减少了很多的遍历和复制
使用事件驱动机制,内核维护了一个链表记录就绪事件,当有socket事件发生的时候,通过回调函数内核将其加入到列表中,当用户调用
epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数(但是还是会拷贝),不需要像select/poll那样轮询整个socket集合
5 BIO NIO AIO
BIO:同步阻塞
- 服务器一个连接一个线程
NIO:同步非阻塞
特点:非阻塞;面向块传输,普通IO是面向流
BIO即阻塞式IO,需要一个服务线程监听一个用户线程,使用多线程技术来实现对不用用户的“异步”,对服务器资源消耗大(线程多了之后CPU切换线程开销大),且利用率不高(阻塞等待的时候客户端不能作别的)。
Java NIO:和标准IO不同的是,NIO把IO抽象成了块(一个byte[]),一次可以读取多个字节
几个要素
- 通道
Channel
:类似于流,但是是双向的。有FileChannel、DatagramChannel、SocketChannel等 - 缓冲区
Buffer
:本质是一个数组,所有数据要先放到buffer中才能传输。有ByteBuffer、CharBuffer、LongBuffer等- 状态变量:最大容量、当前已读取的字节数、还可以读取的字节数(有点类似于滑动窗口)
- 选择器
Selector
:NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
- 通道
- AIO:异步非阻塞
五 数据库
1 概述
https://dsf.berkeley.edu/papers/fntdb07-architecture.pdf
- 核心组件
- 进程管理器
- 网络管理器
- 文件系统管理器
- 内存管理器
- 安全管理器
- 客户端管理器
工具
- 备份恢复管理器
- 查询管理器(解析、重写、优化、执行)
- 数据管理器(事务)
联接运算符
- 合并联接:用于排序的字段比较好
- 哈希联接:比较消耗内存,因为要用额外内存存储哈希
- 嵌套循环联接(复杂度O(m*n)):类似于双循环来匹配查询
实际使用的时候采用动态规划、贪心算法、启发式算法来确定用哪些
2 关系型数据库—MySQL
为什么需要范式?
- 冗余数据:学生2
- 修改异常:修改一个记录中的信息,但是另一个记录的相同信息没有被修改:修改学生2Snam
- 删除异常:删除一个信息,会丢失其他信息:删除课程1,学生1也不见了
- 插入异常:插入一个学生,但是如果这个学生没选课,就无法插入
三大范式
- 第一范式:属性不可分
- 第二范式:每个非主属性完全函数依赖于键码
- 分解后两个表:Sno-Sname-Sdept-Mname 和 Sno-Cname-Grade
- 第三范式:非主属性不传递函数依赖于键码(上述关系中Sno -> Sdept ->Mname)
- 分解后变为三个表:Sno-Sname-Sdept、Sdept-Mname、Sno-Cname-Grade
事务:满足 ACID 特性的一组操作,可以通过Commit提交事务,也可以通过Rollback回滚(通过日志)
- 原子性(Atomicity):所有操作要么全部提交成功,要么全部失败回滚。
- 一致性(Consistency):数据库在事务执行前后都保持一致性状态
- 隔离性(Isolation):一个事务所做的修改在最终提交以前,对其它事务是不可见的。
- 持久性(Durability):一旦事务提交,则其所做的修改将会永远保存到数据库中
并发一致性问题:
- 修改丢失:两个事务修改数据,后面的事务修改覆盖了前面的事务(貌似不算?)
- 脏读:A修改数据(50->100),B读取到新数据后(100),A回滚数据(50),此时B读到了脏数据
- 不可重复读:B读取数据(50),A修改数据(100),B再读(100)前后不一致
- 幻读:B读取数据(100行),A插入一行,B再读取(101行)前后不一致
隔离级别:
- 未提交读(READ UNCOMMITTED)
- 提交读(READ COMMITTED):解决了脏读
- 可重复读(REPEATABLE READ):解决了不可重复读
- 可串行化(SERIALIZABLE):解决了幻读
锁
- 从粒度上划分:行级锁、表级锁
- 类型上划分:读写锁(排它锁/写锁/X锁、共享锁/读锁/S 锁)、意向锁(X/S锁要扫描行耗时,所以搞了IS/IX表锁,即意向锁,要获取X/S锁之前必须先获取IX/IS锁)
- 临键锁Next-Key Locks:
- 记录锁Record Locks:锁定一个记录上的索引,如果没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引
- 间隙锁Gap Locks:锁定索引之间的间隙,但是不包含索引本身
- Next-Key Locks就是记录锁和间隙锁的结合
MVCC:多版本并发控制
- 是Mysql的InnoDB实现隔离级别的一种方式,实现提交读和可重复读这两种隔离级别。(可串行化需要对所有读取的行都加锁,单纯使用 MVCC 无法实现)
- 如何解决幻读:MVCC + 间隙锁(Next-Key Locks)
- 版本号:系统版本号(每开始一个新的事务,系统版本号就会自动递增)、事务版本号(创建/删除)
针对可重复读隔离级别的执行:
- Insert:将当前系统版本号作为数据行快照的创建版本号。
- Delete:将当前系统版本号作为数据行快照的删除版本号。
- Update:将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。(先delete再update)
- Select:如果是正在修改的事务T在读取就不管。如果是不修改数据的事务B要读取,B读取的数据行快照的创建版本号必须小于 B的版本号,且删除版本号必须大于 B 的版本号
MVCC原理
4个隐式字段
- DB_ROW_ID:如果没有主键就会自动创建,并加一个聚餐索引(索引结构和数据一起存放的索引)
- DB_TRX_ID:最近修改/插入的事务ID
- DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本
- DELETED_BIT:记录被更新或删除并不代表真的删除,而是删除flag变了
undolog:只记录insert、update、delete操作
- Insert undo log,至少记录主键,回滚的时候直接删除就行了
- Update undo log,至少要把修改这条记录前的旧值都记录下来
- Delete undo log,至少要把这条记录中的内容都记下来
read view:主要是用来做可见性判断的
Mysql架构:
buffer pool:
默认128MB
存储的是数据页,16KB
基于冷热数据分离的LRU链表存储
存储引擎:MyISAM和InnoDB
- 事务:MyISAM不支持事务,后者可以commit和rollback(undo log)
- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁
- 崩溃恢复能力:InnoDB依靠redo log
日志
- binlog
- undolog:写的是逻辑上的事务,用于事务回滚
- redolog:两阶段提交(保证和binlog的一致性):先写redolog,再写binlog,最后将redolog设置为commit状态
索引——数据结构:
- hash表:快,但是不支持范围查询
- 二叉查找树:性能很依赖其平衡性
- AVL树(高度差不超过1):需要频繁地进行旋转操作来保持平衡,且一个节点只存一个数据,磁盘IO性能开销大
- 红黑树:自平衡二叉查找树,平衡性稍弱(不追求完全的平衡)所以有些查询效率较低(多次IO),但是增删效率高
- B树:所有节点既存放key又存放date
- B+树:多路平衡查找树,更稳定快速。原因:数据存放在叶子节点,保证了其他节点能够存放更多的索引,大大压缩了树的高度,减少磁盘IO次数;且叶子节点之间用双向链表连接
- myisam中叶子节点存放的是数据的地址
- innodb中叶子节点存放的是主键+索引列数据
索引的优缺点
优点:加快搜索;唯一索引可以保证唯一性
缺点:创建索引需要时间空间;修改数据需要额外维护索引
索引——分类
主键索引:加速查询 + 列值唯一(不可以有 NULL)+ 表中只有一个
- 除了这个其他的索引都是二级索引,二级索引数据位置存储的是主键
普通索引:加速查询
唯一索引:加速查询 + 列值唯一(可以有 NULL)
覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值
联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并(最左匹配原则)
最左匹配原则:如果建立了(a,b,c)索引,查表的时候需要包含 a 或者 ab 或者abc,顺序无所谓,优化器会自动优化
- 如果是(a,c)也会走索引,只不过只会走a索引
索引下推:通过联合索引对索引进行过滤,减少回表的次数( 5.6版本)
1
2
3
4
5
6select * from t_user where name like 'l%' and age = 17; # 索引是(name, age)
-- 第一步:取name 比如得到 li lisi lisa ll 四个
-- 第二步:在这四个中取 age = 17的 得到li,对应id为123
-- 第三步:回表去找id=123的数据
-- 如果没有开启索引下推,会回表4次
全文索引:对文本的内容进行分词,一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替
Mysql性能优化
- 优化数据访问:
- 减少请求数据量:列用select,行用limit,使用缓存
- 减少服务端扫描的行数:索引来覆盖查询
- 重构查询方式:
- 切分大查询,防止锁住很多的数据
- 分解大查询,分解为单表查询,在业务逻辑中关联
- 优化数据访问:
Mysql分库分表:垂直/水平
- Sharding策略(水平):哈希取模、范围切分、映射表
- Sharding策略的问题及解决
- 事务:使用分布式事务解决
- 链接:JOIN变为多个单表查询,在业务代码中链接
- ID唯一性:全局唯一ID/每个分片指定ID范围/分布式ID生成器(Snowflake 算法)
Mysql主从复制和读写分离
- binlog线程:负责将主服务器上的数据更改写入二进制日志中。
- I/O线程:负责从主服务器上读取二进制日志,并写入从服务器的中继日志中。
- SQL线程:负责读取中继日志并重放其中的 SQL 语句。
SQL执行过程
- java业务端的数据库连接池:Druid、C3P0、DBCP。数据库也有类似的池子
- 查询缓存池
- 分析器
- 查询优化器:选择查询成本最小的索引
- IO成本:即从磁盘把数据加载到内存的成本
- CPU成本:与行数有关
- 执行器:调用存储引擎的接口完成执行
- 存储引擎:InnoDB
慢查询问题
- 定位:开启慢查询:
slow_query_log = ON
、slow_query_log
为时间 - 分析:explain命令:
EXPLAIN SELECT * FROM t1
- 优化:
- 不使用子查询
- 读取适当的记录,limit M, N
- 分组统计禁止排序
- 联表放到业务代码
- varchar字段建立索引指定索引长度
- 避免索引失效(最左匹配原则)
- 左模糊查询匹配不了索引
- 回表的性能评估
- 定位:开启慢查询:
索引实验 2838426行数据
是否使用索引是由查询优化器决定
1 |
|
- 有1000万行的数据,主键是id,查询
select * from table where id = 122
会发生几次IO- B+ 树存储,一页是16kb
- 主键是int(4字节即可)
-2,147,483,648
到2,147,483,647
。一个非叶子节点有4字节的主键 + 6字节的指针 - 一页就有1600条数据
- 第一层 1600 条、第二层 1600 * 1600 = 256万、第三层就能覆盖
- 所以打开是3次IO
3 NoSQL—Redis
redis:Remote Dictionary Server
特点:
- 读写性能优异
- 数据类型丰富
- 原子性:redis所有操作都是原子性的,同时支持几个操作全合并后的原子性执行
- 持久化:RDB,AOF持久化方式
- 发布/订阅模式
- 分布式:redis cluster
使用场景:
- 热点数据缓存
- 限时业务
- 计数器相关(incrby命令可以实现原子性的递增)
- 分布式锁:setnx
- 延时操作(一般用mq)
- 排行榜(Zset)
- 点赞、好友等相互关系的存储(集合命令求交并差集)
- 队列
redis三种高效缓存读写策略
旁路缓存(常用)
写:先写数据库再删缓存
读:先读缓存:未命中再读数据库,随后写入缓存再返回
读写穿透
写:先读缓存:未命中则更新数据库,命中则更新缓存,利用cache服务同步更新数据库
读:先读缓存:未命中再读数据库,随后写入缓存再返回
异步缓存写入
- 写:只更新缓存,异步批量更新数据库
3.1 数据类型
基本数据类型
- String
- List
- Set
- Zset
- Hash
特殊数据类型
- HyperLogLogs(基数统计):用来算两个set中的不重复元素数量(会有一定的误差量),用于网站的注册IP数,每日IP数等统计
- Bitmap (位存储):用于统计用户活跃度等(0,1)
- geospatial (地理位置)
redis-stream:redis5.0新增的数据结构,是redis对消息队列的完善
redis实现消息队列
- 发布/订阅模式,缺点是无法持久化,如果网络断开、redis宕机消息会丢失
- 基于List LPUSH+BRPOP 或者 基于Sorted-Set的实现,缺点是不支持多播、分组广播
针对上述不足提出了stream结构
key:唯一的索引,首次使用xadd时自动创建
1
2
3
4
5
6
7XADD:添加消息
XTRIM:修剪消息,限制长度
XDEL:删除消息
XLEN:获取流长度
XRANGE:获取消息列表
XREVRANGE:反向获取,id由大到小
XREAD:以阻塞/非阻塞获取消息列表
操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32# string
set counter 2
incr/decr counter # +/- 1
incrby/decrby counter 100 # 加/减100
# List
增:rpush, lpush ,linsert
查:lrange,lindex,llen
删:lpop,rpop,ltrim,ltrim
阻塞操作:blpop,brpop
# Set
增:sadd myset hao hao1 xiaohao hao
删:srem key member1 [member2]
查:smembers myset(返回成员) scard myset(返回数量)
# Hash
增:hset myHash sub-key1 value1
查:hget myHash sub-key1
删:hdel myHash sub-key1
# Zset
增:zadd myscoreset 100 hao 90 xiaohao
查:zscore myscoreset hao # 100
zrange myscoreset 0 10 WITHSCORES # hao xiaohao
删:zrem myscoreset hao
# HyperLogLogs
pfadd key1 a b c
pfadd key2 c d e
pfmerge key3 key1 key2 # 合并key1 key2
pfcount key3 # 5
# Bitmap
setbit key sub-key1 0
setbit key sub-key2 1
getbit key sub-key1
bitcount key # 1
3.2 redis底层数据结构
SDS 简单动态字符串:redis是C语言写的,但是其字符串对象不是,是SDS,分为头部、数据和结尾标识\0(C语言是以空字符结尾)
为什么设计SDS?O(1)获取长度;避免内存溢出;空间预分配
- 特点:字符长度len在头部,O(1)时间就可以获取,也杜绝了缓存区溢出
ZipList 压缩列表
- 从左到右含义:整个ZipList所占内存字节数、最后一个entry的偏移量(以快速完成pop)、entry总量、列表内部数据、终止标识
- 为什么ZipList省内存:不同的entry存储空间不同(一般的list是以最大的entry大小为单位存储),这样一来如何遍历?增加了一个prelen字段记录上一个entry的长度
- 缺点:因为每个entry都没有预留空间,所以修改节点导致容量变大时最坏会导致所有entry重新计算内存O(N)
QuickList 快表:以ZipList为节点的双端链表
HashTable 字典/哈希表
- 哈希冲突怎么解决:链地址法
IntSet 整数集
- int16 or int32:优先int16,当插入一个int32时,所有的修改为int32(删除最后一个int32的时候不会变回int16,节省开支)
ZSkipList 跳表:只在ZSet中使用,log(N)的增删查
- 缺点是存储空间大
- with 平衡树(AVL、红黑树):范围查找平衡树复杂,且增删可能会触发自平衡
- with B+树:B+的核心是减少IO过程快速定位到索引,不是redis的关注方式
3.3 redis持久化
- RDB(Redis DataBase ):快照
- 触发方式:
- 手动触发:
save
和bgsave
,前者会阻塞主进程,后者fork一个子进程 - 自动触发:redis.conf中配置
save m n
,m秒有n次修改会触发;主从复制时会触发;执行debug reload命令重新加载redis时;执行shutdown命令时,如果没有开启AOF持久化会触发
- 手动触发:
- 如何保持数据一致性?(进行RDB的时候发送了数据的写)
- Copy-on-Write,复制一个副本来写
- 优劣:redis加载RDB速度快,文件体积小;无法做到秒级持久化,每次bgsave都要fork进程开销大,二进制文件难读
- 触发方式:
- AOF
- 写后日志,先写内存后写日志。(mysql就是写前)
- 好处是:避免了额外的检查开销(写进来的日志都是成功执行的);不会阻塞当前的写操作
- 风险是:写完然后还没写日志发生宕机丢失数据;主线程写磁盘压力大,导致写盘慢,阻塞后续操作
- 实现:
- 命令追加(append):开启AOF后,将被执行写命令追加到服务器aof_buf缓冲区
- 文件写入(write):分为同步写回、每秒写回、操作系统控制的写回
- 和文件同步(sync)
- AOF重写:优化一些冗余的AOF操作,减少AOF文件的大小。
- 后台fork一个bgrewriteaof进程来进行,数据是写时复制,fork子进程时会复制父进程的页表(类似于指针)
- 什么时候会阻塞主进程:fork子进程拷贝页表;主进程有bigkey写入操作系统会创建页面的副本,并拷贝原有的数据;子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞
- 写后日志,先写内存后写日志。(mysql就是写前)
- AOF和RDB混用:redis4.0推出
- 内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
- 避免了AOF文件过大
- redis启动:
- 判断有无AOF?有则加载AOF启动
- 判断有无RDB?有则加载RDB启动
- 都没有就直接启动
- RDB(Redis DataBase ):快照
3.4 redis事件机制
- 文件事件:用于处理 Redis 服务器和客户端之间的网络IO(基于IO多路复用)
- redis单线程:指的是网络IO和键值对读取是由一个线程完成,但是其他的(持久化、异步删除、集群数据同步等是fork的进程完成)
- 文件事件是对套接字操作的抽象,每当一个套接字准备好执行
accept
、read
、write
和close
等操作时,就会产生一个文件事件
- 时间事件:redis服务中的一些定时操作
- 定时事件、周期事件
- 文件事件:用于处理 Redis 服务器和客户端之间的网络IO(基于IO多路复用)
3.5 redis事务
本质是一组命令的集合
使用
1
2
3
4
5
6
7
8
9
10
11
12
13MULTI:开启事务
EXEC:执行事务
DISCARD:取消事务
WATCH:监视一个或多个key
UNWATCH:取消监视
# 例子
set k1 v1
set k2 v2
MULTI
set k1 12
set k2 24
EXEClua脚本:Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
3.6 redis高可用
主从:故障恢复、负载均衡、高可用
- 读写分离:主库读写,从库读
- 全量同步:主要依靠RDB文件
- 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
哨兵机制:监控、自动故障转移、通知
- 哨兵如何监控主从集群:哨兵向主库发送info,主库返回所有slave列表,从而可以与从库通信
- 哨兵集群:基于redis的 pub/sub 机制来互相获取IP的端口进行通信
- 哨兵如何判断主库下线?主观下线(任何一个哨兵判断)、客观下线(哨兵集群判断)
- 哨兵集群的选举:raft算法:拿到半数赞成,拿到的赞成票大于配置的quorum 值
- 哨兵leader完成主从切换:主库客观下线后,过滤掉不健康的从库,选择优先级最大的,然后向别的从库发送消息新的主库
redis cluster:弥补主从和哨兵的不足:写能力和存储能力依赖于主库的问题
哈希槽(Hash Slot):有16384(即2的14次方)个哈希槽,每个key通过对16384取模后来决定放哪里,cluster每个节点负责一部分hash槽(只有master才可以占据槽)
普通的哈希算法的缺陷:如果节点增加或减少,之前的缓存就失效了需要重新计算存储位置(即重新set),容易引发雪崩
一致性哈希算法:普通哈希算法是对服务器数量取模,一致性哈希算法是对 2^32 取模(形成一个哈希环),对服务器ip或者其他关键字段进行hash后确定其在环上的位置,然后插入新数据的时候计算hash后对2^32 取模得到其在哈希环上的位置,并顺时针找,找到的第一个服务器就是其存储的服务器
为什么不用一致性哈希算法?增删节点可能会带来雪崩(会对相邻的节点产生影响)、也可能会出现数据倾斜的状况
为什么是16384?为了发送槽的全量状态,用bitmap发送,16384只需要2k的空间(16384 / 8 / 1024 = 2k)
缓存问题
一致性问题:最佳实践:先写DB再删缓存
缓存击穿:缓存失效后大量请求打在DB上(同一个key)
- 热点数据不过期;加互斥锁;接口限流;熔断降级
缓存穿透:频繁访问一个DB和缓存都不存在的key
- 增加入口校验;DB中未取到可以设置到缓存中为null(有效时间短一点);布隆过滤器
布隆过滤器
缓存雪崩:大量数据过期,过多请求打在DB上
- 缓存过期时间随机;热点数据部署在不同缓存数据库中;设置热点数据不过期
缓存淘汰策略:不淘汰、设置过期时间、LRU
3.7 解决方案——大value、多key合并方案
单key存储的value大
如果是每次都整存整取:可以尝试将对象拆分为几个kv,用multiGet获取值,这样做的意义在于分拆单次操作压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响
如果是每次只读取部分数据:可以将这个存储在一个hash中,每个field代表一个具体的属性
hash、set、zset、list存储过多的元素:
- 分拆,用取模的方式(比如固定一个桶的数量)确定key,然后将field放在计算出的key中
- 一个集群存储了上亿的key
- 带来的问题:key占存储空间大;集群模式中服务端需要建立slot2key的映射,这些指针也会占用大量空间
- key如果有很强的关联关系:就可以放在hash中
- key如果没有很强的关联关系:还是放在hash中,但是key是由桶的数量取模来计算得到的
- 大Bitmap或者布隆过滤器拆分
美团squirrel:基于redis
如何保证数据可靠性
- 多副本存储策略:同一份数据的多副本存储,保证一个副本宕机的情况下其他副本依旧有全量数据
- 多机房部署容灾策略:将多个副本部署在不同的机房中,避免机房掉电以及断网带来的数据丢失
- 持久化
主从一致:弱一致性,为了保证高性能
4 NoSQL—ES
一般语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29# 一般索引查询
GET /bank/_search
{
"query": {
"match_all": {
}
},
"sort": [
{
"account_number": "asc"
}
],
"from": 10,
"size": 10
}
# 特定字段查询:match_all就改为"match": { "address": "mill lane" } 意思为address中包含mill或者lane
# "match_phrase": { "address": "mill lane" } address包含mill lane
# 条件查询:
"query": {
"bool": {
"must": [
{ "match": { "age": "40" } }
],
"must_not": [
{ "match": { "state": "ID" } }
]
}
}聚合查询,类似于mysql的group by
1
2
3
4
5
6
7
8
9
10
11{
"size": 0, # 返回的文档数为0
"aggs": { # 标识聚合查询
"group_by_state": { # 自定义名称,用于标识聚合查询的结果
"terms": { # 定义了一个聚合
"field": "state.keyword" # 根据state进行分组
}
}
}
}
ps: 字符串类型有text和keyword,后者不会分词基本概念
- 文档:一条完整的数据,包括索引、名称等,以json方式存储
- 字段:文档中的具体字段,类似于表格的列
- 索引:就是相同类型的文档的集合,类似于表。eg:用户的索引、商品的索引、订单的索引…
- 映射(mapping):索引中字段的约束信息,类似于表的结构约束。
倒排索引
六 计算机网络
6.1 base
网络模型
- 应用层:HTTP、websocket、FTP、Telnet、DNS、SMTP等协议(传输单位是消息或报文message)
- 传输层:TCP(面向字节流)、UDP(面向报文,传输单位是段segment)
- 网络层:IP(寻址和路由)(传输单位是包packet)
- 网络接口层:ARP协议(传输单位是数据帧frame)
从键入网址到网页显示,发生了什么
解析url
生成http请求信息,Http报文
查询服务器域名对应的 IP 地址(DNS)
- TCP建立可靠传输,如果Http消息过大会切分数据分块发送,组装好TCP头部交给下一层
- 根据IP协议,生成IP头部组装后交给下一层
- 根据ARP协议,获取到两点MAC地址,组装MAC头部
- 通过网卡将包数据转为电信号,通过网线发送
- 交换机基于mac地址进行路由发送到路由器(之后mac头部就没用了,就会被丢弃)
- 路由器基于ip发送转发内容
- 数据包抵达服务端进行数据处理,并发送响应数据,完成数据传输
6.2 Http
- 为什么不直接使用TCP呢?为了防止粘包:区分出不同批次的数据包,by header
- 超文本传输协议,HyperText Transfer Protocol
- 状态码
- 1xx
- 2xx:成功 200
- 3xx:重定向,需要客户端重新发送请求
- 4xx:客户端错误,请求报文有误 400请求报文有误;403服务端禁止访问资源;404请求资源在服务端找不到
- 5xx:服务端错误 500服务端错误;501请求的功能尚未开放;502通常是网关或者代理时返回;503服务器忙无法处理
- http字段
- Host字段:客户端发送请求的时候,用于表示服务端域名
- Content-Length:服务端返回响应的时候,用于表明数据长度
- Connection:通常用于表示是长连接(
Connection: Keep-Alive
)- http/1.1默认长连接,这个字段是为了兼容老版本http
- Content-Type:用于告诉客户端,本次数据的格式
- Content-Encoding:用于说明数据的压缩方式
- Accept-Encoding:用于客户端表示自己可以接收的压缩方式
- 版本发展
- Http/1.0:无状态
- Http/1.1:长连接;body压缩
- Http/2.0:多路复用;二进制帧;header压缩;服务器推送(客户端请求1个资源的时候服务端一起发送其他的)
- Http/3.0:对udp进行改进,引入quic协议,无需建立连接;
- Http和Https:HTTPS 协议需要向 CA(证书权威机构)申请数字证书
- 对称加密和非对称加密结合的「混合加密」
- Https默认端口是443,http是80
- 多了一个TLS握手的过程。目的是为了通过非对称加密握手协商或者交换出对称加密密钥
如何优化https
- 硬件方面:是计算密集型任务,所以增强CPU,加速TLS过程
- 软件方面:协议优化升级,linux内核升级
- 协议方面:优化密钥交换算法
- 证书优化:
为什么有了HTTP还有websocket
- 为了解决服务器向客户端主动推送数据的需求(也可能通过客户端轮询http请求实现)
- 基于TCP全双工的特性,设计出来的一种新协议,即websocket
- 为了兼容http使用场景,三次握手建立连接后,如果是普通的http请求则维持原状,反之则在http请求头中增强特殊的header,开始进行websocket握手连接,此后就是websocket了。
6.3 TCP
抓包工具:Wireshark
特点:面向连接、可靠、基于字节流。
结构
- 序号(sequence number):标识本报文段所发送数据的第一个字节的序号
- 确认序列(acknowledgement number):期望收到对方下一个报文段的第一个数据字节序号,只有ACK为1的时候才生效
- 数据偏移:TCP报文段数据起始举例TCP报文段起始有多远
- 6个控制位
- URG:紧急位,=1时可以插队
- ACK:确认位,连接后置为1
- PSH:推送位,=1时,接收方尽快交付接收应用进程,不再等到缓存填满再向上交付
- RST:复位,=1时,表明TCP连接中出现严重差错,必须释放连接再重新建立连接
- SYN:同步位,=1时表明是一个连接请求/连接接收报文
- FIN:终止位,=1时候表明此报文发送方数据已发送完,要求释放连接
- 窗口:发送方发送窗口的大小,用来给接收方来调整接收窗口大小
TCP校验和的过程:由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动
覆盖TCP首部和TCP数据
伪首部:都是从IP数据报头获取的,12字节,包含源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP是6)、TCP报文长度(报头+数据)。
计算过程:
把伪首部、TCP报头、TCP数据划分为16位的一个个16进制数
将这些数逐个相加,记得溢出的部分加到最低位上
最后将得到的结果取反,则可以得到检验和位
三次握手
- 客户端发送连接请求报文段,无应用层数据
- SYN=1,seq=x,seq是序号
- 服务端为改TCP连接分配缓存和变量,并给客户端返回确认报文,无应用层数据
- SYN=1,ACK=1,seq=y,ack=x+1,ack是确认序列
- 客户端为该TCP连接分配缓存和变量,并给客户端发送确认,可以携带数据
- SYN=0,ACK=1,seq=x+1,ack=y+1
- 客户端发送连接请求报文段,无应用层数据
四次挥手
·1
- 主动方发送连接释放报文段。进入状态FIN_WAIT_1
- FIN=1,seq=u
- 被动方回送一个确认报文段。进入CLOSED_WAIT状态,主动方接收后进入FIN_WAIT_2
- ACK=1,seq=v,ack=u+1
- 被动方发送完数据,发出连接释放报文段。进入LAST_ACK状态。
- FIN=1,ACK=1,seq=w,ack=u+1
- 主动方回送一个确认报文段。进入TIME_WAIT状态。再等待时间计时器设置的2MSL(最长报文段寿命)后进入CLOSE。
- ACK=1,seq=u+1,ack=w+1
- 主动方发送连接释放报文段。进入状态FIN_WAIT_1
为什么需要四次挥手?
- TCP的半关闭造成的
为什么中间的ACK和FIN不可以像三次握手那样合为一个报文段呢?
- socket网络编程中,执行close()方法会触发内核发送FIN报文(用户态调用的close()),但是如果被动关闭方还有数据要处理,会等数据处理完毕后再调用close(),而ACK报文是系统内核完成,过程很快,所以ACK和FIN不能和为一个包
为什么TIME_WAIT是2MSL?
- 保证最后的ACK可以送达到被动关闭方,能够正常关闭
可靠传输:
- 校验:与UDP一样,增加伪首部
- 序号:一个报文段第一个字节的序号
- 确认:累计确认机制
- 重传:超时重传
重传机制:
超时重传:超时未收到指定的ACK报文就再发送,略大于RTT(一个往返时间)
快速重传:服务A发送seq1~6给服务B,但是seq2丢失,服务A只会收到ACK2,然后就再发送seq2
A只知道seq2未送达,所以发seq2,是否需要一起发送seq3~6?引入SACK
SACK:双方开启,在TCP头部加入SACK,可以将已接收的信息发送到发送方,避免少发或者多发
D-SACK(Duplicate ):
滑动窗口:
- 已发送且收到ACK确认
- 已发送但未收到ACK确认
- 未发送但大小在接收方处理范围
- 未发送但总大小超过接收方处理范围
流量控制:控制接收方窗口,为了防止接收方爆炸
- 通过设置报文段中的窗口字段来实现动态控制
- 零窗口问题:发送方收到了接收方的零窗口通知,启动计时器,一段时间后再询问接收方窗口大小(防止接收方窗口变更消息丢失而引发的死锁)
- 小窗口问题(糊涂窗口综合症):由于接收方处理数据能力,导致缓冲区的大小越来越小(20,10,5,4,2,1…),进而发送方可发送的数据量也越来越小,最终造成流量的浪费。解决:设置最小窗口阈值
拥塞控制:控制发送方窗口cwnd,为了防止网络环境爆炸
慢启动:建立连接后,先发送1个单位,接收ack应答后再发送2个单位,接收后再发送4个单位,直到达到慢启动门限,一般是2^16:65535字节
拥塞避免:达到慢启动门限后,指数增长变成线性(收到一个ack增加一个字节)
超时重传:cwdn变为1,再重新慢启动
快恢复
TCP的半连接队列(SYN队列)和全连接队列(accept 队列)
建立握手前,服务端接收到客户端请求后,内核会将连接存储在半连接队列中,并发送SYN+ACK
接收到客户端的第三次握手后,内核会移除SYN队列,创建完全的连接,加入到accept队列中
半连接队列满之后,就无法接收新的TCP连接了(SYN洪泛攻击就是利用这个特性)
SYN洪泛攻击如何解决?
利用SYN cookie:不保存半连接队列,生成一个序列(cookie)发送给请求连接方,cookie为由源IP、目的IP、源端口、目的端口以及一个服务器密钥组合后生成的hash,请求方第三次的握手需要携带这个序列,然后再由服务端来解析判断是不是同一组请求,如果第三次握手没有携带这个序列,则被判断为是攻击者,不做任何处理,也就避免了半连接队列爆
如何优化TCP连接?
三次握手的优化
- 客户端:SYN重传次数
- 服务端:SYN半连接队列、accept 全连接队列长度、ACK+SYN的重传次数
四次挥手的优化
- 主动方:FIN报文重传次数、FIN_WAIT_2状态时间、TIME_WAIT状态上限
- 被动方:
数据传输的优化:主要是针对滑动窗口
- 扩大窗口大小:连接的缓存区(滑动窗口)根据网络传输能力设置
- 调整发送方缓存区范围
- 调整接收方缓存区范围
- 打开缓存区动态调节
- 调整内存范围
怎么理解TCP是面向字节流的传输?(UDP是面向报文)
- 一次消息通过多次发送——粘包问题
- 解决粘包:固定长度、特殊字符(HTTP)、自定义消息结构体
为什么TCP每次建立连接的时候,初始化序列号不一样?
- 防止历史报文被错误接收。四次挥手的时候不是有一个2MSL的时长吗?(如果没有正常的断开,有错误的可能)
- 随机生成的序列号会冲突吗?是基于时钟计时器递增的,基本不会出现
syn包什么时候会被丢弃
- TCP两个队列满了(半连接SYN队列和全连接accpet队列),造成SYN报文丢弃
已建立连接的TCP,客户端突然掉线了,服务端不知道,客户端再上线的时候发起SYN握手,服务器怎么做?
- 客户端的 IP、服务端 IP、目的端口都没变,关键看源端口和上次是否一致?
- 一致:但是大概率与服务端的期望SYN不一致,服务端会返回期望的ACK报文,客户端收到后发现也不是期望的报文,于是回RST报文,释放连接
- 不一致:相当于建立一个新的连接。原来的连接由客户端内核返回一个RST报文,兜底是服务端检测客户端没有活动,释放连接
- 客户端的 IP、服务端 IP、目的端口都没变,关键看源端口和上次是否一致?
四次挥手的时候如果FIN报文比ACK先到达主动方会发生什么(第三次比第二次快到)
- 在FIN_WAIT_2状态的时候收到了乱序的FIN报文,会被加入到乱序队列中,并不会加入到TIME_WAIT状态,等再次收到数据包的时候(第二次挥手的包),会从乱序队列中找对应的乱序的FIN报文(有FIN标志),则进入TIME_WAIT状态
拔掉网线后,之前的TCP连接还在吗
- 拔掉网线后,有数据传输:如果在超时重传阈值之前恢复,则没有影响,反之服务端会断开连接,客户端再次发送的时候,服务端内核就会恢复RST报文
- 拔掉网线后没有数据传输:没有开启TCP保活机制,则会一直存在,开启的话会探活,超出次数则断开
HTTPS中TCP和TLS顺序?
- 先TCP三次握手再TLS
TCP协议的缺点
- 升级困难,因为是在内核中的,应用程序无法升级
- TCP建立连接延迟:三次握手
- TCP存在队头阻塞的情况:如果seq1~5中seq2丢失,内核是无法处理seq3~5的
- 网络迁移需要重新建立TCP连接:四元组发生了变化(源 IP、源端口、目的 IP、目的端口)
如何基于UDP实现可靠传输?QUIC协议
UDP和TCP可以使用同样的端口吗?可以,因为在内核中是两个完全独立的软件模块。
半包:发送方发送的数据过大,超过了缓冲区,导致数据接收不完整的问题
- 粘包:拆包后进行合并,由于信息不完整导致的信息错乱问题
6.4 IP
类型
- A类: 0 + 7位网络号 + 24位主机号
- B类: 10 + 14位网络号 + 16位主机号
- C类: 110 + 21位网络号 + 8位主机号
- D类:1110 + 28位组播地址,多用于多播
- E类: 1111 + 预留后用
主机号全为0指某个网络,全为1指定某个网络下的所有主机,用于广播
优点:简单明了、选路(基于网络地址)简单(因为可以通过前3位快速判断是ABC类)
缺点:
- 同一网络下没有地址层次
- ABC不能很好的与现实匹配,C类地址的主机只有254,B类却有65534
无分类地址CIDR:表示形式
a.b.c.d/x
,前x位为网络号子网划分:将主机号部分再分为子网网络号 + 子网主机号
IPv6:128位,每16位作为一组
NAT技术:IP地址有32位,最多只能 2 ^ 32 = 4294967296 台设备加入互联网,为了解决地址不足的问题,引入NAT
IP协议的相关技术
DNS 域名解析:越往右层级越高
- 查缓存 ==> 查操作系统本机域名解析文件
hosts
==> 进行DNS域名查询
- 查缓存 ==> 查操作系统本机域名解析文件
ARP 与 RARP 协议:查询IP地址的下一跳对应的MAC地址,RARP相反
- 主机广播发送ARP请求 ==> 同一链路的所有设备查询如果有就返回ARP响应
DHCP 动态获取 IP 地址
- DHCP 客户端进程监听的是 68 端口号,DHCP 服务端进程监听的是 67 端口号
- 客户端发起DHCP发现报文,但是此时客户端没有IP地址,也不知道DHCP服务器IP地址,就通过广播发送UDP
- 服务端收到后发送DHCP 提供报文,也是广播发送:提供可租约的 IP 地址、子网掩码、默认网关、DNS 服务器以及 IP 地址租用期。
- 客户端收到一个或多个报文后,选择一个服务端,发送DHCP 请求报文
- 服务端 DHCP ACK 报文对 DHCP 请求报文进行响应,应答所要求的参数
NAT 网络地址转换:IP 地址 + 端口号一起进行转换
- 缺点:
- 外部无法主动与NAT内部服务建立连接,因为没有转换记录
- 转换地址的性能开销
- 依赖于转换表,如果NAT路由器重启了,所有TCP连接都将会重置
- 如何解决?
- Ipv6
- NAT穿透技术
- 缺点:
ICMP 互联网控制报文协议:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。
- 查询报文(诊断)和差错报文(通知出错原因)
- 常见差错报文类型
- 目标不可达消息 —— 类型 为
3
- 原点抑制消息 —— 类型
4
- 重定向消息 —— 类型
5
- 超时消息 —— 类型
11
- 目标不可达消息 —— 类型 为
IGMP 因特网组管理协议:管理D类地址,组播
Ping的原理:基于ICMP协议进行
断网了还能ping 127.0.0.1吗?可以
localhost 和 127.0.0.1
- 本质上localhost 是域名,是在本机中的hosts文件中定义的指向127.0.0.1
七 操作系统
7.1 硬件结构
内存;CPU;总线;输入、输出设备
存储器的结构:寄存器、CPU Cache(L1,L2,L3)、内存、SSD/HDD 硬盘
- L1 L2是各核心独有的
多核心CPU的缓存一致性(L1 L2)
- 通过写传播和事务的串行化
总线嗅探:写传播的实现,通过广播来告诉其他核心数据变化
MESI协议
- Modified,已修改
- Exclusive,独占
- Shared,共享
- Invalidated,已失效
CPU如何执行任务的
- 如何读写数据:CPU三级缓存
- 伪共享问题:CPU从内存中读取数据是Cache Line 为单位(一组数据),如果两个核AB读取了空间上连续的变量ab,分别只修改了a、b,但是会造成数据的不一致问题
- 如何避免:多个线程共享的热点数据,避免这些数据在同一个Cache Line中
- 如何选择线程:linux内核中,线程和进程都是
task_struct
结构体- 调度算法
- 如何读写数据:CPU三级缓存
0.1 + 0.2 == 0.3?
负数的二进制:1(符号位,正数是0) +( 正数部分的补码+1)
1
2
3
4-1 : 1 1111111 11111111 11111111
1 : 0 0000000 00000000 00000001 (int类型)
补码: 1 1111111 11111111 11111110
再+1: 1 1111111 11111111 11111111
7.2 操作系统结构
- 内核的能力
- 管理进程、线程,决定哪个进程、线程使用 CPU,也就是进程调度的能力;
- 管理内存,决定内存的分配和回收,也就是内存管理的能力;
- 管理硬件设备,为进程与硬件设备之间提供通信能力,也就是硬件通信能力;
- 提供系统调用,如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口。
7.3 内存管理
虚拟内存:为了让不同的进程同时运行而互不干涉,操作系统提供一种映射,将不同进程的虚拟地址和物理地址映射
- 如果没有虚拟内存,同一个代码多线程运行的时候就会产生物理地址冲突
- 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
如何管理虚拟地址和物理地址之间的关系
内存分段:带来内存外碎片和内存交换效率低(解决碎片进行swap重分配的过程)的问题
内存分页:会出现内存内碎片、对进程来说不太友好
- 换入Swap In和换出Swap Out是指操作系统内存不够时候将页表写到硬盘暂时释放的过程,因为一次只写几页,所以效率较高
- 带来的问题:页表内存大 ==> 多级页表
段页式内存管理:对于进程来说看到的是段表,对物理内存来说看到的是页表
操作系统怎么分配内存的?
malloc()
:分配的是虚拟内存,如果没有被访问是不会映射到物理内存的,当程序开始读写的时候才分配内存满了怎么办?当物理内存满了,会发送缺页中断,进程切换到内核态
- 后台内存回收:异步的,不阻塞进程
- 直接内存回收:同步的,会阻塞进程,后台回收更不上的时候触发
- 触发OOM机制:根据算法kill占用物理内存高的进程
回收哪些文件?文件页、匿名页
4GB的物理内存机器申请8GB的内存
- 32位操作系统:无法申请,最多3GB(内核1GB,用户3GB)
- 64位操作系统:可以,但是如果使用的时候超出了4GB,且没有开启swap机制,会OOM
如何避免预读失效和缓存污染的问题?本质是LRU算法
- Redis的做法是LRU,Mysql和Linux的做法是改进的LRU算法
- Linux读取:在文件系统中增加Page Cache页缓存,属于内存里的数据,加快访问速度
Mysqld读取:Innodb存储引擎中设计了一个Buffer Pool缓冲池,属于内存里的数据,修改数据直接修改缓冲池,后台再写入磁盘
Linux和Mysql的LRU操作单位都是页
- 什么是预读?如果需要磁盘A的0-3kb数据,linux会读取一个页(0-4kb),为了减少将来的IO次数,会预读3个page,也就是0-16kb都会读取。
- 什么是预读失效?就是后面预读的数据没有被用到
- 如何避免预读失效?改进的LRU
- Linux:实现两个LRU链表,活跃LRU、非活跃LRU链表
- Mysql:在一个LRU链表中划分为:young 区域 和 old 区域
- 预读的数据就放在非活跃链表/old区域即可
- 什么是缓存污染?只访问一次的数据放到链表头部,多了之后就会淘汰热点数据,如果这些数据长时间不被访问就会造成污染,导致下次访问热点数据的时候产生大量的IO
- 如何解决缓存污染?
- Linux:非活跃链表的数据读取两次才进入活跃LRU头部
- Mysql:old区域被访问两次且两次时间间隔在1s以上才会进入young区域
深入理解虚拟内存
划分:
- 代码段:用户的代码
- 数据段:代码中指定的初始值的全局变量和静态变量
- BSS段:没有指定初始值的全局变量和静态变量,加载进内存后初始化为0
- 堆:以上是编译阶段申请的,堆是用于存放运行过程中动态生成的内存
- 文件映射与匿名映射区:动态链接库中的代码段、数据段、BSS段,以及内存映射区域的文件映射与匿名映射区
- 栈:程序调用函数过程中使用到的局部方法和函数参数
范围
- 32位系统中指针的寻址范围是2^32,所能表达的区域大小就是4GB,其中用户态3GB,内核空间1GB
- 64位系统中指针的寻址范围是2^64,但是只用了48位来描述空间,也就是256TB,内核、用户各一半
内核是如何划分和管理的?
mm_struct
结构体定义上述不同区域的范围,通过task_size 域来划分用户和内核空间若干个
vm_area_struct
结构体一一对应了上述的区域
7.4 进程管理
进程:代码编译为二进制可执行文件,运行文件后装载进入内存,CPU会执行程序中的命令,运行中的命令就是进程
线程:进程由若干个线程组成,操作系统中执独立运行的最小单位
并发:单个CPU一个时间段执行了多个进程
并行:多个CPU一个时间点执行了多个进程
进程状态:
- 多个进程阻塞的时候,会占用物理内存空间,所以在虚拟内存管理的操作系统中,会把阻塞状态的进程的物理内存换出到磁盘,这个时候进程的状态就变成了挂起
PCB(process control block):进程控制块,用于描述进程,1对1
- 进程描述信息:进程标识符、用户标识符(进程归属的进程)
- 进程控制和管理信息:当前进程的状态、进程优先级
- 资源分配清单:有关内存地址空间或虚拟地址空间的信息
- CPU相关信息:CPU中各个寄存器的值,进程被切换的时候需要保存CPU的状态信息
多个PCB通过链表的结构把状态相同的进程串在一起,组成各种队列:阻塞队列、就绪队列等
进程的控制
- 创建进程:申请一个空白的PCB,填入相关信息,分配内存资源,并加入到就绪队列中
- 终止进程(正常结束、异常结束以及外界干预(信号
kill
掉)):查找PCB、如果处于执行状态,就立即结束、如果有子进程就交给1号进程管理、归还资源给操作系统、从PCB队列删除 - 阻塞进程:找到PCB、阻塞、插入到阻塞队列中
- 唤醒进程:找到PCB、设置状态为就绪、插入就绪队列
进程的上下文切换:一个进程切换到另一个进程运行
- CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文。
- 所以就是存储和读取PCB中的CPU相关信息的过程,
线程和进程
- 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销;
为什么线程能减少并发执行的时间和空间开销?
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
- 由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
线程的上下文切换和进程的区别
- 如果是不同进程的线程切换,就和切换进程一样
- 如果是同一进程内的线程切换,只需要切换线程的私有数据、寄存器等不共享的数据,开销会小很多(没了虚拟内存等资源切换)
线程的实现:
- 用户线程:在用户空间实现的线程
- 内核线程:在内核中实现的线程,是由内核管理的线程,多个用户线程对1个内核线程
- 轻量级线程:在内核中来支持用户线程;
用户线程的管理:基于用户态的线程管理库来实现的。线程控制块(Thread Control Block, TCB) 也是在库里面来实现的,
- 对于操作系统而言是看不到这个TCB的,TCB是进程的私有
- TCB中跟踪记录了各个线程的状态信息(PC、栈指针、寄存器)
- 优点:切换由线程管理库实现,不用用户态和内核态切换,速度快;由TCB记录线程信息
- 缺点:操作系统不参与其调度,如果一个线程发起系统调用而阻塞,那进程所包含的用户线程都不能执行了;
内核线程的管理:也是通过TCB
- 内核线程的TCB是操作系统来管理的
- 优点:如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
- 缺点:内核来维护进程和线程的上下文信息,如 PCB 和 TCB;线程的创建、终止和切换都是通过系统调用的方式来进行,开销大
进程之间的通信:关键是如何通过内核空间来通信
管道:
mkfifo myPipe
创建一个管道,本质是内核空间里面的内存(队列),两端分别实现读写- 如果是父子进程,在fork的时候就会创建管道,并绑定到
- 如果不是父子进程,则是通过共同父进程例如shell
- 但是效率低
消息队列:本质是保存在内核中的消息链表
- 通信异步但是不适合大数据的传输
- 还存在用户态到内核态的数据拷贝开销
共享内存
- 解决用户态、内核态的开销,虚拟内存映射到相同的物理内存中即可
- 带来了写写问题
信号量:表示资源的数量
>= 0
表示可以访问- 用于进程之间的互斥和同步
信号:
kill -l
查看所有的信号- 用于异常情况下的工作模式,比如shell中
ctrl + C
产生SIGINT
信号,表示终止进程
- 用于异常情况下的工作模式,比如shell中
socket:用于跨网络与不同主机的进程通信
系统调用:
int socket(int domain, int type, int protocal)
简述TCP
- 服务端、客户端初始化socket,得到文件描述符
- 服务端调用
bind
,绑定IP和端口 - 服务端调用
listen
,进行监听 - 服务端调用
accept
,等待客户端连接 - 客户端调用
connet
,请求连接 - 服务端调用
accept
,返回用于传输socket
的文件描述符 - 客户端调用
write
写数据,服务端调用read
读数据 - 客户端调用
close
断开,服务端read
的时候就会读到EOF
,处理完毕后,服务端调用close
,关闭
多线程冲突问题
- 锁:加锁、解锁
- 信号量:P(-1)、V(+1)操作,实现对临界区的互斥访问
死锁问题
- 互斥条件:多个线程不能同时使用同一个资源
- 持有并等待条件:线程在等待资源的同时不会释放自己持有的资源
- 不可剥夺条件:线程正在使用的资源不可被剥夺
循环等待条件:死锁发生时,两个线程获取资源构成了链
从以上四个条件进行破坏
锁分类
- 乐观锁:CAS
- 悲观锁:互斥锁、自旋锁
一个进程可以创建多少线程
- 进程虚拟内存空间的上限(32位3G,64位128T)
- 系统参数的限制
线程崩溃进程也会崩溃吗?不一定,比如线程崩溃不会导致JVM进程崩溃
- 线程崩溃后通过信号(SIGSEGV )来告诉进程,JVM自定义了自己的信号处理函数,拦截了SIGSEGV 信号
7.5 调度算法
- 进程调度算法
- 先到先服务
- 最短作业时间
- 高响应比优先调度算法:优先权= (等待时间 + 要求服务时间) / 要求服务时间
- 时间片轮转调度算法
- 最高优先级调度算法
- 多级反馈队列调度算法:时间片+ 优先级
- 优先级越高时间片越短,最开始进入优先级最高的队列,没执行完进入下一级队列
- 内存页面置换算法:当出现缺页异常,需调入新页面而内存已满时,选择被置换的物理页面
- 最佳页面置换算法:选择未来最长时间不访问的页面。
- 先进先出置换算法:选择在内存驻留时间很长的页面进行中置换
- 最近最久未使用的置换算法(LRU)
- 时钟页面置换算法
- 磁盘调度算法:优化磁盘的访问请求顺序
- 先来先服务
- 最短寻道时间优先算法
- 扫描算法:避免磁头在一个小区域来回扫动
- 循环扫描算法
- LOOK 与 C-LOOK算法
7.6 文件系统
linux文件数据结构
- 索引节点inode:记录文件的元信息,唯一标识,会被存储于硬盘
- 目录项dentry:记录文件名、与其他目录项的层级关系,由内核维护,缓存于内存(与inode多对一)
文件如何存储在硬盘?
磁盘最小读取单位是扇区 512B,为提高效率,linux以逻辑块为单位:8个扇区 4KB
虚拟文件系统(Virtual File System,VFS)
- 针对文件系统种类繁多(磁盘、内存、网络),为给用户提供统一的接口,在用户层和文件系统中提供的中间层
文件存储
- 连续存储
- 非连续:链表+索引(指向文件头)
空闲空间管理
空闲表法
空闲链表法
位图(linux使用),每位代表 块
假设是在一个块中,共可以表示
4 * 1024 * 8 = 2^15
个空闲块,最大的空间是2^15 * 4 * 1024 = 2^27
个 byte,也就是 128M。N个 一个块的位图 + 一系列的块 称为块组,用来表示文件
软链接和硬链接:给某个文件取个别名
- 硬链接:多个目录项中的「索引节点」指向一个文件,删除所有的硬链接以及源文件才彻底删除
- 软链接:相当于重新创建一个文件,有独立的inode,内容是另一个文件的路径
文件IO
- 缓存IO/非缓存IO:前者是通过标准库的缓存实现文件的加速访问,标准库再通过系统调用访问文件。
- 直接与非直接 IO:是否使用了内核缓存
阻塞与非阻塞IO:用户线程执行
read
线程是否会被阻塞- 阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程
同步与异步IO:
7.7 设备管理
从键盘敲入字母发生了什么
- 输入字符后,键盘控制器就会产生扫描码数据,并将其缓冲在键盘控制器的寄存器中,紧接着键盘控制器通过总线给 CPU 发送中断请求
- CPU 收到中断请求后,操作系统会保存被中断进程的 CPU 上下文,然后调用键盘的中断处理程序
- 键盘中断处理程序:键盘驱动程序初始化注册的,功能是从键盘控制器的寄存器缓冲区读取扫描码,再根据扫描码找到字符,并翻译为对应的ASCII码,放到显示字符读缓冲区队列,由显示器读取
7.8 网络管理
DMA—直接内存访问(Direct Memory Access) 技术
- 为了解决CPU读取磁盘内容带来的CPU中断问题,DMA读取完数据后发送中断信号给CPU,让CPU读数据
- 完成内核态与对应硬件设备(磁盘、网卡等)之间的数据拷贝。
传统的文件传输
- 过程
- CPU发送指令给IO设备的DMA,由DMA拷贝数据从磁盘到内核空间
- DMA拷贝完成,触发CPU的中断,CPU开始将数据从内核空间拷贝到用户空间
- CPU继续将数据从用户空间拷贝到内核的socket buffer
- DMA将数据从socket buffer拷贝到硬件设备(网卡)
- 发生了4次用户态内核态的转换以及4次数据拷贝,如何优化?
- 减少切换次数和数据拷贝次数
- 过程
零拷贝技术(kafka,nginx使用)
mmap()
:将read()
改为mmap()
。改进是在内存中建立一个到磁盘的映射,数据直接同步在内存,由操作系统异步同步内存到硬盘的数据。适合小数据的传输,rocketMQ- 4次上下文切换(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态)和3次拷贝(磁盘DMA拷贝到内核缓冲区 -> 内核缓冲区CPU拷贝到socket缓冲区 -> socket缓冲区DMA拷贝到协议引擎)
sendfile()
:将read()
改为sendfile()
。 改进是直接在内核态中进行缓存区到socket缓冲区的数据拷贝2次上下文切换(用户态 -> 内核态 -> 用户态)和2次拷贝(磁盘DMA拷贝到内核缓冲区 -> 内核缓冲区DMA拷贝到协议引擎)
- 为什么还需要CPU中断?因为DMA不知道拷贝的地址,这个是由CPU指定的,DMA的寻址范围比CPU小,一般会预留低位的地址给DMA使用
对
sendfile()
的改进:去掉步骤2中的CPU拷贝sendfile+DMA scatter/gather
内核缓存区kernel buffer中的数据不再拷贝到socket buffer中,而是直接在后者里面存储一个内存地址和偏移量,这样网卡的DMA访问到socket buffer后可以直接定位到内核缓存区进行数据拷贝,实现CPU的零拷贝
page cache
:缓存最近被访问的数据,零拷贝技术的基础- 解决机械硬盘寻址慢的问题
直接IO与缓存IO:对于大文件,不适合使用page cache这种缓存IO,对于磁盘,异步 I/O 只支持直接 I/O。
网络socket基本过程:见进程管理部分
- 最大连接数限制:文件描述符(socket也是文件)限制1024默认、系统内存限制
IO多路复用:一个进程维护多个Socket
select:将socket放到一个文件描述符集合(固定长度1024的 BitsMap)。需要2次遍历文件描述符集合,2次拷贝文件描述符集合
poll:bitsMap变为了链表
epoll:
文件描述符使用红黑树O(logn),只在内核维护,减少了很多的遍历和复制
使用事件驱动机制,内核维护了一个链表记录就绪事件,当有socket事件发生的时候,通过回调函数内核将其加入到列表中,当用户调用
epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数(但是还是会拷贝),不需要像select/poll那样轮询整个socket集合
Reactor/Proactor:对IO多路复用的封装
- Reactor(同步非阻塞)、Proactor(异步)
- 对象
- reactor:监听和分发事件
- acceptor:获取事件
- handler:处理业务
- 单reactor/单进程线程模型
- 只有一个进程;handler处理业务的时候进程无法处理其他的连接
- 单reactor/多线程进程模型
- reactor通过select(IO多路复用接口)监听,根据事件类型分发给handler和acceptor
- 多reactor/多线程进程模型(nginx)
- 主线程接收连接,子线程处理连接
- Proactor
一致性哈希:解决多个请求分配客户端请求的问题
哈希算法的不足:比如取模,如果节点的数量发生变化,则需要进行额外的数据迁移
一致性哈希算法解决的问题:分布式系统扩容或缩容的数据迁移问题
步骤:计算在哈希环中的位置,顺时针取遇到的第一个节点
- 如果发生节点的增删,只影响前后节点,对全局其他节点无影响
问题:
- 不保证数据的均匀性
改进:增加虚拟节点,环上的点就是虚拟节点,增加一个映射到真实节点
八 工具
1 git
命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30git init
git status
git clone url # 支持http(s) ssh git等协议
git checkout branch|tag|commit # 切换到指定分支
git merge branchName # 将branchName的修改合并到当前分支中
git add .
git commit -m "message"
git push origin master # 将本地的 master 分支推送到 origin 主机的 master 分支
git reset HEAD # 取消已经暂存的文件
git revert HEAD # 撤销前一次操作
git revert commit # 撤销指定操作
git remote add origin [email protected]:JSLite/test.git # 添加一个新的远程仓库 add <remote_name> <remote_url>
git diff --stat # 查看简单的diff结果
git diff branch # 比较Worktree和branch之间的差异
git merge --squash branchName # 将branchName的多次提交统一变更到当分支(此后还需要再add commit push)
git log # 日志相关已经push的操作如何回退?
要回到vesion2
不保留git记录
先reset到要回退的版本:
git reset commitID
然后强制push上去:
git push origin master:master -f
保留git记录
git revert commitID
,选择merge然后push
分支的多次commit变成一次提交到master
方法一:先从版本库回退内容到暂存区,再重新提交工作区的内容
git reset --soft commitID
,此时版本回到commitID对应的状态,但是保留了改动(也就是之后多次的commit修改内容)- 然后git add commit push
方法二:rebase
git log 查看提交记录,找到最早的那次提交的commitID的前一次
- 我想合并commit1678 就找到commit1的前一次的id
git rebase -i commitID
,随后在vim编辑器中修改前面的提交由pick变为fixup- fixup:使用commit,丢弃commit信息。
- pick:使用commit。
- squash:使用commit,将commit信息合入上一个commit。
- reword:使用commit,修改commit信息。
- 可以通过
git reflog
取消rebase
最后
git push --force
即可
2 maven
九 Spring
9.1 简介
- 特性:
- 非侵入
- 控制反转IOC:由框架管理bean,是一种思想
- 依赖注入DI:控制反转的实现方式,是一种实现方式
- 切面AOP
- 容器管理
- core container:spring核心容器,由Beans、Core、Context、SpEL模块组成
- Beans:基础,提供了控制反转和依赖注入
- Core:封装了底层,包括资源访问、类型转化等工具
- Context上下文:ApplicationContext接口
- SpEL:语言表达式支持
- Data Access/Integration:数据访问/集成
- JDBC:提供了JDBC模块,
- ORM:提供了ORM框架集成的API,包括JPA、JDO、Hibernate 和 MyBatis 等
- OXM:提供了XML与Java对象的转化功能
- JMS:Java消息服务,异步通信
- Transactions:编程和声明式事务的管理
- Web:web模块
- Web
- Servlet:提供了spring mvc web框架实现
- WebSocket:实现双向通信
- Webflux
- Portlet 模块
- AOP、Aspects、Instrumentation和Messaging
- Test模块:支持Junit 和 TestNG 测试框架,还有模型http请求等功能
- spring的启动过程
- 加载配置文件:xml,javaConfig类,配置数据库连接、事务管理、AOP等
- 实例化容器:实例化BeanFactory,如创建ApplicationContext;并加载BeanDefinitions
- 解析BeanDefinitions:Bean的元数据、作用域、依赖关系等
- 实例化Bean
- 依赖注入
- 处理Bean生命周期初始化方法
- 处理BeanPostProcessor
- 代理切面处理
- 发布事件
- 完成启动
9.2 控制反转IOC、及DI
三种配置方式
xml
1
2
3
4
5
6
7
8
9
10
11
12
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- services -->
<bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions for services go here -->
</beans>java配置:创建配置类,
@Configuration
注解声明,在bean中用@bean
声明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BeansConfig {
/**
* @return user dao
*/
public UserDaoImpl userDao() {
return new UserDaoImpl();
}
/**
* @return user service
*/
public UserServiceImpl userService() {
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(userDao());
return userService;
}
}注解配置:
@Component
,@Controller
,@Service
,@Repository
,但是要配置spring 的注解扫描器ComponentScan
依赖注入:一种设计模式,用于解耦成员和管理类之间的关系,核心思想是将对象的创建交给外部(IOC)容器管理
构造方法注入,xml中
<constructor-arg>
setter注入,xml中的
<property>
自动装配:是依赖注入的一种自动化形式,不需要开发者显示的指定哪些依赖要注入到哪个类中,由spring自动完成
byName
byType:如果多个bean类型相同,则需要byName
构造器注入:容器查找类型相匹配的bean注入
1
2
3
4
5
6
7// 三个属性Constructor,byType,byName,默认byType(查找set方法)
// byName,如果没有找到会抛异常,防止这个可以@Autowired(required = false)
// byType,javaEE提供
// byName,javaEE提供
// 和Qualifier一样,javaEE提供
private HelloDao helloDao;
spring IOC的设计
- 加载Bean的配置(如xml)
- 根据bean定义加载生成bean的实例,并放置在容器中
- 统一管理bean(工厂模式)
interface BeanFactory
interface ApplicationContext
:IoC容器的接口类- 除了对bean的管理外,还包括了资源访问、国际化、应用事件
循环依赖:三级缓存
一级缓存singletonObjects:已经初始化好的bean,即已经完成初始化好的注入对象的代理
二级缓存earlySingletonObjects:还没有完全被初始化好的中间对象代理
三级缓存singletonFactory:存放的是还未初始化完的bean,不是代理对象
为什么需要三级?
- 针对于AOP,singletonFactory缓存相当于是返回的是A的代理
ps:
- 不能解决构造器的依赖(使用
@Lazy
解决) - 只能解决单例bean的循环依赖(多实例Bean是每次调用一次getBean都会执行一次构造方法并且给属性赋值,根本没有三级缓存,因此不能解决循环依赖)
- 不能解决构造器的依赖(使用
自动装配(spring boot)
- 依赖注入的plus,简化了依赖注入的配置而生成的
- 主启动类上的注解
@SpringbootApplication
是一个复合注解,其中比较重要的@SpringbootConfiguration
:springboot的相关配置@EnableAutoConfiguration
@ComponentScan
:扫描一些包并注入
- 重点
@EnableAutoConfiguration
,也是一个复合注解@import(AutoConfigurationImportSelect.class)
AutoConfigurationImportSelect
implements了DeferredImportSelector
因为是Deferred,所以自动配置类会放在最后加载,方便扩展和覆盖
其中重写了
selectImports()
方法:获取所有符合条件的类的全限定类名,加载到IOC容器中判断是否开启了自动装配:
spring.boot.enableautoconfiguration=true
获取
@EnableAutoConfiguration
中的exclude
和excludeName
读取所有spring boot start下面的classpath:/META-INF/spring.factories文件(key-value形式)
通过
@ConditionalOn
排除无效的自动配置类
- 配置文件的信息如何加载到bean中:classpath:/META-INF/spring.factories文件存储了一些键信息(SpringBoot约定大于配置的理念),然后加载的时候配置文件的值就被加载到这个文件中了
- 两种
@Autowired
:根据类型自动注入@Qualifier
:格外指定bean的id(当IOC根据属性类型去容器中找找到多个相同类型的组件的时候需要使用)
@Resource
:根据bean的名称自动注入- 这个是Java规范
9.3 AOP
- 目的:AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。
- 概念:
- 连接点:表示需要在程序中插入横切关注点的扩展点
- 切入点:选择一组相关连接点的模式,即可以认为连接点的集合
- 通知:前置通知、后置通知、环绕通知、最终通知、异常通知
@After @Around @Before @AfterReturning @AfterThrowing
- 切面:公共代码
- 引入:引入允许我们向现有的类添加新方法或属性
- 织入:把切面应用到目标对象并创建新的代理对象的过程
- 动态织入:通过动态代理完成,运行期织入(Spring AOP采用的方式)
- 静态织入:AspectJ,编译期织入
- AOP代理:JDK(反射)和CGLib(继承)两种方式
- 都是运行期间生成字节码,jdk直接生成class字节码,cglib使用ASM框架写的class字节码。后者更加复杂,代理类生成效率更低
- jdk动态代理是通过反射机制来执行方法,cglib是通过FastClass机制(索引分配直接调用)直接调用方法,后者动态代理类执行效率更高,但是cglib无法增强final方法(AspectJ可以)
最佳实践
@Aspect
:定义切面@pointcut
:定义切点@After @Around @Before @AfterReturning @AfterThrowing
:定义通知
Spring AOP和AspectJ关系:Spring AOP更易用,AspectJ更强大
9.4 Sping MVC
Spring MVC是Spring在Spring Container Core和AOP等技术基础上,遵循上述Web MVC的规范推出的web开发框架,目的是为了简化Java栈的web开发
- 核心组件
- DispatcherServlet:前端控制器,负责将请求分派给相应的处理器(Controller)
- HandlerMapping:适配找到Handler
- HandlerAdapter:适配处理器,用来找到Controller方法
- Controller:处理用户请求,返回ModelAndView对象
- ModelAndView:包含模型和视图
- ViewResolver:解析视图
- 核心组件
拦截器 Interceptor,类似于servlet中的Filter(但是Filter是基于Servlet层面的,会比Interceptor先被执行)
实现
HandlerInterceptor
接口1
2
3
4
5public interface HandlerInterceptor {
default boolean preHandle(...)
default void postHandle(...)
default void afterCompletion(...)
}注册拦截器:实现
WebMvcConfigurer
接口,重写addInterceptors()
方法注册拦截器配置拦截器:
addPathPatterns()
设计对应url路径,excludePathPatterns()
排除路径1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WebMvcConfig implements WebMvcConfigurer {
private PermissionInterceptor permissionInterceptor;
private CookieInterceptor cookieInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(permissionInterceptor).addPathPatterns("/**");
registry.addInterceptor(cookieInterceptor).excludePathPatterns("/path");
}
}配置类上使用
@EnableWebMvc
启动mvc配置
9.5 Bean
生命周期
BeanFactoryPostProcessor
如果和bean关联,则postProcessBeanFactory()
:尝试从Bean工厂中获取BeanInstantiationAwareBeanPostProcessor
- postProcessBeforeInstantiation(),如果这里返回了bean实例,就会直接跳到postProcessAfterInitialization()
- postProcessAfterInstantiation(),bean实例化完后调用
- bean构造函数
- bean调用setter方法初始化
- 如果实现了
xxxAware
接口(bean需要实现这些接口),调用对应的setxxx()
方法BeanNameAware
接口:传入当前 Bean 的 id 值BeanClassLoaderAware
接口:传入classLoader的引用BeanFactoryAware
:传入当前工厂实例的引用EnvironmentAware
传入当前 Environment 实例的引用ApplicationContextAware
传入当前 ApplicationContext 实例的引用
- 如果实现了
BeanPostProcessor
接口postProcessBeforeInitialzation()
,bean的init方法之前,Spring 的 AOP 就是利用它实现的
- 如果实现了
InitializingBean
接口(或者@PostConstruct) ,则调用afterPropertiesSet()
方法 - 调用bean自身的
initMethod()
方法(@Bean
注解) 如果实现了
BeanPostProcessor
接口postProcessAfterInitialization()
,bean初始化后
使用bean
- 如果实现了
DisposableBean
接口,则销毁的时候调用destroy()
方法 - 执行bean自身的
destroyMethod()
方法 (@Bean
注解)
- 作用域
- singleton 单例
- prototype 原型
- request
- session
- application
- websoket
9.6 事务
@Transactional
:原理是基于ThreadLocal,动态代理什么时候spring 的事务会失效?
- 多线程
- 异常被catch了,没有抛出,这样事务无法捕获到异常
- 同一个类方法的调用如果没有用代理类会失败
- 注解作用于非public方法,或者final、static方法,无法被代理,事务失效
- 数据库用的MyISAM,引擎本身不支持事务
9.7 常用注解
1 |
|
十 Spring Boot
- 解决spring配置重量级的问题,约定大于配置
10.1 常用注解
1 | // main |
10.2 开发实践
对参数进行统一校验:为了解决在Controller中频繁对入参对象校验的不优雅
1
2
3
4
5<!-- spring validation是对hibernate validation的二次封装,后者是对validation-api标准的实现 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>定义入参对象类,在字段中添加相关注解
1
2
3
4
5
// 正则匹配Controller中入参添加
@Valid
注释统一异常处理:
@ControllerAdvice
及@ExceptionHandler
接口版本控制:
1
2
3
4
5
6
7
8
9
10相同URL,用不同的版本参数区分
api.man.tech/user?version=v1
api.man.tech/user?version=v2
区分不同的接口域名,不同的版本有不同的子域名, 路由到不同的实例:
v1.man.pdai.tech/user
v2.man.pdai.tech/user
网关路由不同子目录到不同的实例
api.man.tech/v1/user
api.man.tech/v2/user
同一实例,用注解隔离不同版本控制接口文档生成swagger
访问外部接口(调三方服务)
- 采用原生http请求
- feign消费(依赖 +
@EnableFeignClients
):本质也是http - 采用RestTemplate方法
保证接口的幂等性:
哪些操作需要保证幂等性?PUT和POST,DELETE如果需要返回删除的行数则也需要
- 数据库加悲观锁
- 唯一索引:注意是分布式ID,针对插入操作
- 乐观锁:版本号,针对更新操作
- 分布式锁:redis
针对接口进行签名:防篡改
认证和授权
https
接口签名(加密):1. 对请求参数按key进行字母排序 2. 排序完连接用& 3. 加密字符串得到sig,拼接到后面
https://man.xxxx.com/info?key=value&timetamp=xxxx&sign=xxxx-xxx-xxx-xxxx
接口限流
- 单实例:限流总资源数、总并发数、某接口的请求总量、某个时间窗的请求数
- 分布式:redis+lua 或者 nginx+lua
跨域请求
@Configuration
实现WebMvcConfigurer接口,重写addCorsMappings方法,1
2
3
4
5
6
7
8
9
public class MyWebMvcConfigurer implements WebMvcConfigurer {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/user/*")
.allowedOrigins("http://localhost:8080")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
}
10.3 其他
自动装配——SpringBoot约定大于配置的理念的产物
- 依赖注入的plus,简化了依赖注入的配置而生成的
- 主启动类上的注解
@SpringbootApplication
是一个复合注解,其中比较重要的@SpringbootConfiguration
:springboot的相关配置@EnableAutoConfiguration
@ComponentScan
:扫描一些包并注入
- 重点
@EnableAutoConfiguration
,也是一个复合注解@import(AutoConfigurationImportSelect.class)
AutoConfigurationImportSelect
implements了DeferredImportSelector
因为是Deferred,所以自动配置类会放在最后加载,方便扩展和覆盖
其中重写了
selectImports()
方法:获取所有符合条件的类的全限定类名,加载到IOC容器中判断是否开启了自动装配:
spring.boot.enableautoconfiguration=true
获取
@EnableAutoConfiguration
中的exclude
和excludeName
读取所有spring boot start下面的classpath:/META-INF/spring.factories文件(key-value形式)
通过
@ConditionalOn
排除无效的自动配置类
- 配置文件的信息如何加载到bean中:classpath:/META-INF/spring.factories文件存储了一些键信息(SpringBoot约定大于配置的理念),然后加载的时候配置文件的值就被加载到这个文件中了
- 两种
@Autowired
:根据类型自动注入@Qualifier
:格外指定bean的id(当IOC根据属性类型去容器中找找到多个相同类型的组件的时候需要使用)
@Resource
:根据bean的名称自动注入- 这个是Java规范
springboot启动web服务的过程:
run()
方法里面会创建web服务器,默认是TomcatWebServer,还有jettyWebServer、NettyWebServer、UndertowServletWebServer、UndertowWebServer- Tomcat:默认
- Jetty:轻量级,适合嵌入式和IoT应用
- Undertow:非常轻量级,启动速度快,支持非阻塞IO
- Netty:高性能
配置文件优先级:
.properties > .yml
spring boot的jar包结构
1
2
3
4
5
6
7
8
9
10
11
12
13mySpringboot.jar
|—— Boot-INF/
| |—— classes/
| |—— lib/
| |—— META-INF/
|—— MEAT-INF/
|—— MANIFEST.MF 指定了启动类和主类
普通jar.jar
|—— com/
|—— MEAT-INF/
|—— MANIFEST.MF 指定了启动类和主类
10.4 spring cloud
- 四大板块
- 服务注册中心与配置中心
- 负载均衡
- 服务容错
- 服务治理
- 配置中心:Zookeeper,nacos。这两个也可以做注册中心
CAP:一致性、可用性、分区容忍性
CA:
CP:
Eureka(AP)、Zookeeper(CP)、Nacos(AP和CP)
Nacos:分级存储模型
NameSpace
Group
Service
Cluster
instance
服务降级、服务熔断、服务限流
- 网关:路由转发、鉴权、缓存加速、协议转换、日志监控、服务保护
十一 其他中间件
1 Mybatis
- 一种半自动ORM:查询关联对象或关联集合对象时,需要手动编写 sql 来完成
全自动ORM查询时可以不再写SQL。典型的框架如Hibernate
xml开发方式
- 定义xml
1
2
3
4
5
6<mapper namespace="tech.pdai.springboot.mysql57.mybatis.xml.dao.IUserDao"> 定义mapper关联的dao
<resultMap type="tech.pdai.springboot.mysql57.mybatis.xml.entity.User" id="UserResult"> 定义sql返回对象
<select id="findById" parameterType="Long" resultMap="UserResult">
<delete ...>
<update ...>
<insert ...>- 定义dao:
IUserDao
,使用@mapper
注解关联,定义xml中相关的对应方法 - 定义service及实现类调用dao
- 定义controller调用service
注解开发
1
2
3
4
5
6
7
8
9
10
11PageHelper:底层是使用的ThreadLocal存储分页数据-
- 只有紧跟在PageHelper.startPage方法后的第一个Mybatis的查询(Select)方法会被分页
数据库连接池:C3P0、DBCP、BoneCP、Druid(best)
文件上传:异步、分片、断点续传、秒传(如果之前上传过就马上到100%)
- 异步:
@EnableAsync
开启异步,@Async
作用于方法,开启子线程执行 - 分片:先切割再上传
- 断点续传:将分片的chuck md5信息保存在db中,记录上传记录
- 秒传:类似断点续传
- 异步:
2 RabbitMQ
- 优势在于稳定
- 命名中心:namesrv,存储broker、主题、生产消费信息
- 路由键、绑定键
- 对象
- producer
- consumer
- exchange:不存储消息,只是用来转发消息
- direct exchange:根据消息路由键精确匹配队列
- fanout exchange:消息广播
- topic exchange:根据消息路由键匹配队列(通配符* #)
- headers exchange:根据消息头匹配队列
- queue
- brocker
- 如何进入死信队列?
- 消息被拒绝,且requeue参数为false
- 消息在队列中过期(TTL)未消费
- 队列超出长度限制
消息如何路由
- 生产者发送消息到交换机
- 交换机根据路由键和绑定键发送到一个或多个队内中
消息的持久化
将队列和交换机的Durable设置为True
不足
- 性能:相比于kafka来说性能较低,常用于中小企业
- 开发语言限制,是erlang
- rabbitMQ实现延迟队列
- 本身不支持,通过TTL(消息存活时间) 和 DLX(死信交换机)
3 kafka
优势在于快、吞吐量大
命名中心:zookeeper,存储broker、主题、生产消费信息,协调服务实现故障监听与转移
- 特点:强一致性,zookeeper中某个节点发生数据变化,会通知其他节点通知更新,超过半数更新完毕才算写入完成
对象:
producer
consumer
- 需要自己从 broker 拉取消息,一个 consumer 连接到一个 partition ,从中依次读取消息,动态的更新 offset
- 采用pull的方式的好处是可以避免consumer处的消息堆积
- 多个 consumer 构成一个消费组group,一个group只负责一个topic,group会保证一条消息只被一个consumer消费
brocker:服务器,多个brocker组成集群
topic:消息主题
partition:主题内分区,物理上分区,实现负载均衡
- 一个topic分为多个partition,partition内部的消息是有序的(offset偏移量),但是对于topic来说,消息是无序的
- 消息写入partition:hash(注意热点问题)、轮询、自定义
- 多个partition可以放到多个broker,进行水平能力的扩展
group:消费者组
- 1个topic可以对应多个group
特性
- 高吞吐、低延迟,收发消息很快(零拷贝技术
sendfile()
) - 高伸缩性:每个主题topic包含多个分区partition,主题中的分区可以分布在不同的主机brocker中
- 持久行、可靠性:可持久化数据,底层十基于Zookeeper存储的
- 容错性:允许集群中的节点失败,某个节点宕机、kafka集群能够正常工作
- 高吞吐、低延迟,收发消息很快(零拷贝技术
使用场景
- 活动跟踪:跟踪用户行为,浏览数据发送的kafka,生成报告进行个性化展示
- 传递消息
- 日志记录:把数据库的更新发送到Kafka上,用来记录数据库更新,落hive表等
- 流式处理
- 限流、削峰、解耦
模式
- 点对点
- 发布订阅模式
为什么kafka快?
- 顺序读写:
- kafka将来自product的数据,顺序追加在partition,partition就是一个文件,以此实现顺序写入
- consumer从broker读取数据时,因为自带了偏移量,接着上次读取的位置继续读,以此是实现顺序读
- 零拷贝
sendfile()
。最重要- 为什么rocketmq不用
sendfile
而用mmap
,因为其将所有的队列的数据都写入了commitLog,消费者批量消费时需要读出来进行应用层过滤(要进入用户态),所以不用
- 为什么rocketmq不用
- 页缓存(page cache):消息写道page cache中,等系统统一写入磁盘(可能会有丢失,比如断电)
- 批量接收和发送消息。减少网络开销
- 顺序读写:
美团mafka相对于kafka的改进
- 多租户支持,隔离流量
- 性能优化
- 内支持运维工具,兼容性
如何保证消息不丢失?
- 从生产者到broker:做好try catch,以及重试
- brocker存储消息:在消息刷盘后再给生产者响应,集群部署的话还需要写到副本
- 从broker到消费者:保证消费者消费完业务后再返回给brocker信息,然后是注意消息消费的幂等性(版本号、唯一键、关键key等)
如何保证消息不被重复消费?
- 消息队列的幂等性机制,全局id
- 消费者自身维护消息的消费记录
- 分布式锁
消息堆积问题?
- 增加topic分区数量,对消费者扩容
- 下游拉取数据的时候提高获取批量
kafka的事务?让插入的多个topic要么全成功,要么全失败
如何保证消息的有序性?
- 单分区内消息的有序的,消费者根据offset来顺序消费消息
- 生产者:利用key来指定特定的分区,通过单分区的有序性来保证有序性
- 消费者:确保每个分区仅被组内一个消费者实例消费,可通过设置消费者组内消费者的并发度为分区数或小于分区数来达到这个目的。
- 以上是对于并发比较低的情况(所有消息都存储在了一个分区),对于多分区下的顺序消费:对业务加id,比如订单ID,用户ID等
kafka分区数量怎么确定
- 分区对生产者、消费者、broker的影响
- 生产者:客户端producer有个参数batch.size,为分区缓存消息,分区越多,这部分越大
- broker:维护分区成本
- 消费者:数据获取的内存;创建线程的开销
- 分区对生产者、消费者、broker的影响
- 如何确定:
- 创建只有1个分区的topic
- 测试这个topic的生产消费者吞吐量
- 分区数 = 目标吞吐量 / max(生产者,消费者)
如何提高kafka的吞吐量
分区
- 大型数据的时候使用随机选择分区
broker
生产者
- 批量发送(减少IO和网络):batch.size
消费者
- 使用固定大小的缓存区,最好是堆外内存(gc)
- 批量获取
- 消费者数量和分区数量:可以将消费者数量设置为Kafka集群分区数量的两到三倍
十二 RPC
- RPC(Remote Procedure Call),又叫做远程过程调用。它本身并不是一个具体的协议,而是一种调用方式
- gRPC,thrift
- 发展历史:TCP ==> RPC ==> HTTP
- 为什么有了RPC还要有HTTP?
- RPC 是针对于Client/Server (C/S) 架构下使用的,如果只需要连接自家的服务器,用RPC是足够的,但是浏览器Browser/Server (B/S)架构,需要访问其他公司的服务器,所以诞生了HTTP
- RPC框架
- Dubbo:阿里巴巴开源,仅支持java
- Spring Cloud:仅支持java
- gRPC:google开源,多语言
- Thrift:facebook开源,多语言
- Thrift组件
- Transport:传输组件,负责网络读写相关
- Protocol:协议和解编码组件,负责对网络数据传输的序列/反序列化
- Processor:服务调用组件
- Server:服务器
- 基本过程
- 客户端发起调用
- 客户端存根stub处理:根据调用的方法名、参数等打包编码为特定格式的消息体
- 网络传输
- 服务端存根stub处理:进行拆包和解码,获取方法名和参数
- 服务端执行调用
- 逆向返回结果
- 泛化调用
- 在不依赖服务A(没有服务A提供的接口)情况下远程调用服务A的接口
- 只要调用端将服务需要知道的信息:接口名、业务分组名、参数等封装为请求发送给服务器,服务端解析并处理即可
- 实现:
- 通过一个
GenericService
来生成动态代理,来实现在没有接口情况下的RPC调用 - 对于服务端,在获取Tprotocol时,会判断是否泛化调用,如果是泛化调用,会向泛化调用,会向链路中添加泛化调用标识,用于服务端判断是否是泛化调用
- 在调用时,与普通的调用基本一致,会在请求中额外添加被调用的服务名、方法名等信息在请求header中,用于服务端处理时查找相应的类信息
- 处理请求时,根据客户端传递的标识,判断是否是泛化调用,如果是则使用
GenericServiceTProcessor
处理请求
- 通过一个
- 如何手写一个RPC框架?
- 动态代理(屏蔽底层调用细节)
- 序列化(网络传输扁平化数据):二进制、json等
- 协议(识别数据)
- 网络传输(I/O相关,一般使用netty,基于Java NIO做的封装):同步非阻塞IO
- 对于多集群来说,还需要一个注册中心做服务发现、路由、负载均衡、异常重试、限流熔断等
- RPC和HTTP?
- 层级
- Http位于应用层,基于TCP/IP
- RPC不是网络模型中的具体层级的协议,它的实现可以依赖于HTTP(应用层),也可以依赖于TCP(多数)或者UDP
- 服务发现:
- HTTP是通过DNS获取到IP地址和端口信息
- RPC是通过专门的中间服务去做。注册中心
- 底层连接形式:
- 主流的Http1.1建立TCP连接后会保持连接(keep live),之后的请求相应复用
- RPC一般会有一个连接池。不过不少的编程语言会给http加一个连接池(golang)
- 传输内容
- HTTP以传输文本为主,一般header和body使用JSON序列号反序列化做
- RPC定制化程度高,可以采用定制化的协议存储数据结构体
- ps:其实http2之后进行了很多的改进,性能可能比RPC更好,gRPC底层用的就是Http2
- 通信瓶颈:
- RPC主要在于网络、序列反序列化开销、服务器负载、协议开销
- http主要在于TCP连接建立、http头部冗余、请求/响应模型的限制、加密解密的开销
- 层级
十三 异步
- 为什么需要异步?
- 调用多个下游服务,串行调用容易导致时间过长的问题
- 异步和多线程的区别?
- 资源利用率:异步通常基于回调机制,允许在等待IO的时候释放线程,线程池会阻塞等待
- 编码复杂性:多线程需要处理线程同步,线程安全等问题
- 场景:异步适用于IO密集,多线程适用于CPU密集型
13.1 CompletableFuture
https://tech.meituan.com/2022/05/12/principles-and-practices-of-completablefuture.html
实现了两个接口:
Future
和Completionstage
,前者是java5引入的异步计算,后者用于表示异步执行过程中的一个步骤(thenApply()
、thenCombine()
等都是Completionstage
的方法)原理:观察者模式
1
2
3
4
5
6public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
volatile Object result; // 存储CF执行结果
volatile Completion stack; // 栈结构,stack为栈顶。表示当前CF当前完成后需要触发的依赖动作
// CompletableFuture中的每个方法都对应了一个Completion子类,Completion本身是观察者的基类
...
}Future
的局限性- 向线程池提交异步任务发起RPC调用,主线程只能阻塞获取Future结果
- 调用RPC服务通常需要回调的方法完成Future,Future的执行结果依赖向线程池提交的任务返回结果,不支持通过回调的方式来设置返回结果
- 多个RPC串行执行,后一个RPC依赖前一个响应结果,主线程阻塞组装调用关系,没有回调唤醒机制,主线程反复被阻塞
- 多个RPC串行执行,后面的RPC依赖前两个RPC的结果作为入参。主线程阻塞等待,当相互依赖的RPC较多时,代码可读性差
- Future提供的接口没有异常处理的功能,不能异步处理异常,只能try检查get方法是否抛出异常
使用
创建
1
2
3
4public static CompletableFuture<Void> runAsync(Runnable runnable); // 创建无返回任务的异步任务
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor ex); // 指定线程池
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier); // 创建有返回值的...
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor ex); //异步回调
1
2
3
4
5
6<U> CompletionStage<U> thenApply(Function<? super T, ? extends U> var1); // 串行,返回的是一个嵌套的CF,需要get() join()解开
CompletionStage<Void> thenAccept(Consumer<? super T> var1); // 串行,无返回值
thenCombine // 二元依赖 A B都执行完再到C,直接返回CF,比thenApply方便
thenCompose // 串行执行
allOf
anyOf异常处理
1
handle
结果获取(注意设置超时时间)
1
2join
get
13.2 Loader
- 为解决代码难以读懂的问题,一个Loader加载若干个异步调用,通过Loader工厂加载loader
13.3 Director
- 为解决代码难以调试的问题,本质是维护一个有向无环图,来实现异步调用的加载
十四 项目场景
14.1 课程秒杀活动
瞬间流量
前端:
缓存静态资源
防止多次重复点击
前端到后端的限流(若干个请求取随机部分到后端)
后端
- 热点数据行,删除库存变为insert(容易超卖——lua脚本)
- 库存拆分,分解到不同的数据库中减小单库压力
- 消息队列:削峰填谷,非核心业务放到后面做
超卖(数据一致性)
- 分布式锁(redis)
- redis+lua,保证操作的原子性——这里注意添加一条消息存储订单数据
- 幂等性设计
业务降级和代码降级
14.2 订单超时取消
- 定时任务:xxl-job,比如定时1min扫描一次表,以时间戳+状态来取数据,判断数据是否超时
- redis缓存,设置缓存存在时间:在点击下单的时候给redis设置一个定时缓存,支付的时候再判断缓存是否存在
- 延时队列
- 消息队列的延时任务:下单的是发送一条延时队列(rocketMQ支持,rabbit MQ和kafka需要设计)
- redis zset 也可以实现:任务添加到zset,到期时间作为score,获取score值最小的元素与系统时间的关系,没到期就sleep
14.3 统计接口的调用次数
- ConcurrentHashMap<接口名, 次数> + AtomicInteger + 定时任务
- MQ
- 日志打点记录,后台记录到es中
14.4 项目中的文件上传系统
- 大文件上传:分块上传,再合并,比较md5码
- 如何避免重复文件:md5
- 限流:限制上传次数、频次、总文件大小
14.5 限流怎么做
- redis 分布式锁、incr对象
- 漏桶算法(宽进严出):队列实现
- 令牌桶算法:定速往桶里放令牌,请求来的时候如果桶里有令牌就拿令牌走,没令牌就放弃(面对突发流量有更好的表现)
14.6 序列化错误问题
- 设计
StereoWarehouse
的后期新增了一个字段,运行客户端打开之前做的案例的时候发现模型无法正常打开,查看报错发现反序列化失败,后续排查的时候发现BaseObj
对象是被序列化存储在磁盘中的,未显示的指定serialVersionUID
,jdk会根据当前类的结构,继承关系等信息计算出一个序列号,当类的信息发生变化的也发生了变化,导致反序列化失败 - 解决:设计的demo模型比较简单,重新做了一个并fix了这个bug
- 也可以回退版本,显示的指定序列号后保存,再更新到当前版本后打开。
十五 面试
8.23 快手1
- 自我介绍 MT 4min,FS 3min,XC没让介绍了,基本就在问MT实习,问了下实习了多久
- 介绍一下做的项目中给你带来最多收获的 : 统一履约接口,事件分发,RPC,泛化调用,异步框架
- RPC用的什么,为什么快: thrift,
- 自定义传输协议,精简;二进制传输,http是json序列反序列;RPC服务基于TCP/IP,长连接
- 说一下泛化调用 : 调用方没有服务方提供的API(SDK)的情况下,对服务方进行调用
- RPC最主要的是什么 : 一开始说了代理、网关面试官提示了一下,是服务注册上报
- MQ用的什么: kafka,
- 怎么了解的 kafka:对比了rabbitMQ和kafka的区别:分区;顺序读写;零拷贝;页缓存(数据丢失原因);批量收发
- 异步框架是什么:CF,loader,director框架,讲了一下框架的底层原理
- 讲一个业务开发,设计到了什么技术:KA,讲了一下kafka2hive,讲了日志打点的设计发展历程体现一下自己的思考(rpc接口 —> 耦合高;注解解耦 —> 历史代码不好处理;最后才一个一个搞)
- 补充了一下sdk的方式
- MT转正情况
- 怎么理解error和exception:都继承了Throwable,前者不可恢复,后者用于catch处理业务异常IOException和RuntimeException
- 为什么只有java才有exception
- 准确的说是只有java会强制check exception,核心目标是划分责任
- 数据结构:堆和栈
- 堆:堆通常是一个可以被看做一棵树的数组对象
- 栈:是只允许在一端进行插入或删除的线性表
- 操作系统:堆和栈
- 堆:堆是在程序运行时申请的动态内存,而不是在程序编译时,申请某个大小的内存空间。
- 栈:是操作系统在建立某个进程时或者线程,为这个线程建立的存储区域,在编译的时候可以指定需要的栈的大小
- DFS和BFS:深度和广度优先
- 网络4层结构和7层:应用层、传输层、网络层、网络接口层
- 7层:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
- http和https区别:多了一个tls,CA非对称加密算法
为什么用非对称又用对称加密:速度
讲一下设计模式
- build模式:实现构造器的按需创建
- 工厂模式:封装对象的创建,IOC思想
- 装饰模式:IO
- spring中的设计模式
- 工厂模式:Factory
- 代理模式:AOP
- 单例模式:单例bean
- 适配器模式:Spring AOP的
AdvicorAdapter
- 观察者模式:
ApplicationListener
,CF也使用到了观察者模式 - 责任链模式
手撕:两数之和
- 用一个map,O(N),当时没反应过来,用的for
总结:面试官太好了 疯狂给机会
8.24 快手2
自我介绍 MT 3min40s,FS 2min,XC 2min
上线是自己上的还是?mentor
困难点?RPC,异步框架
遇到的问题?升级jar依赖冲突,回滚
事后有复盘吗?没有
BeanFactory和FactoryBean?BeanFactory是IOC基础,常用ApplicationContext;FactoryBean是工厂类的接口,负责创建bean
BeanFactory
- bean实例化和管理:BeanFactory负责创建、初始化和管理Bean的生命周期。它会根据配置文件中定义的Bean定义来创建Bean的实例。
- 依赖注入:BeanFactory负责解决Bean之间的依赖关系,确保每个Bean都能获取它所依赖的其他Bean
- 配置元数据的管理:xml、注解、Java配置的数据
- 延迟初始化:lazy
- AOP支持
- 通常不直接使用,用ApplicationContext
FactoryBean
自定义bean创建过程
懒加载
bean的包装:创建代理
处理复杂逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
T getObject() throws Exception;
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
public class AFactoryBean implements FactoryBean<A> {
public A getObject() throws Exception {
return new A();
}
}
getBean("A")会得到A这个bean
getBean("&A")会得到AFactoryBean
举例一下FactoryBean,可以从框架出发
- mybatis:SqlSessionFactory
spring的设计模式:单例、责任链、代理、观察者、工厂
为什么用单例模式:
- 一个全局使用的类频繁地创建与销毁
- 节省系统资源
消息堆积怎么处理?
- 生产者:业务层面消息精简
- broker:增加分区,先持久化存储
- 消费者:扩容
- 批量消费
- 降级处理
能无线扩容吗?不能,举例了redis集群分片哈希槽
关于跨域问题?
- 由于浏览器的同源策略:协议、主机名、端口号,任何一个不一致都会导致跨域问题
- JSONP:利用了
<script>
标签不受同源策略限制的特性 - CORS:Http请求头,服务器在响应头添加
Access-Control-Allow-Origin
- 反向代理:反向代理服务器和服务端在一个域名下,可以实现转发
- WebSocket:可以用于跨域通信
- spring处理方案
- Controller添加
@CrossOrigin
注解 - 添加
WebMvcConfiguration
全局配置 - Filter
- GateWay端增加CorsFilter拦截器
- Controller添加
ThreadLocal的理解?ThreadLocalMap,对Entry是一个弱引用,内存泄漏风险
- Entry的key是ThreadLocal,v是设置的值
- gc的时候如果外部对ThreadLocal没有强引用会回收ThreadLocal,导致entry的key是null,但是value没有被回收
ThreadLocal可以被继承吗?不能
如果子线程需要继承怎么办?
public class InheritableThreadLocal<T> extends ThreadLocal<T>
mysql的锁机制?
- 乐观锁
- 悲观锁
- 表锁
- 行锁 innodb
- 行锁
- 间隙锁(幻读)
MVCC的理解
- 隐藏主键
- undolog
- readview
MVCC和锁?MVCC不加锁,事务并发修改
如果遇到网站攻击怎么处理?讲了大量请求用黑名单、直接攻击数据库用redis缓存
- CC攻击(应用层):大量的请求——网关黑名单
- DDOS攻击(多个层面):利用大量的计算机和代理服务器同时向服务端发送大量请求。比如SYN攻击、UDP攻击、ICMP攻击
- 流量清洗
- CC攻击模拟了正常请求,更能防御
做的项目用了什么设计模式?
- AOP
手撕:int num交换两位使得数字结果最大,上午刚写完oppo笔试手感火热秒了
聊天环节:电商、直播、广告、游戏,前三者基本饱和,目前发展游戏——自研化、精品化
- 对内平台工具、对外登录、支付、防沉迷等
- 杭州
8.26 淘天1
- 淘工厂
- 自我介绍 MT 3min,FS 2min,XC 跳过
- 转正情况
- MT技术架构
- 项目有没有本地运行
- spring没有启动起来怎么排查问题:启动日志
- 日志输出在哪里?log4j
- 升级jar包后bean没有依赖的问题是什么?这段答的比较差
- mybatis使用的是声明?xml,显式的时候sql
- 显式的使用sql有什么问题
- 写sql的时候考虑的效率问题?慢查询——联表放在业务、命中索引
- 如何考虑建立索引?
- 索引的类型
- 如何分析慢查询?explain
- MT redis使用场景?缓存、分布式锁
- redis和mysql如何保证数据一致性?旁路缓存、读写穿透、异步缓存
- 为什么先更新数据库再删除redis?高并发下的数据一致
- kafka使用场景
- 为什么需要kafka2hive?方便查询
- PRC调用的流程?依赖client、配置bean、调用
- RPC版本升级怎么处理?如果接口中新增参数,会不匹配报错
- 如果接口的对象删除了一个字段有没有问题?
- 用的json,会忽略额外的字段,比如A对象ab 2个字段,传入的JSON有abc 3个字段,那么序列化的时候只会注入ab两个字段
- 反问环节?业务:淘工厂
8.26 字节1
- MT 3min,FS 2min,XC 0
- 遇到的问题?商家履约一体化对RPC和异步的理解
- RPC的超时时间?全局定义,bean定义
- CF的理解?底层是维护了一个栈结构
- 多次部分退背景,面临的问题,如何处理涉及商品券商品的处理
- 灰度、降级开关的目的,设置原则
- 灰度如何实现?代码层面,也有平台层面
- 上线后如何观测?业务上商家反馈,代码上观察日志
- KA大盘上报过程?监控平台提供上报SDK,wm sg yy phf调用
- 可以说一下方式的演变(KS)
- 线上问题举一个例子?网关无限重试问题
- 线下环境一直推送订单给KD,KD因为缓存接口地址错误,导致一直请求到线上,出现大量告警
- 初步措施:网关限流
- 排查:网关回调和通知策略一致,导致失败的时候还进行了重试,引发循环调用
- 为什么线上没有问题:7.9-7.11hive没有失败数据
- 短期方案:改为不额外通知,只有最后失败的时候才通知
- 长期方案:
- 网关对推送加标,进行过滤
- WM差异化处理延迟和回调接口
- 没有做打点吗?这里记错了,客诉产生的同时也出现了线上告警
- 并发安全?分布式锁;并发集合类
- 分布式锁怎么用的?订单索引模型,redis setnx
- 分布式锁过期问题?看门狗机制,代码中没用过
- 线程池?复用线程减少创建销毁的性能开销,核心参数
- 核心线程数设置原则?任务类型,IO or CPU
- 消息队列?解耦,削峰填谷,异步
- 如何保证消息不被重复消费?保证事务的幂等,给消息设置一个全局唯一的id
- spring bean的生命周期
- bean构造函数、setter、aware接口、BeanPostProcessor接口、InitializingBean接口、init方法、BeanPostProcessor接口、使用、DisposableBean接口,DisposableBean方法
- mysql 慢查询问题?explain
- 开启慢查询日志
最左匹配原则,(a,b,c) 如果使用了(a,c)也会使用,只不过只会命中a
链表倒数第k个节点
- 做的中台
8.27 百度1
- MT 2min30s,FS 2min30s,XC 30s
- web服务和客户端的区别?资源的存放
简述一下履约组的业务,怎么学习的,技术架构,如何划分的微服务?订单全生命周期
RPC和HTTP?
HTTP和TCP长连接的含义?
- HTTP协议的长连接和短连接,本质上是TCP协议的长连接和短连接。
- HTTP1.0默认短连接,当客户访问一个web资源的时候,每遇到一个资源就建立一个tcp连接
- HTTP1.1默认长连接,当一个网页打开后不会断开,复用TCP的连接
- 短连接谁主动断开?一般是客户端
- 长连接如何判断包的顺序?比如发出A,B两个请求,如何判断收到的C响应对应的哪个?
- 传输层:TCP层面来说有序列号
- 应用层:HTTP在请求头或者请求体中添加UUID
- HTTP2使用了流来隔离每个请求和响应
- 异步框架?CF
- 线程池如何定义?核心线程数设置原则
- 最大线程数的设置原则
线程池释放空闲线程是怎么选择的?推测可能是LRU
- 根据keepAliveTime,JVM当前状态等,具体会删哪个无法推测
IOC和AOP
- bean的生命周期
- IOC底层的数据结构
循环依赖
kafka?分区、0拷贝
- 0拷贝?没有用户态和内核态的切换,没有CPU的拷贝(DMA)
- 分区数量和生产者、消费者的数量关系
- 1个 producer 可以生产不同 topic 的消息,
- 1个 topic 的消息被存放到不同的分区 partition 中,可以指定也可以 hash 自动计算
- partition 的消息以 offset 的偏移顺序存储
- 1个topic的消息被多个consumer消费,这些consumer组成group,单个partition的消息只能由单个consumer消费,主动拉取的方式并更新offset偏移量
- 1个topic也可以有多个consumer group
- 字符串中第一个只出现1次的字符。用的linkedHashMap
- 用一个int[52] 即可
8.31 得物1(全八股)
自我介绍
mysql索引结构,其他的数据结构呢?hash、二叉树、AVI树、红黑树、B+树
- 如何考虑自建索引?
- mysql事务
不可重复读和幻读的解决?MVCC,以及加间隙锁
间隙锁的缺点?并发性能,死锁问题
死锁条件以及破坏?4个
redis分布式锁
- redis setnx为什么是原子性?单线程
- IO模型,多路复用这些
- 异步模型的应用?讲了Future,
- 算法:三数之和为0
- 三指针,排序,固定a,然后从b=a+1 c=arr[n-1],b c指针向中间靠,复杂度O(n2)
8.31 pdd1
- 自我介绍
- MT主要难点:RPC的异步编排,Loader,Director
- 如果是一个任务不涉及到RPC,用单线程还是多线程?单线程
- ListenableFuture
- 使用线程池的缺点?只讲到了线程上下文切换的性能开销
- 切换性能开销
- 线程池管理开销
- 任务调度延迟
- java的线程是用户态还是内核态?用户态
- 两者区别,从用户切换的内核方式?系统调用
- 中断:当硬件设备(如时钟、中断控制器、I/O设备)发生事件时,会触发中断。中断会强制CPU从当前正在执行的用户态代码切换到内核态,以执行相应的中断处理程序。
- 系统调用
- 异常:异常是由CPU在执行指令时检测到某些错误或特殊情况(如除零、无效内存访问、缺页等)引发的。异常会导致从用户态切换到内核态,以便操作系统处理这些异常。
- ConcurrentHashMap
- java锁的底层锁的是什么?只回答到了对象上的描述文件(忘了叫啥了)
- 对象头的Mark Word:存储锁的信息,如锁的状态、线程ID、哈希码
- java对象头
- Mark Word:哈希码(HashCode)、GC标记、锁标志位、锁的线程ID、偏向锁的线程ID、年龄信息等。
- Class Pointer(类型指针):通过它找到存储的实例
- Array Length(数组长度,只有数组对象有)
- JVM结构
- 堆的划分:老年和新生,CMS和G1
- MT用的什么:CMS
- 新生代细分:Eden,S0,S1
- 手撕:很离谱的手撕(面试官自己想的)
- 一个list存储了若干个Item,Item的内部有一个String text的对象,表示RPC请求内容,要求将这个list表示的若干个请求分发为多次发送,使得每次请求的Item数量不超过一个max值且数量尽可能的均匀,对于每次请求,是由若干个Item组成,要求Item的长度尽可能的相等(一个Item可以被切割为若干份)
- 不知道他在说什么,题目也改来改去
9.2 oppo1
- 自我介绍
- 多次部分退背景,做了哪些开发
- 设计模式,开发过程中用了哪些模式
- fs开发,怎么做的节拍统计
- 遇到的问题?fs的立体仓库堆垛机
- MT的困难?异步的理解过程
- 设计rpc接口的异常处理
9.2 字节2(十分开放)
- 自我介绍
- 学习路线是怎么制定的
- minIO实现分布式的部署怎么去做?类举了redis集群
- 哈希槽理解?哈希映射
- 哈希槽的扩容缩容问题?
- 渐进式rehash
- redis集群?
- 主从
- 集群
- 数据持久化
- es,如果一个es对应多个实体,如何避免回表
- 同步双写
- 异步双写:消息队列、内存队列
- 定期同步
- 数据订阅:利用binlog:canal Server 是一个伪装的slave节点,接收到binlog日志后,发送到MQ, 其他的 存储消费 MQ里边 的binlog日志,实现数据订阅。(业务侵入小、实时性好)
- 增量更新:只更新ES中需要变更的字段,但是更新mysql表的时候需要知道表的列对应的es字段
如果读取到的mysql从节点的数据不是最新的怎么办?利用版本号判断,如果不是采用重试机制
做指标监控?
- 同环比监控
- 接口时延、成功率等进行监控
SLA指标?
- 可用性TP99
- 准确性
- 系统容量QPS
- 如何减少告警后人为的干预?AI训练
- 手撕:字符串的无重复组合。递归秒了
9.2 阿里1
- 20min极速,开门见山,自我介绍都不用(KPI)
- 项目过程中遇到的问题。异步编排
- 技术上的挑战?KA,AOP的思路
- 日志文件怎么收集到kafka?定时任务
- 为什么用kafka,和rabbitMQ对比?kafka的吞吐量、分区
- 如何检测异常?同环比对比
- kafka2Hive,如何减少?用缓存
- mysql索引结构?B+
- es和mysql区别?倒排索引的过程
- docker了解吗?
- JWT原理?无状态,header、payload、签名三部分
- 如果用户退出登录,要让这个JWT立即失效怎么做?
- JWT设置有效时间
- 退出后将用户设置到一个退出名单中(有点session的意思)
- JWT增加版本号,退出后改变版本号
- 服务端设置加密的key,为每个用户生成唯一的key,失效则改变key
9.3 阿里云1
- 八股盛宴
- JDK、JRE、JVM关系
- concurrentHashmap和hashTable
- concurrentHashmap两种实现方法
- jdk8之前:分段锁,segment进行分段加锁
- jdk8及之后:节点锁,对节点进行加锁,粒度更细。同时也支持CAS算法进行无锁操作
- 垃圾回收算法:标记清除、复制、标记整理、分代收集
- 新生代和老年代
- 内存泄漏和内存溢出的区别
- 泄露memory leak:指程序在申请内存后,无法释放已申请的内存空间(threadlocal)
- 溢出out of memory:指程序在申请内存时,没有足够的内存空间供其使用
- 进程被杀掉后泄漏的内存会被回收吗?操作系统会回收这部分的内存
- 线程同步和互斥的方法?
- 同步:信号量、事件、消息队列
- 互斥:互斥锁、临界区、读写锁、原子操作
- 事件的理解
- 事件可以是一个信号、一个消息、一个状态变化等,它使得线程或任务能够基于这个条件进行等待或继续执行。
- 死锁的理解?四大条件
- 操作系统进程和线程区别
- TCP三次握手
- TCP的可靠传输:确认机制、超时重传、流量控制、拥塞控制
Https过程
数据库的事务:原子性、一致性、隔离性、持久性
索引优缺点
索引不用B+树可以吗?IO、范围查询、数据结构优化三方面
设计模式
观察者模式解决的问题:实现对象之间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都得到通知并被自动更新。
- 不用观察者模式怎么做:轮询、发布-订阅模式、回调机制、状态共享或事件驱动架构
- 算法:一个序列非常非常大,找出最大最小值。
- 可以分段,使用并行的方式去找,最后再同步结果
- 一致性哈希:哈希环
- MT最大的挑战
- RPC过程
- FS挑战
- 做的公共云,AI的应用,后续还有两轮技术
9.3 百度2
- 介绍一下多次部分退的内容
- 如何设计一个配置中心,实现热部署?(开放性问题)发生变化时服务端主动的去推送
- 如何保证更新数据顺利推送到?类似于TCP的一个响应报文
- 如果服务有很多怎么做?做配置中心集群,每个集群负责一部分的服务
- 如何保证集群数据的一致性?可以使用缓存记录数据的版本,通过版本来判断当前key是不是最新的
- 如何实现发布的新配置分批次的更新到服务中?(类似于灰度)讲了灰度设计的逻辑,通过机器的mac地址进行运算得到百分比
- 如何得到机器的信息呢?注册中心
- KA大盘的建设怎么做的?将了一下设计思路的演变过程
- 上报的过程中需要注意什么
- 异步的上报,不阻塞主要的逻辑
- 对异常进行处理,不影响到主业务
- hashcode和equals区别
@Service
是线程安全的吗?不安全,因为bean的默认作用范围是单例的,所以@service
对应的bean就是单例的- 可以通过
@Scope
更改
- 可以通过
- 一个url请求的过程
- http和tcp的长连接和短连接?本质是tcp的长短连接
- 如果一个传输的过程中断网了,会发生什么?
- 手撕:一个字符串,去掉所有的非数字字符后能够拿到的最大数字。比如asd612fsad13sd能得到最大的数字是63211
- 用一个list先存储所有的数字,然后排序
- 用一个优先级队列直接存储
- 用两个栈代替优先级队列
- 用一个int[] 数组,长度为10,表示所有数字的出现次数,然后拼接(最优)
- 问了一下如果给offer会来实习吗
9.4 顺丰1
怎么学习java的
8个基本数据类型
string是不是,为什么是不可变
jdk和jre?
jdk包含了什么?
编译器javac等
JRE
开发工具
类库
接口和抽象类的区别
重写和重载
- 只有返回值类型不同的不是重载,JVM是通过方法签名(方法名和参数列表)来判断方法的
线程池的参数
- runnable和callable
- callable有返回值,允许异常抛出
- G1垃圾回收器
- 设计模式
- springboot中如何自定义注解实现aop
- 定义一个注解:声明好
@Target
、@Rerention
等 - 创建一个AOP切面类
@Aspect
- 定义方法,声明增强方式:
@Around("@annotation(注解名)")
- 在方法中去解析注解中的内容
- 最后使用注解
- 定义一个注解:声明好
- mysql支持json吗
- 5.7之后支持JSON字段的基本操作、相关函数及索引使用如何索引JSON字段,之前都是一个text
- 联合索引使用
- redis缓存雪崩
9.5 字节3 全场景
- 部分退的困难点
- 金额的计算问题怎么处理?1元分为了0.335 0.335 0.335,如果三次退会导致超出1元怎么处理
- 值班遇到的线上问题,怎么处理的?优先回滚,事后分析
- 有什么规范性的收获?代码的可降级,异常的处理,缓存的使用
- 接口和子类
- 如果有多个任务abcd,e依赖于abcd,怎么提高效率?异步执行,观察者模式,照着CF原理讲的
- 超时怎么处理?设置异常返回
- 线程池拒绝策略
- 如果有一个任务对实时性要求非常高,用什么策略
- 个人评价和他人对自己评价
- 手撕:返回二叉树倒数第k小的值,用的中序遍历
- 问了一下提升的地方:介绍项目从大到小;线上异常处理
9.6 招商1
- 多次部分退
- 如何防止重复退
- 涉及到商品券怎么处理
- KA监控怎么做的
- 网关重试问题
- 手撕:排序数组返回最大的质数(试除法)
9.7 得物2(吊打)
- 做中间件的,果然牛皮
sleep和wait的区别
sleep()
属于Thread
类方法;wait()
属于Object
类,需要在synchronized
块或方法中调用sleep()
不释放锁,线程一直占用CPU,只是暂停执行一段时间自动苏醒;wait()
会释放锁,由notify()
或notifyAll()
唤醒sleep()
不抛出异常;wait()
会sleep()
用于模拟延迟操作;wait()
用于线程间通信
sleep指定时间苏醒的底层
- 挂起进程(或线程)并修改其运行状态,当时间结束,定时器会触发,内核收到中断后修改进程(或线程)的运行状态。例如线程会被标志为就绪而进入就绪队列等待调度。
- 一个对象有一个short类型一个byte类型的成员变量,new这个对象的时候需要分配多大的空间
- short 2字节,byte 1字节,对象头12~16字节
- 32位JVM:12字节对象头(8字节 Mark Word + 4字节存储类指针);64位JVM:16/12字节(8字节 Mark Word + 8字节存储类指针(压缩指针为4字节))
- 以12字节对象头为例,共15字节。
- Java对象通常是8字节对齐的,15字节对应补齐到16字节
BIO和NIO:同步阻塞IO和同步非阻塞IO
可见性和原子性
- 可见性是怎么保证的
volatile
修饰的话,对这个变量的修改都是在主存中进行,底层是lock
前缀指令,线程写入后会强制刷新主存,根据MESI 协议,其他的线程的缓存数据就无效了- MESI 协议:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
- 使用同步机制比如锁或者
synchronized
使用TCP从服务A发送数据到服务B会发生几次拷贝。使用零拷贝
- 数据存放在内核空间,到A的socket网卡会发生一次DMA拷贝
- 到B的网卡后,B会发生一次DMA拷贝到B的内核空间,如果需要存储,则会再发生一次拷贝。
使用
sendFile
是几次- 连续发送3次http请求,可以发吗
503和504区别
- 502:机器挂了,nginx执行请求的时候,却收到了上游服务器的无效响应
- 503:服务器当前无法处理请求,请求被拒绝
- 504:dns查询过程超时或者nginx配置错误
redis是怎么请求处理的
- 基于reactor模式的事件驱动程序来实现事件的并发处理,即通过epoll来监听多个fd
- 请求过来,注册一个fd到epoll中,并设置回调函数
- readQueryFromClient 读取请求命令:从socket中读取数据放到缓冲区中,并进行解析
- processCommand 处理命令请求:
- addReply 返回执行结果:将结果返回到输出缓冲区,
zset底层:跳表
- 场景:如何实现一个直播间踢出某个用户
9.9 oppo2
- 超出预期的事情?FS
- 怎么做的立库可视化
- 挫败的事情?MT异步调用过程
- 有挑战的事情?线上无限重试问题
- 一个业务或者需求实现的全流程
- 反问:
- 做的供应链这块,面向公司内部的IT平台开发
- base东莞
9.14 HW1
30min问答 + 25min手撕
代码量
如何保证写的代码的正确性(单侧 + 自测)
怎么看待单元测试,怎么用的?方法,接口逻辑判断;mock
实习过程中印象比较深刻的事情
怎么分析的效率
java六大设计原则
- 单一职责原则
- 开放封闭原则: 类,模块,函数等应该是可以扩展的,但是不可以修改
- 里式替换原则:所有引用基类(父类)的地方必须能透明的使用其子类的对象
- 依赖倒置原则:高层模块(调用端)不应该依赖底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节(实现类),细节应该依赖于抽象。
- 迪米特原则:一个软件实体应当少的与其他实体发生相互作用。
- 接口隔离原则:一个类对另一个类的依赖应该建立在最小的接口上
偏向锁和自旋锁
进程、线程、协程
B树、B+树
手撕:数组中前k个最频繁的数(
hashMap + PriorityQueue
)O(NlogN)- 更好的方法:桶排序 O(N)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> frequencyMap = new HashMap<>();
// 统计每个数字出现的频率
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
// 创建桶,桶的索引是频率,值是具有相同频率的数字列表
List<Integer>[] buckets = new List[nums.length + 1]; // 比如1出现了3次,1就会在索引为3的桶出现
for (int key : frequencyMap.keySet()) {
int frequency = frequencyMap.get(key);
if (buckets[frequency] == null) {
buckets[frequency] = new ArrayList<>();
}
buckets[frequency].add(key);
}
// 从高频到低频遍历桶,收集前 k 个频率最高的元素
List<Integer> result = new ArrayList<>();
for (int i = buckets.length - 1; i >= 0; i--) {
if (buckets[i] == null) {
continue;
}
if (result.size() >= k) {
break;
}
int index = 0;
while (result.size() < k && index < buckets[i].size()) {
result.add(buckets[i].get(index++));
}
}
// 转换为数组返回
return result.stream().mapToInt(i -> i).toArray();
}4.24 笔试回顾
t1:输入两行,一行是数组,一行是一个目标数,根据数组构造一个满平衡二叉树,输出查找路径,S表示跟节点,R向右,L向左,Y表示找到,N表示没找到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43import java.util.*;
public class BalancedBST {
// 构建平衡的满二叉搜索树
// TreeNode自定义:int val, TreeNode left,right
private TreeNode buildBST(int[] nums, int start, int end) {
if (start > end) return null;
int mid = start + (end - start) / 2;
TreeNode root = new TreeNode(nums[mid]);
root.left = buildBST(nums, start, mid - 1);
root.right = buildBST(nums, mid + 1, end);
return root;
}
// 搜索给定的数
private String search(TreeNode root, int target) {
if (root == null) return "N";
if (root.val == target) return "Y";
else if (target < root.val) return "L" + search(root.left, target);
else return "R" + search(root.right, target);
}
public static void main(String[] args) {
int[] nums = ....
int target = ....
BalancedBST bst = new BalancedBST();
// 构建平衡的满二叉搜索树
TreeNode root = bst.buildBST(nums, 0, nums.length - 1);
// 搜索给定的数
String result = bst.search(root, target);
// 输出结果
System.out.print("S");
System.out.println(result);
}
}t2:K教练正在对足球队的 n nn 名球员进行射门能力评估。评估共进行 m mm 次训练,每次训练时,若球员射门得分则记为1,否则记为0。现在K教练需要根据以下规则对球员进行排名:
- 进球总数较多的球员排名靠前。
- 如果进球总数相同,则最长连续进球次数较多的球员排名靠前。
- 如果最长连续进球次数也相同,则第一次未进球的训练序号较大的球员排名靠前。如果第一次未进球的训练序号也相同,则比较第二次、第三次……直到比较出结果。
- 如果按照前三条规则仍然无法区分排名,则编号较小的球员排名靠前。
1
// 自定义一个结构体,然后优先级队列
t3:
输入:第一行为n,表示微服务数量;第二行为n的数组edges, edges[i] 表示 服务i对服务edges[i]的调用关系
定义
- 群组:调用关系形成一个环的多个微服务组
- 内聚值 = 群组微服务数量 - 能够调用到该群组内微服务的数量
已知:至少存在一个群组
输出:内聚值最大的群组,按照调用关系输出群组内的服务编号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92// 输入
// 4
// 3 3 0 2
// 输出
// 0 3 2
/*
首先,我们记录每个点的入度,然后进行拓扑排序找环,扫一遍找到每个环上最小的id以及该环内聚值,内聚值显然是等于环的总入度减去环的大小,最后从最小id的位置遍历环,输出答案。
*/
public class MicroserviceGroup {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int[] edges = new int[n]; // n个调用关系
int[] du = new int[n]; // 每个节点的入度
List<List<Integer>> groups = new ArrayList<>(); // 群组
boolean[] visited = new boolean[n]; // 用于搜索
// 读取调用关系并记录入度
for (int i = 0; i < n; i++) {
edges[i] = scanner.nextInt();
du[edges[i]]++;
}
// 拓扑排序查找环
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (du[i] == 0) {
queue.offer(i);
}
}
// 排除掉非环结构,while之后只剩下环了,也就是只剩下群组了
while (!queue.isEmpty()) {
int pos = queue.poll();
if (--du[edges[pos]] == 0) { // edges[pos] 就是 pos调用的下游
queue.offer(edges[pos]);
}
}
// 深度优先搜索找群组
for (int i = 0; i < n; i++) {
if (du[i] != 0 && !visited[i]) {
List<Integer> currentGroup = new ArrayList<>();
dfs(i, edges, visited, currentGroup);
groups.add(currentGroup);
}
}
// 计算内聚值并找出最大的群组
int maxId = n, maxCount = -1, selectedGroupIndex = -1;
for (int i = 0; i < groups.size(); i++) {
int count = 0, id = -1;
for (int j : groups.get(i)) {
count += du[j];
id = Math.max(id, j);
}
count -= groups.get(i).size();
if (count > maxCount) {
maxId = id;
maxCount = count;
selectedGroupIndex = i;
} else if (count == maxCount && id < maxId) {
maxId = id;
selectedGroupIndex = i;
}
}
// 输出结果
List<Integer> selectedGroup = groups.get(selectedGroupIndex);
Collections.sort(selectedGroup);
int startPos = selectedGroup.get(0);
System.out.print(startPos + " ");
while (edges[startPos] != selectedGroup.get(0)) {
startPos = edges[startPos];
System.out.print(startPos + " ");
}
scanner.close();
}
private static void dfs(int pos, int[] edges, boolean[] visited, List<Integer> currentGroup) {
visited[pos] = true;
currentGroup.add(pos);
if (!visited[edges[pos]]) {
dfs(edges[pos], edges, visited, currentGroup);
}
}
}
9.19 联洲1
- 奖励和成绩
- kafka消费的顺序如何保证
- hashmap查询时间复杂度
- 哈希冲突的解决方法:开放地址;再哈希法;链地址法;建立公共溢出区
- 红黑树的特点
- TCP可靠性:校验和、序号、确认机制、重传机制、拥塞避免、滑动窗口
- TCP如何保证修改了包不被发现
- 数据校验和
9.19 中兴1
- 机器学习有了解吗
- 计算机网络状态返回码 1 2 3 4 5含义
- 200和204区别:204没有返回内容
- Hibernate和mybatis的区别:
- 前者是全ORM框架,自动生成sql,对于复杂的sql查询性能不太好
- kafka消息顺序性怎么保证
- 怎么定位瓶颈优化的FS
- 分片多线程
- 怎么定义返回,状态码
- 保研成绩,一等奖学金,竞赛,小论文,专利
- 哪里人,工作地点,独生子女
- 怎么了解中兴文化、工作、生活
- 岗位:软件开发,java后端
- 业务:
- 网管:通信设备的管理系统,OTN
- 工具:设备的巡检和升级(new)
- 巡检:设备的运行状况,温度、cpu、利用率等
- 升级:扩容
9.20 联洲2
- 多次部分退的灰度和降级方案
- 配置中心理解和原理
- docker用过吗
- 日志hive过程
- 为什么使用minio:轻量,golang
- redis类型
- redis缓存问题
- JWT
- mysql索引数据结构
- 手撕:快排
9.22 得物3
kafka还得看
同一个tcp连接发送多个http请求?
- http1.1两个请求的生命周期不能重叠,http2提供了多路传输特性,每个请求通过流(Stream ID)来区分,可以(http1.1时代为了提高响应会维护多个tcp连接)
- 流:ID唯一、双向传输、基于帧来传输,每个帧携带stream ID
9.25 海康1
- 遇到的问题,异步调用
- 线程死锁
- RPC和http
- mybatis两级缓存、
#{}
和%{}
区别(占位符和替换) - 线上问题
- 实习收获
- 学校里遇到的较大的问题
- 机器学习有没有了解
9.25 迈瑞1
- 遇到的问题
- 异步的理解
- 线上问题
- FS优化过程
- 手撕:快排
9.29 迈瑞2
- 开发
- 学习路线
- 自己的优势劣势
- 对工作的看法