前言
golang面经主要参考知乎大佬总结的面试题:https://zhuanlan.zhihu.com/p/519979757
计算机网络参考:https://blog.csdn.net/justloveyou_/article/details/78303617
本文主要对面试中问到的,但是面经中没有的题目进行补充。
一、golang基础
1、谈谈对gin框架的理解
gin源码解析:https://www.liwenzhou.com/posts/Go/gin-sourcecode/
可以从这三个方面回答:
1、gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。
2、gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助c.Next()方法实现嵌套调用。
3、借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。
2、rpc框架 && grpc
参考博客:https://www.liwenzhou.com/posts/Go/rpc/
RPC就是为了解决类似远程、跨内存空间、的函数/方法调用的。要实现RPC就需要解决以下三个问题。
1、如何确定要执行的函数? 在本地调用中,函数主体通过函数指针函数指定,然后调用 add 函数,编译器通过函数指针函数自动确定 add 函数在内存中的位置。但是在 RPC 中,调用不能通过函数指针完成,因为它们的内存地址可能完全不同。因此,调用方和被调用方都需要维护一个{ function <-> ID }映射表,以确保调用正确的函数。
2、如何表达参数? 本地过程调用中传递的参数是通过堆栈内存结构实现的,但 RPC 不能直接使用内存传递参数,因此参数或返回值需要在传输期间序列化并转换成字节流,反之亦然。
3、如何进行网络传输? 函数的调用方和被调用方通常是通过网络连接的,也就是说,function ID 和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制。.例如,一些 RPC 框架使用 TCP 协议,一些使用 HTTP。
以往实现跨服务调用的时候,我们会采用RESTful API的方式,被调用方会对外提供一个HTTP接口,调用方按要求发起HTTP请求并接收API接口返回的响应数据。
其中,gRPC是一种现代化开源的高性能RPC框架,能够运行于任意环境之中。最初由谷歌进行开发。它使用HTTP/2作为传输协议
3、RPC和http的区别
RPC跟HTTP不是对立面,RPC中可以使用HTTP作为通讯协议。RPC是一种设计、实现框架,通讯协议只是其中一部分。
http接口是在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。利用现成的http协议进行传输。但是如果是一个大型的网站,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了:
- 首先(基于TCP协议的情况下)就是长链接,不必每次通信都要像http 一样去3次握手什么的,减少了网络开销
- 其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统 一化的操作。
- 第三个来说就是安全性。
- 最后就是最近流行的服务化架构、服务化治理,RPC框架是一个强力的支撑。
原文链接:https://blog.csdn.net/pearl8899/article/details/118640086
4、ProtoBuf和json的对比
原文链接: https://blog.csdn.net/weixin_49848200/article/details/126969772
5、进程为什么开销大,协程为什么是轻量级的?
- 进程:顾名思义就是正在执行的应用程序,资源分配的最小单位,独立的内存空间
- 线程:轻量级的进程, CPU调度的基本单位,多个线程之间共享进程的资源
- 协程:轻量级线程,协程不受操作系统的调度,调度器由用户应用程序提供
(1)为什么进程切换比线程切换代价大,效率低?
- 关键在于进程切换涉及到TLB(页表缓存/快表)的失效及更新,线程不涉及
- 切换页表这个操作本身是不太耗时的。但是在切换之后,TLB(页表缓存/快表)就失效了,所以在进行地址转化时就需要重新去查找页表,这就造成了程序运行的效率低下。
- 而同一个进程的线程之间是共用一个页表的,所以线程之间的切换是不需要切换页表的,因此线程切换不存在上述代价大,效率低的问题。
为了加速页表的读取,出现了一种存放 程序最常访问页表项的 Cache 高速缓存,称之为TLB,可以极大提高虚拟地址到物理地址的转换速度。
理解:TLB可以看作是一种硬件的哈希表,来快速查找 高速cache 中是否存在特定地址的数据,而其中应用到的内存淘汰策略则是常被提到的LRU内存淘汰策略。
作用:可以加速页表读取,极大提高虚拟地址到物理地址的转换速度。
(2)协程为什么比线程轻量级
- 协程是用户态的,线程是内核态的。内存占有小
- 协程是 go 运行时调度,线程是系统调度。切换成本小。
(3)什么情况会出现线程阻塞
睡眠状态、等待状态(调用wait)、礼让状态(将执行权让给同等级或更高级线程优先执行)
原文链接:https://blog.csdn.net/qq_37102984/article/details/127949307
https://www.cnblogs.com/JonaLin/p/11570858.html
6、设计一个秒杀系统应该怎么设计(加锁和缓存被面试官否决)
这个东西实际上是问你redis+队列的实现应用
用户的秒杀请求堆积到队列里面。redis存着商品数量。首先超出队列长度的请求可以抛弃,其次消费队列的请求时,同步减少redis的数量。这时反而还可以还用去操作数据库。因为redis的原子性和非常牛逼的内存读
单台redis和mq组件选择 都可以撑住10W并发的秒杀了
7、用基本的数据结构设计一个读写锁
参考golang的读写锁底层
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
Mutex用来写协程之间的互斥等待,读协程使用readerSem等待写锁的释放,写协程使用writerSem等待读锁的释放,readerCount 记录读协程个数,readerWait记录写协程之前的读协程个数
8、channel使用要注意什么,会产生死锁吗
1、channel底层实现
channel 底层为环形队列(缓冲区),发送队列,接收队列,Mutex,close
当协程需要在发送队列或接收队列等待时,会被打包成 sudog 这样一个结构。一个 G可能被打包为多个 sudog (结构体)分别挂在不同的等待队列上:
2、channel使用场景
- 协程间通信
- 停止信号
- 任务定时
- 解耦生产者和发送者
- 控制并发数
3、channel使用注意事项
channel 是 goroutine 之间数据通信桥梁,而且是线程安全的。
1、对未初始化(make)的 channel 进行读写关闭操作,会产生死锁deadlock
2、当 for range 遍历 channel 时,如果发送者没有关闭 channel 或在 range 之后关闭,会导致 死锁
3、对于无缓冲区channel,读取和写入要配对出现,并且不能在同一个 goroutine 里
4、对于有缓冲区的channel,读写操作如果在同一个 goroutine 里,写数据操作一定在读数据操作前
参考博客:https://blog.csdn.net/tool007/article/details/124329558
https://blog.csdn.net/diaosssss/article/details/92830782
9、切片的扩容
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一
10、讲一下死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
golang 中的死锁是当 goroutine 被阻塞而没有任何可能被解除阻塞时发生的状态,goroutine 会产生死锁,要么是因为它正在等待管道消息,要么是因为它正在等待同步包中的锁,常见的死锁情形:
第一种情形:无缓存能力的管道,自己写完自己读
第二种情形:协程来晚了
第三种情形:管道读写时,相互要求对方先读/写
第四种情形:读写锁相互阻塞,形成隐形死锁
https://www.jb51.net/article/221391.htm#_label0
11、哈希算法有哪些,讲一下map的底层实现
1、三种哈希算法
开放寻址法,再哈希法,拉链法
https://blog.csdn.net/qq_16570607/article/details/118659466
2、map的底层实现
Golang中的map底层使用的数据结构是hash table,基本原理就和基础的散列表一致,重点是Golang在设计中采用了分桶(Bucket),每个桶里面支持多个key-value元素的这种思路,具体可以参考下面的图,可以看到图中的B就是Bucket,每个桶中会存储多组K/V,默认情况下map首先是指向一个桶的数组,每个桶中最多包含8个key-value对,**对于输入的key首先经过散列函数计算得出散列值,**其实就是1个数字,大部分计算机都是64位的,所以通常这是一个uint64的值,这个哈希值的低位用于选择桶,确定桶之后,哈希值的高位用于定位到桶中的条目,如果桶中的key-value满了,则会标记当前桶溢出同时链接到额外的新桶,将元素放进去。
3、如何解决hash冲突
如果两个不同的key被定位到同一个桶中,其实就可以认为出现了哈希冲突,
- 这种情况下就依次按照顺序从前往后将hash值的高8位写入到数组空闲的元素中,这里思路和链表法是一致的,之所以这么设计是为了提高哈希冲突时比较的速度,因为比较1个字节要比比较一个很长的key快,
- 这时查找key的过程是先通过计算得到的哈希值定位到桶,然后依次遍历tophash和计算hash值的高8位是否相等,如果相等则说明元素大概率是找到了,这个时候再详细比较key是否完全一致即可,否则将继续寻找,直到找到最后一个元素为止,如果都找不到说明要查找的key是不存在的,
- 如果当前桶存储满了,则会继续挂上新的存储桶,也叫溢出桶,这里每个bmap中存在一个overflow指针指向下一个Bucket,和之前一样继续向后存储冲突的key/value,但是随着桶的增多,搜索元素的速度也会下降,所以不会无限的增加桶,而是会在满足某些条件的情况下进行扩容
参考链接:https://www.cnblogs.com/freeweb/p/15898542.html#ref-1
4、存储数据的过程
- 计算hash值 计算key的哈希值,Go语言中使用的是MurmurHash3算法
- 定位到桶 根据哈希值的低位定位到对应的桶。
- 在桶中查找键值对 如果桶中没有键值对,则直接插入新的键值对。如果桶中已经存在键值对,则需要查找并更新该键值对。具体查找过程如下: 依次遍历桶中键值的tophash和插入key计算的hash值的高8位是否相等,如果相等再详细比较key是否完全一致即可,否则将继续寻找,直到找到最后一个元素为止,如果都找不到说明要查找的key是不存在的
5、扩容机制
扩容条件
(1)负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
(2)当溢出桶过多时:
- 当 B < 15 时,如果overflow的bucket数量超过 2^B。
- 当 B >= 15 时,overflow的bucket数量超过 2^15。
满足以上条件均会触发扩容机制。
扩容方案
等量扩容:
- 当一个桶中多次删除和增加数据之后,多次的hash冲突可能导致bmap的溢出桶很多,链表长度的增加导致扫描map的时间变长,而且浪费了大量存储空间。等量扩容实际上是针对这种情况对桶中数据做整理,把溢出桶中的数据向链表头搬迁,并删除空出来的overflow链表。这种情况下,元素会发生重排,但不会换桶。
增量扩容:
这种扩容发生在桶数量不够用时。具体步骤如下:
- 计算新的桶的数量:当键值对数量小于 1024 时,每次扩容增加一倍的桶;当键值对数量大于等于 1024 时,每次扩容增加 25%的桶。这个策略可以保证 map 在性能和空间利用率之间取得一个平衡;
- 为新的桶分配内存空间:使用 Go 的内存分配器(memory allocator)为新的桶分配内存空间;
- 将原来的桶中的键值对重新分配到新的桶中:对于每个桶,遍历桶中存储的所有键值对,计算键的哈希值,并根据新的桶数量计算出键对应的桶的位置;
- 插入键值对到新的桶中:将键值对插入到新的桶中,形成新的链表,并将新的链表连接到哈希表中;
- 释放原来的桶占用的内存空间:释放原来的桶占用的内存空间,这里需要注意的是,由于 Go 使用了指针指向键值对,所以在释放内存空间时需要注意先释放键值对,再释放链表,最后再释放桶。
考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
参考:https://zhuanlan.zhihu.com/p/617371376
6、map遍历为什么是无序的
- 对 map进行遍历时,并不是固定地从第一个数开始遍历,每次都是从随机的一个位置开始遍历
- “无序”写入
正常写入(非哈希冲突写入):是hash到某一个bucket上,而不是按buckets顺序写入
哈希冲突写入:如果存在hash冲突,会写到同一个bucket上。 - 扩容 成倍扩容和等量扩容。这两种方法都会对内存进行扩容,迫使元素顺序变化,导致Go的Map遍历结果无序。
7、map并发读写
Golang 在运行时不允许对 map 的并发读写,需要在多个线程中读写 map 时,解决办法:
- 加读写锁
package main
import (
"sync"
)
func main() {
var counter = struct {
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
go func() {
for {
counter.RLock()
_ = counter.m["some_key"]
counter.RUnlock()
}
}()
go func() {
for {
counter.Lock()
counter.m["some_key"]++
counter.Unlock()
}
}()
select {}
}
- sync.Map
sync.Map底层使用了两个原生map,使得读写和追加分离,read-map,仅用于读写;dirty-map,用于在特定情况下存储最新写入的key-value数据:
从sync.Map中读取数据时,sync.Map会首先查看read-map是否有用户需要的数据(key是否命中),如果有(命中),则通过原子操作将数据读取并返回,这是sync.Map推荐的快路径(fast path)
12、什么情况下会产生内存泄漏, channel没关闭就会产生内存泄漏吗
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
https://blog.csdn.net/weixin_38299404/article/details/126805554
- 定时器使用不当
- select阻塞,使用select时如果有case没有覆盖完全的情况且没有default分支进行处理,最终会导致内存泄漏
- channel阻塞主要分为写阻塞和读阻塞两种情况
- goroutine导致的内存泄漏
(1) 申请过多的goroutine,例如在for循环中申请过多的goroutine来不及释放导致内存泄漏
(2) goroutine阻塞
泄露原因
Goroutine 内进行channel/mutex 等读写操作被一直阻塞。
Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待
泄露场景
如果输出的 goroutines 数量是在不断增加的,就说明存在泄漏 - slice 引起的内存泄漏,当两个slice 共享地址,其中一个为全局变量,另一个也无法被GC;append slice 后一直使用,没有进行清理
- 数组的值传递,由于数组时Golang的基本数据类型,每个数组占用不通的内存空间,生命周期互不干扰,很难出现内存泄漏的情况,但是数组作为形参传输时,遵循的时值拷贝,如果函数被多个goroutine调用且数组过大时,则会导致内存使用激增
对于一个channel来说,如果没有任何goroutine引用,gc会对其进行回收,不会引起内存泄露。
而当goroutine处于接收或发送阻塞状态,channel处于空或满状态时,一直得不到改变,gc则无法回收这类一直处于等待队列中的goroutine,引起内存泄露。
引起内存泄露的几种情况:
发送端channel满了,没有接收端
接收端channel为空,没有发送端
channel未初始化
13、context.Context的理解
Context类型是一个可以帮助我们实现多goroutine 协作流程的同步工具,不但如此,我们还可以通过此类型的值传达撤销信号或传递数据。
Context类型的实际值大体上分为三种,即:根Context值、可撤销的Context值和含数据的Context值。所有的Context值共同构成了一颗上下文树,这棵树的作用域是全局的,而根Context值就是这棵树的根,它是全局唯一的,并且不提供任何额外的功能。
参考原文链接:https://blog.csdn.net/lml200701158/article/details/119214198
14、GMP模型
G 代表着 goroutine,P 代表着上下文处理器,M 代表 thread 线程,在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存到全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个可执行的 G 来执行,当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的操作系统线程来服务于这个 P,当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列,如果这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入到全局队列中。
关于 G,P,M 的个数问题,G 的个数理论上是无限制的,但是受内存限制,P 的数量一般建议与CPU 数量有关,M 的数据默认启动的时候是 10000,内核很难支持这么多线程数,所以整个限制客户忽略,M 一般不做设置,设置好 P,M 一般都是要大于 P
队列轮转
- 每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。
- 每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。
15、go内存模型
1、堆内存 mheap
- go每次申请的虚拟内存单元(heapArena)为64KB,最多4,194,304(2^10)个heapArena所有的heapArena组成了堆内存
- span是内存管理单元,使用内存时的最小单位,每个mspan为N个相同大小的格子,共有68种mspan
- 每个heapArena中的mspan类型都不确定
- mcentral是中心索引,共136个mcentral结构体,指向不同类型的mspan,其中,68个mspan需要GC扫描,68个不需要
- mcache线程缓存,每个P拥有一个mcache,一个mcache拥有136个mspan,其中,68个需要GC扫描,68个不需要。
2、堆内存分配
- 对象分级
Tiny 微对象(0,16B)无指针
Small 小对象 [16B,32KB]
Large 大对象(32KB,+~) - 大对象分配
直接从heapArena中开辟0级mspan,0级的mspan为大对象定制 - 微对象分配
从mcache拿到2级mspan,将多个微对象合并成一个16Byte存入(当前go版本中,1级mspan没有用到) - mcache替换
mcache中每个级别的mspan只有一个
当mspan满了之后会从mcentral中交换一个新的mspan - mcentral扩容
mcentral中只有有限数量的mspan,当mspan缺少时,会从heapArena开辟新的mspan - heapArena扩充
当heapArena空间不足时,向操作系统申请新的heapArena
总结
Go将对象按照大小分为3种,微小对象使用mcache,mcache中的mspan填满后,与mcentral交换新的,mcentral不足时,在heapArena开辟新的mspan,大对象直接在heapArena开辟新的mspan
3、逃逸分析
不是所有变量都放在协程栈上,以下三种情况会触发内存逃逸
- 指针逃逸:函数返回了对象的指针
- 空接口逃逸:因为空接口类型的函数往往使用反射
- 大变量逃逸:过大的变量会导致栈空间不足,64位机器中一般超过64KB的变量会逃逸
逃逸分析工具:go build -gcflags ‘-m’
16、GC算法
Go 的 GC 回收有三次演进过程,
- Go V1.3 之前普通标记清除(mark and sweep)方法,整体过程需要启动 STW,效率极低。
- GoV1.5 三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通。
- GoV1.8 三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要 STW,效率高。
写屏障的作用:破坏强弱三色不变式之一即可保证GC时不丢失对象
GC触发时机:分为系统触发和主动触发。
1)gcTriggerHeap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发。
2)gcTriggerTime:当距离上一个 GC 周期的时间超过一定时间时,将会触发。时间周期以runtime.forcegcperiod 变量为准,默认 2 分钟。
3)gcTriggerCycle:如果没有开启 GC,则启动 GC。
4)手动触发的 runtime.GC 方法。
17、哪些数据类型是并发安全的
- 并发安全:字符型,布尔型,整形,指针,浮点型(取决于操作系统指令集)
- 不安全:字符串 结构体 数组,切片,map,复数型
参考:https://zhuanlan.zhihu.com/p/511128412
18、 内存池
sync.Pool 临时对象池,只有三个方法,New Get Put
package main
import (
"fmt"
"sync"
)
var pool *sync.Pool
type Person struct {
Name string
Age int
}
func init() {
pool = &sync.Pool{
New: func() interface{} {
fmt.Println("creating a new person")
return new(Person)
},
}
}
func main() {
person := pool.Get().(*Person)
fmt.Printf("get an object from pool:%v\n", person)
person.Name = "my"
person.Age = 10
pool.Put(person)
person = pool.Get().(*Person)
fmt.Printf("get an object from pool:%v\n", person)
person = pool.Get().(*Person)
fmt.Printf("get an object from pool:%v\n", person)
}
19、接口是怎么使用的
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
golang接口的意义和其他所有编程语言都是一样的,为了抽象和多态
这样你就可以在某个函数(方法)入参或者结构体(类)属性处使用一个接口变量,这个变量包含了接口定义的行为。
20、100亿数据找出最大的1000个数字
分治法,即大数据里最常用的MapReduce。
a、将100亿个数据分为1000个大分区,每个区1000万个数据
b、每个大分区再细分成100个小分区。总共就有1000100=10万个分区
c、计算每个小分区上最大的1000个数。
为什么要找出每个分区上最大的1000个数?举个例子说明,全校高一有100个班,我想找出全校前10名的同学,很傻的办法就是,把高一100个班的同学成绩都取出来,作比较,这个比较数据量太大了。应该很容易想到,班里的第11名,不可能是全校的前10名。也就是说,不是班里的前10名,就不可能是全校的前10名。因此,只需要把每个班里的前10取出来,作比较就行了,这样比较的数据量就大大地减少了。我们要找的是100亿中的最大1000个数,所以每个分区中的第1001个数一定不可能是所有数据中的前1000个。
d、合并每个大分区细分出来的小分区。每个大分区有100个小分区,我们已经找出了每个小分区的前1000个数。将这100个分区的1000100个数合并,找出每个大分区的前1000个数。
e、合并大分区。我们有1000个大分区,上一步已找出每个大分区的前1000个数。我们将这1000*1000个数合并,找出前1000.这1000个数就是所有数据中最大的1000个数
二、数据库
1、MySQL的数据类型
金额用 decimal 6位精度
日期 date 到秒 datetime
长字符串text 超长字符串用long text
用户头像 Blob
如何防止超卖,即高并发下库存变为负数
- 1.数据库乐观锁
加一个字段版本号
扣库存的时候判断一下版本号 - 2.把库存字段改为unsigned
这样可以保证库存不为负数,如果并发情况下被扣为负数的时候会报错,这个时候try catch然后返回库存不足就可以了 - 3.用redis的List数据类型
把所有秒杀请求插入到redis的队列了,当库存达到阈值后停止插入,然后消费redis里的数据
原文链接:https://blog.csdn.net/baidu_27627251/article/details/88047466
2、数据库慢查询排查,怎么优化宽表
很多时候,慢查询都是因为没有加索引。如果没有加索引的话,会导致全表扫描的。因此,应考虑在where的条件列,建立索引,尽量避免全表扫描。
宽表优化
参考 https://blog.csdn.net/m0_67698950/article/details/125050163
3、主键索引和非主键索引存储的区别
主键索引也被称为聚簇索引,叶子节点存放的是整行数据; 而非主键索引被称为二级索引,叶子节点存放的是主键的值.
如果根据主键查询, 只需要搜索ID这颗B+树
而如果通过非主键索引查询, 需要先搜索k索引树, 找到对应的主键, 然后再到ID索引树搜索一次, 这个过程叫做回表.
总结, 非主键索引的查询需要多扫描一颗索引树, 效率相对更低
参考原文链接:https://blog.csdn.net/fly_zhaohy/article/details/104014391
4、建立索引的原则
1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引;
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替
参考:https://blog.csdn.net/Programer0/article/details/125665810
5、为什么使用自增int作主键效率高
- int 相比varchar、char、text使用更少的存储空间,而且数据类型简单,可以节约CPU的开销,更便于表结构的维护
- 在B+树各个节点上进行比较的时候,int的比较效率高
- 在数据插入时,可以保证逻辑相邻的元素物理也相邻,便于范围查找
- 默认都会在主键上建立主键索引,使用整形作为主键可以将更多的索引载入内存,提高查询性能
https://www.cnblogs.com/xiaomaomao/p/16211762.html
6、什么是回表
通过非主键索引查询要扫描两棵 B+Tree,而通过主键索引查询只需要扫描一棵 B+Tree,第一次搜索 B+Tree 拿到主键值后再去搜索主键索引的 B+Tree,这个过程就是所谓的回表
那么不用主键索引就一定需要回表吗?
不一定!
如果查询的列本身就存在于索引中,那么即使使用二级索引,一样也是不需要回表的
7、MySQL的主从复制
MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式。
MySQL主从复制涉及到三个线程,一个运行在主节点(log dump thread),其余两个(I/O thread, SQL thread)运行在从节点
- 当从节点连接主节点时,主节点会创建一个log dump 线程,用于发送bin-log的内容。在读取bin-log中的操作时,此线程会对主节点上的bin-log加锁,当读取完成,甚至在发动给从节点之前,锁会被释放。
- 当从节点上执行start slave命令之后,从节点会创建一个I/O线程用来连接主节点,请求主库中更新的bin-log。I/O线程接收到主节点binlog dump 进程发来的更新之后,保存在本地relay-log中。
- SQL线程负责读取relay log中的内容,解析成具体的操作并执行,最终保证主从数据的一致性。
详见:https://blog.csdn.net/s_frozen/article/details/124566733
8、 MySQL日志
1、redo log(重做日志)属于MySQL存储引擎InnoDB的事务日志,InnoDB 在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫作 redo log(重做日志),在更新内存写完 redo log 后,就返回给客户端,本次更新成功。找时间把内存里的数据写入磁盘的过程,术语就是 flush。在这个 flush 操作执行之前,内存中的数据和磁盘中的数据是不一致的。当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
采用固定大小,循环写入的格式
2、undo log(回滚日志)也是属于MySQL存储引擎InnoDB的事务日志。 undo log属于逻辑日志,如其名主要起到回滚的作用,它是保证事务原子性的关键。记录的是数据修改前的状态,在数据修改的流程中,同时会记录一条与当前操作相反的逻辑日志到undo log中
3、**bin log(归档日志)**是一种数据库Server层(和什么引擎无关),以二进制形式存储在磁盘中的逻辑日志。bin log记录了数据库所有DDL和DML操作(不包含 SELECT 和 SHOW等命令,因为这类操作对数据本身并没有修改),主要应用于MySQL主从模式(master-slave)中,主从节点间的数据同步;以及基于时间点的数据还原。三种格式:
- Statement(Statement-Based Replication,SBR):每一条会修改数据的 SQL 都会记录在 binlog中。由于 Statement 模式只记录 SQL,极大的减少了 binlog 的日志量,避免了大量的 IO 操作,但是会出现一些数据一致性问题
- Row(Row-Based Replication,RBR):不记录 SQL 语句上下文信息,仅保存哪条记录被修改。不会出现一些数据一致性问题,但是会产生大量的日志,大量的日志也会带来 IO 性能问题。
- Mixed(Mixed-Based Replication,MBR):Statement 和 Row 的混合体,MySQL 会根据执行的每一条具体的 SQL 语句来区别对待记录的日志格式,也就是在 Statement 和 Row 之间选择一种
4、 relay log(中继日志)日志文件具有与bin log日志文件相同的格式,从上边MySQL主从复制的流程可以看出,relay log起到一个中转的作用,slave先从主库master读取二进制日志数据,写入从库本地,后续再异步由SQL线程读取解析relay log为对应的SQL命令执行。
5、慢查询日志(slow query log): 用来记录在 MySQL 中执行时间超过指定时间的查询语句,在 SQL 优化过程中会经常使用到。通过慢查询日志,我们可以查找出哪些查询语句的执行效率低,耗时严重。
6、 一般查询日志(general query log):用来记录用户的所有操作,包括客户端何时连接了服务器、客户端发送的所有SQL以及其他事件,比如 MySQL 服务启动和关闭等等。MySQL服务器会按照它接收到语句的先后顺序写入日志文件。
7、错误日志(error log): 应该是 MySQL 中最好理解的一种日志,主要记录 MySQL 服务器每次启动和停止的时间以及诊断和出错信息。
详情 https://blog.csdn.net/whatday/article/details/126297642
9、mysql优化explain字段有哪些
mysql数据量超过2千万会出现性能问题
select_type:查询类型。主要用来分辨查询的类型事普通查询还是联合查询还是子查询
simple:简单的查询,不包含子查询和 union
primary:查询中若包含任何复杂的子查询,最外层查询则被标记为 primary
type:访问类型,表示以何种方式去访问数据库,最容易想的是全表扫描,即直接暴力的遍历一张表去寻找需要的数据,效率非常低下。
访问的类型有很多,效率从最好到最坏依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
system:表只有一行记录(等于系统表),这是 const 类型的特例,平时不会出现,不需要进行磁盘 io
const:最多只能匹配到一条数据,通常使用主键或唯一索引进行等值条件查询
eq_ref:当进行等值联表查询使用主键索引或者唯一性非空索引进行数据查找时(实际上唯一索引等值查询 type 不是 eq_ref 而是 const)
ref:使用了非唯一性索引进行数据的查找
ref_or_null:对于某个字段既需要关联条件,也需要 null 值的情况下,查询优化器会选择这种访问方式
index_merge:在查询过程中需要多个索引组合使用
unique_subquery:该连接类型类似于 index_subquery,使用的是唯一索引。大多数情况下使用 SELECT 子查询时,MySQL 查询优化器会自动将子查询优化为联表查询,因此 type 不会显示为 index_subquery,而是 eq_ref
index_subquery:利用索引来关联子查询,不再扫描全表。但是大多数情况下使用 SELECT 子查询时,MySQL 查询优化器会自动将子查询优化为联表查询,因此 type 不会显示 index_subquery,而是 ref
range:表示利用索引查询的时候限制了范围,在指定范围内进行查询,这样避免了 index 的全索引扫描。适用的操作符:=, <>, >, >=, <, <=, is null, between,like, or, in
index:全索引扫描这个比 all 的效率要好,主要有两种情况,一种是当前的查询覆盖索引,即需要的数据在索引中就可以索取,或者是使用了索引进行排序,这样就避免了数据的重排序
all:全表扫描,需要扫描整张表,从头到尾找到需要的数据行。一般情况下出现这样的 sql 语句而且数据量比较大的话那么就需要进行优化
一般情况下,要保证查询至少达到 range 级别,最好能达到 ref
possible_keys:显示查询可能使用哪些索引来查找,即显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用
key:这一列显示 mysql 实际采用哪个索引来优化对该表的访问,即实际使用的索引,如果为 null ,则表示没有使用索引
mrr:当表很大且没有存储在存储引擎的缓存中时,在辅助索引上使用范围扫描读取行可能会导致对基本表的许多随机磁盘访问。
通过磁盘扫描多范围读取(MRR)优化,MySQL尝试通过只扫描索引并收集相关行的键来减少范围扫描的随机磁盘访问次数。然后对键进行排序,最后使用主键的顺序从基表检索行。
磁盘扫描MRR的动机是减少随机磁盘访问的数量,而实现对基本表数据的更连续的扫描。
原文链接:https://blog.csdn.net/qq_44734154/article/details/126739951
参考原文链接:https://blog.csdn.net/jiaomubai/article/details/126040506
10、mvcc怎么实现的,其原理是什么
RR级别的事务隔离可以解决脏读和不可重复读,他通过MVCC解决了快照读情况下的幻读问题,当前读下的幻读是以来Innodb的锁机制实现的。所以总结起来就是:
- 1.在快照读情况下,Mysql通过MVCC来避免幻读。
- 2.在当前读的情况下,Mysql通过锁机制来避免幻读。
MySQL InnoDB 的MVCC(多版本并发控制)主要是为了提高数据库并发性能,处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的
详见:https://blog.csdn.net/bbj12345678/article/details/120780051
11、mysql什么时候用表锁,什么时候用行锁,什么时候用间隙锁
表锁
- 在对某个表执行一些诸如ALTER TABLE、DROP TABLE 这类的DDL语句时,添加表锁
- 当索引失效的时候,行锁会升级成表锁
行锁
- 行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。
- 两个事务不能锁同一个索引
- insert,delete,update在事务中都会自动默认加上排它锁。
间隙锁
- 当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁。对于键值在条件范围内但并不存在的记录,叫做“间隙”(GAP),InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key 锁)
12、索引的数据结构及存储引擎的默认参数
索引的数据结构
采用B+树
1、MySQL为什么不用跳表?
跳表比B+树层级更高,需要更多的磁盘IO
2、Redis中为什么不用B+树?
Redis是基于内存的数据库,不用考虑磁盘IO问题,采用跳表,不用考虑B+树页分裂等问题
默认参数
- InnoDB_row_lock:用来分析系统上行锁的争夺情况
show status like ‘innodb_row_lock%’; - sync_binlog:write 和 fsync 的时机,fsync是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync 才占磁盘的 IO。
sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
13、MySQL执行查询流程
- 客户端先发送查询语句给服务器
- 服务器检查缓存,如果存在则返回
- 进行sql解析,生成解析树,再预处理,生成第二个解析树,最后再经过优化器,生成真正的执行计划
- 根据执行计划,调用存储引擎的API来执行查询
- 将结果返回给客户端
14、横表和纵表
横表:通常指我们平时在数据库中建立的表,是一种普通的建表方式。特点是,一个ID对应所有的值信息,以行Key-Value1-Value2-Value3的方式存储
- 优点:横标的有点事显示的较为清晰直观,同时在字段的选择上更为科学合理,具体的字段可以根据具体情况划分字段类型
- 缺点:不方便扩展和公用,也就是说设计了一张横标,只能在固定的某一种特定的相对不变的场景下使用,比如加字段,或者类似的业务想公用一张横表,都有局限
纵表:一般不多见,在表结构不确定的时候,如需增加字段的情况下的一种建表方式。特点是每行仅存储该ID的某一个类别字段的值,以行的方式存储Key-Value的方式存储
- 优点:最大的特点是可以灵活扩展存储的内容,同时具有一定的公用性,因为竖表的存储结构不受字段个数的限制,可以存储具有一定共性的业务数据。
- 缺点:竖表的字段类型要兼容,比如横标可以根据具体的值设计成varchar,decimal,datetime等,横标为了兼容以上字段类型,只能设计成varchar的,可能会浪费一定的空间
链接:https://www.cnblogs.com/wy123/p/6677073.html
15、关系型数据库和非关系型数据库的区别
关系型数据库:mysql oracle
非关系型数据库 redis mongodb
- 1、数据存储方式不同。关系型数据天然就是表格式的,因此存储在数据表的行和列中。与其相反,非关系型数据不适合存储在数据表的行和列中,而是大块组合在一起。非关系型数据通常存储在数据集中
- 2、扩展方式不同。SQL数据库是纵向扩展,也就是说提高处理能力,而NoSQL数据库是横向扩展的。
- 3、对事务性的支持不同。。SQL数据库支持对事务原子性细粒度控制,并且易于回滚事务.虽然NoSQL数据库也可以使用事务操作,但稳定性方面没法和关系型数据库比较
详见:https://blog.csdn.net/QGhurt/article/details/108312315
16、设计一个redis集群,redis 主从模式,主机挂了后,怎么选主机
哨兵模式即为反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
①选择优先级高的从服务器作为主服务器(值越小,优先级越高)
②如果优先级相同,选择偏移量最大的从服务器作为主服务器
③如果优先级和偏移量都相同,选择runid最小的从服务器作为主服务器,每个redis实例启动后都会随机生成一个40位的runid。选择runid最小的
参考:https://blog.csdn.net/qq_46370017/article/details/126327979
17、缓存和数据一致性问题
Cache Aside Pattern(旁路缓存模式)是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景,步骤如下:
写:
- 先更新 DB。
- 然后删除缓存。
读:
- 从 cache 中读取数据,读取到就直接返回
- cache中读取不到的话,就从 DB 中读取数据返回,再把数据放到 cache 中
参考链接:https://blog.csdn.net/lans_g/article/details/124652284
18、redis缓存失效原理
设置了失效时间的主键和具体的失效时间全部都维护在 expires 字典表中
消极方法(passive way),在主键被访问时如果发现它已经失效,那么就删除它
- 一个是发送到 AOF文件,将删除失效主键的这一操作以 DEL Key 的标准命令格式记录下来;
- 另一个就是发送到当前 Redis 服务器的所有 Slave,同样将删除失效主键的这一操作以 DEL Key 的标准命令格式告知这些 Slave 删除各自的失效主键
积极方法(active way),周期性地从设置了失效时间的主键中选择一部分失效的主键删除
- 要实现原理就是遍历处理 Redis 服务器中每个数据库的 expires 字典表中,从中尝试着随机抽样 REDIS_EXPIRELOOKUPS_PER_CRON(默认值为10)个设置了失效时间的主键,检查它们是否已经失效并删除掉失效的主键,如果失效的主键个数占本次抽样个数的比例超过25%,Redis 会认为当前数据库中的失效主键依然很多,所以它会继续进行下一轮的随机抽样和删除,直到刚才的比例低于25%才停止对当前数据库的处理,转向下一个数据库
详见:https://blog.csdn.net/u013530955/article/details/74454174/
19、redis 基础数据类型的底层数据结构
一个哈希表可以是一个数组,数组里面的每一个存储单元都叫做哈希桶(Bucket),比如数组第一个位置(索引为 0)被编为哈希桶 0,第二个位置(索引为 1)被编为哈希桶 1,以此类推。
当我们写入一个键值对的时候,会根据 key 和 value 的指针构建一个 dictEntry 结构体实例。然后通过对 key 进行哈希运算来计算出桶的位置,最后将 dictEntry 结构体实例的指针写入哈希表中。
string类型
假如存储的「字符串是一个字符串值并且长度大于32个字节」就会使用SDS(simple dynamic string)方式进行存储,并且encoding设置为raw;若是「字符串长度小于等于32个字节」就会将encoding改为embstr来保存字符串。
https://blog.csdn.net/qq_41071876/article/details/124422912
20、redis字典的数据结构
1、解决哈希冲突:这里采用的便是拉链法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突
2、扩容和收缩:当哈希表保存的键值对太多或者太少时,就要通过 rehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
1)如果执行扩展操作,会基于原哈希表创建一个大小等于 ht[0].used*2n 的哈希表(也就是每次扩展都是根据原哈希表已使用的空间扩大一倍创建另一个哈希表)。相反如果执行的是收缩操作,每次收缩是根据已使用空间缩小一倍创建一个新的哈希表。
2)重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
3)所有键值对都迁徙完毕后,释放原哈希表的内存空间。
渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的
21、zset怎么实现的
zset的两种实现方式
ziplist(压缩链表):满足以下两个条件的时候
- 元素数量少于128的时候
- 每个元素的长度小于64字节
skiplist(跳跃链表):不满足上述两个条件就会使用跳表,具体来说是组合了map和skiplist
- map用来存储member到score的映射,这样就可以在O(1)时间内找到member对应的分数
- skiplist按从小到大的顺序存储分数
- skiplist每个元素的值都是[score,value]对
跳跃表是有序单链表的一种改进,其查询、插入、删除也是O(logN)的时间复杂度
跳表其实就是一种可以进行二分查找的有序链表,跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了
为什么采用跳表,而不使用哈希表或平衡树实现呢?
- 哈希表不是有序的
- 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速
详见:https://blog.csdn.net/qq_42410605/article/details/122517927
22、redis的持久化机制,各有什么优缺点
Redis持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失
1、RDB:指定时间间隔内将当前数据的快照写入磁盘,过程:写时复制技术,有一个fork子进程,先将数据集写入临时文件,再用临时文件替换原来的快照文件,然后子进程退出
优点:
- 只有一个文件 dump.rdb,方便持久化;
- 容灾性好,一个文件可以保存到安全的磁盘;
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了Redis的高性能;
- 相对于数据集大时,比 AOF 的启动效率更高;
缺点:
- 数据安全性低;
- RDB 是间隔一段时间进行持久化,如果在持久化时Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;
2、AOF:默认不开启,采用日志形式记录每个写操作,并追加到文件中
优点:
- 数据安全,AOF持久化可以配置 appendfsync 属性,有always属性,每进行一次命令操作就记录到AOF文件中一次;
- 通过append模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题;
- AOF机制的rewrite模式。AOF文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall);
缺点:
- AOF文件比RDB文件大,且恢复速度慢;
- 数据集大的时候,比RDB启动效率低;
3、RDB和AOF的对比
- AOF文件比RDB更新频率高,优先使用AOF还原数据;
- AOF比RDB更安全也更大;
- RDB性能比AOF好;
- 如果两个都配了优先加载AOF;
原文链接:https://blog.csdn.net/weixin_42601136/article/details/122759402
23、redis缓存淘汰策略
随着系统的运行,redis的数据越来越多,会导致物理内存不足。通过使用虚拟内存(VM),将很少访问的数据交换到磁盘上,腾出内存空间的方法来解决物理内存不足的情况。虽然能够解决物理内存不足导致的问题,但是由于这部分数据是存储在磁盘上,如果在高并发场景中,频繁访问虚拟内存空间会严重降低系统性能。
Redis内存不足的缓存淘汰策略提供了8种:
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键。
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法(最近最少使用)驱逐最久没有使用的键。
- volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键。
- allkeys-random:加入键的时候如果过限,从所有key随机删除。
- volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐。
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键。
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键。
- allkeys-lfu:使用LFU算法(使用频率最小),从所有键中驱逐使用频率最少的键
24、redis一次请求的过程,查询慢该怎样排查
Redis客户端执行一条命令,分为4部分:发送命令=>命令排队=> 命令执行=> 返回结果
第一步:确定Redis是否真的变慢了
- 业务服务器到 Redis 服务器之间的网络存在问题,例如网络线路质量不佳,网络数据包在传输时存在延迟、丢包等情况
- Redis 本身存在问题,需要进一步排查是什么原因导致 Redis 变慢
第二步:查看slowlog(慢日志)
- 原因1:使用复杂度过高的命令
- 原因2:操作bigkey(value很大)
- 原因3:集中过期
- 原因4:开启AOF
- 原因5:fork耗时严重
- 原因6:碎片整理
- 原因7:绑定CPU
参考链接:https://blog.csdn.net/feiying0canglang/article/details/106184505
三、其他
1、 Linux常考命令
查看CPU:top命令
查看内存:free
查看隐藏文件:ls -a
检查所有监听端口:netstat -tln
查看网络连接、哪些端口被打开: netstat -anp
Linux使用正则表达式:https://blog.csdn.net/qq_48391148/article/details/125566389
2、 select poll epoll 区别
select==>时间复杂度O(n)
3 - 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
poll==>时间复杂度O(n)
- poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,
但是它没有最大连接数的限制,原因是它是基于链表来存储的.
epoll==>时间复杂度O(1)
- epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
原文链接:https://blog.csdn.net/weixin_52622200/article/details/112891861
3、 用户态和内核态
用户态和内核态是操作系统的两种运行状态,操作系统主要是为了对访问能力进行限制,用户态的权限较低,而内核态的权限较高
- 用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说,CPU 能够被其他程序获取
- 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。
内核态切换到用户态是通过设置程序状态字PSW
用户态到内核态的切换
- 系统调用 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作
- 异常 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常
- 外围设备的中断 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,
原文链接:https://blog.csdn.net/zydybaby/article/details/127080392
4、 进程间通信和线程间通信
线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。
进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。
进程通信
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
线程通信
- 锁机制:包括互斥锁、条件变量、读写锁
- 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
- 信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
原文链接:https://blog.csdn.net/weichi7549/article/details/107590240
5、进程调度算法有哪些
原文链接:https://blog.csdn.net/qq_36221862/article/details/55670604
基本的操作系统进程调度算法包括先来先服务(first come first serve),时间片轮转(round robin),多级反馈轮转法(round robin with multiple feedback),优先级法(静态优先级法/动态优先级法),短作业优先法(shortest job first),最高响应比优先法(highest response_ratio next)。
6、虚拟地址
虚拟内存技术:提供一种虚拟地址到实际物理地址的映射,将连续的虚拟地址暴露给程序,而实际上他们在物理内存(比如内存条)上面是不连续的。
虚拟内存能够很好的帮助程序员避免麻烦的内存管理与冲突等问题,并且将内存作为模块化独立出来。
内存分页:是把整个虚拟内存和物理内存空间切成一段段固定大小的尺寸。这样一个连续并且尺寸固定的内存空间叫做页(Page)。虚拟页和物理页的大小是一样的,通常为4KB。
页表:记录【进程 虚拟地址】与【内存 物理地址】的映射关系。每个进程都拥有自己的虚拟地址空间,也拥有一个页表。
对于进程来说,使用的都是虚拟地址。每个进程维护一个单独的页表。页表是一种数组结构,存放着各虚拟页的状态,是否映射,是否缓存。
为什么用虚拟地址
直接操作物理内存,这种方式存在几个问题:
- 地址空间不隔离:程序操作相同地址空间会造成互相影响甚至崩溃,而且安全性也得不到保证;
- 使用效率低:没有特别好的策略保证多个进程对超过物理内存大小的内存需求的满足;
- 程序运行地址不确定:程序运行时,都需要分配空闲区域,而空闲位置不确定,会带来一些重定位问题;
参考:https://blog.csdn.net/weixin_39790760/article/details/110815922
7、内存分段分页
分段:将程序分为代码段、数据段、堆栈段等;
分段地址通过段表,转换成线性地址; 分段地址包括段号和段内地址; 段表,包括短号、段长、基址;
分页:将段分成均匀的小块,通过页表映射物理内存;
分页机制就是将虚拟地址空间分为大小相等的页;物理地址空间也分为若干个物理块(页框);页和页框大小相等。实现离散分配; 分页机制的实现需要 MMU 硬件实现;负责分页地址转换; 页大小(粒度)太大浪费;太小,影响分配效率。
原文链接:https://blog.csdn.net/weixin_45304503/article/details/126690435
8、TCP的拥塞控制机制
该算法包括 4个主要部分: 慢启动、拥塞避免、快重传、快恢复
TCP发送速率起始慢,但在慢启动阶段以指数增长。
慢开始:
不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小;
拥塞避免
拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长
快重传
快重传要求接收方在收到一个 失序的报文段 后就立即发出 重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
快恢复
当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半,但是接下去并不执行慢开始算法:因为如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。
9、tcp阻塞和非阻塞
(1)对于TCP的send系统调用发送数据,如果socket是阻塞的,我们需要这样理解:send操作将会等待所有数据均被拷贝到发送缓冲区后才会返回。
- 如果发送缓冲区可用大小为0或者比要发送的数据长度要小,则会阻塞,直到发送缓冲区里的数据被系统发送出去后,可用缓冲区大小比要发送的数据长度大时,send返回成功,否则一直阻塞等待。因此,send返回的发送大小,一定是参数中发送长度的大小。
- 例如:如果当前发送缓冲区总大小为8192,个字节,通过send已经拷贝到缓冲区的数据为8000字节,那缓冲区剩余大小为192字节,而现在上层应用需要发送2000字节的数据,那么send就会等待缓冲区足够把所有2000字节数据拷贝进去,如第一次拷贝进去192字节,当缓冲区成功发送出1808字节后,再把上层应用buf中剩余的1808字节拷贝到发送缓冲区,最后send返回成功拷贝到发送缓冲区的字节数。
(2)对于TCP的send系统调用,如果socket是非阻塞的,send会立即返回。
- 例如,当发送缓冲区中有192字节,但是需要发送2000字节,此时send调用立即返回,并且返回值为192。因此,非阻塞send仅仅是尽自己的能力向发送缓冲区拷贝尽可能多的数据。如果发送缓冲区剩余空间为0,这时send立即返回,send返回值为EAGAIN,这是应用层最好休息一下再尝试发送
原文链接:https://blog.csdn.net/singularity0001/article/details/127663650
10、TCP四次挥手及所处状态
- 第一次挥手:假设客户端打算关闭连接,发送一个TCP首部FIN被置为1的FIN报文给服务端
客户端状态变为FIN_WAIT_1。
- 第二次挥手:服务端收到以后,向客户端发送ACK应答报文
服务端状态由ESTABLISHED变为CLOSE_WAIT,并继续处理数据,
当客户端收到服务端发送的ACK之后,状态由FIN_WAIT_1变为FIN_WAIT_2
- 第三次挥手:等待服务端处理完数据之后,向客户端发送FIN报文
发送FIN报文后,服务端状态变为LAST_ACK
- 第四次挥手:客户端收到FIN报文后返回一个ACK应答报文。
客户端接收到服务端发送的FIN后,状态变为TIME_WAIT,并且在等待2MSL后连接关闭。
- 服务端收到ACK报文之后,进入close状态,此时服务器完成连接关闭。
中间状态time_wait,close_wait过多的危害
- (1)close_wait过多:建立连接会占用文件描述符;而在liunx系统中,一个进程最大可以同时打开的文件描述符是有限的,当达到上限时,服务端进程无法再创建socket来响应新的请求,导致服务不可用。
- (2)time_wait过多:导致定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用,导致端口号不足,服务器无法响应。
11种状态说明
LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
1.可靠的实现tcp全双工连接的终止;
2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
原文链接:https://blog.csdn.net/ckckckcckckc/article/details/127839708
11、https的过程
1)客户端发起一个http请求,告诉服务器自己支持哪些hash算法。
2)服务端把自己的信息以数字证书的形式返回给客户端(证书内容有密钥公钥,网站地址,证书颁发机构,失效日期等)。证书中有一个公钥来加密信息,私钥由服务器持有。
3)验证证书的合法性
客户端收到服务器的响应后会先验证证书的合法性(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。
4)生成随机密码(RSA签名)
如果验证通过,或用户接受了不受信任的证书,浏览器就会生成一个随机的对称密钥(session key)并用公钥加密,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。
5)生成对称加密算法
验证完服务端身份后,客户端生成一个对称加密的算法和对应密钥,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。
详情:https://zhuanlan.zhihu.com/p/60033345
ssl过程
数字签名
12、HTTP1.0, HTTP1.1,HTTP2.0 区别
(1)HTTP1.0特点:无状态、短连接
HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(短连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)
(2)HTTP1.1特点:长连接、请求管道化、缓存处理、Host字段、断点传输
(3)HTTP2.0特点:二进制传输、多路复用、头部压缩、服务器推送
长连接: 客户端和服务端建立连接后不进行断开,之后客户端再次访问这个服务器上的内容时,继续使用这一条连接通道。
短连接: 客户端和服务端建立连接,发送完数据后立马断开连接。下次要取数据,需要再次建立连接
原文链接:https://blog.csdn.net/tdcqfyl/article/details/122923972
13、http状态码
- 1×× : 请求处理中,请求已被接受,正在处理
- 2×× : 请求成功,请求被成功处理
200 OK - 3×× : 重定向,要完成请求必须进行进一步处理
301 : 被请求的资源已永久性转移到新位置
302 :暂时性转移
304 : 已缓存 - 4×× : 客户端错误,请求不合法
400:Bad Request,请求有语法问题
403:拒绝请求
404:客户端所访问的页面不存在 - 5×× : 服务器端错误,服务器不能处理合法请求
500 :服务器内部错误
502:网关错误,无效网关;在互联网中表示一种网络错误。表现在WEB浏览器中给出的页面反馈。它通常并不意味着上游服务器已关闭(无响应网关/代理) ,而是上游服务器和网关/代理使用不一致的协议交换数据。鉴于互联网协议是相当清楚的,它往往意味着一个或两个机器已不正确或不完全编程。
503 : 服务不可用,稍等
504 :代表网关超时 (Gateway timeout),是指服务器作为网关或代理,但是没有及时从上游服务器收到请求。**这通常意味着上游服务器已关闭(**不响应网关 / 代理),而不是上游服务器和网关/代理在交换数据的协议上不一致
原文链接:https://blog.csdn.net/justloveyou_/article/details/78303617
14、http请求头
HTTP Request Header 请求头
Accept:指定客户端能够接收的内容类型。
Accept-Charset:浏览器可以接受的字符编码集。
Accept-Encoding:指定浏览器可以支持的web服务器返回内容压缩编码类型。
Accept-Language:浏览器可接受的语言。
Accept-Ranges:可以请求网页实体的一个或者多个子范围字段。
AuthorizationHTTP:授权的授权证书。
Cache-Control:指定请求和响应遵循的缓存机制。
Connection:表示是否需要持久连接。(HTTP 1.1默认进行持久连接)
CookieHTTP:请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。
Content-Length:请求的内容长度。
Content-Type:请求的与实体对应的MIME信息。
Date:请求发送的日期和时间。
Expect:请求的特定的服务器行为。
From:发出请求的用户的Email。
Host:指定请求的服务器的域名和端口号。
If-Match:只有请求内容与实体相匹配才有效。
If-Modified-Since:如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码。
If-None-Match:如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变。
If-Range:如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。
If-Unmodified-Since:只在实体在指定时间之后未被修改才请求成功。
Max-Forwards:限制信息通过代理和网关传送的时间。
Pragma:用来包含实现特定的指令。
Proxy-Authorization:连接到代理的授权证书。
Range:只请求实体的一部分,指定范围。
Referer:先前网页的地址,当前请求网页紧随其后,即来路。
TE:客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息。
Upgrade:向服务器指定某种传输协议以便服务器进行转换(如果支持。
User-AgentUser-Agent:的内容包含发出请求的用户信息。
Via:通知中间网关或代理服务器地址,通信协议。
Warning:关于消息实体的警告信息
15、TCP请求头,UDP请求头,IP数据包头
1、TCP请求头
1)源端口和目的端口:用于寻找发端和收端的应用程序。这两个值加上IP首部的源端IP和目的端IP唯一确定一个TCP连接;
2)序号(Seq):标识从TCP发端向TCP收端发送的数据字节流,它标识在这个报文段中的第一个数据字节的序号。如果将字节流看作在两个应用程序间的单向流动,
则TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达2的32次方减一后又从0开始。SYN标志消耗一个序号;
3)确认序号(ACK):如果上次成功收到数据字节序号加一。只有ACK标志为1时确认序号才有效,ACK = Seq + 1;
4)数据偏移:标识该TCP头部有多少个32bit(4字节),4比特最大表示15,TCP头部最长为60字节;
5)窗口:TCP流量控制的手段,告诉对方,我的TCP接收端缓冲区还能容纳多少个字节,这样对方能控制发送数据的速度;
6)校验和:由发送端填充,接收端对TCP报文执行CRC算法,以检验TCP报文段是否损毁。不仅校验头部,还包括数据部分;
7)紧急指针:也称为紧急偏移。紧急指针是一个正的偏移量,和序号字段的值相加表示最后一个紧急指针的下一字节的序号。是相对于当前序号的偏移。紧急指针
是发送端向接收端发送紧急数据的方法;
8)六个标志位:
a)URG:表示紧急指针是否有效;
b)ACK:表示确认号是否有效,携带ACK标志的数据报文段为确认报文段;
c)PSH:提示接收端的应用程序应该立即从TCP接受缓冲区中读走数据,为接受后数据腾出空间;
d)RST:表示要求对方重新建立连接,携带RST标志位的TCP报文段称为复位报文段;
e)SYN: 表示请求建立一个连接,携带SYN标志的TCP报文段称为同步报文段;
f)FIN:通知对方本端要关闭了,带FIN标志的TCP报文段称为结束报文段;
9)TCP头部选项:头部选项是一个可变长的信息,这部分最多包含40个字节(前面20字节是固定的)
头部选项的实际运用:
a)最大报文传输段(Maxinum Segment Size——MSS,后续进行详解)
b)窗口扩大选项(window scaling)
c)选择确认选项(Selective Acknowledgements——SACK)
d)NOP
原文链接:https://blog.csdn.net/www_dong/article/details/113419548
2、UDP请求头
UDP首部有8个字节,由4个字段构成,每个字段都是两个字节,
1.源端口: 主机的应用程序使用的端口号。
2.目的端口:目的主机的应用程序使用的端口号。
3.长度:是指UDP头部和UDP数据的字节长度。因为UDP头 部长度为8字节,所以该字段的最小值为8。
4.校验和:检测UDP数据报在传输中是否有错,有错则丢弃。
原文链接:https://blog.csdn.net/weixin_43142797/article/details/105648071
3、IP数据包头
- IP数据包是可变长度的,它有两部分组成,首部和数据
- 首部由两个份组成,固定部分和可变部分,固定部分20字节,可变部分有一些数据项组成,最多40字节
版本: IPV4 或 IPV6 (4bit)
首部长度:标识首部长度(4bit)
优先级于服务类型:标识数据包在网络中的优先级(8bit)
总长度: 标识数据包的总长度(16bit)
标识符:通过于标记字段和段偏移量一起用于数据包的分段。如果数据包原始长度超过数据包所要经过的数据链路的最大传输单元(MTU),那么必须将数据包分段为更小的数据包。
标志:决定当前的数据包是不是需要分片(3bit)
片偏移: 确认和
参考:https://www.cnblogs.com/makuo/p/16412689.html
16、从输入网址到获得页面的过程
(1). 浏览器查询 DNS,获取域名对应的IP地址;
(2). 浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;
(3). TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求;
(4). 服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
(5). 浏览器解析并渲染视图,若遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;
(6). 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。
原文链接:https://blog.csdn.net/justloveyou_/article/details/78303617
17、跨域
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
跨域的解决方案
1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域
原文链接:https://blog.csdn.net/miachen520/article/details/125569792
https://blog.csdn.net/weixin_66375317/article/details/124545878
18、红黑树
红黑树的性质
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
- 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN ),红黑树不追求绝对平衡,只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
https://blog.csdn.net/mz474920631/article/details/124982316
19、B树和B+树
二叉树每个根节点只有两个子树,在数据量很大的时候,树的深度会很大。B树的存在解决了这个问题。
B树的节点包含键和值,是key-value的形式。一个节点可以包含多个key,并不确定,需要看具体实现
B+树和B树相似,不同的地方是只有叶子节点存储形式为key-value,其他节点存储的只有key值。树的所有叶节点构成一个有序链表,可以按照key排序的次序依次遍历全部数据。
B+ 树的优点在于:
- B+树在非叶子结点上不包含数据的值,只当做索引使用,因此在内存相同的情况下,能够存放更多的key。
- B+树的叶子结点都是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。像有索引的链表。
B树的优点在于:
- 只需要找到key所在的位置,就能找到value,不用像B+树要一直找到叶子结点才能找到value
B+树的插入与删除操作都仅用O(1)的时间复杂度,查找的时间复杂度O(logN)
原文链接:https://blog.csdn.net/qq_57014350/article/details/118759152
https://blog.csdn.net/wufeifan_learner/article/details/109724836
20、数组和链表的区别
数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
- 插入和删除的时间复杂度是o(n)
- 查找的时间复杂度是o(1)
链表和数组一样,链表也是一种线性表。从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构
- 插入和删除的时间复杂度是o(1)
- 查找的时间复杂度是o(n)
21、Nginx的作用
- 正向代理:为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理
- 反向代理:以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器
- 负载均衡算法:
轮询
加权轮询
IP哈希
fair 响应时间,按后端服务器的响应时间来分配请求,响应时间短的优先分配,
URLhash 按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效 - 动静分离:让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路
参考:https://blog.csdn.net/dong123ohyes/article/details/118854614
22、正向代理和反向代理
相同点:
- 都是把客户端的请求转发给服务器的,再把服务器的响应转发给客户端
不同点:
- 正向代理是客户端的代理,服务器不知道真正的客户端是谁;反向代理是服务器的代理,客户端不知道真正的服务器是谁
- 正向代理一般是客户端来设置的,反向代理一般是服务器架设的
- 正向代理主要用来解决访问限制的问题,反向代理则提供负载均衡、安全防护等作用,二者都能提高访问速度
23、solid设计原则
Solid设计原则有五个主要方面,分别是单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖反转原则(DIP)。