1.Java基础
1. 异常机制
Error常见的有StackOverFlowError,OutOfMemoryError等等。
Exception可分为运行时异常和非运行时异常。对于运行时异常,可以利用try catch的方式进行处理,也
可以不处理。对于非运行时异常,必须处理,不处理的话程序无法通过编译。
ClassNotFoundException是在类加载第一个阶段,加载这个动作的时候,类加载器不能找到class文件,则会报ClassNotFoundException。比如在A中有段代码使用了Class.forName(B)去加载B,此时B的class文件不见了,则会报错ClassNotFoundException。它报这个错是说B的加载阶段出错了。(A类去加载B类,A类会报ClassNotFound B类)
NoClassDefError这个类是继承LinkageError(连接错误),所以也可以大概的猜出,NoClassDefError是在类加载的连接这个阶段报出来的。当A类中引用了B类,然后A类进行类的加载,加载成功后,然后进行接下来的连接阶段的时候,如果涉及到引用到B类却找不到B类的时候,就会报NoClassDefError。它报这个错是说A的连接阶段出错了。(A类去加载B类,A类会报B类NoClassDef)
Linux实现CAS的指令cmpxchgl
初始化顺序
- ①默认初始化
- ②显式初始化/⑤在代码块中赋值
- ③构造器中初始化
- ④有了对象以后,可以通过"对象.属性"或"对象.方法"的方式,进行赋值
2. CPU飙高
在 thread dump 中,要留意下面几种状态
- 死锁,Deadlock(重点关注)
- 等待资源,Waiting on condition(重点关注)
- 等待获取监视器,Waiting on monitor entry(重点关注)
- 阻塞,Blocked(重点关注)
- 执行中,Runnable
- 暂停,Suspended
- 对象等待中,Object.wait() 或 TIMED_WAITING
- 停止,Parked
3. 红黑树二叉树区别
二叉搜索树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉搜索树。搜索复杂度为o(logn)最差为o(n)
平衡二叉搜索树AVL树:本质上是带了平衡功能的二叉查找树,每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。
红黑树:需要满足以下性质:红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点,即空结点(NIL)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。也就是说从每个叶子到根的所有路径上不能有两个连续的红色节点
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。
4. a+=b和a=a+b
short a = 4;
a += 5;
//a = a + 3会报错
a = (short) (a + 3);
+=是一个运算符,编译器会自动转换类型
a = a+b
5. 常见集合
- Map接口和Collection接口是所有集合框架的父接口:
- Collection集合主要有List和Set两大接口
- List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
- Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
- ArrayList的优缺点:基于数组实现,适合读多写少。LinkedList基于链表,适合写多读少
- 优点:
ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomA***ess 接口,因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。 - 缺点:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场
6. IO
阻塞IO
非阻塞NIO
7. 排序算法
堆排序
堆的数据结构
8. HashMap
- 长度为啥是2的n次幂?
求hash时,先求出hashCode,再向右移16位,与原来hashcode做&运算(保留高低位的特征,增加散列性)
2的n次幂(1000),当求索引时,是hash&(length - 1),减一变成0111,能够保持hash值的散列性,减少碰撞 - 扩容时为啥是2倍?
为了能够保持长度为2的n次幂
为了扩容时,快速求出元素扩容后所在的索引,只需要比较hash的前一位是1还是0,如果是0就不变,如果是1就原索引+oldCap。 - 链表长度为啥在8时改为红黑树?
如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。 - 扩容(加载)因子为何默认是 0.75f
在空间占用与查询时间之间取得较好的权衡;大于这个值,空间节省了,但链表就会比较长影响性能;小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多 - 为什么红黑树退化为链表的阈值是6
主要是一个过渡,避免链表和红黑树之间频繁的转换。如果阈值是7的话,删除一个元素红黑树就必须退化为链表,增加一个元素就必须树化,来回不断的转换结构无疑会降低性能,所以阈值才不设置的那么临界
9. 抽象类与接口
相同点
(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点
(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为 public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的,1.8 可以有static方法,可以有default方法,子类可以不实现。
抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;可以有非抽象方法,抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。抽象类中可以有非抽象方法,有抽象方法的一定是抽象类
10. mysql varchar和text
TEXT类型一般分为 TINYTEXT(255长度)、TEXT(65535)、 MEDIUMTEXT(int最大值16M),和LONGTEXT(long最大值4G)这四种,对于text列,插入时MySQL不会对它进行填充,并且select时不会删除任何末尾的字节。
text的最大限制也是64k个字节,但是本质是溢出存储,innodb默认只会存放前768字节在数据页中,而剩余的数据则会存储在溢出段中。text类型的数据,将被存储在元数据表之外地方,但是varchar/char将和其他列一起存储在表数据文件中,值得注意的是,varchar列在溢出的时候会自动转换为text类型。text数据类型实际上将会大幅度增加数据库表文件尺寸。
除此之外,二者还有以下的区别
1、当text作为索引的时候,必须 制定索引的长度,而当varchar充当索引的时候,可以不用指明。
2、text列不允许拥有默认值。
3、当text列的内容很多的时候,text列的内容会保留一个指针在记录中,这个指针指向了磁盘中的一块区域,当对这个表进行select *的时候,会从磁盘中读取text的值,影响查询的性能,而varchar不会存在这个问题。
11. 单例模式
- 双重检验锁+volatile 懒汉式
public final class Singleton {
private Singleton() {
}
// 问题1:解释为什么要加 volatile?为了保证赋值和构造器不要发送重排序,否则第二个线程可能拿到不完整的对象。
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗?为了防止第二个线程等第一个线程释放锁后,再次创建新对象
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
- 枚举 饿汉式
// 问题1:枚举单例是如何限制实例个数的 ==static final 变量
// 问题2:枚举单例在创建时是否有并发问题 ==没有并发,jvm保证
// 问题3:枚举单例能否被反射破坏单例 ==不能
// 问题4:枚举单例能否被反序列化破坏单例 不能,枚举类实现了序列化
// 问题5:枚举单例属于懒汉式还是饿汉式 ==饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做== 加构造器
enum Singleton {
INSTANCE;
}
- 静态内部类 懒汉式
public final class Singleton {
private Singleton() { }
// 问题1:属于懒汉式还是饿汉式 ==懒汉式,不调用方法不加载静态类
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题 ==没有,静态成员变量,在类加载的时候完成,jvm保证
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
12. 微服务优缺点
单体架构的优缺点如下:
**优点:**架构简单、部署成本低
**缺点:**耦合度高(维护困难、升级困难)
分布式架构的优缺点:
**优点:**降低服务耦合、有利于服务升级和拓展
**缺点:**服务调用关系错综复杂
13. 踢人下线/黑名单
当我们需要封禁一个账号时,只需要将其账号的status
值修改为0即可,对方再次登录系统时,我们便可以检测到status
值不为1禁止登录。由于我们只在登录时检测status
值,这也就代表:如果对方不主动注销账号,他的会话还是会一直存在且有效。
那怎么才可以做到在封禁账号后立即生效?
你可能会想到使用拦截器,拦截用户的所有请求检测账号状态:status=0
时禁止访问,status=1
时再对请求放行。
可以用户数据库+status状态、登录时判断、拦截器请求时判断、redis维护黑名单。
使用Sa-Token框架
14. 设计原则
-
开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
想要达到这样的效果,我们需要使用接口和抽象类。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。 -
里氏代换原则
任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。 -
依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。 -
接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。 -
迪米特法则
只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。 - 单一职责
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
单一职责原则的优点
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。
- 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
- 提高类的可读性。复杂性降低,自然其可读性会提高。
- 提高系统的可维护性。可读性提高,那自然更容易维护了。
- 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响
15. Unicode UTF-8
Unicode就是一种编码:它包含了世界上所有的符号,并且每一个符号都是独一无二的
UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有两条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码
GBK/GB2312/GB18030
GBK和GB2312都是针对简体字的编码,只是GB2312只支持六千多个汉字的编码,而GBK支持1万多个汉字编码。而GB18030是用于繁体字的编码。汉字存储时都使用两个字节来储存
16.synchronized
修饰普通方法:对象锁
修饰静态方法:类锁
修饰代码块:类锁synchronized(A.class)、对象锁synchronized(this)
17. 线程池的优缺点
为啥先进阻塞队列再创建最大线程
线程池创建线程需要获取mainlock这个全局锁,会影响并发效率,所以使用阻塞队列把第一步创建核心线程与第三步创建最大线程隔离开来,起一个缓冲的作用。引入阻塞队列,是为了在执行execute()方法时,尽可能的避免获取全局锁。
优点:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
缺点:
- **导致OOM:**都是无界的可能会导致OOM
- **死锁:**线程池中的线程都在等待在阻塞队列中另一任务的执行结果,但这一任务却因为没有额外的线程而不能运行,可能会发生死锁。当线程池被用来实现涉及许多交互对象的模拟,被模拟的对象可以相互发送查询,这些查询接下来作为排队的任务执行,查询对象又同步等待着响应时,会发生这种情况。
-
线程泄露:当从池中拿一个一个线程去执行一项任务,而在任务完成后该线程却没有返回池时,会发生线程泄露况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。 -
资源有限:程池的一个优点在于:相对于其它替代调度机制而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。线程会消耗内存和其它系统资源等大量资源。除了 Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。
如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件。这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。
18. 线程池参数设计规则
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,CPU 不总是处于繁忙状态。例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。
经验公式如下 线程数 = 核数 _ 期望 CPU 利用率 _ 总时间(CPU计算时间+等待时间) / CPU 计算时间
例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式 4 _ 100% _ 100% / 50% = 8
例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式 4 _ 100% _ 100% / 10% = 40
核心线程数 corePoolSize
任务队列长度 workingQueue
最大线程数 maximumPoolSize
最大空闲时间
线程池状态:
状态 | 描述 |
---|---|
RUNNING | 能接受新提交的任务,并且也能处理阻塞队列中的任务 |
SHUTDOWN | 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态) |
STOP | 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态 |
TIDYING | 如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态 |
TERMINATED | 在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做 |
线程池原理
2. 计网
1. 三次握手
过程
- TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
- TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
- TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
- TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
- 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BtYK92Q8-1683542094229)(计网操作系统等面经.assets/image-20220305170145297.png#id=WA8GT&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
第一次握手丢失:客户端重传SYN
第二次握手丢失:客户端重传SYN、服务器都重传SYN-ACK
第三次握手丢失:服务端重传SYN-ACK,客户端的ACK不会重传
序列号可以用来解决网络包乱序的问题,确认号可以⽤来解决网络包丢失的问题
为啥三次握手
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:
- 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
- 那么此时服务端就会回一个
SYN + ACK
报文给客户端; - 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送
RST
报文给服务端,表示中止这一次连接
2. 四次挥手
过程
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2*MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
客户端最后等待2MSL
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文
- 保证最后的 ACK 报文,接收方能收到,一定能收到(如果收不到,对方会重发 FIN 报文)
- 确保在创建新连接时,先前网络中残余的数据都在网络中消失了。
为啥四次挥手
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
已建立连接,客户端故障
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接
大量close_wait
大量TCP连接处在close_wait状态,形成原因和解决办法
客户端发送FIN客户端收到后,客户端自动ACK后进入close_wait状态,socket.close因为阻塞关闭不及时。
Server端在某些异常情况时,没有关闭Socket。关闭socket不及时:例如I/O线程被意外阻塞,或者I/O线程执行的用户自定义Task比例过高,导致I/O操作处理不及时,链路不能被及时释放。
3. DNS域名解析过程
浏览器–>本地域名服务器缓存–>根域名服务器(全球有13个)–>***顶级域名服务器–>baidu.***主域名(权威)服务器–>找到ip地址 缓存到本地域名服务器
DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。
DNS使用UDP还是TCP:区域传送时使用TCP,域名解析时使用UDP协议
DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议;
DNS的规范规定了2种类型的DNS服务器,**一个叫主DNS服务器,一个叫辅助DNS服务器。**在一个区中主DNS服务器从自己本机的数据文件中读取该区的DNS数据信息,而辅助DNS服务器则从区的主DNS服务器中读取该区的DNS数据信息。当一个辅助DNS服务器启动时,它需要与主DNS服务器通信,并加载数据信息,这就叫做区传送(zone transfer)。
为什么既使用TCP又使用UDP?
首先了解一下TCP与UDP传送字节的长度限制:
UDP报文的最大长度为512字节,而TCP则允许报文长度超过512字节。当DNS查询超过512字节时,协议的TC标志出现删除标志,这时则使用TCP发送。通常传统的UDP报文一般不会大于512字节。
区域传送时使用TCP,主要有一下两点考虑:
1.辅域名服务器会定时(一般时3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用TCP而不是UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。
2.TCP是一种可靠的连接,保证了数据的准确性。
域名解析时使用UDP协议:
客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。虽然从理论上说,客户端也可以指定向DNS服务器查询的时候使用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。
4. http请求访问80端口
http请求一定要访问80端口吗
不一定。80端口只是客户端(浏览器)发出的http请求默认去访问的服务器端口,只要请求的端口和服务器程序运行的端口保持一致就可以。我们日常开发使用的tomcat就运行在8080端口,在浏览器输入http://localhost:8080
也可以访问tomcat的。只是一般网站服务器程序都运行在80端口上,我们只需要输入网址即可,不用再输入类似ww.google.***:80这样带有端口号的地址,比较方便。
5. Cookie和Session区别
- Cookie:用来保存用户信息;存储在浏览器端;不安全,加密存到服务端,单个cookie保存的数**<=4KB**,一个站点最多保存20个Cookie。cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。
- Session:通过服务端记录用户的状态;存储在服务器,占用服务器性能。
简单的说,当你登陆一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上,客户端每次请求服务器的时候会发送当前会话sessionid,服务器根据当前sessionid判断相应的用户数据标志,以确定用户是否登陆或具有某种权限。由于数据是存储在服务器上面,所以你不能伪造
- Token:令牌,是服务器生成的一串字符串,作为客户端进行请求的一个标识。是无状态的,服务器不需要保存会话信息,可以放在Authorization header中或者cookie中,对于token而言,服务器不需要去查看你是谁,不需要保存你的会话。当用户logout的时候cookie和服务器的session都会注销;但是当logout时候token只是注销浏览器信息,不查库。
最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)
假如客户端禁用了cookie,或者不支持cookie,则会话跟踪会失效。关于WAP上的应用,常规的cookie就派不上用场了。运用session需要使用URL地址重写的方式。一切用到session程序的URL都要进行URL地址重写,否则session会话跟踪还会失效。
cookie什么情况下会丢失
1、 Cookie 的Domain设置不正确 ;
2、 Cookie 超时 ;
3、 Cookie 中含有一些非法字符,致使浏览器丢弃 Cookie
4、程序源码可能有多处重复设置或取消 Cookie — Domain的值设置为 ".hi scr.***"时,表示hi scr.***下的所有二级…
Json web token (JWT):
- 头部(header):两部分信息,声明类型、声明加密的算法 。
- 载荷(playload):存放有效信息,包含三个部分:标准中注册的声明、公共的声明、私有的声明
- 签名(signature):签证信息由三部分组成:header (base64后的)、payload (base64后的)、secret。这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。
由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session。典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。 服务端在HTTP协议中告诉客户端 在cookie中存放session的 id 下次发送请求的时候都会带上id,这样服务端就知道是哪个session了。
6. TCP 协议如何保证可靠传输
- 应用数据被分割成 TCP 认为最适合发送的数据块。
- TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
- 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
- TCP 的接收端会丢弃重复的数据。
- 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
- 拥塞控制: 当网络拥塞时,减少数据的发送。
- 停止等待协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就- 停止发送,等待对方确认。在收到确认后再发下一个分组。 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
6. TCP拥塞控制
步骤:当拥塞窗口cwnd小于慢开始门限ssthresh时,使用慢开始算法(1 2 4 8指数),如果大于ssthresh时就使用拥塞避免算法(1 2 3线性),如果报文段的超时计数器超时发生重传,就把ssthresh变为原来的一半,cwnd变为1,再开始慢开始算法;如果收到3个连续的重复确认,就开始**快恢复,**就是将cwnd和ssthresh设置为当前值的一半,开始拥塞避免算法。
- 慢开始:
- 拥塞避免:
- 快重传:就是使发送方尽快进行重传,而不是等超时重传计时器超时在重传。
要求接收方不要等待自己发送数据时才捎带确认,而是要立即发送确认;
即使收到了失序的报文段也要立即发送对已收到的报文段的重复确认;
发送方一旦收到连续3个的重复确认,就将相应的报文段立即重传,而不是等超时重传计时器超时在重传。 - 快恢复:发送方将慢开始门限ssthresh值和拥塞窗口cwnd调整为当前窗口的一半,开始执行拥塞避免算法。
7. TCP滑动窗口 流量控制
首先接受方会发送ACK(按序到达的最大序列号)和接收窗口rwnd给接收方,接收方收到确认后可以将窗口向前移动,如果超时重传计时器超时就会重传。
8. ARQ协议
ARQ 协议(Automatic Repeat-reQuest) 是是 OSI 模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。
-
停止等待 ARQ 协议: 每次发送一个报文,确认后发送下一个报文,发送窗口和接受窗口均是 1,停止等待 ARQ 所需缓冲区小但是效率低.
优点: 简单
缺点: 信道利用率低,等待时间长 -
连续 ARQ 协议: 连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组之前的所有分组都已经正确收到了。
优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。
缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5 条 消息,中间第三条丢失(3 号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退N),表示需要退回来重传已经发送过的 N 个消息。
9. ARP协议
根据 IP 地址,查到MAC地址
操作系统通常会把第一次通过 ARP 获取的 MAC 地址缓存起来,以便下次直接从缓存中找到对应 IP 地址的 MAC 地址。
10. TCP UDP 协议的区别
TCP:面向连接、传输可靠、字节流、效率慢、所需资源多、仅支持一对一两点通信–文件传输 发送、接收邮件、远程登陆
UDP:无连接、传输不可靠、数据报文段、效率快、所需资源少、支持一对一、一对多、多对多的交互通信–QQ电话 视频、直播等
1. 连接
- TCP 是面向连接的传输层协议,传输数据前先要建立连接。
- UDP 是不需要连接,即刻传输数据。
2. 服务对象
- TCP 是一对一的两点服务,即一条连接只有两个端点。
- UDP 支持一对一、一对多、多对多的交互通信
3. 可靠性
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
- UDP 是尽最大努力交付,不保证可靠交付数据。
4. 拥塞控制、流量控制
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
- UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
5. 首部开销
- TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
20
个字节,如果使用了「选项」字段则会变长的。 - UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
6. 传输方式
- TCP 是流式传输,没有边界,但保证顺序和可靠。
- UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
7. 分片不同
- TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
- UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
10. HTTP 和 HTTPS区别
- 端口 :HTTP 的 URL 由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。
- 安全性和资源消耗: HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
- 对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有 DES、AES 等;
- 非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有 RSA、DSA 等。
HTTPS 采⽤的是对称加密和非对称加密结合的混合加密⽅式: 在通信建立前采⽤非对称加密的⽅式交换会话秘钥,后续就不再使⽤非对称加密。 在通信过程中全部使⽤对称加密的会话秘钥的⽅式加密明⽂数据。
HTTPS首先要解决的是:认证的问题
- 客户端是需要确切地知道服务端是不是「真实」,所以在HTTPS中会有一个角色:CA(公信机构)
- 服务端在使用HTTPS前,需要去认证的CA机构申请一份「数字证书」。数字证书里包含有证书持有者、证书有效期、「服务器公钥」等信息
- CA机构也有自己的一份公私钥,在发布数字证书之前,会用自己的「私钥」对这份数字证书进行加密
- 等到客户端请求服务器的时候,服务端返回证书给客户端。客户端用CA的公钥对证书解密(因为CA是公信机构,会内置到浏览器或操作系统中,所以客户端会有公钥)。这个时候,客户端会判断这个「证书是否可信/有无被篡改」
- 私钥加密,公钥解密我们叫做「数字签名」(这种方式可以查看有无被篡改)
TLS握手:
客户端发送Client Hello(TLS版本、支持的加密套件、第一随机数)给服务端;
服务端发送(TLS版本、选择一个客户端支持的版本、第二随机数)给客户端;发送证书和公钥给客户端;
客户端用公钥加密生成的预主密钥,发给服务端,并通过第一第二随机数预主密钥生成会话密钥,告诉服务端以后用会话密钥通信;
服务端用私钥解密加密后的预主密钥,然后用第一第二随机数预主密钥生成会话密钥。
11. HTTP 1.1 2.0 3.0 区别
HTTP 1.1
HTTP 1.1 较于1.0的改进:长连接、管道传输、断点续传
- 使用 TCP 长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
- 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
HTTP/1.1 还是有性能瓶颈:
- 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩
Body
的部分;发送冗长的首部。每次互相发送相同的首部造成的浪费较多; - 服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头阻塞;
- 没有请求优先级控制;
- 请求只能从客户端开始,服务器只能被动响应。
HTTP 2.0
HTTP/2 相比 HTTP/1.1 性能上的改进:头部压缩、二进制分帧层、多路复用、服务端推送
- 头部压缩:HTTP/2 会压缩头,如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。
在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了 - 二进制格式:HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧和数据帧。
- 数据流:每个请求或回应的所有数据包,称为一个数据流(
Stream
)。每个数据流都标记着一个独一无二的编号,其中规定客户端发出的数据流编号为奇数, 服务器发出的数据流编号为偶数。
客户端还可以指定数据流的优先级。优先级高的请求,服务器就先响应该请求。 - 多路复用:HTTP/2 是可以在一个连接中并发多个请求或回应,而不用按照顺序一一对应。
移除了 HTTP/1.1 中的串行请求,不需要排队等待,也就不会再出现「队头阻塞」问题,降低了延迟,大幅度提高了连接的利用率 - 服务器推送:HTTP/2 还在一定程度上改善了传统的「请求 - 应答」工作模式,服务不再是被动地响应,也可以主动向客户端发送消息。
举例来说,在浏览器刚请求 HTML 的时候,就提前把可能会用到的 JS、CSS 文件等静态资源主动发给客户端,减少延时的等待,也就是服务器推送(Server Push,也叫 Cache Push)
HTTP 1.1:可以一下发多个请求,但是服务器必须按顺序处理,队头阻塞
HTTP 2.0:服务器可以不按顺序处理,一定程度解决了队头阻塞
HTTP 3
HTTP 3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。
QUIC 协议的特点:
- 无队头阻塞,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,也不会有底层协议限制,某个流发生丢包了,只会影响该流,其他流不受影响;
- 建立连接速度快,因为 QUIC 内部包含 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
- 连接迁移,QUIC 协议没有用四元组(源 IP、源端口、目的 IP、目的端口)的方式来“绑定”连接,而是通过「连接 ID 」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本;
-
单向流同步动态表,HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞问题。
动态表是具有时序性的,如果首次出现的请求发生了丢包,后续的收到请求,对方就无法解码出 HPACK 头部,因为对方还没建立好动态表,因此后续的请求解码会阻塞到首次请求中丢失的数据包重传过来。
4G切换到WIFI
基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接,那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接,而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。
而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
总结:HTTP 1.1 较于1.0的改进,默认使用长连接、断点续传(利用HTTP消息头使用分块传输编码,将实体主体分块进行传输)、增加了更多的缓存控制策略。
HTTP 2.0新增特性:**多路复用、二进制分帧层、头部压缩、服务端推送**。
HTTP 2.0不再以文本的方式传输,采用二进制分帧层,对头部进行了压缩,支持流控,最主要就是HTTP 2是支持多路复用的(通过单一的TCP连接并行发起多个的请求和响应消息)。
HTTP1.1提出的管线化只能串行(一个响应必须完全返回后,下一个请求才会开始传输)。
HTTP 2.0多路复用则是利用分帧数据流,把HTTP协议分解为互不依赖的帧(为每个帧标序发送,接收回来的时候按序重组),进而可以乱序发送避免一定程度上的队头阻塞问题。但是,无论是HTTP1.1还是HTTP 2.0,response响应的处理顺序总是需要跟request请求顺序保持一致的。假如某个请求的response响应慢了,还是同样会有阻塞的问题。这受限于HTTP底层的传输协议是TCP,没办法完全解决队头阻塞的问题
HTTP 2.0协议的多路复用机制解决了HTTP层的队头阻塞问题,但是在TCP层****仍然存在队头阻塞问题。
response响应的「处理顺序」总是需要跟request请求顺序保持一致的
HTTP1.1 较于1.0的改进:
- 长连接 : 在HTTP 1.0中,默认使用的是短连接,也就是说每次请求都要重新建立一次连接。HTTP 是基于TCP/IP协议的,每一次建立或者断开连接都需要三次握手四次挥手的开销,如果每次请求都要这样的话,开销会比较大。因此最好能维持一个长连接,可以用个长连接来发多个请求。HTTP 1.1起,默认使用长连接 ,默认开启Connection:keep-alive。 HTTP 1.1的持续连接有非流水线方式和流水线方式 。流水线方式是客户在收到HTTP的响应报文之前就能接着发送新的请求报文。与之相对应的非流水线方式是客户在收到前一个响应后才能发送下一个请求。
- 错误状态响应码 :在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
- 缓存处理 :在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略。例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
-
管道机制(pipelining):即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。以前,在同一个TCP连接里,先发送A请求,待服务器响应后,再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求。
总结:即可在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
但是服务器还是按照顺序,先回应 A 请求,完成后再回应 B 请求。要是 前面的回应特别慢,后面就会有许多请求排队等着。这称为「队头堵塞」。 - 带宽优化及网络连接的使用 :HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
HTTP2.0优点:
- 使用了多路复用
HTTP2 复用TCP连接,在一个连接里,客户端和浏览器都可以「同时」发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了队头堵塞。
多路复用允许同时通过单一的 HTTP 2 连接发起多重的请求-响应消息。在 HTTP 1.1 协议中浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞。 HTTP 2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP 2 连接发起多重的请求-响应消息。因此 HTTP 2 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,HTTP 2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。 - 增加了二进制分帧层
解决了HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层中, HTTP 2 会将所有传输的信息分割为更小的信息和帧(frame),并对它们采用二进制格式的编码 - HTTP 2 通信都在一个连接上完成
HTTP 2 通过让所有数据流共用同一个连接,可以更有效地使用 TCP 连接,让高带宽也能真正的服务于 HTTP 的性能提升。 -
首部压缩
HTTP 2 会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的分。可以提高速度
HTTP 1.1并不支持 HTTP 首部压缩,为此 SPDY 和 HTTP/2 应运而生, SPDY 使用的是通用的DEFLATE 算法,而 HTTP/2 则使用了专门为首部压缩而设计的 HPACK 算法。 -
服务端推送(Server Push)
在 HTTP 2 中,服务器可以对客户端的一个请求发送多个响应。
13. GET和POST区别
- GET用于获取资源,Post用于传输实体。
- GET请求在URL中传送的参数只能进行url编码,而且有浏览器会对 URL 的长度有限制,不安全;而POST放在Request body,支持多种编码方式,没有长度限制。
- GET具有幂等性,而POST不具有幂等性。GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存位书签。
14. OSI 七层的结构
- 应用层(application layer)
- 应用层(application layer):是体系结构中的最高。直接为用户的应用进程(例如电子邮件、文件传输和终端仿真)提供服务。
- 在因特网中的应用层协议很多,如支持万维网应用的HTTP协议,支持电子邮件的SMTP协议,支持文件传送的FTP协议,DNS,POP3,SNMP,Tel***等等。
- 表示层
- 会话层
- 运输层(transport layer)
- 运输层(transport layer):负责向两个主机中进程之间的通信提供服务。由于一个主机可同时运行多个进程,因此运输层有复用和分用的功能。
- 复用,就是多个应用层进程可同时使用下面运输层的服务。
- 分用,就是把收到的信息分别交付给上面应用层中相应的进程。
- 网络层(***work Layer)
- 网络层的任务就是选择合适的网间路由和交换结点,确保数据及时传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在TCP/IP体系结构中,由于网络层使用IP协议,因此分组也叫IP数据报,简称数据报。此外,网络层还可以实现拥塞控制、网际互连等功能;
- 数据链路层
- 数据链路层通常简称为链路层。俩台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。在俩个相邻的节点之间传送数据时,数据链路层将网络层交下来的IP数据报组装成帧,在俩个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。
- 物理层
- 在物理层上所传送的数据单位是比特。物理层的作用是实现相邻计算机节点之间的比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异,使其上面的数据链路层不必考虑网络的具体传输介质是什么。
TCP/IP ⽹络模型共有 4 层,分别是应⽤层、传输层、⽹络层和⽹络接⼝层,每⼀层负责的职能如下:
应⽤层,负责向⽤户提供⼀组应⽤程序,⽐如 HTTP、DNS、FTP 等;
传输层,负责端到端的通信,⽐如 TCP、UDP 等;
⽹络层,负责⽹络包的封装、分⽚、路由、转发,⽐如 IP、ICMP 等;
⽹络接⼝层,负责⽹络包在物理⽹络中的传输,⽐如⽹络包的封帧、 MAC 寻址、差错检测,以及通 过⽹卡传输⽹络帧等;
15. 从浏览器输入url显示页面的过程
HTTPS 也就是在 HTTP 与 TCP 层之间增加了 SSL/TLS 安全传输层,HTTP/3 甚至把 TCP 层换成了基于 UDP 的 QUIC。
- 【应用层】浏览器对 URL 进行解析,生成HTTP请求信息。请求行、请求头、请求体状态行、响应头、响应体
- DNS解析:浏览器–>本地域名服务器缓存–>根域名服务器(全球有13个)–>***顶级域名服务器–>baidu.***主域名(权威)服务器–>找到ip地址 缓存到本地域名服务器
- 【传输层】添加TCP头部,包含源端口号和目标端口,三次握手,检验和、拥塞控制、流量控制、ARQ协议
- 【网络层】添加IP头部,包含源IP地址 和 目标IP地址,协议号(TCP)
- 客户端有多个网卡,就会有多个 IP 地址,根据路由表规则,来判断哪一个网卡作为源地址 IP。将目标IP地址与子网掩码进行与运算,看得到的与路由表中的条目(destination)是否匹配,匹配就用当前网卡,如果都不匹配,就用默认网关。
- 【网络层】添加MAC头部,包含发送方 MAC 地址和接收方目标 MAC 地址,用于两点之间的传输。
- ARP 协议在以太网中以广播的形式,找到路由器的 MAC 地址。ARP 缓存
- 【网络接口层】网卡:将数字信息转换为电信号,加上帧头和帧尾,开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列。网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方。需要将数字信息转换为电信号,才能在网线上传输,数据发送过程。负责执行这一操作的是网卡,要控制网卡还需要靠网卡驱动程序。
- 起始帧分界符是一个用来表示包起始位置的标记
- 末尾的 FCS(帧校验序列)用来检查包传输过程是否有损坏
- 交换机:交换机根据MAC地址表查找 MAC 地址,然后将信号发送到相应的端口。
- 交换机的 MAC 地址表主要包含两个信息:一个是设备的 MAC 地址,另一个是该设备连接在交换机的哪个端口上
- 地址表中找不到指定的 MAC 地址,将包转发到除了源端口之外的所有端口上,无论该设备连接在哪个端口上都能收到这个包。此外,如果接收方 MAC 地址是一个广播地址,那么交换机会将包发送到除源端口之外的所有端口。
- 路由器:通过包末尾的 FCS 进行错误校验。如果没问题则检查 MAC 头部中的接收方 MAC 地址,看看是不是发给自己的包,如果是就放到接收缓冲区中,否则就丢弃这个包。完成包接收操作之后,路由器就会去掉包开头的 MAC 头部,根据 MAC 头部后方的 IP 头部中的内容进行包的转发操作。
- 首先是查询路由表判断转发目标,将目标 IP 地址和子网掩码进行与,看得到的是否与路由表中的 IP 地址相同。
- 包的发送操作,根据路由表的网关列判断对方的地址,如果网关是一个 IP 地址,继续转发,如果网关为空,到达终点。
- 知道对方的 IP 地址之后,接下来需要通过 ARP 协议根据 IP 地址查询 MAC 地址,并将查询的结果作为接收方 MAC 地址
- 服务器接收到请求,MVC处理请求流程,返回 HTTP 响应报文。
- 浏览器解析渲染页面
路由器和交换机是有区别的。
- 都是通过查表判断包转发的目标。
- 因为路由器是基于 IP 设计的,俗称三层网络设备,路由器的各个端口都具有 MAC 地址和 IP 地址;
- 而交换机是基于以太网设计的,俗称二层网络设备,交换机的端口不具有 MAC 地址。
16. 重定向和转发区别
重定向:
- 原理:用户第一次通过【手动方式】通知浏览器访问OneServlet。OneServlet工作完毕后,将TwoServlet地址写入到响应头location属性中,导致Tomcat将302状态码写入到状态行。在浏览器接收到响应包之后,会读取到302状态。此时浏览器自动根据响应头中location属性地址发起第二次请求,访问TwoServlet去完成请求中剩余任务
- 请求地址:可以是当前网站资源地址也可以是其他网站资源地址。
- 请求次数:浏览器至少发送两次请求,但是只有第一次请求是用户手动发送。后续请求都是浏览器自动发送的。
- 请求方式:一定是GET。
- 缺点:重定向解决方案需要在浏览器与服务器之间进行多次往返,大量时间消耗在往返次数上,增加用户等待服务时间。
请求转发:
- 原理:用户第一次通过手动方式要求浏览器访问OneServlet。 OneServlet工作完毕后,通过当前的请求对象代替浏览器向Tomcat发送请求,申请调用TwoServlet。Tomcat在接收到这个请求之后,自动调用TwoServlet来完成剩余任务。
- 请求次数:在请求转发过程中,浏览器只发送一次请求。
- 请求地址:只能向Tomcat服务器申请调用当前网站下资源文件地址。
- 请求方式:在请求转发过程中,浏览器只发送一个了个Http请求协议包。参与本次请求的所有Servlet共享同一个请求协议包,因此这些Servlet接收的请求方式与浏览器发送的请求方式保持一致。
- 优点:
1)无论本次请求涉及到多少个Servlet,用户只需要手动通过浏览器发送一次请求
2)Servlet之间调用发生在服务端计算机上,节省服务端与浏览器之间往返次数增加处理服务速度
17. HTTP状态码
3.操作系统
1. 进程线程通信
- 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
- 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 消息队列:
- 消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。
- 管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。比 FIFO 更有优势。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段,不能传递复杂消息,只能用来同步。
- 套接字(Sockets) :此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
进程的上下⽂切换不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄存器等 内核空间的资源。
Java线程的通信方式
- volatile
- 等待/通知机制
- join方式
- threadLocal
2. MESI CPU缓存一致性
总线锁是锁总线,对共享变量的修改在相同的时刻只允许一个CPU操作。
MESI协议:修改、互斥、共享、无效
读取数据时,其他CPU如果修改了该缓存行的数据,要先把修改的数据写回主内存中。
修改数据时,要使其他CPU中该缓存行状态变为无效。
- MESI协议的原理大概就是:当每个CPU读取共享变量之前,会先识别数据的对象状态(是修改、还是共享、还是独占、还是无效)。
- 如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取
- 如果是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但还没被修改
- 如果是修改,说明当前CPU正在修改该变量的值,同时会向其他CPU发送该数据状态为invalid(无效)的通知,得到其他CPU响应后(其他CPU将数据状态从共享(share)变成invalid(无效)),当前CPU会将高速缓存的数据写到主存,并把自己的状态从modify(修改)变成exclusive(独占)
- 如果是无效,说明当前数据是被改过了,需要从主存重新读取最新的数据。
小结:其实MESI协议做的就是判断对象状态,根据对象状态做不同的策略。关键就在于某个CPU在对数据进行修改时,需要同步通知其他CPU,表示这个数据被我修改了,你们不能用了。比较于「总线锁」,MESI协议的**”锁粒度”更小**了,性能那肯定会更高了。
- 当CPU修改数据时,需要「同步」告诉其他的CPU,等待其他CPU响应接收到invalid(无效)后,它才能将高速缓存数据写到主存。
- 优化思路就是从「同步」变成「异步」。现在则把最新修改的值写到「store buffer」中,并通知其他CPU记得要改状态,随后CPU就直接返回干其他事了。等到收到其它CPU发过来的响应消息,再将数据更新到高速缓存中。
- 其他CPU接收到invalid(无效)通知时,也会把接收到的消息放入「invalid queue」中,只要写到「invalid queue」就会直接返回告诉修改数据的CPU已经将状态置为「invalid」。
问题:即便CPU2收到了invalid(无效)通知,但CPU1的值还没写到主存,那CPU2再次向主存读取的时候,还是旧值…
- 总体而言,由于CPU对「缓存一致性协议」进行的异步优化「store buffer」「invalid queue」,很可能导致后面的指令很可能查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序),这种现象很多时候被称作CPU乱序执行。
- 「内存屏障」其实就是为了解决「异步优化」导致「CPU乱序执行」/「缓存不及时可见」的问题。内存屏障可以分为三种类型:写屏障,读屏障以及全能屏障(包含了读写屏障)。
- 写屏障:CPU当发现写屏障的指令时,会把该指令「之前」存在于「store Buffer」所有写指令刷入高速缓存。通过这种方式就可以让CPU修改的数据可以马上暴露给其他CPU,达到「写操作」可见性的效果。
- 读屏障:CPU当发现读屏障的指令时,会把该指令「之前」存在于「invalid queue」所有的指令都处理掉。
- 写屏障:共享变量之前的改动同步到主存中,不会将写屏障之前的代码重排到写屏障之后。
- 读屏障:共享变量之后读取的都是最新值,不会将读屏障之后的代码重排在读屏障之前。
3. BIO NIO AIO
BIO 同步阻塞式 IO:适用于连接数目比较小且固定的结构。它对服务器资源要求比较高,并发局限于应用中,JDK1.4之前唯一选择,但程序直观简单易理解,如之前在 Apache 中使用。每个连接创建一个线程,阻塞等待文件描述符主备好
NIO 同步非阻塞 IO:适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中,变成比较复杂。JDK1.4开始支持,如在 Nginx、***ty 中使用。同步就是要等文件准备好,用轮询的方式询问,阻塞就是不用一直等
AIO 异步非堵塞 IO:适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持,在成长中,***ty 曾经使用过,后来放弃。异步就是准备好了就会告诉你准备好了
BIO 同步阻塞式 IO:当用户程序执行 read
,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read
才会返回。
NIO 同步非阻塞 IO:非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read
调用才可以获取到结果。数据准备过程不需要等待,要轮询,但是数据拷贝到应用程序缓冲区时,需要阻塞等待。
AIO 异步非堵塞 IO:「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。当我们发起 aio_read
(异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wMW6S11D-1683542094239)(计网操作系统等面经.assets/image-20220520162101586.png#id=rzLcb&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
4. IO多路复用的三种实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c0smoi5B-1683542094239)(计网操作系统等面经.assets/image-20220519211347393.png#id=c3XwZ&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1iZ0IQJk-1683542094241)(计网操作系统等面经.assets/image-20220520110000455.png#id=Sdobi&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sdPHa7U0-1683542094242)(计网操作系统等面经.assets/image-20220520115357876.png#id=RBNtg&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tPTu5w1g-1683542094244)(计网操作系统等面经.assets/image-20220520141807810.png#id=HyPCA&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
流程:调用epoll_create创建epoll实例、创建serverSocket得到FD、调用epoll_ctl注册到红黑树上,开始监听、调用epoll_wait等待FD是否就绪
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3jowlpr-1683542094244)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/image-20220521154339928.png#id=rnoA4&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
-
select/poll
select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合(以比特位来标记是哪个文件描述符),然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为1024
,只能监听 0~1023 的文件描述符。
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长
很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。 -
epoll
epoll 通过两个方面,很好解决了 select/poll 的问题。
第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过epoll_ctl()
函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是O(logn)
,通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
文件就绪基于回调,什么场景需要遍历红黑树呢?插入新的FD时,需要先判断是否存在,基于红黑树查询效率高,判断效率自然高
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。
- 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
- LT缺点:重复通知效率低;出现惊群现象,多个进程监听一个fd,fd就绪后通知所有进程,可能前几个进程就能够把fd处理完了,后面的进程就白唤醒了
边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read
和 write
)返回错误,错误类型为 EAGAIN
或 EWOULDBLOCK
。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jVn7BaL-1683542094245)(计网操作系统等面经/image-20220520145124177.png#id=cpy6I&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jEsVr3Xe-1683542094245)(计网操作系统等面经.assets/image-20220323164504043.png#id=l3pZX&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
6. 高性能网络模式:Reactor 和 Proactor
常见的 Reactor 实现方案有三种。
第一种方案单 Reactor 单进程 / 线程,不用考虑进程间通信以及数据同步的问题,因此实现起来比较简单,这种方案的缺陷在于无法充分利用多核 CPU,而且处理业务逻辑的时间不能太长,否则会延迟响应,所以不适用于计算机密集型的场景,适用于业务处理快速的场景,比如 Redis 采用的是单 Reactor 单进程的方案。
第二种方案单 Reactor 多线程,通过多线程的方式解决了方案一的缺陷,但它离高并发还差一点距离,差在只有一个 Reactor 对象来承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
第三种方案多 Reactor 多进程 / 线程,通过多个 Reactor 来解决了方案二的缺陷,主 Reactor 只负责监听事件,响应事件的工作交给了从 Reactor,***ty 和 Memcache 都采用了「多 Reactor 多线程」的方案,Nginx 则采用了类似于 「多 Reactor 多进程」的方案。
Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。
因此,真正的大杀器还是 Proactor,它是采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。
不过,无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件
-
select
每次调用select,都需要把文件描述符集合从用户态拷贝到内核态,固定长度的 BitsMap
对socket进行扫描时是线性扫描,即采用轮询的方法,不管哪个Socket是活跃的,都遍历一遍,效率较低。
select支持的文件描述符数量太小了,32位机默认是1024个,64位机默认是2048,即能监听端口的大小有限。 -
poll
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
但是它没有最大连接数的限制,原因是它是基于链表来存储的。
poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。 -
epoll
epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll有EPOLLLT和EPOLLET两种触发模式
LT模式(水平触发,默认的模式)
只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作。
ET模式(边缘触发) “高速”模式
它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。
ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者遇到EAGAIN错误。
5. 零拷贝技术
- DMA 技术,直接内存访问(Direct Memory A***ess) 技术。
什么是 DMA 技术?简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KWw7dVKz-1683542094247)(计网操作系统等面经.assets/DRM I_O 过程.png#id=Qlx6j&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
-
传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IoVZavUB-1683542094248)(计网操作系统等面经.assets/传统文件传输.png#id=iEaUE&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
首先,期间共**发生了 4 次用户态与内核态的上下文切换**,因为发生了两次系统调用,一次是 `read()` ,一次是 `write()`,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
其次,还**发生了 4 次数据拷贝**,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝.
零拷贝技术实现的方式通常有 2 种:
- mmap + write
- sendfile
-
mmap + write
mmap()
系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
具体过程如下:
- 应用进程调用了
mmap()
后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区; - 应用进程再调用
write()
,操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据; - 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NOaMKjoN-1683542094249)(计网操作系统等面经.assets/mmap + write 零拷贝.png#id=B647I&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]共需要3次拷贝,减少了一次拷贝,但仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
-
sendfile
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数sendfile()
,函数形式如下:
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
首先,它可以替代前面的 read()
和 write()
这两个系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就可以减少一次系统调用,即只有 2 次上下文切换的开销,和 3 次数据拷贝。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xAYejYl-1683542094250)(计网操作系统等面经.assets/senfile-3次拷贝.png#id=QQiLk&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
于是,从 Linux 内核 2.4
版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile()
系统调用的过程发生了点变化,具体过程如下:
- 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
- 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
所以,这个过程之中,只进行了 2 次数据拷贝,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RLA98I7C-1683542094251)(计网操作系统等面经.assets/senfile-零拷贝.png#id=pzE0f&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上。
-
使用零拷贝项目
事实上,Kafka 这个开源项目,就利用了「零拷贝」技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一。如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的transferTo
方法:
另外,Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率 -
大文件传输
零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能,同时,为了解决机械硬盘寻址慢的问题,它还协助 I/O 调度算法实现了 IO 合并与预读,这也是顺序读比随机读性能好的原因。这些优势,进一步提升了零拷贝的性能。
需要注意的是,零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送。
另外,当传输大文件时,不能使用零拷贝,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到PageCache,并且大文件的缓存命中率不高,这时就需要使用**「异步 IO (直接 IO)」的方式。
在 Nginx 里,可以通过配置,设定一个文件大小**阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝。
绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。
所以,传输文件的时候,我们要根据文件的大小来使用不同的方式:
- 传输大文件的时候,使用「异步 I/O + 直接 I/O」;
- 传输小文件的时候,则使用「零拷贝技术」;
Nginx io多路复用(多 Reactor 多进程)、零拷贝技术(阈值,大小文件)
Redis io多路复用(单 Reactor 单进程)、
7. 进程调度算法
- 先来先服务—FCFS:对长作业有利,对短作业不好,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。
- 短作业优先—SJF:对短作业有利,对长作业不好,容易出现饥饿现象。
- 高响应比优先—HRRN:等待时间 + 要求服务时间 / 要求服务时间;等待时间相同,短作业优先;要求服务时间相同时,等待时间长的优先。
- 时间片轮转—RR:如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;如果设得太长又可能引起对短作业进程的响应时间变长;不区分任务紧急程度。
-
优先级调度算法:从就绪队列中选择最高优先级的进程进行运行。
进程的优先级可以分为,静态优先级和动态优先级:
- 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
- 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级。
该算法也有两种处理优先级高的方法,非抢占式和抢占式:
- 非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
- 抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。
但是依然有缺点,可能会导致低优先级的进程永远不会运行。
- 多级反馈队列调度算法:可能导致饥饿
- 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
- 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;
- 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
- 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行
8. 死锁
产生的四个条件:
- 互斥条件:多个线程不能同时使用同一个资源
- 请求与保持条件:当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1
- 不可剥夺条件:当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
- 环路等待条件:在死锁发生的时候,两个线程获取资源的顺序构成了环形链
如何预防死锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IHscapxx-1683542094252)(计网操作系统等面经.assets/image-20220328155120636.png#id=U***Pn&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
避免死锁-银行家算法
系统总共资源数,当前剩余的资源数,每个线程最大资源需求数
每个线程有一个最大资源需求数,已经分配了一些资源,如果有线程还想申请一些资源,判断申请的资源数是否大于剩余的资源数,如果大于就试着分配,然后用安全性算法检测此次分配是否会导致系统进入不安全状态,如果进入就不分配,等待。
安全性算法:检测当前剩余的资源数是否满足某个进程的最大需求,如果满足就将其加入安全序列,并将该进程持有的资源回收,继续查看下一个,最终看是否能让所有进程都加入安全序列。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCJmQyyl-1683542094253)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/image-20220328192152564.png#id=sMTBX&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
死锁的检测和解除
定义一个资源分配图:
两种节点:进程节点、资源节点;
两种边:进程节点–>资源节点 进程正在请求资源的边;资源节点–>进程节点 已经分配给进程的资源的边
检测死锁:一次消除与不阻塞进程相连的边,直到无边可消,若资源分配图不可完全简化(不可以消除所有的边)就说明发生了死锁。
解除死锁:资源剥夺法(将进程挂起,剥夺资源)、终止进程法、进程回退法
杀死哪种进程:杀死进程优先级低的、杀死运行时间短的、杀死使用资源多的(为了解除死锁)、杀死批处理式的保留交互式的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ixSXSo8j-1683542094254)(计网操作系统等面经.assets/image-20220328194442014.png#id=ASnWo&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VzaWGQbe-1683542094255)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzkxNDYwNA==,size_16,color_FFFFFF,t_70.png#id=syvmK&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
死锁的检测和解除
定义一个资源分配图:
两种节点:进程节点、资源节点;
两种边:进程节点–>资源节点 进程正在请求资源的边;资源节点–>进程节点 已经分配给进程的资源的边
检测死锁:一次消除与不阻塞进程相连的边,直到无边可消,若资源分配图不可完全简化(不可以消除所有的边)就说明发生了死锁。
解除死锁:资源剥夺法(将进程挂起,剥夺资源)、终止进程法、进程回退法
杀死哪种进程:杀死进程优先级低的、杀死运行时间短的、杀死使用资源多的(为了解除死锁)、杀死批处理式的保留交互式的
4. Redis
1. Redis为啥快?
- 内存存储:因为是使用内存存储数据,可以避免频繁的进行写盘操作,大大降低响应时间。
- 单线程:单线程避免了多线程的频繁上下文切换问题。redis的单线程并不是指在整个redis的服务端只有一个线程在进行工作,只是在接受客户端的IO请求响应进行读写的时候是单线程的操作;redis本身是存在多线程的使用场景的,比如:异步删除、持久化、集群同步。redis核心业务部分,比如处理命令时是单线程,在4.0时引入多线程异步处理耗时长的任务,删除big key,后台一个线程删除;6.0中在核心网络模型中引入多线程,提高对于多核CPU的利用率
- 多路IO复用机制:利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-caJlDuaO-1683542094257)(计网操作系统等面经.assets/image-20220520162452027.png#id=QZDhu&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
2. Redis可以用来干啥?
- 缓存:毫无疑问这是Redis当今最为人熟知的使用场景。再提升服务器性能方面非常有效。
- 一些频繁被访问的数据:经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在redis中,因为redis 是放在内存中的可以很高效的访问。
- Session共享:以PHP为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。
- 好友关系:利用集合Set的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能。
- 排行榜:在使用传统的关系型数据库(mysql oracle 等)来做这个事,非常的麻烦,而利用Redis的SortSet(有序集合)数据结构能够简单的搞定。
- 计算器/限速器:利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力。实现:用户每访问一次incur(key)如果是第一次访问就设置过期时间位1秒,为保证incur和expire原子性可以用Lua脚本。
- 简单消息队列:除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦。
3. Redis数据结构
3.1 SDS简单动态字符串
- C 语言的字符串不足:
- 获取字符串长度的时间复杂度为 O(N);
- 字符串的结尾是以 “\0” 字符标识,只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据;
- 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止;
- 不可修改
-
SDS结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tsy0MGWG-1683542094258)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/image-20220511153428956.png#id=EH3ez&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷
- len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
-
alloc,分配给字符数组的空间长度。这样在修改字符串的时候,SDS API 通过
alloc - len
计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。而且,当判断出缓冲区大小不够用时,Redis 会**自动将扩大 **SDS 的空间大小(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容),以满足修改所需的大小。所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。有效的减少内存分配次数;内存预分配,分配内需要切换到内核态,减少消耗 - flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,uint32_t 表示字符数组长度和分配空间大小不能超过 2 的 32 次方。
- buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YCdN5Tub-1683542094259)(计网操作系统等面经.assets/516738c4058cdf9109e40a7812ef4239.png#id=lutg4&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
3.2 双向链表
- list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义实现的 节点值复制函数dup、节点值释放函数free、节点值比较函数match 函数。举个例子,下面是由 list 结构和 3 个 listNode 结构组成的链表。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Eu2otah-1683542094259)(计网操作系统等面经.assets/cadf797496816eb343a19c2451437f1e.png#id=kmK94&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
- Redis 的链表优点:
- listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),头尾节点指向 NULL,所以链表是无环链表;
- list 有表头指针 head 和表尾节点 tail,获取链表的表头节点和表尾节点的时间复杂度只需O(1);
- list有链表节点数量 len,获取链表中的节点数量的时间复杂度只需O(1);
- listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;
- 链表的缺陷:
- 链表每个节点之间的内存是不连续的,意味着无法很好利用 CPU 缓存。能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。
- 保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
3.3 压缩列表
- 压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组
压缩列表在表头有三个字段:
- zlbytes,记录整个压缩列表占用对内存字节数;
- zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
- zllen,记录压缩列表包含的节点数量;
- zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QI736MVS-1683542094261)(计网操作系统等面经.assets/a3b1f6235cf0587115b21312fe60289c.png#id=Bb2s3&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
压缩列表节点包含三部分内容:
- prevlen,若前一个节点的长度小于 254 字节, prevlen 属性用 1 字节的空间来保存这个长度值,大于等于254,用5个字节保存
- encoding,记录了当前节点实际数据的类型以及长度,当前节点的数据是整数,则 encoding 会使用 1 字节的空间进行编码;当前节点的数据是字符串,根据字符串的长度大小,encoding 会使用 1 字节/2字节/5字节的空间进行编码。
- data,记录了当前节点的实际数据;
- 缺点:
- 查找复杂度高:在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素。
- 连锁更新:虽然压缩列表紧凑型的内存布局能节省内存开销,但是如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,造成访问压缩列表性能的下降,最糟糕的是会有「连锁更新」的问题。压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。
Redis 对象(List 对象、Hash 对象、Zset 对象)包含的元素数量较少,或者元素值不大的情况才会使用压缩列表作为底层数据结构。
3.4 哈希表字典
- 哈希表结构
- Redis 定义一个 dict 结构体,这个结构体里定义了两个哈希表(ht[2])。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cszsU6Ys-1683542094262)(计网操作系统等面经.assets/2fedbc9cd4cb7236c302d695686dd478.png#id=kFPY2&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
- dictht:哈希表数组(table)、哈希表大小(size)、哈希表大小掩码(sizemask)用于计算索引值、该哈希表已有的节点数量(used)
- dictEntry:键值对中的键(key)、值是一个联合体 ,可以是指针或无符号的 64 位整数或有符号的 64 位整数或double 类、下一个节点(next);这么做的好处是可以节省内存空间,因为当「值」是整数或浮点数时,就可以将值的数据内嵌在 dictEntry 结构里,无需再用一个指针指向实际的值,从而节省了内存空间。
-
哈希冲突
如果在一次迁移过程中,再进行rehash咋办?
- Redis 采用了「链式哈希」的方法来解决哈希冲突。
- 渐进式 rehash:dict 结构体,里定义了两个哈希表(ht[2])。
- 给「哈希表 2」 分配空间;
- 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
- 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点呢,会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
- rehash 触发条件:负载因子 = 已保存节点数量 / 哈希表长度
- 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
- 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
3.5 整数集合
- intset结构:整数集合本质上是一块连续内存空间
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
- 如果 encoding 属性值为 INTSET_ENC_INT16,那么 contents 就是一个 int16_t /int32_t /int64_t 类型的数组,数组中每一个元素的类型都是 int16_t;不同类型的 contents 数组,意味着数组的大小也会不同
- 整数集合的升级操作:整数集合升级的过程不会重新分配一个新类型的数组,而是在原本的数组上扩展空间
假设有一个整数集合里有 3 个类型为 int16_t 的元素。如果再添加一个65535需要用int32_t 类型来保存,所以整数集合要进行升级操作,首先需要为 contents 数组扩容,在原本空间的大小之上再扩容多 80 位(4x32-3x16=80),这样就能保存下 4 个类型为 int32_t 的元素。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u25yBWaG-1683542094263)(计网操作系统等面经.assets/5dbdfa7cfbdd1d12a4d9458c6c90d472.png#id=HvJFC&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
扩容完 contents 数组空间大小后,需要将之前的三个元素转换为 int32_t 类型,并将转换后的元素放置到正确的位上面,并且需要维持底层数组的有序性不变。从后向前,将之前的元素转换为32位。
- 优点:整数集合升级的好处是节省内存资源。不支持降级操作,一旦对数组进行了升级,就会一直保持升级后的状态。
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。
3.6 跳表
- skipList结构:
- header、tail:指向跳跃表的表头节点、表尾节点;访问表头表尾节点的时间复杂度就为O(1)
- level:记录目前跳跃表内节点层数最大层数,通过这个属性可以再O(1)的时间复杂度内获取层高的节点的层数。
- length:记录跳跃表的长度,O(1)的时间复杂度跳跃表的长度。
- zskiplistNode结构:
- sds ele:Zset 对象的元素值
- score:元素权重值
- backward:后向指针
- 节点的level数组,保存每层上的前向指针和跨度,跨度实际上是为了计算这个节点在跳表中的排位。
- 跳表节点层数设置:
- randomLevel() 方法,随机生成 1~MAX_LEVEL 之间的数(MAX_LEVEL表示索引的最高层数64),且有 1/2的概率返回 1、1/4的概率返回 2、1/8的概率返回 3 …
Redis 只有在 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
3.7 quicklist
- Redis 3.2 的时候,List 对象的底层改由 quicklist 数据结构实现。quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sXv2NSw5-1683542094266)(计网操作系统等面经.assets/f46cbe347f65ded522f1***3fd8dba549.png#id=hB0ef&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
- 优点:
- 虽然压缩列表是通过紧凑型的内存布局节省了内存开销,但是因为它的结构设计,如果保存的元素数量增加,或者元素变大了,压缩列表会有「连锁更新」的风险,一旦发生,会造成性能下降。
- quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能
- 在向 quicklist 添加一个元素的时候,不会像普通的链表那样,直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构
3.8 listpack
- listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 末尾也有个结尾标识 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UkHTJtlC-1683542094267)(计网操作系统等面经.assets/c5fb0a602d4caaca37ff0357f05b0abf.png#id=BQA5T&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
每个 listpack 节点结构包含三个方面:
- encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
- data,实际存放的数据;
- len,encoding+data的总长度;
- 优点:
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
4. 数据类型
string(字符串):常规key-value缓存应用。常规计数: 微博数, 粉丝数。
hash(哈希):string可以存储对象时是将对象序列化为Json字符串存储,当需要修改对象某个字段时不方便,Hash可以将每个对象中字段独立存储,可以针对单个字段做CRUD,key为用户ID,value为name:Tom,age:18
list(列表):关注列表,粉丝列表,简单的消息队列
set(集合):好友关系,利用集合Set的一些命令,比如求交集、并集、差集等1、共同好友 2、利用唯一性,统计访问网站的所有独立ip 3、好友推荐时,根据tag求交集,大于某个阈值就可以推荐
zsetsorted(有序集合):排行榜:在使用关系型数据库(mysql )来做,非常的麻烦,而利用Redis的SortSet(有序集合)数据结构能够简单的搞定。能够记录每个玩家的分数;能够对玩家的分数进行更新;能够查询每个玩家的分数和名次
Redis 对象(List 对象、Hash 对象、Zset 对象)包含的元素数量较少,或者元素值不大的情况才会使用压缩列表作为底层数据结构。
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集合作为底层实现。
为啥用跳表不用红黑树?
- 共同点:红黑树和跳表的插入O(logn)、删除O(logn)、查找O(logn)以及迭代输出的时间复杂度是一样的。
- 跳表在区间查询的时候效率是高于红黑树的,它查找时,以O(logn)的时间复杂度定位到区间的起点,然后在原始链表往后遍历就可以了 ,其它插入和单个条件查询,更新两者的复杂度都是相同的O(logn)。
- 跳表更加灵活,它在并发环境下可以通过改变索引构建策略,有效平衡执行效率和内存消耗。红黑树的平衡通过左旋转和右旋转来实现平衡。
4.1 String类型:
Redis的string类型一共有三种存储方式,当字符串长度小于等于44字节,底层采用embstr,是连续内存分配,44字节加上redisObject和sdshdr8的空间共有64字节,redis底层内存分配算法是采用2的n次方分配的;当字符串长度大于44,底层采用raw;当设置是整数,底层则采用int。
当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。
4.2 List类型
4.3 Set
4.4 Zset
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UUz1DXwb-1683542094272)(计网操作系统等面经.assets/image-20220514150259778.png#id=bihhy&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
4.5 Hash
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-55uSKhgX-1683542094274)(计网操作系统等面经.assets/image-20220514153751799.png#id=J4XTV&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
5. 持久化机制
1. RDB
- Redis 提供了两个命令来生成 RDB 文件,分别是
save
和bgsave
,他们的区别就在于是否在「主线程」里执行:
- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
- 执行了 bgsava 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
- 流程:fork主进程得到子进程,拷贝页表,共享内存空间,使用copy-on-write技术,主内存修改时拷贝一份,读取内存数据,写入新RDB文件,交由下一次的 bgsave 快照。
- 极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。
- AOF 文件的内容是操作命令,AOF文件会比RDB文件大的多。
- RDB 文件的内容是二进制数据。
2. AOF
- 在
redis.conf
配置文件中的appendfsync
配置项可以有以下 3 种参数可填:- Always,这个单词的意思是「总是」,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
- Everysec,这个单词的意思是「每秒」,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
- No,不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
- 如果要高性能,就选择 No 策略;
- 如果要高可靠,就选择 Always 策略;
- 如果允许数据丢失一点,但又想性能高,就选择 Everysec 策略
- 如果想要应用程序向文件写入数据后,能立马将数据同步到硬盘,就可以调用
fsync()
函数,这样内核就会将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。
- Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数;
- Everysec 策略就会创建一个异步任务来执行 fsync() 函数;
- No 策略就是永不执行 fsync() 函数;
- AOF 重写机制:如果AOF太大,就会重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,子进程共享内存数据开始重写AOF
【流程】:bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:
- 执行客户端发来的命令;
- 将执行后的写命令追加到 「AOF 缓冲区」;
- 将执行后的写命令追加到 「AOF 重写缓冲区」;
当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
- 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
- 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
信号函数执行完后,主进程就可以继续像往常一样处理命令了。
在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。
【写时复制】:
在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。
主进程修改了已经存在 key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的。所以如果这个阶段修改的是一个 bigkey,也就是数据量比较大的 key-value 的时候,这时复制的物理内存数据的过程就会比较耗时,有阻塞主进程的风险。
3.混合持久化
在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。aof-use-rdb-preamble yes
当开启了混合持久化时,在 AOF 重写日志时,fork
出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
6. 过期删除策略
删除达到过期时间的key。
- 1)定时删除:对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除。该策略可以立即清除过期的数据,对内存较友好,但是缺点是占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间。
- 2)惰性删除:当访问一个key时,才判断该key是否过期,过期则删除。该策略能最大限度地节省CPU资源,但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。
- 3)定期删除:每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key。该策略是前两者的一个折中方案,还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时,在不同情况下使得CPU和内存资源达到最优的平衡效果。
在Redis中,同时使用了定期删除和惰性删除。不过Redis定期删除采用的是随机抽取的方式删除部分Key,因此不能保证过期key 100%的删除。
Redis过期策略采用定期删除和惰性删除两部分。定期删除是在Redis内部有一个定时任务,会定期抽取一些key检查是否过期。惰性删除是当用户查询某个Key时,会检查这个Key是否已经过期,如果没过期则返回用户,如果过期则删除。
但是这两个策略都无法保证过期key一定删除,漏网之鱼越来越多,还可能导致内存溢出。当发生内存不足问题时,Redis还会做内存回收。内存回收采用LRU策略,就是最近最少使用。其原理就是记录每个Key的最近使用时间,内存回收时,随机抽取一些Key,比较其使用时间,把最老的几个删除。
内存淘汰策略
Redis会在每一次处理命令的时候(process***mand函数调用freeMemoryIfNeeded)判断当前redis是否达到了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的key。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g8E05Oj4-1683542094275)(计网操作系统等面经.assets/image-20220523211827887.png#id=kjghb&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
惰性删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AnqKAQO7-1683542094276)(计网操作系统等面经.assets/image-20220523211742363.png#id=V2IqP&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
周期删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bugkjMu4-1683542094278)(计网操作系统等面经.assets/image-20220523211410484.png#id=CZ9OH&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
内存淘汰
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cjAsNoSA-1683542094279)(计网操作系统等面经.assets/image-20220523212648652.png#id=FcEjS&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
7. RESP协议
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-afL63XxD-1683542094281)(计网操作系统等面经.assets/image-20220521160531117.png#id=zF3Z0&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
5. spring
1. 三级缓存解决循环依赖
单例池:存放完整的对象,经过完整声明周期的bean对象concurrentHashMap
二级缓存:不完整的对象,AOP生成的代理对象,或者原始对象HashMap
三级缓存:执行Bean AOP的SingletonFactory工厂,lamda表达式,每个单例对象都会放入三级缓存HashMap
正在创建的bean
填充aService属性的时候: 先在单例池中找Bean–>找不到–>二级缓存–>找不到–>看看是否正在被创建set–>出现了循环依赖–>到三级缓存找–>找到了λ表达式–>执行AOP–>得到aService代理对象–>将代理对象放到二级缓存中(要是aService没有切面,就生成原始对象,放入到二级缓存中)
从三级缓存中找对象就是在执行AOP,找到以后就将其从三级缓存中删除,放入到二级缓存中为了保证只执行一次AOP。getSingleton加锁,保证添加到二级缓存和删除三级缓存的原子性
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 查询一级缓存
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//若一级缓存内不存在,查询二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
//若二级缓存内不存在,查询三级缓存
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//若三级缓存中的,则通过工厂获得对象,并清除三级缓存,提升至二级缓存
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Vn6MR5x-1683542094281)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/Image-16473997723591.png#id=fICEt&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rhdUKfOc-1683542094283)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/Image-164706706896711.png#id=qvP1A&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
2. Spring Bean生命周期
Spring在启动的时候需要「扫描」在XML/注解/JavaConfig 中需要被Spring管理的Bean信息,将这些信息封装成BeanDefinition,最后会把这些信息放到一个beanDefinitionMap中,这个Map的key应该是beanName,value则是BeanDefinition对象
首先根据BeanDefinition得到类的元数据信息,
总共分为四个阶段:实例化、填充属性、初始化、销毁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJvoznk0-1683542094285)(计网操作系统等面经.assets/Image-16467250646771-16470670662249.png#id=dDUDK&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qn3UZ276-1683542094286)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/v2-5b38cefe3a94a211e42eaa8e49b66a92_720w-16470670644978.jpg#id=QAaue&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0LIbzPF-1683542094288)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/image-20220307093958729-16470670589596.png#id=HKD7L&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NZ8RcSj9-1683542094289)(计网操作系统等面经.assets/image-20220307094003990-16470670609197.png#id=MlLcy&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
3. 拦截器过滤器区别
- Interceptor是基于Java反射的,是spring MVC框架的对象,由其创建;Filter是基于函数回调的,是servlet中的对象由Tomcat创建。
- 拦截器有三个方法,三个执行时间;过滤器就一个执行时间,在请求之前。
- 拦截器是用来拦截请求的,对请求做登录验证的;过滤器是侧重对request、response的参数、属性做设置值的。
- 拦截器是对Controller的请求做拦截的;过滤器是对jsp、html、css、js用过滤器。
拦截器的三个方法:
- prehandler在请求处理之前执行,该方法的返回值是布尔值 Boolean 类型的,当它返回为 false 时,表示请求结束,后续的 Interceptor 和 Controller 都不会再执行;当返回值为 true 时,就会继续调用下一个 Interceptor 的 preHandle 方法,如果已经是最后一个 Interceptor 的时候,就会是调用当前请求的 Controller 中的方法。
- postHandler 方法在当前请求进行处理之后,也就是在 Controller 中的方法调用之后执行,但是它会在 DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对 Controller 处理之后的 ModelAndView 对象进行操作。
- after***pletion该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行,这个方法的主要作用是用于进行资源清理的工作。像异常处理资源释放会放在这一步。
多个拦截器的执行顺序: 拦截器A的preHandler–>拦截器B的preHandler–>B的postHandler–>A的postHandler–>B的after***pletion–>A的after***pletion
4. Spring Boot自动装配
@EnableAutoConfiguration、@Configuration、@ConditionalOnClass是自动配置的核心。
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,这些自动配置类都是以AutoConfiguration结尾来命名的。每一个自动配置类对应一个以xxxProperties.java结尾命名的配置文件,会读取配置文件进行自动配置功能。
怎么告诉spring容器@Configuration的类路径
这个就是springboot要做的事。
springboot启动的时候会扫描所以依赖的jar包下面的META-INF文件夹下面的spring.factories文件。
然后找这个文件的关键字org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
,这个关键字后面的字符串就是告诉spring要加载哪些@Configuration
的类
5. Spring中Bean的作用域
Spring 容器在初始化一个 Bean 的实例时,同时会指定该实例的作用域。Spring3 为 Bean 定义了五种作用域,具体如下。
1)singleton:单例模式,唯一 bean 实例,Spring 中的 bean 默认都是单例的,对单例设计模式的应用。
2)prototype:原型模式,每次通过 Spring 容器获取 prototype 定义的 Bean 时,容器都将创建一个新的 Bean 实例。
3)request:在一次 HTTP 请求中,容器会返回该 Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Request 内有效。
4)session:在一次 HTTP Session 中,容器会返回该 Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Session 内有效。
5)global Session:在一个全局的 HTTP Session 中,容器会返回该 Bean 的同一个实例。该作用域仅在使用 portlet context 时有效。
在容器中获取bean的方式:
- @Autowired:spring提供的,默认按照byType找,如果想按照byName需要搭配@Qualifier(“name”)
- @Resourse:jdk自带的,默认按照byName找,找不到再按照byType找;
- 通过ApplicationText
6. 事务传播机制
- Propagation.Required:如果有事务则加入事务,如果没有事务,则创建一个新的(默认值)。当两个方法的传播机制都是REQUIRED时,如果一旦发生回滚,两个方法都会回滚。
- PROPAGATION_REQUIRES_NEW:内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
- PROPAGATION_NESTED:外层事务的回滚可以引起内层事务的回滚。而内层事务回滚并不会导致外层事务的回滚。
- SUPPORTS:如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。
- NOT_SUPPORTED:总是非事务地执行,并挂起任何存在的事务。
- Mandatory:如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
- Never:总是非事务地执行,如果存在一个活动事务,则抛出异常。
总结:
- NESTED和REQUIRED修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是REQUIRED是加入外围方法事务,所以和外围事务同属于一个事务,一旦REQUIRED事务抛出异常被回滚,外围方法事务也将被回滚。而NESTED是外围方法的子事务,有单独的保存点,所以NESTED方法抛出异常被回滚,不会影响到外围方法的事务。
- NESTED和REQUIRES_NEW都可以做到内部方法事务回滚而不影响外围方法事务。但是因为NESTED是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而REQUIRES_NEW是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。
- 两个内层回滚不会影响外层回滚,new 外层回滚不会回滚内层,nested外层回滚会回滚内层
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-INB2qsFZ-1683542094289)(计网操作系统等面经.assets/image-20220307154322032-16470670521525.png#id=WzdOP&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
7. 事务失效
- 抛出检查异常导致事务不能正确回滚
- 原因:Spring 默认只会回滚非检查异常(RuntimeException和Error的子类)
- 解法:配置 rollbackFor 属性 [@Transactional(rollbackFor ](/Transactional(rollbackFor ) = Exception.class)
- 业务方法内自己 try-catch 异常导致事务不能正确回滚
- 原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉
- 解法1:异常原样抛出在 catch 块添加 throw new RuntimeException(e);
- 解法2:手动设置 TransactionStatus.setRollbackOnly() 在 catch 块添加TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
-
非 public 方法导致的事务失效
- 原因:Spring 为方法创建代理、添加事务通知、前提条件都是该方法是 public 的
- 解法1:改为 public 方法
- 解法2:添加 bean 配置如下(不推荐)
@Bean
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource(false);
}
-
调用本类方法导致传播行为失效
- 原因:本类方法调用不经过代理,因此无法增强
- 解法1:依赖注入自己(代理)来调用
- 解法2:通过 ApplicationContext 拿到代理对象,来调用
- 解法3:通过 CTW,LTW 实现功能增强
动态代理
基于接口的 JDK 动态代理:需要实现接口
基于继承的 CGLib 动态代理:目标对象不需要实现接口,底层是通过继承目标对象产生代理子对象,代理子对象中继承了目标对象的方法,并可以对该方法进行增强。
8. Spring MVC执行流程
- 浏览器提交请求到中央调度器
- 中央调度器直接将请求转给处理器映射器。
- 处理器映射器会根据请求,找到处理该请求的处理器,并将其封装为处理器执行链后 返回给中央调度器。
- 中央调度器根据处理器执行链中的处理器,找到能够执行该处理器的处理器适配器。
- 处理器适配器调用执行处理器。
- 处理器将处理结果及要跳转的视图封装到一个对象 ModelAndView 中,并将其返回给处理器适配器。
- 处理器适配器直接将结果返回给中央调度器
- 中央调度器调用视图解析器,将 ModelAndView 中的视图名称封装为视图对象。
- 视图解析器将封装了的视图对象返回给中央调度器
- 中央调度器调用视图对象,让其自己进行渲染,即进行数据填充,形成响应对象。
- 中央调度器响应浏览器。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qYxc3AmQ-1683542094290)(计网操作系统等面经.assets/image-20220309104228346-16470669191702-165607988230287.png#id=UxtVH&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
9. IOC/AOP
**IOC:**IOC容器是一种设计思想,它能够帮助我们创建对象和管理对象之间的依赖关系,他有一个强大的功能DI依赖注入,我们可以通过Java代码或者xml配置文件的方式,把我们想要注入对象的所有依赖的bean自动地注入进去,通过byname或者bytype的方式,正是有了依赖注入IOC才有了强大的解耦的功能。
比如说JDBCtemplate 注入到容器中他是需要数据源的,如果JDBCtemplate和德鲁伊数据源强耦合在一起就会导致一个问题,我们使用JDBCtemplate的时候就必须使用德鲁伊,IOC容器帮助我们让JDBCtemplate只依赖于一DataSource 接口,不需要依赖具体的实现;这样我们自动给容器注入一个德鲁伊的数据源他就自动的给注入到JDBCtemplate中了,如果我们注入其他的数据源也是一样的,这样JDBCtemplate和数据源就实现了解耦合。
AOP:在日常工作中会遇到许多重复的代码,比如说事物、日志,我们需要在很多类里面重复的写这些代码,比如说事物,我们需要在所有的service层里面开启事、 提交、回滚,AOP就可以将这些重复的代码提取封装起来,我们可以在需要的时候切入到我们想要切入到的类里, 这样可以减少系统中的冗余代码,降低了模块间的耦合度,同时提高了系统的复用性和可维护性。AOP的实现是依赖于动态代理实现的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,就会使用CGlib动态代理生成一个被代理对象的子类来作为代理。
10. mybatis执行过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1C49Xyeq-1683542094292)(计网操作系统等面经.assets/v2-11c9b17f5f79909cf4cbae580ba63493_720w-16470669237673.jpg#id=nUW9b&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
11. Mybatis一级缓存和二级缓存
- **一级缓存:**默认开启,sqlSession级别的缓存,是SqlSession级别的一个Map。 在同一个SqlSession中,执行相同的SQL查询时;第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取。 当执行SQL时候两次查询中间发生了增删改的操作,则SQLSession的缓存会被清空。 每次查询会先去缓存中找,如果找不到,再去数据库查询,然后把结果写到缓存中。
- **一级缓存失效的四种情况:**sqlsession不同(会话不同)、sqlsession相同,查询缓存中没有的数据、sqlsession相同,但两次查询之间执行了增删改操作、sqlsession相同,但手动清除了一级缓存(缓存清空)openSession.clearCache();
- 二级缓存:基于 mapper文件的namespace名称空间级别的缓存,多个sqlSession可以共享一个mapper中的二级缓存区域;二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 --> 一级缓存 --> 数据库。
- 实际上MyBatis 用了一个装饰器类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
- 二级缓存清除策略有:
-
LRU
– 最近最少使用:移除最长时间不被使用的对象,默认。 -
FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。 -
SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。 -
WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
6. 项目和开放性问题
1. 分布式事务
- seata
1)XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入。
优点:• 事务的强一致性,满足ACID原则 • 常用数据库都支持,实现简单,并且没有代码侵入
缺点:• 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差 • 依赖关系型数据库实现事务
过程:
• RM一阶段的工作: ① 注册分支事务到TC ② 执行分支业务sql但不提交 ③ 报告执行状态到TC
• TC二阶段的工作:TC检测各分支事务执行状态。① 如果都成功,通知所有RM提交事务② 如果有失败,通知所有RM回滚事务
• RM二阶段的工作: 接收TC指令,提交或回滚事务
2)AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式。数据库快照,一阶段执行完提交,二阶段如果有问题再根据快照回滚。
- 过程:
• 阶段一RM的工作:• 注册分支事务 • 记录undo-log(数据快照)• 执行业务sql并提交 • 报告事务状态
• 阶段二提交时RM的工作:• 删除undo-log即可
• 阶段二回滚时RM的工作:• 根据undo-log恢复数据到更新前 - 脏写问题:事务A,获取DB锁–保存快照–执行业务–释放DB锁;在其释放完DB锁后,根据快照回滚之前,有其他业务修改完数据,再回滚时,会出现脏写。可以使用全局锁(TC)来解决。
- AT模式与XA模式区别
• XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
• XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
• XA模式强一致;AT模式最终一致
3)T***模式:最终一致的分阶段事务模式,有业务侵入。支持非关系型数据库redis
- 过程:
Try:资源的检测和预留;
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
Cancel:预留资源释放,可以理解为try的反向操作。 - 空回滚:当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚。
- 业务悬挂:应当阻止执行空回滚后的try操作,避免悬挂。
资源预留、幂等性
- 最终采用
2PC模式:seata的AT高并发场景不太合适
最终采用:基于RabbitMQ的异步的可靠消息、最终一致性方案
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F8lWGvpC-1683542094293)(计网操作系统等面经.assets/image-20220316185122347.png#id=W2a84&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2roIq61T-1683542094293)(计网操作系统等面经.assets/image-20220318152254738.png#id=SImLW&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
2. 库存超卖
-
常规商品:可以采用悲观锁或者客观锁。
最终采用加乐观锁保证库存不超买,即先查库存,如果大于购买数量,在减库存的时候再次判断库存是否大于购买数量
<update id="lockSkuStock">
UPDATE `wms_ware_sku`
SET stock_locked = stock_locked + #{num}
WHERE sku_id = #{skuId} AND ware_id= #{wareId} AND stock - stock_locked > #{num}
</update>
- **秒杀商品:**预热提前将商品库存放入redis,通过lua脚本,判断库存是否充足、用户是否下单、扣减库存、将userId存入已经购买的set集合中,一人一单。
3. 缓存一致性
解决方案:
- 双写:写完数据库,后写缓存。容易导致缓存不一致,有产生脏数据的风险;加锁、缓存过期时间
- 失效模式:写完数据库,后删缓存。
- 延迟双删:先删除缓存、再写数据库、休眠500毫秒(根据具体的业务时间来定)、再次删除缓存。
- 用canal订阅MySQL
- 过期时间+读写锁
- spring-cache
最终采用:spring-cache。缓存数据加过期时间,数据过期下次查询触发主动更新;读数据时,加上分布式的读写锁,保证写的时候,不出现不一致。
实在不行,用canal订阅MySQL
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eM0rDyjn-1683542094295)(计网操作系统等面经.assets/image-20220316145347774.png#id=A9PLv&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
可能出现的问题:
-
缓存击穿:热点key;互斥锁、逻辑过期
1)互斥锁:线程一发现缓存过期,就去获取锁,查数据库,其他线程此时再来查时,发现过期去数据库查时,就会阻塞。
优缺点:没有额外的内存消耗;保持一致性;实现简单;线程需要等待,性能受到影响;可能死锁(两个不同业务,互相拿到对方的 锁)
实现:可以使用redisson的分布式锁进行实现,也可以自己实现,要保证加锁和设置超时时间原子性、查看锁验锁和删除锁的原子 性。
2)逻辑过期:添加一个逻辑过期的字段,如果线程一发现缓存逻辑过期后,就开启新的线程取更新缓存,自己返回旧的数据,此时如果有别的线程来获取数据,发现过期后,再开启新线程时发现有锁,就返回旧的数据。比如存一个字段,是放缓存时的时间加三十分钟,线程一取来数据判断是否逻辑过期。
优缺点:线程无需等待,性能好;不保证一致性;有额外的内存消耗;实现复杂
实现:判断是否过期,是则获取分布式锁,开启新的线程更新数据,其他线程获取不到分布式锁,判断过期直接返回旧数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UyP80KsM-1683542094295)(%E8%AE%A1%E7%BD%91%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AD%89%E9%9D%A2%E7%BB%8F.assets/image-20220321145206435.png#id=l4WTc&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)] -
缓存穿透:null;redis缓存空值、布隆过滤
1)缓存空对象:
优点:实现简单,维护方便
缺点:额外的内存消耗、可能造成短期的不一致
2)布隆过滤:
优点:内存占用较少,没有多余key
缺点:实现复杂、存在误判可能;不存在的时候真的不存在,存在的时候去查库可能不存在。
原理:如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成**多个哈希值,**并对每个生成的哈希值指向的 bit 位置 1 -
缓存雪崩:加随机时间、提高redis集群的高可用性、给缓存业务添加限流策略、给业务添加多级缓存
1)限流策略:通过sential
2)多级缓存: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aOugbzRO-1683542094296)(计网操作系统等面经.assets/image-20220316190629840.png#id=D9MX0&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
4. 分布式锁
可以通过redis自己实现分布式锁、或者redisson实现
-
加锁和设置过期时间(为了保证某个线程异常或者删锁的时候断电导致死锁)要保持原子性,用redis的
set name value ex 300 nx
value设置为UUID每个锁设置的不同的锁,防止删锁的时候,删掉别人的。 - 判断和删锁要保持原子性:用lua脚本保持原子性;先获取值,对比成功删除保持原子
- 自动续期:如果业务代码执行时间超过锁的过期时间,要自动续期
redisson加分布式锁,可以保证可重入、可重试、可自动续期、分布式下保证主从一致
•可重入:利用hash结构记录线程id和重入次数
•可重试:利用信号量和 PubSub 功能实现等待、唤醒,获取锁失败的重试机制
•超时续约:利用watchDog,每隔一段时间(releaseTime / 3),重置超时时间
•主从一致性:联锁,利用多个独立redis节点代替主从,所有节点都获取锁成功才算获取锁成功,将锁放入到List中,依次获取锁,都获取到才算成功,在都获取到后如果设置了超时时间,就会重置所有结点的超时时间,没设置就用看门狗续期。
5. 限流
- 基于Redis的数据结构zset
- 基于Redis的令牌桶算法
- 每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码。依靠List的leftPop来获取令牌
- 再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以用UUID进行了生成。
- 通过sentinel实现限流
限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法。
而Sentinel内部却比较复杂:
- 默认限流模式是基于滑动时间窗口算法
- 排队等待的限流模式则基于漏桶算法
- 而热点参数限流则是基于令牌桶算法
6. MyISAM为啥比InnoDB快
MyISAM为啥比InnoDB快:InnoDB只有一个文件,数据和索引都在一个文件中,而MyISAM有两个文件,一个数据一个索引;InnoDB有非聚簇索引可能需要回表,聚簇索引存放的是数据,在内存中放得少,MyISAM都是非聚簇,叶子节点存放数据地址。MyISAM只缓存索引,不缓存真实数据;InnoDB不仅缓存索引还要缓存真实数据, 对内存要求较高,而且内存大小对性能有决定性的影响。
InnoDB要维护MV***,每当select时要看查询出来的记录是否对当前事务可见,适合数据量大的情况。
MyISAM增加和查询快一些,适合数据量小的情况。
7. 索引相关
Explain中的字段:
type:
const:**主键/唯一**索引的**等值**查询
eq_ref:**连接**查询时,被驱动表通过**主键/唯一**索引**等值**匹配时
ref:**普通索引**与常量进行**等值**匹配时
range:只检索给定范围的行,使用一个索引来选择行,能根据索引做范围的扫描
index:可以使用覆盖索引,但仍需要扫描全部的索引记录
possible_keys、key、key_len
extra:
Using index:使用覆盖索引并且不用回表
Using index condition:索引下推
Using temporary、Using filesort、Using Where、Using join buffer (Block Nested Loop)
show profies;
show profile cpu,block io for query 2;
8. 海量数据
- 从100万个数据中找出最大的N个数
如果求前10个最小的数,则先拿前10个数建一个大顶堆,然后遍历剩下的数,只要比堆顶数字大就舍弃,比堆顶数字小,就移除堆顶数字,替换为比它小的这个数字,然后重新调整堆为大顶堆;遍历完成后,大顶堆中的10个数就是100万中最小的10个数
如果求前10个最大的数,则先拿前10个数建一个小顶堆,同样遍历剩下的数,比堆顶小的舍弃,比堆顶大的,则移除堆顶,替换为比它大的这个元素,然后重新调整堆为小顶堆;遍历完成后,小顶堆中的10个数就是100万中最大的10个数 - 海量日志数据,统计出某日访问百度次数最多的那个IP
解决方式:IP地址最多有 2^32 = 4G 种取值情况,所以不能完全加载到内存中进行处理,采用 hash分解+ 分而治之 + 归并 方式: (1)按照 IP 地址的 Hash(IP)%1024 值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址。(2)对于每一个小文件,构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址 。
(3)然后再在这1024组最大的IP中,找出那个频率最大的IP。
补充知识:
- 布隆过滤器:通过多个独立的哈希函数,映射到不同的位上置1。
Counting Bloom Filter:将标准Bloom Filter位数组的每一位扩展为一个小的计数器(Counter),在插入元素时给对应的k(k为哈希函数个数)个Counter的值分别加1,删除元素时给对应的k个Counter的值分别减1,Counting Bloom Filter通过多占用几倍的存储空间的代价,给Bloom Filter增加了删除操作。
Spectral Bloom Filter: 可以统计频次,在CBF中加入一个元素时,k个哈希位置的counter都要加1,也就是说,如果不考虑碰撞(collision),出现次数为n的元素对应的k个counter的值都为n。即使考虑到碰撞的因素,只要k个位置不全出现碰撞,k个counter中的最小值仍是n - 字典树:Trie树是一种非常强大的处理海量字符串数据的工具。尤其是大量的字符串数据 中存在前缀时,Trie树特别好用。Trie树在字典的存储,字符串的查找,求取海量 字符串的公共前缀,以及字符串统计等方面发挥着重要的作用。用于存储时,Trie 树因为不重复存储公共前缀,节省了大量的存储空间;用于以字符串的查找时, Trie树依靠其特殊的性质,实现了在任意数据量的字符串集合中都能以O(len)的时 间复杂度完成查找(len为要检索的字符串长度);在字符串统计中,Trie树能够 快速记录每个字符串出现的次数。
9. 频繁发生full gc 如何排查
开启 -XX:+HeapDumpBeforeFullGC,导出dump文件,
10. 一致性哈希
- 轮询
考虑到每个节点的硬件配置有所区别,我们可以引入权重值,将硬件配置更好的节点的权重值设高,然后根据各个节点的权重值,按照一定比重分配在不同的节点上,让硬件配置更好的节点承担更多的请求,这种算法叫做加权轮询。
加权轮询算法使用场景是建立在每个节点存储的数据都是相同的前提。所以,每次读数据的请求,访问任意一个节点都能得到结果。
但是,加权轮询算法是无法应对「分布式系统」的,因为分布式系统中,每个节点存储的数据是不同的 - 一致性哈希算法
一致哈希算法也用了取模运算,但与哈希算法不同的是,哈希算法是对节点的数量进行取模运算,而一致哈希算法是对 2^32 进行取模运算,是一个固定的值。
一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响。但是一致性哈希算法不能够均匀的分布节点,会出现大量请求都集中在一个节点的情况,在这种情况下进行容灾与扩容时,容易出现雪崩的连锁反应。
为了解决一致性哈希算法不能够均匀的分布节点的问题,就需要引入虚拟节点,对一个真实节点做多个副本。不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。
另外,虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高。而且,有了虚拟节点后,还可以为硬件配置更好的节点增加权重,比如对权重更高的节点增加更多的虚拟机节点即可。
在实际的工程中,虚拟节点的数量会大很多,比如 Nginx 的一致性哈希算法,每个权重为 1 的真实节点就含有160 个虚拟节点。
总结:
首位相连的哈希环上,增加/删除节点只影响顺时针相邻的节点。
不够均匀,多个请求会到一个节点,通过加虚拟节点来提高节点的均衡度;提高稳定性,节点发生变化时,会有不同节点分担系统变化;还可以通过控制虚拟节点的数量来增加权重。
11. Nginx正反向代理
正向代理:首先请求代理服务器,然后代理服务器帮我们去快速访问国外的网站,对于这种代理方式,我们就称之为正向代理
反向代理:机房通讯通常采用局域网交换机,inter***网用户请求是无法直接访问到局域网内的web服务的,因此这个时候,你需要一台反向代理服务器来接收inter*** web请求,然后将请求分发到局域网中的不同主机上进行处理,处理完成之后再做出响应
12. MQ异步通信好处
同步通信:
优点:时效性较强,可以立即得到结果
缺点:耦合度高、性能和吞吐能力下降、有额外的资源消耗、有级联失败问题
异步通信:
好处:
- 吞吐量提升:无需等待订阅者处理完成,响应更快速
- 故障隔离:服务没有直接调用,不存在级联失败问题
- 调用间没有阻塞,不会造成无效的资源占用
- 耦合度极低,每个服务都可以灵活插拔,可替换
- 流量削峰:不管发布事件的流量波动多大,都由Broker接收,订阅者可以按照自己的速度去处理事件
缺点:
- 架构复杂了,业务没有明显的流程线,不好管理
- 需要依赖于Broker的可靠、安全、性能
7. MySQL
1. 加锁流程
- 唯一索引等值查询
- 当查询的记录是存在的,在用「唯一索引进行等值查询」时,next-key lock 会退化成**「记录锁」**。
- 当查询的记录是不存在的,在用「唯一索引进行等值查询」时,next-key lock 会退化成**「间隙锁」**。
- 非唯一索引等值查询
- 当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
select id from table where b = 8 for update;普通索引 b 上共有两个锁,分别是 next-key lock (4,8] 和间隙锁 (8,16) 。 - 当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。
- next-key lock 锁的是索引,而不是数据本身,所以如果 update 语句的 where 条件没有用到索引列,那么就会全表扫描,在一行行扫描的过程中,不仅给行加上了行锁,还给行两边的空隙也加上了间隙锁,相当于锁住整个表,然后直到事务结束才会释放锁。
所以在线上千万不要执行没有带索引条件的 update 语句,不然会造成业务停滞,
8.集群
1. MySQL集群
- 复制的基本原则:每个 Slave 只有一个 Master 、每个 Slave 只能有一个唯一的服务器ID 、每个 Master 可以有多个 Slave
- 保证数据一致性:异步复制、半同步复制、组复制
- 配置主机id,开启bin_log;配置从机id;在主机上建立账户授权给从机,给表的操作权限;从机配置主机ip、主机名、密码、bin-log,position具体值,启动slave同步
- 若想要减少主从延迟的时间,可以采取下面的办法:
- 降低多线程大事务并发的概率,优化业务逻辑 ,减少锁竞争,如果查询导致大量的表锁定,需要考虑重构查询语句,尽量避免过多的锁
- 优化SQL,避免慢SQL, 减少批量操作 ,建议写脚本以update-sleep这样的形式完成。
- 提高从库机器的配置 ,减少主库写binlog和从库读binlog的效率差。
- 尽量采用 短的链路 ,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输 的网络延时。
- 实时性要求的业务读强制走主库,从库只做灾备,备份。实时性要求不高的读取操作可以放到slave服务器,实时性要求高的读取操作放到master服务器
- 为了保障较高的数据安全性,配置sync_binlog=1,innodb_flush_log_at_trx_***mit=1等设置。而Slave可以关闭binlog,innodb_flush_log_at_trx_***mit也可以设置为0来提高sql的执行效率
2. Redis集群
**哨兵集群:**哨兵监测、故障转移、通知,每秒ping、主观下线,客观下线;AP
选择新主结点:
• 断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点。
• 判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
• 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
• 最后是判断slave节点的运行id大小,越小优先级越高
**分片集群:**0-16384散列插槽,对{key}取hash,然后对16384取余。
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
• 海量数据存储问题
• 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
• 集群中有多个master,每个master保存不同数据
• 每个master都可以有多个slave节点
• master之间通过ping监测彼此健康状态
• 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
3. RabbitMQ集群
- **普通集群:**或者叫标准集群(classic cluster),具备下列特征:
- 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。
- 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
- 队列所在节点宕机,队列中的消息就会丢失
- 镜像集群:本质是主从模式,具备下面的特征:
- 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
- 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
- 一个队列的主节点可能是另一个队列的镜像节点
- 所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成新的主
- 仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:
- 与镜像队列一样,都是主从模式,支持主从数据同步
- 使用非常简单,没有复杂的配置
- 主从同步基于Raft协议,强一致
4. ElasticSearch集群
职责划分:
- master节点:对CPU要求高,但是内存要求底
- data节点:对CPU和内存要求都高
- coordinating节点:对网络带宽、CPU要求高
**分片集群:**elasticsearch会通过hash算法来计算文档应该存储到哪个分片
- _routing默认是文档的id
- 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!
脑裂问题:
elasticsearch的分布查询分成两个阶段:
- scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
- gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户
**故障转移:**集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全。CP
kafka怎么解决消息有序性
点赞不用set和bitmap该怎么做,如果有十万人点赞该如何处理