本篇文章是博主自己找工作时收集总结的,涵盖了Go语言、MySQL数据库、计算机网络、操作系统、Redis、Python语言等多种面经,主要是根据博主自己的简历来制作的,编写不易,先赞后看。
话不多说,开搞!
Slice底层实现原理
切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一 个只读对象,其工作机制类似数组指针的一种封装。
切片对象非常小,是因为它是只有 3 个字段的数据结构:
指向底层数组的指针
切片的长度
切片的容量
Slice扩容机制
在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存,将原 slice 拷贝到新 slice ,然后返回新 slice。扩容后再将数据追加进去。
扩容操作只对容量,扩容后的 slice 长度不变,容量变化规则如下:
Slice与数组区别
array是固定长度的数组,使用前必须确定数组长度,是值类型。
slice是一个引用类型,是一个动态的指向数组切片的指针。
slice是一个不定长的,总是指向底层的数组array的数据结构,可以动态扩容。
创建方式不一样,Slice使用make创建或者根据数组创建。
作为函数参数时,数组传递的是数组的副本,而slice传递的是指针。
Map底层实现原理
Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫 hmap(a header for a go map),一个叫 bmap(a bucket for a Go map,通常叫其 bucket)。
hmap 哈希表
hmap是Go map的底层实现,每个hmap内都含有多个bmap(buckets桶、oldbuckets旧桶、overflow溢出桶),既每个哈希表都由多个桶组成。
buckets
buckets是一个指针,指向一个bmap数组,存储多个桶。
oldbuckets
oldbuckets是一个指针,指向一个bmap数组,存储多个旧桶,用于扩容。
overflow
overflow是一个指针,指向一个元素个数为2的数组,数组的类型是一个指针,指向一个slice,slice的元素是桶(bmap)的地址,这些桶都是溢出桶。为什么有两个?因为Go map在哈希冲突过多时,会发生扩容操作。[0]表示当前使用的溢出桶集合,[1]是在发生扩容时,保存了旧的溢出桶集合。overflow存在的意义在于防止溢出桶被gc。
bmap 哈希桶
bmap是一个隶属于hmap的结构体,一个桶(bmap)可以存储8个键值对。如果有第9个键值对被分配到该桶,那就需要再创建一个桶,通过overflow指针将两个桶连接起来。在hmap中,多个bmap桶通过overflow指针相连,组成一个链表。
Map进行有序的排序
map每次遍历,都会从一个随机值序号的桶,再从其中随机的cell开始遍历,并且扩容后,原来桶中的key会落到其他桶中,本身就会造成失序
如果想顺序遍历map,先把key放到切片排序,再按照key的顺序遍历map。
或者可以先把map中的key,通过sort包排序,再遍历map。
map 为什么是不安全的
Go map 默认是并发不安全的,同时对 map 进行并发读写的时,程序会 panic,原因如下:Go 官方经过长时间的讨论,认为 map 适配的场景应该是简单的(不需要从多个 gorountine 中进行安全访问的),而不是为了小部分情况(并发访问),导致大部分程序付出锁的代价,因此决定了不支持。
map 在扩缩容时,需要进行数据迁移,迁移的过程并没有采用锁机制防止并发操作,而是会对某个标识位标记为 1,表示此时正在迁移数据。如果有其他 goroutine 对 map 也进行写操作,当它检测到标识位为 1 时,将会直接 panic。
如果想实现map线程安全,有两种方式:
方式一:使用读写锁 map
+ sync.RWMutex
方式二:使用golang提供的 sync.Map
Map扩容策略
扩容时机:
向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容
扩容条件:
超过负载 map元素个数 > 6.5(负载因子) * 桶个数
溢出桶太多
当桶总数<2^15时,如果溢出桶总数>=桶总数,则认为溢出桶过多
当桶总数>215时,如果溢出桶总数>=215,则认为溢出桶过多
扩容机制:
双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets。
等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取。
渐进式扩容:
插入修改删除key的时候,都会尝试进行搬迁桶的工作,每次都会检查oldbucket是否nil,如果不是nil则每次搬迁2个桶,蚂蚁搬家一样渐进式扩容
Map和Slice区别
Map总结
介绍一下Channel(有缓冲和无缓冲)
Go 语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进程,是通过 goroutine 和 channel 来实现的。
所以 channel 收发遵循先进先出 FIFO,分为有缓存和无缓存,channel 中大致有 buffer(当缓冲区大小部位 0 时,是个 ring buffer)、sendx 和 recvx 收发的位置(ring buffer 记录实现)、sendq、recvq 当前 channel 因为缓冲区不足 而阻塞的队列、使用双向链表存储、还有一个 mutex 锁控制并发、其他原属等。
// 无缓冲的channel由于没有缓冲发送和接收需要同步
ch := make(chan int)
//有缓冲channel不要求发送和接收操作同步
ch := make(chan int, 2)
channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据;channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
Channel实现原理
channel 内部维护了两个 goroutine 队列,一个是待发送数据的 goroutine 队列,另一个是待读取数据的 goroutine 队列。
每当对 channel 的读写操作超过了可缓冲的 goroutine 数量,那么当前的 goroutine 就会被挂到对应的队列上,直到有其他 goroutine 执行了与之相反的读写操作,将它重新唤起。
Channel读写流程
向 channel 写数据:
若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。
从 channel 读数据
若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。
如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。
如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。
将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。
关闭 channel
1.关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。
panic 出现的场景还有:
Channel为什么能做到线程安全
Channel 可以理解是一个先进先出的队列,通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。
Channel是同步进行还是异步的(Channel的三种状态)
Channel是异步进行的, channel存在3种状态:
操作 | 一个零值nil通道 | 一个非零值但已关闭的通道 | 一个非零值且尚未关闭的通道 |
---|---|---|---|
关闭 | 产生恐慌 | 产生恐慌 | 成功关闭 |
发送数据 | 永久阻塞 | 产生恐慌 | 阻塞或者成功发送 |
接收数据 | 永久阻塞 | 永不阻塞 | 阻塞或者成功接收 |
进程、线程和协程的区别
进程: 进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程: 线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程: 协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
线程和协程的区别
介绍一下Goroutine
Goroutine 是一个与其他 goroutines 并行运行在同一地址空间的 Go 函数或方法。
goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。它在语言层面已经内置了调度和上下文切换的机制。
goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。在Go语言中,每一个并发的执行单元叫作一个goroutine。我们只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。
context包结构原理和用途
Context(上下文)是Golang应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。
Context 只定义了接口,凡是实现该接口的类都可称为是一种 context。
goroutine调度
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
如何避免Goroutine泄露和泄露场景
gorouinte 里有关于 channel 的操作,如果没有正确处理 channel 的读取,会导致 channel 一直阻塞住, goroutine 不能正常结束
waitgroup 用法和原理
waitgroup 内部维护了一个计数器,当调用 wg.Add(1)
方法时,就会增加对应的数量;当调用 wg.Done()
时,计数器就会减一。直到计数器的数量减到 0 时,就会调用
runtime_Semrelease 唤起之前因为 wg.Wait()
而阻塞住的 goroutine。
使用方法:
实现原理:
WaitGroup 主要维护了 2 个计数器,一个是请求计数器 v,一个是等待计数器 w,二者组成一个 64bit 的值,请求计数器占高 32bit,等待计数器占低 32bit。
每次 Add 执行,请求计数器 v 加 1,Done 方法执行,请求计数器减 1,v为0 时通过信号量唤醒 Wait()。
GMP是什么
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。
优先从 P 的本地队列获取 goroutine 来执行;如果本地队列没有,从全局队列获取,如果全局队列也没有,会从其他的 P 上偷取 goroutine。
GMP goroutine调度策略
调度器的设计策略
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。
抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。
全局 G 队列:,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
CSP 模型是“以通信的方式来共享内存”,不同于传统的多线程通过共享内存来通信。用于描述两个独立的并发实体通过共享的通讯 channel (管道)进行通信的并发模型。
两种抢占式调度
协作式的抢占式调度
在 1.14 版本之前,程序只能依靠 Goroutine 主动让出 CPU 资源才能触发调度,存在问题
某些 Goroutine 可以长时间占用线程,造成其它 Goroutine 的饥饿
垃圾回收需要暂停整个程序(Stop-the-world,STW),最长可能需要几分钟的时间,导致整个程序无法工作。
基于信号的抢占式调度
在任何情况下,Go 运行时并行执行(注意,不是并发)的 goroutines 数量是小于等于 P 的数量的。为了提高系统的性能,P 的数量肯定不是越小越好,所以官方默认值就是 CPU 的核心数,设置的过小的话,如果一个持有 P 的 M, 由于 P 当前执行的 G 调用了 syscall 而导致 M 被阻塞,那么此时关键点:GO 的调度器是迟钝的,它很可能什么都没做,直到 M 阻塞了相当长时间以后,才会发现有一个 P/M 被 syscall 阻塞了。然后,才会用空闲的 M 来强这个 P。通过 sysmon 监控实现的抢占式调度,最快在 20us,最慢在 10-20ms 才 会发现有一个 M 持有 P 并阻塞了。操作系统在 1ms 内可以完成很多次线程调度(一般情况 1ms 可以完成几十次线程调度),Go 发起 IO/syscall 的时候执行该 G 的 M 会阻塞然后被 OS 调度走,P 什么也不干,sysmon 最慢要 10-20ms才能发现这个阻塞,说不定那时候阻塞已经结束了,宝贵的 P 资源就这么被阻塞的 M 浪费。
GMP 调度过程中存在哪些阻塞
I/O,select
block on syscall
channel
等待锁
runtime.Gosched()
GMP 调度流程
每个 P 有个局部队列,局部队列保存待执行的 goroutine(流程 2),当 M 绑 定的 P 的的局部队列已经满了之后就会把 goroutine 放到全局队列(流程 2- 1)
每个 P 和一个 M 绑定,M 是真正的执行 P 中 goroutine 的实体(流程 3),M 从绑定的 P 中的局部队列获取 G 来执行
当 M 绑定的 P 的局部队列为空时,M 会从全局队列获取到本地队列来执行G(流程 3.1),当从全局队列中没有获取到可执行的 G 时候,M 会从其他 P 的局部队列中偷取 G 来执行(流程 3.2),这种从其他 P 偷的方式称为 work stealing
当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找新的 idle 的 M,若没有 idle 的 M 就会新建一个 M(流程 5.1)。
当 G 因 channel 或者 network I/O 阻塞时,不会阻塞 M,M 会寻找其他 runnable 的 G;当阻塞的 G 恢复后会重新进入 runnable 进入 P 队列等待执 行(流程 5.3)
GC 原理
垃圾回收就是对程序中不再使用的内存资源进行自动回收的操作。
三色标记法
STW(Stop The World)
GC 的触发条件
主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。
被动触发,分为两种方式:
Golang为什么小对象多了会造成gc压力
通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配。
GC的屏障介绍
写屏障(Write Barrier)
写屏障:并发gc会产生黑色节点引用白色节点情况,导致正常的指针变量错误的被清除;解决方法为写屏障;
主要包括强三色不变式和弱三色不变式;
强三色不变:黑色节点不能引用白色节点,如果引用白色节点需要将白色节点置灰(插入写屏障);
弱三色不变:黑节点可以引用白节点,但白节点有其他灰色节点或递归指向存在灰色节点,删除白色节点引用时,需要把白色节点置灰(删除写屏障);
栈上变量较小,且频繁开辟或删除,不开启写屏障;需要之后一次rescan;
stw时机:
插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;(1.5版本采用)
删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象;
混合写屏障:1.8版本加入
原因是stw需要耗时;加入混合写屏障,解决这个问题;
流程:
1.开始标记时候,栈上可达节点均置黑,之后不进行rescan,不用stw;
2.gc时产生的在栈上创建的对象,均置黑;
3.堆空间删除的对象置灰;
4.堆空间插入的对象置灰;
特点
混合写屏障继承了插入写屏障的优点,起始无需 STW 打快照,直接并发扫描垃圾即可;
混合写屏障继承了删除写屏障的优点,赋值器是黑色赋值器,GC 期间,任何在栈上创建的新对象,均为黑色。扫描过一次就不需要扫描了,这样就消除了插入写屏障时期最后 STW 的重新扫描栈;
混合写屏障扫描精度继承了删除写屏障,比插入写屏障更低,随着带来的是 GC 过程全程无 STW;
混合写屏障扫描栈虽然没有 STW,但是扫描某一个具体的栈的时候,还是要停止这个 goroutine 赋值器的工作的哈(针对一个 goroutine 栈来说,是暂停扫的,要么全灰,要么全黑哈,原子状态切换)。
GC 的流程是什么
当前版本的 Go 以 STW 为界限,可以将 GC 划分为五个阶段:
阶段说明赋值器状态 GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障 STWGCMark 扫描标记阶段,与赋值器并发执行,写屏障开启并发
GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 STWGCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭并发
GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭并发
GC 如何调优
优化内存的申请速度,尽可能少申请内存,复用已申请的内存。三个关键字:控制、减少、复用。
通过 go tool pprof 和 go tool trace 等工具
new和make的区别
go的内存分配是怎么样的
Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、arenas、mcentral、mcache。它们以 mspan 作为基本分配单位。具体的分配逻辑如下:
竞态、内存逃逸
竞态
资源竞争,就是在程序中,同一块内存同时被多个 goroutine 访问。我们使用 go build、go run、go test 命令时,添加 -race 标识可以检查代码中是否存在资源竞争。
解决这个问题,我们可以给资源进行加锁,让其在同一时刻只能被一个协程来操作。
逃逸分析
「逃逸分析」就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的。堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
在 Go 里变量的内存分配方式则是由编译器来决定的。如果变量在作用域(比如函数范围)之外,还会被引用的话,那么称之为发生了逃逸行为,此时将会把对象放到堆上,即使声明为值类型;如果没有发生逃逸行为的话,则会被分配到栈上,即使 new 了一个对象。
逃逸场景:
什么是 rune 类型
rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。
go语言触发异常的场景有哪些
空指针解析
下标越界
除数为0
调用panic函数
go的接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计。
Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。
相比较于其他语言, Go 有什么优势或者特点
Go代码的设计是务实的。每个功能和语法决策都旨在让程序员的生活更轻松。
Golang 针对并发进行了优化,并且在规模上运行良好。
由于单一的标准代码格式,Golang 通常被认为比其他语言更具可读性。
自动垃圾收集明显比 Java 或 Python 更有效,因为它与程序同时执行。
defer、panic、recover 三者的用法
defer 函数调用的顺序是后进先出,当产生 panic 的时候,会先执行 panic 前面的 defer 函数后才真的抛出异常。一般的,recover 会在 defer 函数里执行并捕获异常,防止程序崩溃。
Go反射
介绍
Go语言提供了一种机制在运行时更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。
反射是指在程序运行期对程序本身进行访问和修改的能力,程序在编译时变量被转换为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时程序无法获取自身的信息。
Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。
反射三定律
Go语言函数传参是值类型还是引用类型
Go语言中的内存对齐
CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问。比如 32 位的 CPU ,字长为 4 字节,那么 CPU 访问内存的单位也是 4 字节。
CPU 始终以字长访问内存,如果不进行内存对齐,很可能增加 CPU 访问内存的次数,例如:
变量 a、b 各占据 3 字节的空间,内存对齐后,a、b 占据 4 字节空间,CPU 读取 b 变量的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取 b 变量的值需要进行 2 次内存访问。第一次访问得到 b 变量的第 1 个字节,第二次访问得到 b 变量的后两个字节。
内存对齐对实现变量的原子性操作也是有好处的,每次内存访问是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。
简言之:合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。
空 struct{} 的用途
因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。
值传递和地址传递(引用传递)
Go 语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct 等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等 这些),这样就可以修改原内容数据。
Golang 的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分 配内存和初始化成员结构,返回对象而非指针。
原子操作
一个或者多个操作在 CPU 执行过程中不被中断的特性,称为原子性 (atomicity)。
这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。而在现实世界中,CPU不可能不中断的执行一系列操作,但如果我们在执行多个操作时,能让他们的中间状态对外不可见,那我们就可以宣城他们拥有了“不可分割”的原子性。
在 Go 中,一条普通的赋值语句其实不是一个原子操作。列如,在 32 位机器上写 int64 类型的变量就会有中间状态,因为他会被拆成两次写操作(MOV)——写低 32 位和写高 32 位。
什么是Gin框架
Gin是一个用Go语言编写的web框架,,优点是封装比较好,API友好,源码注释比较明确,具有快速灵活,容错方便等特点。它具有运行速度快,分组的路由器,良好的崩溃捕获和错误处理,非常好的支持中间件和 json。
Gin路由的实现
gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的Trie tree(或者只是Radix Tree)。具有公共前缀的节点也共享一个公共父节点。
gin框架使用的是定制版本的httprouter,其路由的原理是大量使用公共前缀的树结构,它基本上是一个紧凑的Trie tree(或者只是Radix Tree)。具有公共前缀的节点也共享一个公共父节点。
路由树是由一个个节点构成的,gin框架路由树的节点由node
结构体表示
在gin的路由中,每一个HTTP Method
(GET、POST、PUT、DELETE…)都对应了一棵 radix tree
,我们注册路由的时候会调用addRoute`函数
注册路由的逻辑主要有addRoute
函数和insertChild
方法。
路由树构造的详细过程:
路由树构造的详细过程:
路由匹配是由节点的 getValue
方法实现的。getValue
根据给定的路径(键)返回nodeValue
值,保存注册的处理函数和匹配到的路径参数数据。
gin框架路由使用前缀树,路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。
Gin的中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()
、c.Abort()
、c.Set()
、c.Get()
。
gin框架的中间件函数和处理函数是以切片形式的调用链条存在的,我们可以顺序调用也可以借助c.Next()
方法实现嵌套调用。
借助c.Set()
和c.Get()
方法我们能够在不同的中间件函数中传递数据。
gin默认中间件
gin.Default()默认使用了Logger和Recovery中间件,其中:
Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
对微服务的了解
使用一套小服务来开发单个应用的方式,每个服务运行在独立的进程里,一般采用轻量级的通讯机制互联,并且它们可以通过自动化的方式部署
微服务特点
微服务架构的优势和缺点
优点
1、易于开发和维护
2、启动较快
3、局部修改容易部署
4、技术栈不受限
5、按需伸缩
缺点
1、运维要求较高
2、分布式的复杂性
3、接口调整成本高
4、重复劳动
RPC协议
RPC调用流程
微服务架构下数据交互一般是对内 RPC,对外 REST
将业务按功能模块拆分到各个微服务,具有提高项目协作效率、降低模块耦合度、提高系统可用性等优点,但是开发门槛比较高,比如 RPC 框架的使用、后期的服务监控等工作
一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用
流行RPC框架:Dubbo、Motan、Thrift、gRPC
gRPC介绍
gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统
gRPC 是一个高性能、开源、通用的RPC框架,基于HTTP2协议标准设计开发,默认采用Protocol Buffers数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库。
在gRPC客户端可以直接调用不同服务器上的远程程序,使用起来就像调用本地程序一样,很容易去构建分布式应用和服务。和很多RPC系统一样,服务端负责实现定义好的接口并处理客户端的请求,客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用gRPC支持的不同语言实现。
gRPC主要特性
gRPC使用ProtoBuf来定义服务,ProtoBuf是由Google开发的一种数据序列化协议(类似于XML、JSON、hessian)。ProtoBuf能够将数据进行序列化,并广泛应用在数据存储、通信协议等方面。
gRPC支持多种语言,并能够基于语言自动生成客户端和服务端功能库。目前已提供了C版本grpc、Java版本grpc-java 和 Go版本grpc-go,其它语言的版本正在积极开发中,其中,grpc支持C、C++、Node.js、Python、Ruby、Objective-C、PHP和C#等语言,grpc-java已经支持Android开发。
gRPC基于HTTP2标准设计,所以相对于其他RPC框架,gRPC带来了更多强大功能,如双向流、头部压缩、多复用请求等。这些功能给移动设备带来重大益处,如节省带宽、降低TCP链接次数、节省CPU使用和延长电池寿命等。同时,gRPC还能够提高了云端服务和Web应用的性能。gRPC既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现客户端和服务器端的通信和简化通信系统的构建。
Protobuf介绍
Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化、或者说序列化。它很适合做数据存储或RPC数据交换格式。可以用于即时通讯、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式
protobuf的核心内容包括:
定义消息:消息的结构体,以message标识。
定义接口:接口路径和参数,以service标识。
通过protobuf提供的机制,服务端与服务端之间只需要关注接口方法名(service)和参数(message)即可通信,不需关注繁琐的链路协议和字段解析,极大降低服务端的设计开发成本。
Micro介绍和主要功能
go-micro简介
go-micro的主要功能
服务发现:自动服务注册和名称解析。
负载均衡:基于服务发现构建的客户端负载均衡。
消息编码:基于内容类型的动态消息编码。
请求/响应:基于RPC的请求/响应,支持双向流。
Async Messaging:PubSub是异步通信和事件驱动架构的一流公民。
可插拔接口:Go Micro为每个分布式系统抽象使用Go接口,因此,这些接口是可插拔的,并允许Go Micro与运行时无关,可以插入任何基础技术
go-micro特性
Micro通信流程
Server监听客户端的调用,和Brocker推送过来的信息进行处理。并且Server端需要向Register注册自己的存在或消亡,这样Client才能知道自己的状态
Register服务的注册的发现,Client端从Register中得到Server的信息,然后每次调用都根据算法选择一个的Server进行通信,当然通信是要经过编码/解码,选择传输协议等一系列过程的
如果有需要通知所有的Server端可以使用Brocker进行信息的推送,Brocker 信息队列进行信息的接收和发布
consul
Consul是用于实现分布式系统的服务发现与配置,Consul是分布式的、高可用的、可横向扩展的。
注册中心Consul关键功能
服务发现:
客户端可以注册服务,程序可以轻松找到它们所依赖的服务
运行状况检查:
Consul客户端可以提供任意数量的运行状况检查
KV 存储:
应用程序可以将Consul的层级键/值存储用于任何目的,包括动态配置,功能标记,协调,领导者选举等
安全服务通信:
Consul 可以为服务生成和分发TLS证书,建立相互的TLS连接
多数据中心:
Consul 支持多个数据中心
注册中心Consul两个重要协议
Gossip Protocol (八卦协议)
Raft Protocol ( 选举协议)
Jaeger
什么是链路追踪:
分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等
链路追踪主要功能:
故障快速定位:可以通过调用链结合业务日志快速定位错误信息
链路性能可视化:各个阶段链路耗时、服务依赖关系可以通过可视化界面展现出来
链路分析:通过分析链路耗时、服务依赖关系可以得到用户的行为路径,汇总分析应用在很多业务场景
jaeger链路追踪作用
Jaeger组成
Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent。
Agent - 它是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。它被设计成一个基础组件,部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。
Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。
Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search。
Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示。Query 是无状态的,您可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面。
分布式追踪系统发展很快,种类繁多,但核心步骤一般有三个:代码埋点,数据存储、查询展示
Prometheus
promethues介绍
promethues工作流程
Prometheus server定期从配置好的jobs/exporters/Pushgateway中拉数据
Prometheus server记录数据并且根据报警规则推送alert数据
Alertmanager 根据配置文件,对接收到的警报进行处理,发出告警。
在图形界面中,可视化采集数据
promethues重要组件
Prometheus Server:用于收集和存储时间序列数据。
Client Library:客户端库成相应的metrics并暴露给Prometheus server
Push Gateway:主要用于短期的jobs
Exporters: 用于暴露已有的第三方服务的metrics给Prometheus
Alertmanager: 从Prometheus server端接收到alerts后,会进行
grafana看板
熔断降级、限流、负载均衡
熔断降级
服务熔断也称服务隔离或过载保护。在微服务应用中,服务存在一定的依赖关系,形成一定的依赖链,如果某个目标服务调用慢或者有大量超时,造成服务不可用,间接导致其他的依赖服务不可用,最严重的可能会阻塞整条依赖链,最终导致业务系统崩溃(又称雪崩效应)。此时,对该服务的调用执行熔断,对于后续请求,不再继续调用该目标服务,而是直接返回,从而可以快速释放资源。等到目标服务情况好转后,则可恢复其调用。
关闭 (Closed):在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在某个时间窗口内,失败的失败率达到预设的阈值,则切换到断开状态,此时开启一个超时时间,当到达该时间则切换到半关闭状态,该超时时间是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态。在关闭状态下,调用错误是基于时间的,在特定的时间间隔内会重置,这能够防止偶然错误导致熔断器进去断开状态
打开 (Open):在该状态下,发起请求时会立即返回错误,一般会启动一个超时计时器,当计时器超时后,状态切换到半打开状态,也可以设置一个定时器,定期的探测服务是否恢复
半打开 (Half-Open):在该状态下,允许应用程序一定数量的请求发往被调用服务,如果这些调用正常,那么可以认为被调用服务已经恢复正常,此时熔断器切换到关闭状态,同时需要重置计数。如果这部分仍有调用失败的情况,则认为被调用方仍然没有恢复,熔断器会切换到关闭状态,然后重置计数器,半打开状态能够有效防止正在恢复中的服务被突然大量请求再次打垮。
常见的有三种熔断降级策略
服务降级
当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度。
关于降级,这里有两种场景:
限流
在微服务架构下,若大量请求超过微服务的处理能力时,可能会将服务打跨,甚至产生雪崩效应、影响系统的整体稳定性。比如说你的用户服务处理能力是1w/s,现在因为异常流量或其他原因,有10w的并发请求访问你的服务,那你的服务肯定扛不住啊。这种情况下,我们可以在流量超出承受阈值时,直接进行”限流”、拒绝部分请求,从而保证系统的整体稳定性。
限流算法
固定时间窗口
基于固定时间窗口的限流算法是非常简单的。首先需要选定一个时间起点,之后每次接口请求到来都累加计数器,如果在当前时间窗口内,根据限流规则(比如每秒钟最大允许 100 次接口请求),累加访问次数超过限流值,则限流熔断拒绝接口请求。当进入下一个时间窗口之后,计数器清零重新计数。
滑动时间窗口算法
滑动时间窗口算法是对固定时间窗口算法的一种改进,流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。对比固定时间窗口限流算法,滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,对内存的占用会比较多。
漏桶和令牌桶算法
漏桶算法(Leaky Bucket):主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。
请求先进入到漏桶里,漏桶以一定的速度出水,当水请求过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
令牌桶算法(Token Bucket):是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
漏桶和令牌桶算法的区别
令牌桶算法,主要放在服务端,用来保护服务端(自己),主要用来对调用者频率进行限流,为的是不让自己被压垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制=桶大小),那么实际处理速率可以超过配置的限制(桶大小)。
而漏桶算法,主要放在调用方,这是用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
自适应限流
一般的限流常常需要指定一个固定值(qps)作为限流开关的阈值,这个值一是靠经验判断,二是靠通过大量的测试数据得出。但这个阈值,在流量激增、系统自动伸缩或者某某commit了一段有毒代码后就有可能变得不那么合适了。并且一般业务方也不太能够正确评估自己的容量,去设置一个合适的限流阈值。那么我们就可以考虑用自适应限流来解决这个问题。
对于自适应限流来说, 一般都是结合系统的 Load、CPU 使用率以及应用的入口 QPS、平均响应时间和并发量等几个维度的监控指标,通过自适应的流控策略, 让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定。
分布式限流
上面使用的限流算法,都是基本单节点限流的。但线上业务出于各种原因考虑,多是分布式系统,单节点的限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。比如说我希望某个接口的QPS的1000次/秒,服务部署在5台机器上,虽然我们可以通过配置每台节点200次/秒来限流。但如果节点收缩或者扩容,那么久不能满足需求了。而且不同服务的物理配置不一定相同,可能有些节点处理得比较快,那么配置均值来限流,就不是一个好方法了。
常见的分布式限流策略
网关层限流:将限流规则应用在所有流量的入口处,比如nigix+lua
中间件限流:将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量。
负载均衡
Load balancing,即负载均衡,是一种计算机技术,用来在多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的。
负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。
负载均衡算法
1、轮询法
将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
2、随机法
通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
3、源地址哈希法
源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。
4、加权轮询法
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
5、加权随机法
与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
6、最小连接数法
最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前。
积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
什么是Viper
Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。
特性:
JSON
、TOML
、YAML
、HCL
、envfile
和Java properties
格式的配置文件读取配置信息Viper支持什么功能
Viper能够为你执行下列操作:
JSON
、TOML
、YAML
、HCL
、INI
、envfile
和Java properties
格式的配置文件。什么是Swagger
Swagger本质上是一种用于描述使用JSON表示的RESTful API的接口描述语言。Swagger与一组开源软件工具一起使用,以设计、构建、记录和使用RESTful Web服务。Swagger包括自动文档,代码生成和测试用例生成。
想要使用gin-swagger
为你的代码自动生成接口文档,一般需要下面三个步骤:
Swagger的优势
在前后端分离的项目开发过程中,如果后端同学能够提供一份清晰明了的接口文档,那么就能极大地提高大家的沟通效率和开发效率。可是编写接口文档历来都是令人头痛的,而且后续接口文档的维护也十分耗费精力。
最好是有一种方案能够既满足我们输出文档的需要又能随代码的变更自动更新,而Swagger正是那种能帮我们解决接口文档问题的工具。
什么是Zap
Zap是在 Go 中实现超快、结构化、分级的日志记录。
Zap日志能够提供下面这些功能:
1、能够将事件记录到文件中,也可以在应用控制台输出
2、日志切割-可以根据文件大小,时间或间隔来切割日志文件
3、支持不同的日志级别。例如 INFO、DEBUG、ERROR等
4、能够打印基本信息,如调用文件/函数名和行号,日志时间等。
zap的基本配置
Zap提供了两种类型的日志记录器—Sugared Logger 和 Logger 。
在性能很好但不是很关键的上下文中,使用 SugaredLogger 。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger 。它甚至比 SugaredLogger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
这个日志程序中唯一缺少的就是日志切割归档功能。添加日志切割归档功能,我们将使用第三方库Lumberjack来实现。
什么是JWT
JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。
JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。
一个JWT Token就像这样:
eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyODAx0DcyNzQ40DMyMzU4NSwiZ
XhwIjoxNTk0NTQwMjkxLCJpc3MiOiJibHV1YmVsbCJ9.1k_ZrAtYGCeZhK3iupHxP1kgjBJzQTVTtX0iZYFx9wU
JWT的实现
JWT由.分割的三部分组成,这三部分依次是:
JWT的优势
JWT就是一种基于Token的轻量级认证模式,服务端认证通过后,会生成一个JSON对象,经过签名后得到一个Token(令牌)再发回给用户,用户后续请求只需要带上这个Token,服务端解密之后就能获取该用户的相关信息了。
JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。
而JWT的最大优势是服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度。但这也是JWT最大的劣势,由于有效期存储在Token中,JWT Token-旦签发,就会在有效期内-直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单纯使用JWT就无法做到了。
为什么使用索引
索引的分类
什么时候需要/不需要创建索引
索引最大的好处是提高查询速度,但是索引也是有缺点的,比如:
所以,索引不是万能钥匙,它也是根据场景来使用的。
什么时候适用索引?
WHERE
查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。GROUP BY
和 ORDER BY
的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。什么时候不需要创建索引?
WHERE
条件,GROUP BY
,ORDER BY
里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的。优化索引的方法
前缀索引优化;
前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。
使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
覆盖索引优化;
覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。
使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。
主键索引最好是自增的;
如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高。
主键字段的长度不要太大,因为主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小。
防止索引失效;
用上了索引并不意味着查询的时候会使用到索引,所以我们心里要清楚有哪些情况会导致索引失效,从而避免写出索引失效的查询语句,否则这样的查询效率是很低的。
发生索引失效的情况:
like %xx
或者 like %xx%
这两种方式都会造成索引失效;索引使用的注意事项
MySQL 索引通常是被用于提高 WHERE 条件的数据行匹配时的搜索速度,在索引的使用过程中,存在一些使用细节和注意事项。
函数,运算,否定操作符,连接条件,多个单列索引,最左前缀原则,范围查询,不会包含有NULL值的列,like 语句不要在列上使用函数和进行运算
索引为什么使用B+树作为索引
主要原因:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。
B+tree的磁盘读写代价更低,B+tree的查询效率更加稳定 数据库索引采用B+树而不是B树的主要原因:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。
B+树的特点
索引失效有哪些
like %xx
或者 like %xx%
这两种方式都会造成索引失效;MyISAM和InnoDB实现B树索引方式的区别是什么
MyISAM,B+Tree叶节点的data域存放的是数据记录的地址,在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的key存在,则取出其data域的值,然后以data域的值为地址读取相应的数据记录,这被称为“非聚簇索引”
InnoDB,其数据文件本身就是索引文件,相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的节点data域保存了完整的数据记录,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引,这被称为“聚簇索引”或者聚集索引,而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。
在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。因此,在设计表的时候,不建议使用过长的字段为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1pQCH7oH-1678081583393)(https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E7%B4%A2%E5%BC%95/%E7%B4%A2%E5%BC%95%E6%80%BB%E7%BB%93.drawio.png)]
事务的四大特性
事务的脏读、不可重复读、幻读问题
脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
丢弃修改:两个写事务T1 T2同时对A=0进行递增操作,结果T2覆盖T1,导致最终结果是1 而不是2,事务被覆盖
不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
事务的隔离级别有哪些
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
READ-UNCOMMITTED 未提交读 | √ | √ | √ |
READ-COMMITTED 提交读 | × | √ | √ |
REPEATABLE-READ 重复读 | × | × | √ |
SERIALIZABLE 可串行化读 | × | × | × |
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)
这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别 下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以 说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要 求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。
Read View的作用
Read View 有四个重要的字段:
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
MySQL可重复读级别完全解决幻读了吗
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同:
这两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。
两个发生幻读场景的例子。
第一个例子:对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。
第二个例子:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。
所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。
MySQL中为什么要有事务回滚机制
在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志,然后在对数据库中的对应行进行写入。 当事务已经被提交后,就无法再次回滚了。
回滚日志作用: 1)能够在发生错误或者用户执行 ROLLBACK 时提供回滚相关的信息 2) 在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。
InnoDB介绍
InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,InnoDB是默认的MySQL引擎。
InnoDB主要特性有:
InnoDB给MySQL提供了具有提交、回滚和崩溃恢复能力的事物安全(ACID兼容)存储引擎。
InnoDB锁定在行级并且也在SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中,可以自由地将InnoDB类型的表和其他MySQL的表类型混合起来,甚至在同一个查询中也可以混合。
InnoDB是为处理巨大数据量的最大性能设计。它的CPU效率可能是任何其他基于磁盘的关系型数据库引擎锁不能匹敌的。
InnoDB存储引擎完全与MySQL服务器整合,InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上。
InnoDB支持外键完整性约束,存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB会为每一行生成一个6字节的ROWID,并以此作为主键。
InnoDB被用在众多需要高性能的大型数据库站点上。InnoDB不创建目录,使用InnoDB时,MySQL将在MySQL数据目录下创建一个名为ibdata1的10MB大小的自动扩展数据文件,以及两个名为 ib_logfile0 和 ib_logfile1 的5MB大小的日志文件。
MyISAM介绍
MyISAM基于ISAM存储引擎,并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。MyISAM拥有较高的插入、查询速度,但不支持事物。
MyISAM主要特性有:
MEMORY介绍
MEMORY存储引擎将表中的数据存储到内存中,未查询和引用其他表数据提供快速访问。
MEMORY主要特性有:
Archive介绍
archive储存引擎的应用场景就是它的名字的缩影,主要用于归档。archive储存引擎仅支持select和insert,最出众的是插入快,查询快,占用空间小。
文件系统存储特性
功能特点
数据库引擎InnoDB与MyISAM的区别
InnoDB
MyISAM
总结
Commit
和 Rollback
语句。适用场景: MyISAM适合: 插入不频繁,查询非常频繁,如果执行大量的SELECT,MyISAM是更好的选择, 没有事务。 InnoDB适合: 可靠性要求比较高,或者要求事务; 表更新和查询都相当的频繁, 大量的INSERT或UPDATE
MySQL有哪些锁(全局锁/表级锁/行级锁)
全局锁
MyISAM 只支持表锁,InnoDB 支持表锁和行锁,默认为行锁。
表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低。
行级锁:开销大,加锁慢,会出现死锁。锁粒度小,发生锁冲突的概率小,并发度最高。
MySQL是怎么加锁的
MySQL 行级锁的加锁规则。
唯一索引等值查询:
非唯一索引等值查询:
非唯一索引和主键索引的范围查询的加锁规则不同之处在于:
MySQL记录锁+间隙锁解决幻读问题
在 MySQL 的可重复读隔离级别下,针对当前读的语句会对索引加记录锁+间隙锁,这样可以避免其他事务执行增、删、改时导致幻读的问题。
有一点要注意的是,在执行 update、delete、select … for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。
死锁的四个必要条件
互斥条件:一个资源每次只能被一个进程使用;
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;
如何解决MySQL死锁问题
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
数据库悲观锁和乐观锁的原理和应用场景
悲观锁,先获取锁,再进行业务操作,一般就是利用类似 SELECT … FOR UPDATE 这样的语句,对数据加锁,避免其他事务意外修改数据。 当数据库执行SELECT … FOR UPDATE时会获取被select中的数据行的行锁,select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
乐观锁,先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过。Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。
MySQL的内部构造一般可以分为哪两个部分
可以分为服务层和存储引擎层两部分,其中:
服务层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认的存储引擎。
undo log、redo log、binlog有什么用
redo log是InnoDB引擎特有的,只记录该引擎中表的修改记录。binlog是MySQL的Server层实现的,会记录所有引擎对数据库的修改。
redo log是物理日志,记录的是在具体某个数据页上做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑。
redo log是循环写的,空间固定会用完;binlog是可以追加写入的,binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
补充
1、redolog记录修改内容(哪一页发生了什么变化),写于事务开始前,用于数据未落磁盘,但数据库挂了后的数据恢复
2、binlog记录修改SQL,写于事务提交时,可用于读写分离
3、undolog记录修改前记录,用于回滚和多版本并发控制
什么是Buffer pool
Innodb 存储引擎设计了一个缓冲池(*Buffer Pool*),来提高数据库的读写性能。
缓存什么
InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样需要按「页」来划分。
在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB
的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。
Innodb 通过三种链表来管理缓页:
InnoDB 对 LRU 做了一些优化,我们熟悉的 LRU 算法通常是将最近查询的数据放到 LRU 链表的头部,而 InnoDB 做 2 点优化:
innodb_old_blocks_time
阈值(默认为1秒)」**时,才会将页插入到 young 区域,否则还是插入到 old 区域,目的是为了解决批量数据访问,大量热数据淘汰的问题。可以通过调整 innodb_old_blocks_pct
参数,设置 young 区域和 old 区域比例。
DROP、DELETE 与 TRUNCATE 的区别
三种都可以表示删除,其中的细微区别之处如下:
DROP | DELETE | TRUNCATE | |
---|---|---|---|
SQL 语句类型 | DDL | DML | DDL |
回滚 | 不可回滚 | 可回滚 | 不可回滚 |
删除内容 | 从数据库中 删除表,所有的数据行,索引和权限也会被删除 | 表结构还在,删除表的 全部或者一部分数据行 | 表结构还在,删除表中的 所有数据 |
删除速度 | 删除速度最快 | 删除速度慢,需要逐行删除 | 删除速度快 |
因此,在不再需要一张表的时候,采用 DROP;在想删除部分数据行时候,用 DELETE;在保留表而删除所有数据的时候用 TRUNCATE。
SQL语法中内连接、自连接、外连接(左、右、全)、交叉连接的区别分别是什么
内连接:只有两个元素表相匹配的才能在结果集中显示。
外连接:
左外连接: 左边为驱动表,驱动表的数据全部显示,匹配表的不匹配的不会显示。
右外连接:右边为驱动表,驱动表的数据全部显示,匹配表的不匹配的不会显示。
全外连接:连接的表中不匹配的数据全部会显示出来。
MySQL中CHAR和VARCHAR的区别有哪些
数据库中的主键、超键、候选键、外键是什么
主键为候选键的子集,候选键为超键的子集,而外键的确定是相对于主键的。
MySQL优化
SQL语句执行流程
Server层按顺序执行sql的步骤为:
简单概括:
数据库三范式是什么
对MVCC的了解
数据库并发场景:
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。
MVCC 可以为数据库解决以下问题:
主从复制中涉及到哪三个线程?
主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。
数据库如何保证持久性
主要是利用Innodb的redo log。重写日志, 正如之前说的,MySQL是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再写回到磁盘上。如果此时突然宕机,内存中的数据就会丢失。 怎么解决这个问题? 简单啊,事务提交前直接把数据写入磁盘就行啊。 这么做有什么问题?
于是,决定采用redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
采用redo log的好处?
其实好处就是将redo log进行刷盘比对数据页刷盘效率高,具体表现如下:
数据库如何保证原子性
主要是利用 Innodb 的undo log。 undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的 SQL语句,他需要记录你要回滚的相应日志信息。 例如
undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
数据库如何保证一致性
数据库高并发的解决方案
数据库结构优化的手段
关系型和非关系型数据库的区别
非关系型数据库也叫NOSQL,采用键值对的形式进行存储。
它的读写性能很高,易于扩展,可分为内存性数据库以及文档型数据库,比如 Redis,Mongodb,HBase等等。
合使用非关系型数据库的场景:
数据库为什么要进行分库和分表
分库与分表的目的在于,减小数据库的单库单表负担,提高查询性能,缩短查询时间。
通过分表,可以减少数据库的单表负担,将压力分散到不同的表上,同时因为不同的表上的数据量少了,起到提高查询性能,缩短查询时间的作用,此外,可以很大的缓解表锁的问题。 分表策略可以归纳为垂直拆分和水平拆分: 水平分表:取模分表就属于随机分表,而时间维度分表则属于连续分表。 如何设计好垂直拆分,我的建议:将不常用的字段单独拆分到另外一张扩展表. 将大文本的字段单独拆分到另外一张扩展表, 将不经常修改的字段放在同一张表中,将经常改变的字段放在另一张表中。 对于海量用户场景,可以考虑取模分表,数据相对比较均匀,不容易出现热点和并发访问的瓶颈。
库内分表,仅仅是解决了单表数据过大的问题,但并没有把单表的数据分散到不同的物理机上,因此并不能减轻 MySQL 服务器的压力,仍然存在同一个物理机上的资源竞争和瓶颈,包括 CPU、内存、磁盘 IO、网络带宽等。
分库与分表带来的分布式困境与应对之策 数据迁移与扩容问题----一般做法是通过程序先读出数据,然后按照指定的分表策略再将数据写入到各个分表中。 分页与排序问题----需要在不同的分表中将数据进行排序并返回,并将不同分表返回的结果集进行汇总和再次排序,最后再返回给用户。
TCP/IP网络模型有哪几层,分别有什么作用
应用层
最上层的,也是我们能直接接触到的就是应用层(Application Layer),我们电脑或手机使用的应用软件都是在应用层实现。
所以,应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP等。
应用层是不用去关心数据是如何传输的,而且应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。
传输层
应用层的数据包会传给传输层,传输层(Transport Layer)是为应用层提供网络支持的。
在传输层会有两个传输协议,分别是 TCP 和 UDP。
TCP 的全称叫传输控制协议(Transmission Control Protocol),大部分应用使用的正是 TCP 传输层协议,比如 HTTP 应用层协议。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。
UDP 相对来说就很简单,简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高。当然,UDP 也可以实现可靠传输,把 TCP 的特性在应用层上实现就可以,不过要实现一个商用的可靠 UDP 传输协议,也不是一件简单的事情。
应用需要传输的数据可能会非常大,如果直接传输就不好控制,因此当传输层的数据包大小超过 MSS(TCP 最大报文段长度) ,就要将数据包分块,这样即使中途有一个分块丢失或损坏了,只需要重新发送这一个分块,而不用重新发送整个数据包。在 TCP 协议中,我们把每个分块称为一个 TCP 段(TCP Segment)。
网络层
我们不希望传输层协议处理太多的事情,只需要服务好应用即可,让其作为应用间数据传输的媒介,帮助实现应用到应用的通信,而实际的传输功能就交给下一层,也就是网络层(Internet Layer)。
网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文,如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文。
IP 协议的寻址作用是告诉我们去往下一个目的地该朝哪个方向走,路由则是根据「下一个目的地」选择路径。寻址更像在导航,路由更像在操作方向盘。
网络接口层
生成了 IP 头部之后,接下来要交给网络接口层(Link Layer)在 IP 头部的前面加上 MAC 头部,并封装成数据帧(Data frame)发送到网络上。
以太网在判断网络包目的地时和 IP 的方式不同,因此必须采用相匹配的方式才能在以太网中将包发往目的地,而 MAC 头部就是干这个用的,所以,在以太网进行通讯要用到 MAC 地址。
MAC 头部是以太网使用的头部,它包含了接收方和发送方的 MAC 地址等信息,我们可以通过 ARP 协议获取对方的 MAC 地址。
所以说,网络接口层主要为网络层提供「链路级别」传输的服务,负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标识网络上的设备。
输入网址到网页显示,期间发生了什么
背后有哪些技术
1、查浏览器缓存,看看有没有已经缓存好的,如果没有
2 、检查本机host文件,
3、调用API,Linux下Socket函数 gethostbyname
4、向DNS服务器发送DNS请求,查询本地DNS服务器,这其中用的是UDP的协议
5、如果在一个子网内采用ARP地址解析协议进行ARP查询如果不在一个子网那就需要对默认网关进行DNS查询,如果还找不到会一直向上找根DNS服务器,直到最终拿到IP地址(全球400多个根DNS服务器,由13个不同的组织管理)
6、这个时候我们就有了服务器的IP地址 以及默认的端口号了,http默认是80 https是 443 端口号,会,首先尝试http然后调用Socket建立TCP连接,
7、经过三次握手成功建立连接后,开始传送数据,如果正是http协议的话,就返回就完事了,
8、如果不是http协议,服务器会返回一个5开头的的重定向消息,告诉我们用的是https,那就是说IP没变,但是端口号从80变成443了,好了,再四次挥手,完事,
9、再来一遍,这次除了上述的端口号从80变成443之外,还会采用SSL的加密技术来保证传输数据的安全性,保证数据传输过程中不被修改或者替换之类的,
10、这次依然是三次握手,沟通好双方使用的认证算法,加密和检验算法,在此过程中也会检验对方的CA安全证书。
11、确认无误后,开始通信,然后服务器就会返回你所要访问的网址的一些数据,在此过程中会将界面进行渲染,牵涉到ajax技术之类的,直到最后我们看到色彩斑斓的网页
什么是HTTP协议
HTTP 是超文本传输协议,也就是HyperText Transfer Protocol。
HTTP 的名字「超文本协议传输」,它可以拆成三个部分:
GET和POST
GET 用于获取资源,而 POST 用于传输实体主体。
get是获取数据,post是修改数据
get把请求的数据放在url上, 以?分割URL和传输数据,参数之间以&相连,所以get不太安全。而post把数据放在HTTP的包体内(request body 相对安全)
get提交的数据最大是2k( 限制实际上取决于浏览器), post理论上没有限制。
GET产生一个TCP数据包,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); POST产生两个TCP数据包,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
本质区别:GET是幂等的,而POST不是幂等的
这里的幂等性:幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。
正因为它们有这样的区别,所以不应该且不能用get请求做数据的增删改这些有副作用的操作。因为get请求是幂等的,在网络不好的隧道中会尝试重试。如果用get请求增数据,会有重复操作的风险,而这种重复操作可能会导致副作用(浏览器和操作系统并不知道你会用get请求去做增操作)。
HTTP缓存技术
对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,那么下次就直接读取本地的数据,不必在通过网络获取服务器的响应了,这样的话 HTTP/1.1 的性能肯定肉眼可见的提升。
所以,避免发送 HTTP 请求的方法就是通过缓存技术,HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存。
强制缓存
强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。
强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:
Cache-Control
, 是一个相对时间;Expires
,是一个绝对时间;如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control 的优先级高于 Expires 。
Cache-control 选项更多一些,设置更加精细,所以建议使用 Cache-Control 来实现强缓存。具体的实现流程如下:
协商缓存
协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存。
协商缓存可以基于两种头部来实现。
第一种:请求头部中的 If-Modified-Since
字段与响应头部中的 Last-Modified
字段实现,这两个字段的意思是:
Last-Modified
:标示这个响应资源的最后修改时间;If-Modified-Since
:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。第二种:请求头部中的 If-None-Match
字段与响应头部中的 ETag
字段,这两个字段的意思是:
Etag
:唯一标识响应资源;If-None-Match
:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。
当使用 ETag 字段实现的协商缓存的过程:
当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;
当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期:
服务器再次收到请求后,
会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较:
如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源。
HTTP 如何实现长连接?在什么时候会超时?
通过在头部(请求和响应头)设置 Connection: keep-alive,HTTP1.0协议支持,但是默认关闭,从HTTP1.1协议以后,连接默认都是长连接
1、HTTP 一般会有 httpd 守护进程,里面可以设置 keep-alive timeout,当 tcp 链接闲置超过这个时间就会关闭,也可以在 HTTP 的 header 里面设置超时时间
2、TCP 的 keep-alive 包含三个参数,支持在系统内核的 net.ipv4 里面设置:当 TCP 链接之后,闲置了 tcp_keepalive_time,则会发生侦测包,如果没有收到对方的 ACK,那么会每隔 tcp_keepalive_intvl 再发一次,直到发送了 tcp_keepalive_probes,就会丢弃该链接。
(1)tcp_keepalive_intvl = 15
(2)tcp_keepalive_probes = 5
(3)tcp_keepalive_time = 1800
实际上 HTTP 没有长短链接,只有 TCP 有,TCP 长连接可以复用一个 TCP 链接来发起多次 HTTP 请求,这样可以减少资源消耗,比如一次请求 HTML,可能需要请求后续的 JS/CSS/图片等
HTTP 1.0/1.1/2.0/3.0的介绍、优点和缺点
HTTP/1.0
1996年5月,HTTP/1.0 版本发布,为了提高系统的效率,HTTP/1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。
HTTP/1.0中浏览器与服务器只保持短暂的连接,连接无法复用。也就是说每个TCP连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。
我们知道TCP连接的建立需要三次握手,是很耗费时间的一个过程。所以,HTTP/1.0版本的性能比较差。
HTTP/1.1
HTTP 最突出的优点是「简单、灵活和易于扩展、应用广泛和跨平台」。
HTTP 协议里有优缺点一体的双刃剑,分别是「无状态、明文传输」,同时还有一大缺点「不安全」。
为了解决HTTP/1.0存在的缺陷,HTTP/1.1于1999年诞生。相比较于HTTP/1.0来说,最主要的改进就是引入了持久连接。所谓的持久连接即TCP连接默认不关闭,可以被多个请求复用。
客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。或者客户端在最后一个请求时,主动告诉服务端要关闭连接。
HTTP/1.1版还引入了管道机制(pipelining),即在同一个TCP连接里面,客户端可以同时发送多个请求。这样就进一步改进了HTTP协议的效率。
有了持久连接和管道,大大的提升了HTTP的效率。但是服务端还是顺序执行的,效率还有提升的空间。
HTTP/2
HTTP/2 是 HTTP 协议自 1999 年 HTTP 1.1 发布后的首个更新,主要基于 SPDY 协议。
HTTP/2 为了解决HTTP/1.1中仍然存在的效率问题,HTTP/2 采用了多路复用。即在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应。能这样做有一个前提,就是HTTP/2进行了二进制分帧,即 HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码。
而这个负责拆分、组装请求和二进制帧的一层就叫做二进制分帧层。
除此之外,还有一些其他的优化,比如做Header压缩、服务端推送等。
Header压缩就是压缩老板和员工之间的对话。
服务端推送就是员工事先把一些老板可能询问的事情提现发送到老板的手机(缓存)上。这样老板想要知道的时候就可以直接读取短信(缓存)了。
HTTP/2 相比 HTTP/1.1 性能上的改进:
HTTP/2 有什么缺陷?
HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。
HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。
HTTP3.0
HTTP/2 虽然具有多个流并发传输的能力,但是传输层是 TCP 协议,于是存在以下缺陷:
HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。
QUIC 协议的特点:
另外 HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞问题。
HTTPS的介绍
HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
HTTPS是如何保证数据传输的安全,整体的流程是什么?(SSL是怎么工作保证安全的)
(1)客户端向服务器端发起SSL连接请求;
(2) 服务器把公钥发送给客户端,并且服务器端保存着唯一的私钥
(3)客户端用公钥对双方通信的对称秘钥进行加密,并发送给服务器端
(4)服务器利用自己唯一的私钥对客户端发来的对称秘钥进行解密,
(5)进行数据传输,服务器和客户端双方用公有的相同的对称秘钥对数据进行加密解密,可以保证在数据收发过程中的安全,即是第三方获得数据包,也无法对其进行加密,解密和篡改。
因为数字签名、摘要是证书防伪非常关键的武器。 “摘要”就是对传输的内容,通过hash算法计算出一段固定长度的串。通过发送方的私钥对这段摘要进行加密,加密后得到的结果就是“数字签名”
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
补充:SSL/TLS的四次握手,目前网上的主流答案都在重复阮一峰老师的博客,属于TLS 1.0版本的答案,使用RSA密钥交换算法。但是现在TLS 1.2已经成为主流,使用ECDHE算法,如果面试可以说出这个版本的答案,应该会更好。
什么是SSL/TLS
SSL代表安全套接字层。它是一种用于加密和验证应用程序(如浏览器)和Web服务器之间发送的数据的协议。 身份验证 , 加密Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。
SSL/TLS协议作用:认证用户和服务,加密数据,维护数据的完整性的应用层协议加密和解密需要两个不同的密钥,故被称为非对称加密;加密和解密都使用同一个密钥的
对称加密:优点在于加密、解密效率通常比较高 ,HTTPS 是基于非对称加密的, 公钥是公开的。
TLS 握手过程
TLS 协议是如何解决 HTTP 的风险的呢?
TLS 的握手过程,其中每一个「框」都是一个记录(record),记录是 TLS 收发数据的基本单位,类似于 TCP 里的 segment。多个记录可以组合成一个 TCP 包发送,所以通常经过「四个消息」就可以完成 TLS 握手,也就是需要 2个 RTT 的时延,然后就可以在安全的通信环境里发送 HTTP 报文,实现 HTTPS 协议。
HTTPS 是应用层协议,需要先完成 TCP 连接建立,然后走 TLS 握手过程后,才能建立通信安全的连接。
HTTPS RSA握手过程
HTTPS 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。
确保传输安全过程(其实就是rsa原理):
使用 RSA 密钥协商算法的最大问题是不支持前向保密。因为客户端传递随机数(用于生成对称加密密钥的条件之一)给服务端时使用的是公钥加密的,服务端收到后,会用私钥解密得到随机数。所以一旦服务端的私钥泄漏了,过去被第三方截获的所有 TLS 通讯密文都会被破解。
为了解决这个问题,后面就出现了 ECDHE 密钥协商算法。
HTTPS ECDHE握手过程
ECDHE 算法是在 DHE 算法的基础上利用了 ECC 椭圆曲线特性,可以用更少的计算量计算出公钥,以及最终的会话密钥。
TLS 第一次握手
客户端首先会发一个「Client Hello」消息,消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(*Client Random*)。
TLS 第二次握手
服务端收到客户端的「打招呼」,同样也要回礼,会返回「Server Hello」消息,消息面有服务器确认的 TLS 版本号,也给出了一个随机数(*Server Random*),然后从客户端的密码套件列表选择了一个合适的密码套件。
接着,服务端为了证明自己的身份,发送「Certificate」消息,会把证书也发给客户端。
这一步就和 RSA 握手过程有很大的区别了,因为服务端选择了 ECDHE 密钥协商算法,所以会在发送完证书后,发送「Server Key Exchange」消息。
这个过程服务器做了三件事:
为了保证这个椭圆曲线的公钥不被第三方篡改,服务端会用 RSA 签名算法给服务端的椭圆曲线公钥做个签名。
随后,就是「Server Hello Done」消息,服务端跟客户端表明:“这些就是我提供的信息,打招呼完毕”。
至此,TLS 两次握手就已经完成了,目前客户端和服务端通过明文共享了这几个信息:Client Random、Server Random 、使用的椭圆曲线、椭圆曲线基点 G、服务端椭圆曲线的公钥,这几个信息很重要,是后续生成会话密钥的材料。
TLS 第三次握手
客户端收到了服务端的证书后,自然要校验证书是否合法,如果证书合法,那么服务端到身份就是没问题的。校验证书的过程会走证书链逐级验证,确认证书的真实性,再用证书的公钥验证签名,这样就能确认服务端的身份了,确认无误后,就可以继续往下走。
客户端会生成一个随机数作为客户端椭圆曲线的私钥,然后再根据服务端前面给的信息,生成客户端的椭圆曲线公钥,然后用「Client Key Exchange」消息发给服务端。
算好会话密钥后,客户端会发一个「Change Cipher Spec」消息,告诉服务端后续改用对称算法加密通信。
接着,客户端会发「Encrypted Handshake Message」消息,把之前发送的数据做一个摘要,再用对称密钥加密一下,让服务端做个验证,验证下本次生成的对称密钥是否可以正常使用。
TLS 第四次握手
最后,服务端也会有一个同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。于是,就可以正常收发加密的 HTTP 请求和响应了。
RSA 和 ECDHE 握手过程的区别
FIN_WAIT_2,CLOSE_WAIT状态和TIME_WAIT状态
对称密钥加密和非对称密钥加密
对称秘钥加密
对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。
非对称秘钥加密
非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。
公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。
HTTPS如何优化
对于硬件优化的方向,因为 HTTPS 是属于计算密集型,应该选择计算力更强的 CPU,而且最好选择支持 AES-NI 特性的 CPU,这个特性可以在硬件级别优化 AES 对称加密算法,加快应用数据的加解密。
对于软件优化的方向,如果可以,把软件升级成较新的版本,比如将 Linux 内核 2.X 升级成 4.X,将 openssl 1.0.1 升级到 1.1.1,因为新版本的软件不仅会提供新的特性,而且还会修复老版本的问题。
对于协议优化的方向:
对于证书优化的方向:
对于重连 HTTPS 时,我们可以使用一些技术让客户端和服务端使用上一次 HTTPS 连接使用的会话密钥,直接恢复会话,而不用再重新走完整的 TLS 握手过程。
常见的会话重用技术有 Session ID 和 Session Ticket,用了会话重用技术,当再次重连 HTTPS 时,只需要 1 RTT 就可以恢复会话。对于 TLS1.3 使用 Pre-shared Key 会话重用技术,只需要 0 RTT 就可以恢复会话。
这些会话重用技术虽然好用,但是存在一定的安全风险,它们不仅不具备前向安全,而且有重放攻击的风险,所以应当对会话密钥设定一个合理的过期时间。
WebSocket协议
websocket是一种浏览器与服务器进行全双工通信的网络技术,属于 应用层协议。它 基于TCP传输协议,并 复用HTTP 的握手通道,用来弥补HTTP协议在持久通信能力上的不足。
websocket的特点有哪些?
HTTP如何实现长连接
HTTP长连接
HTTP/1.1
版本中,默认的连接都是长连接,我们可以通过Connection: keep-alive
字段进行指定。TCP保活机制
为什么要有保活机制?
保活机制
首先保活机制的工作原理就是,通过在服务器端设置一个保活定时器,当定时器开始工作后就定时的向网络通信的另一端发出保活探测的TCP报文,如果接收到了ACK报文,那么就证明对方存活,可以继续保有连接;否则就证明网络存在故障。
上面只是在原理层面简单的介绍,根据文献[1],我们可以了解到详细的内容:
HTTP和HTTPS的区别
Http协议运行在TCP之上,明文传输,客户端与服务器端都无法验证对方的身份;Https是身披SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP之上,是添加了加密和认证机制的HTTP。二者之间存在如下不同:
1、端口不同:Http与Https使用不同的连接方式,用的端口也不一样,前者是80,后者是443;
2、资源消耗:和HTTP通信相比,Https通信会由于加减密处理消耗更多的CPU和内存资源;
3、开销:Https通信需要证书,而证书一般需要向认证机构购买;
4、安全性:HTTP 的连接很简单,是无状态的;HTTPS 协议是由 TLS+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全
Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。
websocket应用场景有哪些?
HTTP请求方法有多少
客户端发送的 请求报文 第一行为请求行,包含了方法字段。
根据 HTTP 标准,HTTP 请求可以使用多种请求方法。
HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD方法。
HTTP1.1 新增六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。
序 号 | 方法 | 描述 |
---|---|---|
1 | GET | 请求指定的页面信息,并返回实体主体。 |
2 | HEAD | 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头 |
3 | POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。 |
4 | PUT | 从客户端向服务器传送的数据取代指定的文档的内容。 |
5 | DELETE | 请求服务器删除指定的页面。 |
6 | CONNECT | HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 |
7 | OPTIONS | 允许客户端查看服务器的性能。 |
8 | TRACE | 回显服务器收到的请求,主要用于测试或诊断。 |
9 | PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新 。 |
TCP三次握手和四次挥手的过程
三次握手过程
closed(关闭)
状态,服务器处于 listen(监听)
状态。SYN = 1
同步序列号和初始化序列号seq = x
发送给服务端,发送完之后客户端处于SYN_Send
状态。(验证了客户端的发送能力和服务端的接收能力)SYN
请求报文之后,如果同意连接,会以自己的同步序列号SYN(服务端) = 1
、初始化序列号 seq = y
和确认序列号(期望下次收到的数据包)ack = x+ 1
以及确认号ACK = 1
报文作为应答,服务器为SYN_Receive
状态。(问题来了,两次握手之后,站在客户端角度上思考:我发送和接收都ok,服务端的发送和接收也都ok。但是站在服务端的角度思考:哎呀,我服务端接收ok,但是我不清楚我的发送ok不ok呀,而且我还不知道你接受能力如何呢?所以老哥,你需要给我三次握手来传个话告诉我一声。你要是不告诉我,万一我认为你跑了,然后我可能出于安全性的考虑继续给你发一次,看看你回不回我。)SYN + ACK
之后,知道可以下次可以发送了下一序列的数据包了,然后发送同步序列号 ack = y + 1
和数据包的序列号 seq = x + 1
以及确认号ACK = 1
确认包作为应答,客户端转为established
状态。(分别站在双方的角度上思考,各自ok)为什么需要三次握手,两次不行吗
弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。
三个方面分析三次握手的原因:
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
四次挥手过程
假如客户端发送的数据已经发送完毕,发送FIN = 1 告诉服务端,客户端所有数据已经全发完了,服务端你可以关闭接收了,但是如果你们服务端有数据要发给客户端,客户端照样可以接收的。此时客户端处于FIN = 1等待服务端确认释放连接状态。
为什么需要挥手四次
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
TCP协议介绍
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
特点
TCP三次握手、四次挥手丢失,会发生什么
三次握手丢失
第一次握手丢失了,会发生什么?
如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。每次超时的时间是上一次的 2 倍。
第二次握手丢失了,会发生什么?
因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
因此,当第二次握手丢失了,客户端和服务端都会重传
第三次握手丢失了,会发生什么?
客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH
状态。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
挥手四次丢失
第一次挥手丢失了,会发生什么?
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries
参数控制。
当客户端重传 FIN 报文的次数超过 tcp_orphan_retries
后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close
状态。
第二次挥手丢失了,会发生什么?
当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。
在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
第三次挥手丢失了,会发生什么?
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT
状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retrie
s 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
第四次挥手丢失了,会发生什么?
当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT
状态。
在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制。
2MSL等待状态
2MSL等待状态
TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能被使用。这个连接只能在2MSL结束后才能再被使用。
为什么TIME_WAIT状态需要经过2MSL才能返回到CLOSE状态
理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
客户端给服务端发送的ACK = 1丢失,服务端等待 1MSL没收到,然后重新发送消息需要1MSL。如果再次接收到服务端的消息,则重启2MSL计时器,发送确认请求。客户端只需等待2MSL,如果没有再次收到服务端的消息,就说明服务端已经接收到自己确认消息;此时双方都关闭的连接,TCP 四次分手完毕
TIME_WAIT介绍
MSL
是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL
字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
为什么需要 TIME_WAIT 状态?
主动发起关闭连接的一方,才会有 TIME-WAIT
状态。
需要 TIME-WAIT 状态,主要是两个原因:
TIME_WAIT 过多有什么危害?
过多的 TIME-WAIT 状态主要的危害有两种:
32768~61000
,也可以通过 net.ipv4.ip_local_port_range
参数指定范围。如何优化 TIME_WAIT?
这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:
什么是半连接队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。 注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…
常见TCP的连接状态有哪些
TCP头部中有哪些信息
RTO,RTT和超时重传分别是什么
TCP重传
TCP 实现可靠传输的方式之一,是通过序列号与确认应答。
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。
常见的重传机制:
超时重传
重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据,也就是我们常说的超时重传。
TCP 会在以下两种情况发生超时重传:
RTT
指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。
超时重传时间是以 RTO
(Retransmission Timeout 超时重传时间)表示。
根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。
快速重传
TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。
快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。
SACK 方法
还有一种实现重传机制的方式叫:SACK
( Selective Acknowledgment), 选择性确认。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
Duplicate SACK
Duplicate SACK 又称 D-SACK
,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
D-SACK
有这么几个好处:
在 Linux 下可以通过 net.ipv4.tcp_dsack
参数开启/关闭这个功能(Linux 2.4 后默认打开)。
TCP滑动窗口
TCP 利用滑动窗口实现流量控制的机制。滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,就有了滑动窗口机制来解决此问题。
TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。
TCP流量控制
TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
TCP拥塞控制
拥塞控制目的是防止数据过多注入到网络中导致网络资源(路由器、交换机等)过载。因为拥塞控制涉及网络链路全局,所以属于全局控制。控制拥塞使用拥塞窗口。
拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。
慢热启动算法 – Slow Start
所谓慢启动,也就是TCP连接刚建立,一点一点地提速,试探一下网络的承受能力,以免直接扰乱了网络通道的秩序。
慢启动算法:
拥塞避免算法 – Congestion Avoidance
如同前边说的,当拥塞窗口大小cwnd大于等于慢启动阈值ssthresh后,就进入拥塞避免算法。算法如下:
过了慢启动阈值后,拥塞避免算法可以避免窗口增长过快导致窗口拥塞,而是缓慢的增加调整到网络的最佳值。
拥塞发生状态时的算法
一般来说,TCP拥塞控制默认认为网络丢包是由于网络拥塞导致的,所以一般的TCP拥塞控制算法以丢包为网络进入拥塞状态的信号。对于丢包有两种判定方式,一种是超时重传RTO[Retransmission Timeout]超时,另一个是收到三个重复确认ACK。
超时重传是TCP协议保证数据可靠性的一个重要机制,其原理是在发送一个数据以后就开启一个计时器,在一定时间内如果没有得到发送数据报的ACK报文,那么就重新发送数据,直到发送成功为止。
但是如果发送端接收到3个以上的重复ACK,TCP就意识到数据发生丢失,需要重传。这个机制不需要等到重传定时器超时,所以叫 做快速重传,而快速重传后没有使用慢启动算法,而是拥塞避免算法,所以这又叫做快速恢复算法。
超时重传RTO[Retransmission Timeout]超时,TCP会重传数据包。TCP认为这种情况比较糟糕,反应也比较强烈:
最为早期的TCP Tahoe算法就只使用上述处理办法,但是由于一丢包就一切重来,导致cwnd又重置为1,十分不利于网络数据的稳定传递。
所以,TCP Reno算法进行了优化。当收到三个重复确认ACK时,TCP开启快速重传Fast Retransmit算法,而不用等到RTO超时再进行重传:
快速恢复算法 – Fast Recovery
TCP Tahoe是早期的算法,所以没有快速恢复算法,而Reno算法有。在进入快速恢复之前,cwnd和ssthresh已经被更改为原有cwnd的一半。快速恢复算法的逻辑如下:
流量控制和拥塞控制的区别
TCP 协议如何保证可靠传输
如何优化TCP
从三个角度来阐述提升 TCP 的策略,分别是:
TCP 三次握手的性能提升
客户端的优化
当客户端发起 SYN 包时,可以通过 tcp_syn_retries
控制其重传的次数。
服务端的优化
当服务端 SYN 半连接队列溢出后,会导致后续连接被丢弃,可以通过 netstat -s
观察半连接队列溢出的情况,如果 SYN 半连接队列溢出情况比较严重,可以通过 tcp_max_syn_backlog、somaxconn、backlog
参数来调整 SYN 半连接队列的大小。
服务端回复 SYN+ACK 的重传次数由 tcp_synack_retries
参数控制。如果遭受 SYN 攻击,应把 tcp_syncookies
参数设置为 1,表示仅在 SYN 队列满后开启 syncookie 功能,可以保证正常的连接成功建立。
服务端收到客户端返回的 ACK,会把连接移入 accpet 队列,等待进行调用 accpet() 函数取出连接。
可以通过 ss -lnt
查看服务端进程的 accept 队列长度,如果 accept 队列溢出,系统默认丢弃 ACK,如果可以把 tcp_abort_on_overflow
设置为 1 ,表示用 RST 通知客户端连接建立失败。
如果 accpet 队列溢出严重,可以通过 listen 函数的 backlog
参数和 somaxconn
系统参数提高队列大小,accept 队列长度取决于 min(backlog, somaxconn)。
绕过三次握手
TCP Fast Open 功能可以绕过三次握手,使得 HTTP 请求减少了 1 个 RTT 的时间,Linux 下可以通过 tcp_fastopen
开启该功能,同时必须保证服务端和客户端同时支持。
TCP 四次挥手的性能提升
客户端和服务端双方都可以主动断开连接,通常先关闭连接的一方称为主动方,后关闭连接的一方称为被动方。
针对 TCP 四次挥手的优化,我们需要根据主动方和被动方四次挥手状态变化来调整系统 TCP 内核参数。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vIOGjqUw-1678081583394)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%82%E6%95%B0/39.jpg)]
主动方的优化
主动发起 FIN 报文断开连接的一方,如果迟迟没收到对方的 ACK 回复,则会重传 FIN 报文,重传的次数由 tcp_orphan_retries
参数决定。
当主动方收到 ACK 报文后,连接就进入 FIN_WAIT2 状态,根据关闭的方式不同,优化的方式也不同:
tcp_fin_timeout
秒内没有收到对方的 FIN 报文,连接就直接关闭。同时,为了应对孤儿连接占用太多的资源,tcp_max_orphans
定义了最大孤儿连接的数量,超过时连接就会直接释放。当主动方接收到 FIN 报文,并返回 ACK 后,主动方的连接进入 TIME_WAIT 状态。这一状态会持续 1 分钟,为了防止 TIME_WAIT 状态占用太多的资源,tcp_max_tw_buckets
定义了最大数量,超过时连接也会直接释放。
当 TIME_WAIT 状态过多时,还可以通过设置 tcp_tw_reuse
和 tcp_timestamps
为 1 ,将 TIME_WAIT 状态的端口复用于作为客户端的新连接,注意该参数只适用于客户端。
被动方的优化
被动关闭的连接方应对非常简单,它在回复 ACK 后就进入了 CLOSE_WAIT 状态,等待进程调用 close 函数关闭连接。因此,出现大量 CLOSE_WAIT 状态的连接时,应当从应用程序中找问题。
当被动方发送 FIN 报文后,连接就进入 LAST_ACK 状态,在未等到 ACK 时,会在 tcp_orphan_retries
参数的控制下重发 FIN 报文。
TCP 数据传输的性能提升
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvueg1Zy-1678081583395)(https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/TCP-%E5%8F%82%E6%95%B0/49.jpg)]
TCP 可靠性是通过 ACK 确认报文实现的,又依赖滑动窗口提升了发送速度也兼顾了接收方的处理能力。
可是,默认的滑动窗口最大值只有 64 KB,不满足当今的高速网络的要求,要想提升发送速度必须提升滑动窗口的上限,在 Linux 下是通过设置 tcp_window_scaling
为 1 做到的,此时最大值可高达 1GB。
滑动窗口定义了网络中飞行报文的最大字节数,当它超过带宽时延积时,网络过载,就会发生丢包。而当它小于带宽时延积时,就无法充分利用网络带宽。因此,滑动窗口的设置,必须参考带宽时延积。
内核缓冲区决定了滑动窗口的上限,缓冲区可分为:发送缓冲区 tcp_wmem 和接收缓冲区 tcp_rmem。
Linux 会对缓冲区动态调节,我们应该把缓冲区的上限设置为带宽时延积。发送缓冲区的调节功能是自动打开的,而接收缓冲区需要把 tcp_moderate_rcvbuf 设置为 1 来开启。其中,调节的依据是 TCP 内存范围 tcp_mem。
但需要注意的是,如果程序中的 socket 设置 SO_SNDBUF 和 SO_RCVBUF,则会关闭缓冲区的动态整功能,所以不建议在程序设置它俩,而是交给内核自动调整比较好。
UDP协议
提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
特点
(1)UDP是无连接的传输层协议;
(2)UDP使用尽最大努力交付,不保证可靠交付;
(3)UDP是面向报文的,对应用层交下来的报文,不合并,不拆分,保留原报文的边界;
(4)UDP没有拥塞控制,因此即使网络出现拥塞也不会降低发送速率;
(5)UDP支持一对一 一对多 多对多的交互通信;
(6)UDP的首部开销小,只有8字节.
UDP 如何实现可靠传输
UDP不属于连接协议,具有资源消耗少,处理速度快的优点,所以通常音频,视频和普通数据在传送时,使用UDP较多,因为即使丢失少量的包,也不会对接受结果产生较大的影响。
传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
最简单的方式是在应用层模仿传输层TCP的可靠性传输。下面不考虑拥塞处理,可靠UDP的简单设计。
详细说明:送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。
目前有如下开源程序利用udp实现了可靠的数据传输。分别为*RUDP、RTP、UDT*。
TCP和UDP的区别
(1)TCP是可靠传输,UDP是不可靠传输;
(2)TCP面向连接,UDP无连接;
(3)TCP传输数据有序,UDP不保证数据的有序性;
(4)TCP不保存数据边界,UDP保留数据边界;
(5)TCP传输速度相对UDP较慢;
(6)TCP有流量控制和拥塞控制,UDP没有;
(7)TCP是重量级协议,UDP是轻量级协议;
(8)TCP首部较长20字节,UDP首部较短8字节;
TCP应用场景:
效率要求相对低,但对准确性要求相对高的场景。因为传输中需要对数据确认、重发、排序等操作,相比之下效率没有UDP高。举几个例子:文件传输(准确高要求高、但是速度可以相对慢)、接受邮件、远程登录。
UDP应用场景:
效率要求相对高,对准确性要求相对低的场景。举几个例子:QQ聊天、在线视频、网络语音电话(即时通讯,速度要求高,但是出现偶尔断续不是太大问题,并且此处完全不可以使用重发机制)、广播通信(广播、多播)
基于TCP和UDP的常用协议
HTTP、HTTPS、FTP、TELNET、SMTP(简单邮件传输协议)协议基于可靠的TCP协议。DNS、DHCP、TFTP、SNMP(简单网络管理协议)、RIP基于不可靠的UDP协议
TCP黏包和拆包
TCP黏包
TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
解决:
URI和 URL之间的区别
URL,即统一资源定位符 (Uniform Resource Locator ),URL 其实就是我们平时上网时输入的网址,它标识一个互联网资源,并指定对其进行操作或获取该资源的方法。例如 https://leetcode-cn.com/problemset/all/ 这个 URL,标识一个特定资源并表示该资源的某种形式是可以通过 HTTP 协议从相应位置获得。
而 URI 则是统一资源标识符,URL 是 URI 的一个子集,两者都定义了资源是什么,而 URL 还定义了如何能访问到该资源。URI 是一种语义上的抽象概念,可以是绝对的,也可以是相对的,而URL则必须提供足够的信息来定位,是绝对的。简单地说,只要能唯一标识资源的就是 URI,在 URI 的基础上给出其资源的访问方式的就是 URL。
IP协议的作用
网络层的主要作用是:实现主机与主机之间的通信,也叫点对点(end to end)通信。
IP协议的作用主要是在相互连接的网络之间传递IP数据包,主要功能有两方面,分别是寻址与路由和分段与重组,而在IP协议当中,最重要的就是TTL(IP允许通过的最大网段数量)字段(八位),规定该数据包能穿过几个路由之后才会被抛弃。
IPV4和IPV6
IPv4 的地址是 32 位的,大约可以提供 42 亿个地址,但是早在 2011 年 IPv4 地址就已经被分配完了。
IPv6 不仅仅只是可分配的地址变多了,它还有非常多的亮点。
40
字节,去掉了包头校验和,简化了首部结构,减轻了路由器负荷,大大提高了传输的性能。IPv6 地址的结构
IPv6 类似 IPv4,也是通过 IP 地址的前几位标识 IP 地址的种类。
IPv6 的地址主要有以下类型地址:
IPv6 单播地址类型
对于一对一通信的 IPv6 地址,主要划分了三类单播地址,每类地址的有效范围都不同。
IPv6 相比 IPv4 的首部改进:
40
字节。IPV4 地址不够如何解决
目前主要有以下两种方式:
1、其实我们平时上网,电脑的 IP 地址都是属于私有地址,我无法出网关,我们的数据都是通过网关来中转的,这个其实 NAT 协议,可以用来暂缓 IPV4 地址不够。
2、IPv6 :作为接替 IPv4 的下一代互联网协议,其可以实现 2 的 128 次方个地址,这个数量级,即使给地球上每一颗沙子都分配一个IP地址,该协议能从根本上解决 IPv4 地址不够用的问题。
IP地址和MAC地址有什么区别?各自的用处?
简单着说,IP 地址主要用来网络寻址用的,就是大致定位你在哪里,而 MAC 地址,则是身份的唯一象征,通过 MAC 来唯一确认这人是不是就是你,MAC 地址不具备寻址的功能。
网络层常见协议
议 | 名称 | 作用 |
---|---|---|
IP | 网际协议 | IP协议不但定义了数据传输时的基本单元和格式,还定义了数据报的递交方法和路由选择 |
ICMP | Internet控制报文协议 | ICMP就是一个“错误侦测与回报机制”,其目的就是让我们能够检测网路的连线状况﹐也能确保连线的准确性,是ping和traceroute的工作协议 |
RIP | 路由信息协议 | 使用“跳数”(即metric)来衡量到达目标地址的路由距离 |
IGMP | Internet组管理协议 | 用于实现组播、广播等通信 |
什么是DNS和工作原理
DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。
通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。
工作原理
将主机域名转换为ip地址,属于应用层协议,使用UDP传输。
过程: 总结: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。 一、主机向本地域名服务器的查询一般都是采用递归查询。 二、本地域名服务器向根域名服务器的查询的迭代查询。
DNS负载均衡是什么策略
当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会崩掉。处理办法就是用DNS负载均衡技术,它的原理是在DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器,从而达到负载均衡的目的。例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等。
数据链路层常见协议
议 | 名称 | 作用 |
---|---|---|
ARP | 地址解析协议 | 根据IP地址获取物理地址 |
RARP | 反向地址转换协议 | 根据物理地址获取IP地址 |
PPP | 点对点协议 | 主要是用来通过拨号或专线方式建立点对点连接发送数据,使其成为各种主机、网桥和路由器之间简单连接的一种共通的解决方案 |
ARP和RARP协议
RARP
概括: 反向地址转换协议,网络层协议,RARP与ARP工作方式相反。 RARP使只知道自己硬件地址的主机能够知道其IP地址。RARP发出要反向解释的物理地址并希望返回其IP地址,应答包括能够提供所需信息的RARP服务器发出的IP地址。
原理: (1)网络上的每台设备都会有一个独一无二的硬件地址,通常是由设备厂商分配的MAC地址。主机从网卡上读取MAC地址,然后在网络上发送一个RARP请求的广播数据包,请求RARP服务器回复该主机的IP地址。
(2)RARP服务器收到了RARP请求数据包,为其分配IP地址,并将RARP回应发送给主机。
(3)PC1收到RARP回应后,就使用得到的IP地址进行通讯。
ARP
网络层的 ARP 协议完成了 IP 地址与物理地址的映射。首先,每台主机都会在自己的 ARP 缓冲区中建立一个 ARP 列表,以表示 IP 地址和 MAC 地址的对应关系。当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP 列表中是否存在该 IP 地址对应的 MAC 地址:如果有,就直接将数据包发送到这个 MAC 地址;如果没有,就向本地网段发起一个 ARP 请求的广播包,查询此目的主机对应的 MAC 地址。
此 ARP 请求数据包里包括源主机的 IP 地址、硬件地址、以及目的主机的 IP 地址。网络中所有的主机收到这个 ARP 请求后,会检查数据包中的目的 IP 是否和自己的 IP 地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的 MAC 地址和 IP 地址添加到自己的 ARP 列表中,如果 ARP 表中已经存在该 IP 的信息,则将其覆盖,然后给源主机发送一个 ARP 响应数据包,告诉对方自己是它需要查找的 MAC 地址;源主机收到这个 ARP 响应数据包后,将得到的目的主机的 IP 地址和 MAC 地址添加到自己的 ARP 列表中,并利用此信息开始数据的传输。如果源主机一直没有收到 ARP 响应数据包,表示 ARP 查询失败
DHCP/NAT协议
DHCP
DHCP 在生活中我们是很常见的了,我们的电脑通常都是通过 DHCP 动态获取 IP 地址,大大省去了配 IP 信息繁琐的过程。
DHCP 客户端进程监听的是 68 端口号,DHCP 服务端进程监听的是 67 端口号。
这 4 个步骤:
一旦客户端收到 DHCP ACK 后,交互便完成了,并且客户端能够在租用期内使用 DHCP 服务器分配的 IP 地址。
DHCP 交互中,全程都是使用 UDP 广播通信。
NAT
网络地址转换 NAT 的,缓解了 IPv4 地址耗尽的问题。简单的来说 NAT 就是同个公司、家庭、教室内的主机对外部通信时,把私有 IP 地址转换成公有 IP 地址。
由于 NAT/NAPT 都依赖于自己的转换表,因此会有以下的问题:
如何解决 NAT 潜在的问题呢?
解决的方法主要有两种方法。
第一种就是改用 IPv6、第二种 NAT 穿透技术
ICMP/IGMP协议
ICMP
ICMP 全称是 Internet Control Message Protocol,也就是互联网控制报文协议。
ICMP
主要的功能包括:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。
在 IP
通信中如果某个 IP
包因为某种原因未能达到目标地址,那么这个具体的原因将由 ICMP 负责通知。
ICMP 大致可以分为两大类:
IGMP
我们知道了组播地址,也就是 D 类地址,既然是组播,那就说明是只有一组的主机能收到数据包,不在一组的主机不能收到数组包,怎么管理是否是在一组呢?那么,就需要 IGMP
协议了。
IGMP 是因特网组管理协议,工作在主机(组播成员)和最后一跳路由之间
常规查询与响应工作机制
224.0.0.1
(表示同一网段内所有主机和路由器) IGMP 常规查询报文。离开组播组工作机制
离开组播组的情况一,网段中仍有该组播组:
离开组播组的情况一,网段中仍有该组播组:
ICMP 有哪些应用
ICMP 主要有两个应用,一个是 Ping,一个是 Traceroute。
1. Ping
Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。
2. Traceroute
Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。
Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。
应用层常见协议知道多少
协议 | 名称 | 默认端口 | 底层协议 |
---|---|---|---|
HTTP | 超文本传输协议 | 80 | TCP |
HTTPS | 超文本传输安全协议 | 443 | TCP |
Telnet | 远程登录服务的标准协议 | 23 | TCP |
FTP | 文件传输协议 | 20传输和21连接 | TCP |
TFTP | 简单文件传输协议 | 69 | UDP |
SMTP | 简单邮件传输协议(发送用) | 25 | TCP |
POP | 邮局协议(接收用) | 110 | TCP |
DNS | 域名解析服务 | 53 | 服务器间进行域传输的时候用TCP 客户端查询DNS服务器时用 UDP |
常见的HTTP状态码有哪些
状态码 | 类别 | 含义 |
---|---|---|
1XX | Informational(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出 |
1xx 信息
100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
2xx 成功
3xx 重定向
4xx 客户端错误
5xx 服务器错误
OSI的七层模型和功能
简要概括
说明
总结
网络五层模型,每一层的职责
一台计算机与另一台计算机要进行通信,第一件要做的事是什么?当然是要把这台计算机与另外的其他计算机连起来啊,这样,我们才能把数据传输过去。例如可以通过光纤啊,电缆啊,双绞线啊等介质把他们连接起来,然后才能进行通信。
物理层负责把两台计算机连起来,然后在计算机之间通过高低电频来传送0,1这样的电信号。
物理层它只是单纯着负责把计算机连接起来,并且在计算机之间传输0,1这样的电信号。如果这些0,1组合的传送毫无规则的话,计算机是解读不了的。
因此,我们需要制定一套规则来进行0,1的传送。例如多少个电信号为一组啊,每一组信号应该如何标识才能让计算机读懂啊等等。
于是,有了以太网协议。
1. 以太网协议
以太网协议规定,一组电信号构成一个数据包,我们把这个数据包称之为帧。每一个桢由标头(Head)和数据(Data)两部分组成。
帧的大小一般为 64 – 1518 个字节。假如需要传送的数据很大的话,就分成多个桢来进行传送。
对于表头和数据这两个部分,他们存放的都是一些什么数据呢?我猜你眯着眼睛都能想到他们应该放什么数据。 毫无疑问,我们至少得知道这个桢是谁发送,发送给谁的等这些信息吧?所以标头部分主要是一些说明数据,例如发送者,接收者等信息。而数据部分则是这个数据包具体的,想给接守者的内容。
把一台计算的的数据通过物理层和链路层发送给另一台计算机,究竟是谁发给谁的,计算机与计算机之间如何区分,,你总得给他们一个唯一的标识吧?
于是,MAC 地址出现了。
2. MAC 地址
连入网络的每一个计算机都会有网卡接口,每一个网卡都会有一个唯一的地址,这个地址就叫做 MAC 地址。计算机之间的数据传送,就是通过 MAC 地址来唯一寻找、传送的。
MAC地址 由 48 个二进制位所构成,在网卡生产时就被唯一标识了。
3. 广播与ARP协议
(1). 广播
如图,假如计算机 A 知道了计算机 B 的 MAC 地址,然后计算机 A 想要给计算机 B 传送数据,虽然计算机 A 知道了计算机 B 的 MAC 地址,可是它要怎么给它传送数据呢?计算机 A 不仅连着计算机 B,而且计算机 A 也还连着其他的计算机。 虽然计算机 A 知道计算机 B 的 MAC 地址,可是计算机 A 却不知道知道计算机 B 是分布在哪边路线上,为了解决这个问题,于是,有了广播的出现。
在同一个子网中,计算机 A 要向计算机 B 发送一个数据包,这个数据包会包含接收者的 MAC 地址。当发送时,计算机 A 是通过广播的方式发送的,这时同一个子网中的计算机 C, D 也会收到这个数据包的,然后收到这个数据包的计算机,会把数据包的 MAC 地址取出来,与自身的 MAC 地址对比,如果两者相同,则接受这个数据包,否则就丢弃这个数据包。这种发送方式我们称之为广播,就像我们平时在广场上通过广播的形式呼叫某个人一样,如果这个名字是你,你就理会一下,如果不是你,你就当作听不见。
(2). ARP 协议。
那么问题来了,计算机 A 是如何知道计算机 B 的 MAC 地址的呢?这个时候就得由 ARP 协议这个家伙来解决了,不过 ARP 协议会涉及到IP地址,我们下面才会扯到IP地址。因此我们先放着,就当作是有这么一个 ARP 协议,通过它我们可以知道子网中其他计算机的 MAC 地址。
3. 网络层
上面我们有说到子网这个关键词,实际上我们所处的网络,是由无数个子网络构成的。广播的时候,也只有同一个子网里面的计算机能够收到。
假如没有子网这种划分的话,计算机 A 通过广播的方式发一个数据包给计算机 B , 其他所有计算机也都能收到这个数据包,然后进行对比再舍弃。世界上有那么多它计算机,每一台计算机都能收到其他所有计算机的数据包,那就不得了了。那还不得奔溃。 因此产生了子网这么一个东西。
那么问题来了,我们如何区分哪些 MAC 地址是属于同一个子网的呢?假如是同一个子网,那我们就用广播的形式把数据传送给对方,如果不是同一个子网的,我们就会把数据发给网关,让网关进行转发。
为了解决这个问题,于是,有了 IP 协议。
1. IP协议
IP协议,它所定义的地址,我们称之为IP地址。IP协议有两种版本,一种是 IPv4,另一种是 IPv6。不过我们目前大多数用的还是 IPv4,我们现在也只讨论 IPv4 这个版本的协议。
这个 IP 地址由 32 位的二进制数组成,我们一般把它分成4段的十进制表示,地址范围为0.0.0.0~255.255.255.255。
每一台想要联网的计算机都会有一个IP地址。这个IP地址被分为两部分,前面一部分代表网络部分,后面一部分代表主机部分。并且网络部分和主机部分所占用的二进制位数是不固定的。
可是问题来了,你怎么知道网络部分是占几位,主机部分又是占几位呢?也就是说,单单从两台计算机的IP地址,我们是无法判断他们的是否处于同一个子网中的。
这就引申出了另一个关键词————子网掩码。子网掩码和IP地址一样也是 32 位二进制数,不过它的网络部分规定全部为 1,主机部分规定全部为 0.也就是说,假如上面那两个IP地址的网络部分为 24 位,主机部分为 8 位的话,那他们的子网掩码都为 11111111.11111111.11111111.00000000,即255.255.255.0。
那有了子网掩码,如何来判端IP地址是否处于同一个子网中呢。显然,知道了子网掩码,相当于我们知道了网络部分是几位,主机部分是几位。我们只需要把 IP 地址与它的子网掩码做与(and)运算,然后把各自的结果进行比较就行了,如果比较的结果相同,则代表是同一个子网,否则不是同一个子网。
2. ARP协议
有了上面IP协议的知识,我们回来讲一下ARP协议。
有了两台计算机的IP地址与子网掩码,我们就可以判断出它们是否处于同一个子网之中了。
假如他们处于同一个子网之中,计算机A要给计算机B发送数据时。我们可以通过ARP协议来得到计算机B的MAC地址。
ARP协议也是通过广播的形式给同一个子网中的每台电脑发送一个数据包(当然,这个数据包会包含接收方的IP地址)。对方收到这个数据包之后,会取出IP地址与自身的对比,如果相同,则把自己的MAC地址回复给对方,否则就丢弃这个数据包。这样,计算机A就能知道计算机B的MAC地址了。
可能有人会问,知道了MAC地址之后,发送数据是通过广播的形式发送,询问对方的MAC地址也是通过广播的形式来发送,那其他计算机怎么知道你是要传送数据还是要询问MAC地址呢?其实在询问MAC地址的数据包中,在对方的MAC地址这一栏中,填的是一个特殊的MAC地址,其他计算机看到这个特殊的MAC地址之后,就能知道广播想干嘛了。
假如两台计算机的IP不是处于同一个子网之中,这个时候,我们就会把数据包发送给网关,然后让网关让我们进行转发传送
3. DNS服务器
这里再说一个问题,我们是如何知道对方计算机的IP地址的呢?这个问题可能有人会觉得很白痴,心想,当然是计算机的操作者来进行输入了。这没错,当我们想要访问某个网站的时候,我们可以输入IP来进行访问,但是我相信绝大多数人是输入一个网址域名的,例如访问百度是输入 www.baidu.com 这个域名。其实当我们输入这个域名时,会有一个叫做DNS服务器的家伙来帮我们解析这个域名,然后返回这个域名对应的IP给我们的。
因此,网络层的功能就是让我们在茫茫人海中,能够找到另一台计算机在哪里,是否属于同一个子网等。
4. 传输层
通过物理层、数据链路层以及网络层的互相帮助,我们已经把数据成功从计算机A传送到计算机B了,可是,计算机B里面有各种各样的应用程序,计算机该如何知道这些数据是给谁的呢?
这个时候,**端口(Port)**这个家伙就上场了,也就是说,我们在从计算机A传数据给计算表B的时候,还得指定一个端口,以供特定的应用程序来接受处理。
传输层的功能就是建立端口到端口的通信。相比网络层的功能是建立主机到主机的通信。
也就是说,只有有了IP和端口,我们才能进行准确着通信。这个时候可能有人会说,我输入IP地址的时候并没有指定一个端口啊。其实呢,对于有些传输协议,已经有设定了一些默认端口了。例如http的传输默认端口是80,这些端口信息也会包含在数据包里的。
传输层最常见的两大协议是 TCP 协议和 UDP 协议,其中 TCP 协议与 UDP 最大的不同就是 TCP 提供可靠的传输,而 UDP 提供的是不可靠传输。
5. 应用层
虽然我们收到了传输层传来的数据,可是这些传过来的数据五花八门,有html格式的,有mp4格式的,各种各样。你确定你能看的懂?
因此我们需要指定这些数据的格式规则,收到后才好解读渲染。例如我们最常见的 Http 数据包中,就会指定该数据包是 什么格式的文件了。
一次完整的HTTP请求过程包括哪些内容
第一种回答
第二种回答
域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户。
谈谈你对停止等待协议的理解
停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组;在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。主要包括以下几种情况:无差错情况、出现差错情况(超时重传)、确认丢失和确认迟到。
谈谈你对 ARQ 协议的理解
自动重传请求 ARQ 协议
停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求 ARQ。
连续 ARQ 协议
连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。
Cookie是什么与用途
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销.。
Session知识总结
Session 存储在服务器端,存储在服务器端的信息更加安全。
Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。
使用 Session 维护用户登录状态的过程如下:
工作原理
session 的工作原理是客户端登录完成之后,服务器会创建对应的 session,session 创建完之后,会把 session 的 id 发送给客户端,客户端再存储到浏览器中。这样客户端每次访问服务器时,都会带着 sessionid,服务器拿到 sessionid 之后,在内存找到与之对应的 session 这样就可以正常工作了。
Cookie与Session的对比
Cookie和Session都是客户端与服务器之间保持状态的解决方案 1,存储的位置不同,cookie:存放在客户端,session:存放在服务端。Session存储的数据比较安全 2,存储的数据类型不同 两者都是key-value的结构,但针对value的类型是有差异的 cookie:value只能是字符串类型,session:value是Object类型 3,存储的数据大小限制不同 cookie:大小受浏览器的限制,很多是是4K的大小, session:理论上受当前内存的限制, 4,生命周期的控制 cookie的生命周期当浏览器关闭的时候,就消亡了 (1)cookie的生命周期是累计的,从创建时,就开始计时,20分钟后,cookie生命周期结束, (2)session的生命周期是间隔的,从创建时,开始计时如在20分钟,没有访问session,那么session生命周期被销毁
使用 Session 的过程是怎样的
过程如下:
注意:Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
保活计时器的作用
除时间等待计时器外,TCP 还有一个保活计时器(keepalive timer)。设想这样的场景:客户已主动与服务器建立了 TCP 连接。但后来客户端的主机突然发生故障。显然,服务器以后就不能再收到客户端发来的数据。应当有措施使服务器不要再白白等待下去。这就需要使用保活计时器了。
服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两个小时。若两个小时都没有收到客户端的数据,服务端就发送一个探测报文段,以后则每隔 75 秒钟发送一次。若连续发送 10个 探测报文段后仍然无客户端的响应,服务端就认为客户端出了故障,接着就关闭这个连接。
DDos攻击、Sql注入、SYN攻击、XSS攻击、CSRF攻击
DDos攻击
客户端向服务端发送请求链接数据包,服务端向客户端发送确认数据包,客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认 没有彻底根治的办法,除非不使用TCP DDos 预防: 1)限制同时打开SYN半链接的数目 2)缩短SYN半链接的Time out 时间 3)关闭不必要的服务
SQL注入攻击
攻击者在HTTP请求中注入恶意的SQL代码,服务器使用参数构建数据库SQL命令时,恶意SQL被一起构造,并在数据库中执行。 用户登录,输入用户名 lianggzone,密码 ‘ or ‘1’=’1 ,如果此时使用参数构造的方式,就会出现 select * from user where name = ‘lianggzone’ and password = ‘’ or ‘1’=‘1’ 不管用户名和密码是什么内容,使查询出来的用户列表不为空。如何防范SQL注入攻击使用预编译的PrepareStatement是必须的,但是一般我们会从两个方面同时入手。 Web端 1)有效性检验。 2)限制字符串输入的长度。 服务端 1)不用拼接SQL字符串。 2)使用预编译的PrepareStatement。 3)有效性检验。(为什么服务端还要做有效性检验?第一准则,外部都是不可信的,防止攻击者绕过Web端请求) 4)过滤SQL需要的参数中的特殊字符。比如单引号、双引号。
SYN攻击
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。
常见的防御 SYN 攻击的方法有如下几种:
XSS攻击
跨站点脚本攻击,指攻击者通过篡改网页,嵌入恶意脚本程序,在用户浏览网页时,控制用户浏览器进行恶意操作的一种攻击方式。如何防范XSS攻击 1)前端,服务端,同时需要字符串输入的长度限制。 2)前端,服务端,同时需要对HTML转义处理。将其中的”<”,”>”等特殊字符进行转义编码。 防 XSS 的核心是必须对输入的数据做过滤处理。
CSRF攻击
跨站点请求伪造,指攻击者通过跨站请求,以合法的用户的身份进行非法操作。可以这么理解CSRF攻击:攻击者盗用你的身份,以你的名义向第三方网站发送恶意请求。CRSF能做的事情包括利用你的身份发邮件,发短信,进行交易转账,甚至盗取账号信息
如何防范CSRF攻击
安全框架,例如Spring Security。 token机制。在HTTP请求中进行token验证,如果请求中没有token或者token内容不正确,则认为CSRF攻击而拒绝该请求。 验证码。通常情况下,验证码能够很好的遏制CSRF攻击,但是很多情况下,出于用户体验考虑,验证码只能作为一种辅助手段,而不是最主要的解决方案。 referer识别。在HTTP Header中有一个字段Referer,它记录了HTTP请求的来源地址。如果Referer是其他网站,就有可能是CSRF攻击,则拒绝该请求。但是,服务器并非都能取到Referer。很多用户出于隐私保护的考虑,限制了Referer的发送。在某些情况下,浏览器也不会发送Referer,例如HTTPS跳转到HTTP。 1)验证请求来源地址; 2)关键操作添加验证码; 3)在请求地址添加 token 并验证。
服务器出现大量close_wait的连接的原因是什么?有什么解决方法
close_wait状态是在TCP四次挥手的时候收到FIN但是没有发送自己的FIN时出现的,服务器出现大量close_wait状态的原因有两种:
处理方法:
MTU和MSS分别是什么
MTU:maximum transmission unit,最大传输单元,由硬件规定,如以太网的MTU为1500字节。
MSS:maximum segment size,最大分节大小,为TCP数据包每次传输的最大数据分段大小,一般由发送端向对端TCP通知对端在每个分节中能发送的最大TCP数据。MSS值为MTU值减去IPv4 Header(20 Byte)和TCP header(20 Byte)得到。
Ping命令基于什么协议?原理是什么
ping是基于网络层的ICMP协议实现的。通过向对方发送一个ICMP回送请求报文,如果对方主机可达的话会收到该报文,并响应一个ICMP回送回答报文。
扩展:ICMP报文的介绍。ICMP报文分为两个种类:
进程和线程基础知识
进程:进程是系统进行资源分配和调度的一个独立单位,是系统中的并发执行的单位。
线程:线程是进程的一个实体,也是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,有时又被称为轻权进程或轻量级进程。
进程
运行中的程序,就被称为「进程」(Process)。
在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。
当然,进程还有另外两个基本状态:
挂起状态可以分为两种:
PCB 进程控制块 是进程存在的唯一标识
PCB 具体包含什么信息呢?
进程描述信息:
进程控制和管理信息:
资源分配清单:
CPU 相关信息:
线程
线程是进程当中的一条执行流程。
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程的实现
主要有三种线程的实现方式:
第一种关系是多对一的关系,也就是多个用户线程对应同一个内核线程
第二种是一对一的关系,也就是一个用户线程对应一个内核线程
第三种是多对多的关系,也就是多个用户线程对应到多个内核线程
用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
线程的优点:
线程的缺点:
内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责。
进程/线程上下文切换
进程
一个进程切换到另一个进程运行,称为进程的上下文切换。
进程的上下文切换到底是切换什么呢?
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行
发生进程上下文切换有哪些场景?
线程
线程上下文切换的是什么?
这还得看线程是不是属于同一个进程:
所以,线程的上下文切换相比进程,开销要小很多。
进程/线程间通信方式
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC 的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams 等。其中 Socket 和 Streams 支持不同主机上的两个进程 IPC。
管道
命名管道
消息队列
信号量
共享内存
Socket通信
前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。
以上,就是进程间通信的主要机制了。你可能会问了,那线程通信间的方式呢?
同个进程下的线程之间都是共享进程的资源,只要是共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:
线程、进程崩溃发生什么
一般来说如果线程是因为非法访问内存引起的崩溃,那么进程肯定会崩溃,为什么系统要让进程崩溃呢,这主要是因为在进程中,各个线程的地址空间是共享的,既然是共享,那么某个线程对地址的非法访问就会导致内存的不确定性,进而可能会影响到其他线程,这种操作是危险的,操作系统会认为这很可能导致一系列严重的后果,于是干脆让整个进程崩溃
崩溃机制
注意上面的第五步,如果进程没有注册自己的信号处理函数,那么操作系统会执行默认的信号处理程序(一般最后会让进程退出),但如果注册了,则会执行自己的信号处理函数,这样的话就给了进程一个垂死挣扎的机会,它收到 kill 信号后,可以调用 exit() 来退出,但也可以使用 sigsetjmp,siglongjmp 这两个函数来恢复进程的执行
守护进程、僵尸进程和孤儿进程
守护进程
指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等
创建守护进程要点:
(1)让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。
(2)调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。
(3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。
(4)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
(5)将当前目录更改为根目录。
(6)子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
(7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。
孤儿进程
如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程
如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。
设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。
如何避免僵尸进程
第一种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。
进程和线程的比较
线程是调度的基本单位,而进程则是资源拥有的基本单位。
线程与进程的比较如下:
对于,线程相比进程能减少开销,体现在:
所以,不管是时间效率,还是空间效率线程比进程都要高。
1、线程启动速度快,轻量级
2、线程的系统开销小
3、线程使用有一定难度,需要处理数据一致性问题
4、同一线程共享的有堆、全局变量、静态变量、指针,引用、文件等,而独自占有栈
物理地址、逻辑地址、虚拟内存的概念
虚拟内存有什么好处
内存管理
内存管理的概念就是操作系统对内存的划分和动态分配。
内存管理功能:
内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,是程序员摆脱存储分配的麻烦,提高编程效率。
地址转换:将逻辑地址转换成相应的物理地址。
内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充主存。
存储保护:保证各道作业在各自的存储空间内运行,互不干扰。
创建进程首先要将程序和数据装入内存。将用户源程序变为可在内存中执行的程序,通常需要以下几个步骤:
编译:由编译程序将用户源代码编译成若干目标模块(把高级语言翻译成机器语言)
链接:由链接程序将编译后形成的一组目标模块及所需的库函数连接在一起,形成一个完整的装入模块(由目标模块生成装入模块,链接后形成完整的逻辑地址)
装入:由装入程序将装入模块装入内存运行,装入后形成物理地址
程序的链接有以下三种方式:
静态链接:在程序运行之前,先将各目标模块及它们所需的库函数连接成一个完整的可执行文件(装入模块),之后不再拆开。
装入时动态链接:将各目标模块装入内存时,边装入边链接的链接方式。
运行时动态链接:在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享。
内存的装入模块在装入内存时,有以下三种方式:
重定位:根据内存的当前情况,将装入模块装入内存的适当位置,装入时对目标程序中的指令和数据的修改过程称为重定位。
静态重定位:地址的变换通常是在装入时一次完成的。一个作业装入内存时,必须给它分配要求的全部内存空间,若没有足够的内存,则不能装入该作业。此外,作业一旦装入内存,整个运行期间就不能在内存中移动,也不能再申请内存空间。
动态重定位:需要重定位寄存器的支持。可以将程序分配到不连续的存储区中;在程序运行之前可以只装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存。
内存分配前,需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响。内存保护可采取如下两种方法:
在CPU中设置一对上、下限寄存器,存放用户作业在主存中的上限和下限地址,每当CPU要访问一个地址时,分别和两个寄存器的值相比,判断有无越界。
采用重定位寄存器(或基址寄存器)和界地址寄存器(又称限长存储器)来实现这种保护。重定位寄存器包含最小的物理地址值,界地址寄存器含逻辑地址的最大值。每个逻辑地址值必须小于界地址寄存器;内存管理机构动态得将逻辑地址与界地址寄存器进行比较,若未发生地址越界,则加上重定位寄存器的值后映射成物理地址,再送交内存单元。
常见的内存分配方式
(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
常见内存分配内存错误
(1)内存分配未成功,却使用了它。
(2)内存分配虽然成功,但是尚未初始化就引用它。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
(4)忘记了释放内存,造成内存泄露。
(5)释放了内存却继续使用它。常见于以下有三种情况:
malloc如何分配内存
从操作系统层面上看,malloc是通过两个系统调用来实现的: brk和mmap
通常,分配的内存小于128k时,使用brk调用来获得虚拟内存,大于128k时就使用mmap来获得虚拟内存。
进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,这样内存分配才算完成。
如何避免预读失效和缓存污染
预读失效
这些被提前加载进来的页,并没有被访问,相当于这个预读工作是白做了,这个就是预读失效。
传统的 LRU 算法法无法避免下面这两个问题:
为了避免「预读失效」造成的影响,Linux 和 MySQL 对传统的 LRU 链表做了改进:
缓存污染
如果这些大量的数据在很长一段时间都不会被访问的话,那么整个活跃 LRU 链表(或者 young 区域)就被污染了。
为了避免「缓存污染」造成的影响,Linux 操作系统和 MySQL Innodb 存储引擎分别提高了升级为热点数据的门槛:
Linux 操作系统:在内存页被访问第二次的时候,才将页从 inactive list 升级到 active list 里。
MySQL Innodb:在内存页被访问
第二次
的时候,并不会马上将该页从 old 区域升级到 young 区域,因为还要进行
停留在 old 区域的时间判断:
通过提高了进入 active list (或者 young 区域)的门槛后,就很好了避免缓存污染带来的影响。
物理内存管理
操作系统物理内存管理主要包括程序装入、交换技术、连续分配管理方式和非连续分配管理方式(分页、分段、段分页)。
连续分配管理方式
连续内存分配
内存碎片
当给程序分配空间时,可能会出现一些无法被利用的空闲碎片空间。
1.外部碎片:分配单元之间无法被利用的内存碎片
2.内部碎片:分配给任务内存大小大于任务所需内存大小时,多出来的内存碎片。
分区的动态分配
连续内存分配情况:将应用程序从硬盘加载到内存中,给内存分配一块内存。应用程序运行访问数据,数据的连续内存空间。
内存分配算法:
具体问题具体分析适配算法。
首次适配:
定义:使用第一块内存大小大于需求大小的可用空闲块
实现方法:需要有一个按地址排序的空闲块列表。寻找第一个满足内存需求的空闲块。对于回收,要考虑空闲块与相邻空闲块合并问题。
优点:简单,易于产生更大的空闲块,想着地址空间结尾分配
缺点:产生外部碎片,具有不确定性
最优适配:
定义:寻找整个空闲块中,最满足分配请求的空闲块,内存差值最小。
实现方法:需要有一个按尺寸排序的空闲块列表,寻找最满足分配的内存块。对于回收,要考虑空闲块与相邻空闲块合并问题。
优点:避免分割大空闲块,最小化外部碎片的产生,简单
缺点:外部碎片很细,有很多微小碎片,不利于后续分配管理。
最差适配:
定义:找到差距最大的内存空闲块。
实现:需要有一个按尺寸排序的空闲块列表,寻找差距最大的内存块。对于回收,要考虑空闲块与相邻空闲块合并问题。
优点:避免产生微小外部碎片,分配效率高,适用于分配中大快
缺点:对于大块请求带来一定影响
减少内存碎片方法
紧致:压缩式碎片整理
调整运行程序的位置。
1.重定位的时机。不能在程序运行时进行,可以在程序等待时拷贝。
2.内存拷贝开销很大。
swaping:交换式碎片整理
把硬盘作为一个备份。把等待的程序包括数据(主存)挪到硬盘上。当硬盘上程序需要执行时再拷贝到主存上。
1.交换那个程序,减小开销
2.交换的时机
非连续内存分配
连续内存分配和非连续内存分配
连续内存分配缺点:1.分配给一个程序的物理内存是连续的,内存利用率较低,由外碎片内碎片的问题。
非连续内存分配优点:1.程序物理地址是非连续的 2.更好的内存利用和管理 3.允许共享代码与数据 4.支持动态加载和动态链接
非连续内存分配缺点:建立虚拟地址到物理地址的映射,软件开销太大,可以用硬件支持
-》硬件两种管理方案:分段和分页
分段
分段地址空间
对于一段程序内存可以分为:程序(主程序+子程序+共享库)+变量(栈、堆、共享数据段)
分段:更好的分离与共享,将逻辑地址空间分散到多个物理地址空间
逻辑地址是连续的,将具有不同功能的映射到物理空间中,这些段大小不一,位置不一
硬件实现分段寻址机制
一维逻辑地址有不同段组成,首先将逻辑地址分为两段:段寻址(段号)+段偏移寻址(addr)
通过段号在段表找到逻辑内存的段起始地址,看段起始地址是否满足段大小限制,不满足返回内存异常,满足将逻辑地址加偏移量是物理地址。
段表:
1.存储逻辑地址段段号到物理地址段号之间的映射关系
2.存储段大小,起始地址
段表的建立:操作系统在寻址前建立。
分页
分页地址空间
需要页号和页地址偏移。相比分段,分页页帧大小固定不变。
可以划分物理内存至固定大小的帧,将逻辑地址的页也划分至相同内存大小。大小是2的幂。
建立方案
页帧(Frame):物理内存被分割为大小相等的帧
一个内存物理地址是一个二元组(f,o)
物理地址 = 2^S*f+o
f是帧号(F位,2F个帧),o是帧内偏移(S位,每帧2S字节),
页(Page):逻辑地址空间分割为大小相等的页
页内偏移大小 = 帧内偏移大小(页帧大小和页大小一致)
页号大小和帧号大小可能不一致
一个逻辑地址是一个二元组(p,o)
逻辑地址 = 2^S*p+o
p:页号(P位,2P个页),o:页内偏移(S位,每页2S个字节)
页寻址机制
CPU寻址(逻辑地址),逻辑地址包含两部分(p,o),首先把p(页号)作为索引,再加上页表基址查页表(pagetable)中对应帧号(物理地址中f),知道帧号加上页内偏移就是物理地址(f,o)。
页表:以页号为索引的对应的帧号(物理地址),为此需要知道页表的基址位置(页号从哪个地址开始查)。
页表的建立:操作系统初始化时,enable分页机制前就需要建立好
分页与分段:
分页:相比分段,分页页内存固定,导致页内偏移大小范围是固定的。不需要想分段一样考虑分页大小不一致的问题。
逻辑地址和物理地址空间
1.总逻辑页大小和总物理帧大小不一致,一般逻辑地址空间大于物理地址空间。
2.逻辑地址空间连续,物理地址空间不连续。减少内外碎片。
页表
页表结构
页表是个数组,索引是页号,对应的数组项内容里有帧号。
分页机制性能问题
1.时间开销:访问一个内存单元需要两次内存访问
页表不能放在CPU里,只能放在内存里,CPU先做内存寻址找页表基址,再进行页表访问,进行两次内存访问,访问速度很慢
2空间代价:页表占用空间
1.64位计算机,每页1KB,页表大小?2^54个数的页表,很大
2.多个程序有多个页表
解决方法
时间上:缓存 ——快表,TLB,Translation Look-aside Buffer
TLB:
位于CPU中的一块缓存区域,存放常用的页号-帧号对,采用关联内存的方式实现,具有快速访问功能。
-CPU寻址时会先通过页号在TLB查找,看是否存在页号的Key,对应得到帧号,进而得到物理地址(减少对物理地址的访问)。TLB未命中,把该项更新到TLB中(x86CPU这个过程是硬件完成,对于mps是操作系统完成)。
编写程序时,把访问的地址写在一个页号里。
空间上:间接访问(多级页表机制),以时间换空间
二级页表:
对于逻辑地址(p1,p2,o)
CPU寻址时先通过p1查找一级页表,一级页表中存放的是二级页表的p2的起始地址,再在二级页表对应起始地址查找偏移p2,对应存放的就是帧号。提高时间开销,但一级页表中不存在页表项就不需要占用二级页表项的内存,节省空间。
多级页表
页号分为K个部分,建立页表“树”
快表
快表,又称联想寄存器(TLB) ,是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问的若干页表项,以加速地址变换的过程。与此对应,内存中的页表常称为慢表。
地址变换过程 | 访问一个逻辑地址的访存次数 | |
---|---|---|
基本地址变换机构 | ①算页号、页内偏移量 ②检查页号合法性 ③查页表,找到页面存放的内存块号 ④根据内存块号与页内偏移量得到物理地址 ⑤访问目标内存单元 | 两次访存 |
具有快表的地址变换机构 | ①算页号、页内偏移量 ②检查页号合法性 ③查快表。若命中,即可知道页面存放的内存块号,可直接进行⑤;若未命中则进行④ ④查页表,找到页面存放的内存块号,并且将页表项复制到快表中 ⑤根据内存块号与页内偏移量得到物理地址 ⑥访问目标内存单元 | 快表命中,只需一次访存 快表未命中,需要两次访存 |
内存交换技术
交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)
换入:把准备好竞争CPU运行的程序从辅存移到内存。 换出:把处于等待状态(或CPU调度原则下被剥夺运行权力)的程序从内存移到辅存,把内存空间腾出来。
**交换时机:**内存交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程;如果缺页率明显下降,就可以暂停换出。
关键点
分页与分段的区别
进程调度算法详细介绍
选择一个进程运行这一功能是在操作系统中完成的,通常称为调度程序(scheduler)。
调度时机
在进程的生命周期中,当进程从一个运行状态到另外一状态变化的时候,其实会触发一次调度。
比如,以下状态的变化都会触发操作系统的调度:
因为,这些状态变化的时候,操作系统需要考虑是否要让新的进程给 CPU 运行,或者是否让当前进程从 CPU 上退出来而换另一个进程运行。
另外,如果硬件时钟提供某个频率的周期性中断,那么可以根据如何处理时钟中断 ,把调度算法分为两类:
调度原则
调度算法
调度算法是指:根据系统的资源分配策略所规定的资源分配算法。常用的调度算法有:先来先服务调度算法、时间片轮转调度法、短作业优先调度算法、最短剩余时间优先、高响应比优先调度算法、优先级调度算法等等。
先来先服务调度算法是一种最简单的调度算法,也称为先进先出或严格排队方案。当每个进程就绪后,它加入就绪队列。当前正运行的进程停止执行,选择在就绪队列中存在时间最长的进程运行。该算法既可以用于作业调度,也可以用于进程调度。先来先服务比较适合于常作业(进程),而不利于段作业(进程)。
时间片轮转调度算法主要适用于分时系统。在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中第一个进程执行,即先来先服务的原则,但仅能运行一个时间片。
短作业优先调度算法是指对短作业优先调度的算法,从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。 短作业优先调度算法是一个非抢占策略,他的原则是下一次选择预计处理时间最短的进程,因此短进程将会越过长作业,跳至队列头。
最短剩余时间是针对最短进程优先增加了抢占机制的版本。在这种情况下,进程调度总是选择预期剩余时间最短的进程。当一个进程加入到就绪队列时,他可能比当前运行的进程具有更短的剩余时间,因此只要新进程就绪,调度程序就能可能抢占当前正在运行的进程。像最短进程优先一样,调度程序正在执行选择函数是必须有关于处理时间的估计,并且存在长进程饥饿的危险。
高响应比优先调度算法主要用于作业调度,该算法是对 先来先服务调度算法和短作业优先调度算法的一种综合平衡,同时考虑每个作业的等待时间和估计的运行时间。在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行。
优先级调度算法每次从后备作业队列中选择优先级最髙的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行。
磁盘调度算法详细介绍
常见的磁盘调度算法有:
先来先服务
先来先服务(First-Come,First-Served,FCFS),顾名思义,先到来的请求,先被服务。
最短寻道时间优先
最短寻道时间优先(Shortest Seek First,SSF)算法的工作方式是,优先选择从当前磁头位置所需寻道时间最短的请求
扫描算法
最短寻道时间优先算法会产生饥饿的原因在于:磁头有可能再一个小区域内来回得移动。
为了防止这个问题,可以规定:磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向,这就是扫描(*Scan*)算法。
这种算法也叫做电梯算法,比如电梯保持按一个方向移动,直到在那个方向上没有请求为止,然后改变方向。
循环扫描算法
扫描算法使得每个磁道响应的频率存在差异,那么要优化这个问题的话,可以总是按相同的方向进行扫描,使得每个磁道的响应频率基本一致。
循环扫描(Circular Scan, CSCAN )规定:只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且返回中途不处理任何请求,该算法的特点,就是磁道只响应一个方向上的请求。
LOOK 与 C-LOOK算法
扫描算法和循环扫描算法,都是磁头移动到磁盘「最始端或最末端」才开始调换方向。
那这其实是可以优化的,优化的思路就是磁头在移动到「最远的请求」位置,然后立即反向移动。
针对 SCAN 算法的优化叫 LOOK 算法,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中会响应请求。
针对C-SCAN 算法的优化叫 C-LOOK,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中不会响应请求。
页面置换算法详细介绍
请求调页,也称按需调页,即对不在内存中的“页”,当进程执行时要用时才调入,否则有可能到程序结束时也不会调入。而内存中给页面留的位置是有限的,在内存中以帧为单位放置页面。为了防止请求调页的过程出现过多的内存页面错误(即需要的页面当前不在内存中,需要从硬盘中读数据,也即需要做页面的替换)而使得程序执行效率下降,我们需要设计一些页面置换算法,页面按照这些算法进行相互替换时,可以尽量达到较低的错误率。常用的页面置换算法如下:
先进先出,即淘汰最早调入的页面。
选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。
即选择最近最久未使用的页面予以淘汰
时钟置换算法也叫最近未用算法 NRU(Not RecentlyUsed)。该算法为每个页面设置一位访问位,将内存中的所有页面都通过链接指针链成一个循环队列。
什么是零拷贝
为了提高文件传输的性能,于是就出现了零拷贝技术,它通过一次系统调用(sendfile
方法)合并了磁盘读取与网络发送两个操作,降低了上下文切换次数。另外,拷贝数据都是发生在内核中的,天然就降低了数据拷贝的次数。
Kafka 和 Nginx 都有实现零拷贝技术,这将大大提高文件传输的性能。
零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能,同时,为了解决机械硬盘寻址慢的问题,它还协助 I/O 调度算法实现了 IO 合并与预读,这也是顺序读比随机读性能好的原因。这些优势,进一步提升了零拷贝的性能。
I/O多路复用
既然为每个请求分配一个进程/线程的方式不合适,那有没有可能只使用一个进程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术。
一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。
我们熟悉的 select/poll/epoll 内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件。
select/poll/epoll 是如何获取网络事件的呢?在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。
select/poll/epoll
select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。
在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。
很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。
epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。
而且,epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。
高性能网络模式:Reactor和Proactor
常见的 Reactor 实现方案有三种。
第一种方案单 Reactor 单进程 / 线程,不用考虑进程间通信以及数据同步的问题,因此实现起来比较简单,这种方案的缺陷在于无法充分利用多核 CPU,而且处理业务逻辑的时间不能太长,否则会延迟响应,所以不适用于计算机密集型的场景,适用于业务处理快速的场景,比如 Redis(6.0之前 ) 采用的是单 Reactor 单进程的方案。
第二种方案单 Reactor 多线程,通过多线程的方式解决了方案一的缺陷,但它离高并发还差一点距离,差在只有一个 Reactor 对象来承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
第三种方案多 Reactor 多进程 / 线程,通过多个 Reactor 来解决了方案二的缺陷,主 Reactor 只负责监听事件,响应事件的工作交给了从 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案,Nginx 则采用了类似于 「多 Reactor 多进程」的方案。
Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。
因此,真正的大杀器还是 Proactor,它是采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。
不过,无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。
一致性哈希介绍
一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上,如果增加或者移除一个节点,仅影响该节点在哈希环上顺时针相邻的后继节点,其它数据也不会受到影响。
但是一致性哈希算法不能够均匀的分布节点,会出现大量请求都集中在一个节点的情况,在这种情况下进行容灾与扩容时,容易出现雪崩的连锁反应。
为了解决一致性哈希算法不能够均匀的分布节点的问题,就需要引入虚拟节点,对一个真实节点做多个副本。不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。
引入虚拟节点后,可以会提高节点的均衡度,还会提高系统的稳定性。所以,带虚拟节点的一致性哈希方法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景。
什么是死锁和产生死锁原因
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 如下图所示:如果此时有一个线程 A,已经持有了锁 A,但是试图获取锁 B,线程 B 持有锁 B,而试图获取锁 A,这种情况下就会产生死锁。
产生死锁原因
由于系统中存在一些不可剥夺资源,而当两个或两个以上进程占有自身资源,并请求对方资源时,会导致每个进程都无法向前推进,这就是死锁。
例如:系统中只有一台打印机,可供进程 A 使用,假定 A 已占用了打印机,若 B 继续要求打印机打印将被阻塞。
系统中的资源可以分为两类:
例如:进程 A 和 进程 B 互相等待对方的数据。
死锁产生的必要条件?
如何避免死锁
预防死锁、避免死锁、检测死锁、解除死锁
预防死锁
解除死锁
什么是悲观锁、乐观锁
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。
互斥锁、自旋锁、读写锁,都是属于悲观锁。
哲学家进餐问题
生产者-消费者问题描述:
我们对问题分析可以得出:
那么我们需要三个信号量,分别是:
mutex
:用于互斥访问缓冲区,初始化值为 1;fullBuffers
:用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为 0(表明缓冲区一开始为空);emptyBuffers
:用于生产者询问缓冲区是否有空位,有空位则生成数据,初始化值为 n (缓冲区大小);如果消费者线程一开始执行 P(fullBuffers)
,由于信号量 fullBuffers
初始值为 0,则此时 fullBuffers
的值从 0 变为 -1,说明缓冲区里没有数据,消费者只能等待。
接着,轮到生产者执行 P(emptyBuffers)
,表示减少 1 个空槽,如果当前没有其他生产者线程在临界区执行代码,那么该生产者线程就可以把数据放到缓冲区,放完后,执行 V(fullBuffers)
,信号量 fullBuffers
从 -1 变成 0,表明有「消费者」线程正在阻塞等待数据,于是阻塞等待的消费者线程会被唤醒。
消费者线程被唤醒后,如果此时没有其他消费者线程在读数据,那么就可以直接进入临界区,从缓冲区读取数据。最后,离开临界区后,把空槽的个数 + 1。
生产者、消费者问题
哲学家就餐的问题描述:
5
个老大哥哲学家,闲着没事做,围绕着一张圆桌吃面;5
支叉子,每两个哲学家之间放一支叉子;一、让偶数编号的哲学家「先拿左边的叉子后拿右边的叉子」,奇数编号的哲学家「先拿右边的叉子后拿左边的叉子」。
在 P 操作时,根据哲学家的编号不同,拿起左右两边叉子的顺序不同。另外,V 操作是不需要分支的,因为 V 操作是不会阻塞的。
二、用一个数组 state 来记录每一位哲学家的三个状态,分别是在进餐状态、思考状态、饥饿状态(正在试图拿叉子)。
那么,一个哲学家只有在两个邻居都没有进餐时,才可以进入进餐状态。
上面的程序使用了一个信号量数组,每个信号量对应一位哲学家,这样在所需的叉子被占用时,想进餐的哲学家就被阻塞。
读者写者问题
读者只会读取数据,不会修改数据,而写者即可以读也可以修改数据。
读者-写者的问题描述:
使用信号量的方式来尝试解决:
wMutex
:控制写操作的互斥信号量,初始值为 1 ;rCount
:正在进行读操作的读者个数,初始化为 0;rCountMutex
:控制对 rCount 读者计数器的互斥修改,初始值为 1;这种实现,是读者优先的策略,因为只要有读者正在读的状态,后来的读者都可以直接进入,如果读者持续不断进入,则写者会处于饥饿状态。
那既然有读者优先策略,自然也有写者优先策略:
在方案一的基础上新增如下变量:
rMutex
:控制读者进入的互斥信号量,初始值为 1;wDataMutex
:控制写者写操作的互斥信号量,初始值为 1;wCount
:记录写者数量,初始值为 0;wCountMutex
:控制 wCount 互斥修改,初始值为 1;注意,这里 rMutex
的作用,开始有多个读者读数据,它们全部进入读者队列,此时来了一个写者,执行了 P(rMutex)
之后,后续的读者由于阻塞在 rMutex
上,都不能再进入读者队列,而写者到来,则可以全部进入写者队列,因此保证了写者优先。
同时,第一个写者执行了 P(rMutex)
之后,也不能马上开始写,必须等到所有进入读者队列的读者都执行完读操作,通过 V(wDataMutex)
唤醒写者的写操作。
既然读者优先策略和写者优先策略都会造成饥饿的现象,那么我们就来实现一下公平策略。
公平策略:
对并发和并行的理解
什么是用户态和内核态
用户态和内核态是操作系统的两种运行状态。
内核态
:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。用户态
:处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。两大局部性原理是什么
主要分为时间局部性和空间局部性。
**时间局部性:**如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)
**空间局部性:**一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的)
异常和中断是什么,有什么区别
中断
当我们在敲击键盘的同时就会产生中断,当硬盘读写完数据之后也会产生中断,所以,我们需要知道,中断是由硬件设备产生的,而它们从物理上说就是电信号,之后,它们通过中断控制器发送给CPU,接着CPU判断收到的中断来自于哪个硬件设备(这定义在内核中),最后,由CPU发送给内核,有内核处理中断。
异常
CPU处理程序的时候一旦程序不在内存中,会产生缺页异常;当运行除法程序时,当除数为0时,又会产生除0异常。所以,大家也需要记住的是,异常是由CPU产生的,同时,它会发送给内核,要求内核处理这些异常。
相同点
不同点
原子操作
**处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。**首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。Pentium 6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
(1)使用总线锁保证原子性 第一个机制是通过总线锁保证原子性。
所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
(2)使用缓存锁保证原子性 第二个机制是通过缓存锁定来保证原子性。
服务器高并发解决方案
抖动你知道是什么吗?它也叫颠簸现象
刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)
为进程分配的物理块太少,会使进程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率 为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程工作集” 的概念
什么是Redis
Redis是一个数据库,不过与传统数据库不同的是Redis的数据库是存在内存中,所以读写速度非常快,因此 Redis被广泛应用于缓存方向。
除此之外,Redis也经常用来做分布式锁,Redis提供了多种数据类型来支持不同的业务场景。除此之外,Redis 支持事务持久化、LUA脚本、LRU驱动事件、多种集群方案。
Redis有哪几种数据类型
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
Redis 五种数据类型的应用场景:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AZ9MTye1-1678081583396)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/%E4%BA%94%E7%A7%8D%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B.png)]
随着 Redis 版本的更新,后面又支持四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 Redis 后续版本又支持四种数据类型,它们的应用场景如下:
详细介绍Redis的五种基本数据类型
String 类型内部实现
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。 SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:
List 类型内部实现
List 类型的底层数据结构是由双向链表或压缩列表实现的:
但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
Hash 类型内部实现
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
Set 类型内部实现
Set 类型的底层数据结构是由哈希表或整数集合实现的:
ZSet 类型内部实现
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
Redis数据结构详解
Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
其实SDS等同于C语言中的char * ,但它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’来标识字符串的结 束,因此它必然有个长度字段。
优点
它具有很常规的 set/get 操作,value 可以是String也可以是数字,一般做一些复杂的计数功能的缓存。
当有一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的额字符串时,Redis就会使用链表作为列表建的底层实现。
特性
字典的底层是哈希表,类似 C++中的 map ,也就是键值对。
哈希算法
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash算法。这种算法的优点在于即使输入的键是规律的,算法仍能给出一个个很好的随机分布性,并且算法的计算速度非常快。
哈希冲突的解决方式
Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用这个单向链表连接起来,这就解决了键冲突的问题。
特性
字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。
Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
Redis使用MurmurHash2算法来计算键的哈希值。
哈希表使用链地址法来解决键冲突。
跳跃表
Redis 只有 Zset 对象的底层实现用到跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:
如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
特性
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
特性
看他的名字就能看出来,是为了节省内存造的列表结构。
其实 quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。
quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
listpack 采用了压缩列表的很多优秀的设计,比如还是用一块连续的内存空间来紧凑地保存数据,并且为了节省内存的开销,listpack 节点会采用不同的编码方式保存不同大小的数据。
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。整数集合本质上是一块连续内存空间。
整数集合会有一个升级规则,就是当我们将一个新元素加入到整数集合里面,如果新元素的类型(int32_t)比整数集合现有所有元素的类型(int16_t)都要长时,整数集合需要先进行升级,也就是按新元素的类型(int32_t)扩展 contents 数组的空间大小,然后才能将新元素加入到整数集合里,当然升级的过程中,也要维持整数集合的有序性。
整数集合升级的过程不会重新分配一个新类型的数组,而是在原本的数组上扩展空间,然后在将每个元素按间隔类型大小分割,如果 encoding 属性值为 INTSET_ENC_INT16,则每个元素的间隔就是 16 位。
整数集合升级的好处是节省内存资源。
Redis的线程模式
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的
关闭文件、AOF 刷盘、释放内存
Redis 单线程模式是怎样的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IMjNfraQ-1678081583397)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E5%85%AB%E8%82%A1%E6%96%87/redis%E5%8D%95%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B.drawio.png)]
图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情:
初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:
使用Redis的好处有哪些
1、访问速度快,因为数据存在内存中,类似于Java中的HashMap或者C++中的哈希表(如unordered_map/unordered_set),这两者的优势就是查找和操作的时间复杂度都是O(1)
2、数据类型丰富,支持String,list,set,sorted set,hash这五种数据结构
3、支持事务,Redis中的操作都是原子性,换句话说就是对数据的更改要么全部执行,要么全部不执行,这就是原子性的定义
4、特性丰富:Redis可用于缓存,消息,按key设置过期时间,过期后将会自动删除。
Memcached与Redis的区别都有哪些
1、存储方式
2、数据支持类型
3、使用底层模型不同
4、集群模式:Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 Redis 目前 是原生支持 cluster 模式的.
5、Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
6、Value 值大小不同:Redis 最大可以达到 512MB;Memcached 只有 1MB。
单线程的Redis为什么这么快
Redis的全部操作都是纯内存的操作;
Redis采用单线程,有效避免了频繁的上下文切换;
采用了非阻塞I/O多路复用机制。
Hash 冲突怎么办
Redis 通过链式哈希解决冲突:也就是同一个 桶里面的元素使用链表保存。但是当链表过长就会导致查找性能变差可能,所以 Redis 为了追求快,使用了两个全局哈希表。用于 rehash 操作,增加现有的哈希桶数量,减少哈希冲突。
开始默认使用 「hash 表 1 」保存键值对数据,「hash 表 2」 此刻没有分配空间。当数据越来越多触发 rehash 操作,则执行以下操作:
值得注意的是,将 hash 表 1 的数据重新映射到 hash 表 2 的过程中并不是一次性的,这样会造成 Redis 阻塞,无法提供服务。
而是采用了渐进式 rehash,每次处理客户端请求的时候,先从「 hash 表 1」 中第一个索引开始,将这个位置的 所有数据拷贝到 「hash 表 2」 中,就这样将 rehash 分散到多次请求过程中,避免耗时阻塞。
Redis的过期删除策略
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
Redis中同时使用了惰性过期和定期过期两种过期策略。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPoc3v2v-1678081583397)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/%E8%BF%87%E6%9C%9F%E5%88%A0%E9%99%A4%E7%AD%96%E7%95%A5.jpg)]
Redis的内存淘汰策略
Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
1、不进行数据淘汰的策略
noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,则会触发 OOM,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。
2、进行数据淘汰的策略
针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。
在设置了过期时间的数据中进行淘汰:
在所有数据范围内进行淘汰:
Redis 是如何实现 LRU 算法的?
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
Redis 实现的 LRU 算法的优点:
什么是 LFU 算法?
LFU 全称是 Least Frequently Used 翻译为最近最不常用,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
Redis 是如何实现 LFU 算法的?
LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。
访问频次(访问频率)的 logc 会随时间推移而衰减的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MVmju7O5-1678081583398)(https://cdn.xiaolincoding.com/gh/xiaolincoder/redis/%E8%BF%87%E6%9C%9F%E7%AD%96%E7%95%A5/%E5%86%85%E5%AD%98%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5.jpg)]
Redis如何实现持久化
Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机 器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis 共有三种数据持久化的方式:
AOF日志实现
AOF(append-only file)持久化
Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。 在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:
AOF 日志过大,会触发AOF 重写机制
AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
Redis 提供了 AOF 重写机制,它会直接扫描数据中所有的键值对数据,然后为每一个键值对生成一条写操作命令,接着将该命令写入到新的 AOF 文件,重写完成后,就替换掉现有的 AOF 日志。重写的过程是由后台子进程完成的,这样可以使得主进程可以继续正常处理命令。
RDB快照实现
快照(snapshotting)持久化(RDB持久化)
将某一时刻的内存数据,以二进制的方式写入磁盘;RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
RDB 在执行快照的时候,数据能修改吗?
可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW)。
执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。
混合持久化
混合持久化方式 Redis 4.0 对于持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
大Key对Redis持久化有什么影响
当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork()
函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):
大 key 除了会影响持久化之外,还会有以下的影响。
如何避免大 Key 呢?
最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。
主从复制模式介绍
Redis多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略。
优点:
缺点:
主从复制是怎么实现的
主从复制共有三种模式:全量复制、基于长连接的命令传播、增量复制。
主从服务器第一次同步的时候,就是采用全量复制,此时主服务器会两个耗时的地方,分别是生成 RDB 文件和传输 RDB 文件。为了避免过多的从服务器和主服务器进行全量复制,可以把一部分从服务器升级为「经理角色」,让它也有自己的从服务器,通过这样可以分摊主服务器的压力。
第一次同步完成后,主从服务器都会维护着一个长连接,主服务器在接收到写操作命令后,就会通过这个连接将写命令传播给从服务器,来保证主从服务器的数据一致性。
如果遇到网络断开,增量复制就可以上场了,不过这个还跟 repl_backlog_size 这个大小有关系。
如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率。
集群模式的工作原理是什么
基本通信原理
集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通信。
集中式是将集群元数据(节点信息、故障等等)集中存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 storm
。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。
Redis 维护集群元数据采用另一个方式, gossip
协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
集中式的好处在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;不好在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
ping
消息,同时其它几个节点接收到 ping
之后返回 pong
。gossip 协议
gossip 协议包含多种消息,包含 ping
, pong
, meet
, fail
等等。
其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。
ping 消息深入
ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。
每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 cluster_node_timeout / 2
,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeout
可以调节,如果调得比较大,那么会降低 ping 的频率。
每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3
个其它节点的信息,最多包含 总节点数减 2
个其它节点的信息。
分布式寻址算法
hash 算法
来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。
一致性 hash 算法
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
Redis cluster 的 hash slot 算法
Redis cluster 有固定的 16384
个 hash slot,对每个 key
计算 CRC16
值,然后对 16384
取模,可以获取 key 对应的 hash slot。
Redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag
来实现。
任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。
Redis cluster 的高可用与主备切换原理
Redis cluster 的高可用的原理,几乎跟哨兵是类似的。
判断节点宕机
如果一个节点认为另外一个节点宕机,那么就是 pfail
,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是 fail
,客观宕机,跟哨兵的原理几乎一样,sdown,odown。
在 cluster-node-timeout
内,某个节点一直没有返回 pong
,那么就被认为 pfail
。
如果一个节点认为某个节点 pfail
了,那么会在 gossip ping
消息中, ping
给其他节点,如果超过半数的节点都认为 pfail
了,那么就会变成 fail
。
从节点过滤
对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。
检查每个 slave node 与 master node 断开连接的时间,如果超过了 cluster-node-timeout * cluster-slave-validity-factor
,那么就没有资格切换成 master
。
从节点选举
每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node (N/2 + 1)
都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。
从节点执行主备切换,从节点切换为主节点。
与哨兵比较
整个流程跟哨兵相比,非常类似,所以说,Redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
哨兵模式的作用
哨兵的介绍
sentinel,中文名是哨兵。哨兵是 Redis 集群架构中非常重要的一个组件,主要有以下功能:
哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
哨兵的核心知识
哨兵集群必须部署 2 个以上节点,如果哨兵集群仅仅部署了 2 个哨兵实例,quorum = 1。
配置 quorum=1
,如果 master 宕机, s1 和 s2 中只要有 1 个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 majority,也就是大多数哨兵都是运行的。
如果此时仅仅是 M1 进程宕机了,哨兵 s1 正常运行,那么故障转移是 OK 的。但是如果是整个 M1 和 S1 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 majority 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。
配置 quorum=2
,如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 master 宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 majority 是 2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。
Redis哨兵是怎么工作的
哨兵主备切换的数据丢失问题
导致数据丢失的两种情况
主备切换的过程,可能会导致数据丢失:
因为 master->slave 的复制是异步的,所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。
脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的脑裂。
此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
数据丢失问题的解决方案
进行如下配置:
表示,要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。
有了 min-slaves-max-lag
这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。
如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。
哨兵集群的自动发现机制
哨兵互相之间的发现,是通过 Redis 的 pub/sub
系统实现的,每个哨兵都会往 __sentinel__:hello
这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
每隔两秒钟,每个哨兵都会往自己监控的某个 master+slaves 对应的 __sentinel__:hello
channel 里发送一个消息,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置。
每个哨兵也会去监听自己监控的每个 master+slaves 对应的 __sentinel__:hello
channel,然后去感知到同样在监听这个 master+slaves 的其他哨兵的存在。
每个哨兵还会跟其他哨兵交换对 master
的监控配置,互相进行监控配置的同步。
Redis 如何实现服务高可用
要想设计一个高可用的 Redis 服务,一定要从 Redis 的多服务节点来考虑,比如 Redis 的主从复制、哨兵模式、切片集群。
主从复制
主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。
主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。
也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。
注意,主从服务器之间的命令复制是异步进行的。
具体来说,在主从服务器命令传播阶段,主服务器收到新的写命令后,会发送给从服务器。但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。
所以,无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。
哨兵模式
在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。
为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
切片集群模式
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步:
接下来的问题就是,这些哈希槽怎么被映射到具体的 Redis 节点上的呢?有两种方案:
然后在集群运行的过程中,key1 和 key2 计算完 CRC16 值后,对哈希槽总个数 4 进行取模,再根据各自的模数结果,就可以被映射到哈希槽 1(对应节点1) 和 哈希槽 2(对应节点2)。
需要注意的是,在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存击穿、缓存降级全搞定!
缓存雪崩
缓存雪崩指的是缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
看不懂?那我说人话。
我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
解决办法
缓存穿透
一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量 请求而崩掉。
这也看不懂?那我再换个说法好了。
缓存穿透是指查询一个一定不存在的数据,由于缓存不命中,接着查询数据库也无法查询出结果,因此也不会写入到缓存中,这将会导致每个查询都会去请求数据库,造成缓存穿透。
解决办法
1、布隆过滤器
这是最常见的一种解决方法了,它是将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压 力。
对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
这里稍微科普一下布隆过滤器。
布隆过滤器是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
该算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是布隆过滤器的基本思想,一般用于在大数据量的集合中判定某元素是否存在。
2、缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;如果一个查询返回的数据为空(不管是数据不存 在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
我们可以从适用场景和维护成本两方面对这两汇总方法进行一个简单比较:
适用场景:缓存空对象适用于1、数据命中不高 2、数据频繁变化且实时性较高 ;而布隆过滤器适用1、数据命中不高 2、数据相对固定即实时性较低
维护成本:缓存空对象的方法适合1、代码维护简单 2、需要较多的缓存空间 3、数据会出现不一致的现象;布隆过滤器适合 1、代码维护较复杂 2、缓存空间要少一些
缓存预热
缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询事先被预热的缓存数据!
解决思路 1、直接写个缓存刷新页面,上线时手工操作下; 2、数据量不大,可以在项目启动的时候自动进行加载; 3、定时刷新缓存;
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:定时删除和惰性删除,其中: (1)定时删除:定时去清理过期的缓存; (2)惰性删除:当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开一个洞。
比如常见的电商项目中,某些货物成为“爆款”了,可以对一些主打商品的缓存直接设置为永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。mutex key互斥锁基本上是用不上的,有个词叫做大道至简。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 以参考日志级别设置预案: (1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; (2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; (3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级; (4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
数据库和缓存如何保证一致性
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的 情况,最好不要做这个方案,最好将读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
最经典的缓存+数据库读写的模式,就是 预留缓存模式Cache Aside Pattern。
在高并发的业务场景下,数据库的性能瓶颈往往都是用户并发访问过大。所以,一般都使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接去访问MySQL等数据库,从而减少网络请求的延迟响应。
如何保证缓存与数据库的双写一致性
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
Cache Aside Pattern
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。
比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。
最初级的缓存不一致问题及解决方案
问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
解决思路 1:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。
解决思路 2:延时双删。依旧是先更新数据库,再删除缓存,唯一不同的是,我们把这个删除的动作,在不久之后再执行一次,比如 5s 之后。
删除的动作,可以有多种选择,比如:1. 使用 DelayQueue
,会随着 JVM 进程的死亡,丢失更新的风险;2. 放在 MQ
,但编码复杂度为增加。总之,我们需要综合各种因素去做设计,选择一个最合理的解决方案。
比较复杂的数据不一致问题分析
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了…
为什么上亿流量高并发场景下,缓存会出现这个问题?
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。
解决方案如下:
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据+更新缓存”的操作,根据唯一标识路由之后,也发送到同一个 jvm 内部队列中。
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
高并发的场景下,该解决方案要注意的问题:
由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。
该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。
另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每个库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。
一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。
如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。
其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。
这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。
但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。
常见的数据优化方案
一、缓存双淘汰法
二、异步淘汰缓存
上述的步骤,都是在业务线里面执行,新增一个线下的读取binlog异步淘汰缓存模块,读取binlog总的数据,然后进行异步淘汰。
这里简单提供一个思路
1.思路:
MySQL binlog增量发布订阅消费+消息队列+增量数据更新到Redis
1)读请求走Redis:热数据基本都在Redis
2)写请求走MySQL: 增删改都操作MySQL
3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
2.Redis更新
1)数据操作主要分为两块:
这里说的是增量,指的是mysql的update、insert、delate变更数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新,就无需在从业务线去操作缓存内容。
Django的优点和缺点
什么MVC、MTV模式
M:Model,模型,和数据库进行交互
V:View,视图,负责产生 Html 页面
C:Controller,控制器,接收请求,进行处理,与 M 和 V 进行交互,返回应答。
M:Model,模型,和 MVC 中的 M 功能相同,和数据库进行交互
V:view,视图,和 MVC 中的 C 功能相同,接收请求,进行处理,与 M 和 T 进行交互,返回应答
T:Template,模板,和 MVC 中的 V 功能相同,产生 Html 页面
Django中间件
Django 在中间件中预置了六个方法,这六个方法的区别在于不同的阶段执行,对输入或输出进行干预,方法如下:
1.初始化:无需任何参数,服务器响应第一个请求的时候调用一次,用于确定是否启用当前中间件。
2.处理请求前:在每个请求上调用,返回 None 或 HttpResponse 对象。
3.处理视图前:在每个请求上调用,返回 None 或 HttpResponse 对象。
4.处理模板响应前:在每个请求上调用,返回实现了 render 方法的响应对象。
5.处理响应后:所有响应返回浏览器之前被调用,在每个请求上调用,返回 HttpResponse 对象。
6.异常处理:当视图抛出异常时调用,在每个请求上调用,返回一个 HttpResponse 对象。
Django如何提升性能
对一个后端开发程序员来说,提升性能指标主要有两个一个是并发数,另一个是响应时间网站性能的优化一般包括 web 前端性能优化,应用服务器性能优化,存储服务器优化。
(1)合理的使用缓存技术,对一些常用到的动态数据,比如首页做一个缓存,或者某些常用的数据做个缓存,设置一定得过期时间,这样减少了对数据库的压力,提升网站性能。
(2) 使用 celery 消息队列,将耗时的操作扔到队列里,让 worker 去监听队列里的任务,实现异步操 作,比如发邮件,发短信。
(3) 就是代码上的一些优化,补充:nginx 部署项目也是项目优化,可以配置合适的配置参数,提升效率,增加并发量。
(4) 如果太多考虑安全因素,服务器磁盘用固态硬盘读写,远远大于机械硬盘,这个技术现在没有普及,主要是固态硬盘技术上还不是完全成熟, 相信以后会大量普及。
(5) 另外还可以搭建服务器集群,将并发访问请求,分散到多台服务器上处理。
你用过的爬虫框架或者模块有哪些?谈谈他们的区别或者优缺点?
(1) Python 自带:urllib、urllib2
urllib 和 urllib2 模块都做与请求 URL 相关的操作,但他们提供不同的功能。
urllib2:urllib2.urlopen 可以接受一个 Request 对象或者 url,(在接受 Request 对象时候,并以此可以来设置一个 URL 的 headers),urllib.urlopen 只接收一个 url。
urllib 有 urlencode,urllib2 没有,因此总是 urllib,urllib2 常会一起使用的原因。
(2) 第三方:requests
request 是一个 HTTP 库, 它只是用来,进行请求,对于 HTTP 请求,他是一个强大的库,下载,解析全部自己处理,灵活性更高,高并发与分布式部署也非常灵活,对于功能可以更好实现
(3) 框架: Scrapy
scrapy 是封装起来的框架,它包含了下载器,解析器,日志及异常处理,基于多线程,twisted 的方式处理,对于固定单个网站的爬取开发,有优势,但是对于多网站爬取,并发及分布式处理方面,不够灵活,不便调整与括展。
Scrapy 优点
scrapy 是异步的
采取可读性更强的 xpath 代替正则
强大的统计和 log 系统
同时在不同的 url 上爬行
支持 shell 方式,方便独立调试
写 middleware,方便写一些统一的过滤器
通过管道的方式存入数据库
Scrapy 缺点
基于 python 的爬虫框架,扩展性比较差
基于 twisted 框架,运行中的 exception 是不会干掉 reactor,并且异步框架出错后是不会停掉其他任务的,数据出错后难以察觉。
你做过什么爬虫案例
常见的反爬虫和应对方法?
(1) 通过 Headers 反爬虫
从用户请求的 Headers 反爬虫是最常见的反爬虫策略。很多网站都会对 Headers 的 User-Agent 进行检测,还有一部分网站会对 Referer 进行检测(一些资源网站的防盗链就是检测 Referer)。如果遇到了这类反爬虫机制,可以直接在爬虫中添加 Headers,将浏览器的 User-Agent 复制到爬虫的 Headers 中;或者将 Referer 值修改为目标网站域名。对于检测 Headers 的反爬虫,在爬虫中修改或者添加 Headers 就能很好的绕过。
(2) 基于用户行为反爬虫
还有一部分网站是通过检测用户行为,例如同一 IP 短时间内多次访问同一页面,或者同一账户短时间内多次进行相同操作。
大多数网站都是前一种情况,对于这种情况,使用 IP 代理就可以解决。可以专门写一个爬虫,爬取网上公开的代理 ip,检测后全部保存起来。这样的代理 ip 爬虫经常会用到,最好自己准备一个。有了大量代理 ip 后可以每请求几次更换一个 ip,这在 requests 或者 urllib2 中很容易做到,这样就能很容易的绕过第一种反爬虫。
对于第二种情况,可以在每次请求后随机间隔几秒再进行下一次请求。有些有逻辑漏洞的网站,可以通过请求几次,退出登录,重新登录,继续请求来绕过同一账号短时间内不能多次进行相同请求的限制。
(3) 动态页面的反爬虫
上述的几种情况大多都是出现在静态页面,还有一部分网站,我们需要爬取的数据是通过 ajax 请求得到,或者通过 JavaScript 生成的。首先用 Fiddler 对网络请求进行分析。如果能够找到 ajax 请求,也能分析出具体的参数和响应的具体含义,我们就能采用上面的方法,直接利用 requests 或者 urllib2模拟 ajax 请求,对响应的 json 进行分析得到需要的数据。
能够直接模拟 ajax 请求获取数据固然是极好的,但是有些网站把 ajax 请求的所有参数全部加密了。我们根本没办法构造自己所需要的数据的请求。这种情况下就用 selenium+phantomJS,调用浏览器内核,并利用 phantomJS 执行 js 来模拟人为操作以及触发页面中的 js 脚本。从填写表单到点击按钮再到滚动页面,全部都可以模拟,不考虑具体的请求和响应过程,只是完完整整的把人浏览页面获取数据的过程模拟一遍。
用这套框架几乎能绕过大多数的反爬虫,因为它不是在伪装成浏览器来获取数据(上述的通过添加Headers 一定程度上就是为了伪装成浏览器),它本身就是浏览器,phantomJS 就是一个没有界面的浏览器,只是操控这个浏览器的不是人。利 selenium+phantomJS 能干很多事情,例如识别点触式(12306)或者滑动式的验证码,对页面表单进行暴力破解等。
验证码的解决
(1) 图形验证码
干扰、杂色不是特别多的图片可以使用开源库 Tesseract 进行识别,太过复杂的需要借助第三方打码平台。
(2) 滑块验证码
点击和拖动滑块验证码可以借助 selenium、无图形界面浏览器(chromedirver 或者 phantomjs)和pillow 包来模拟人的点击和滑动操作,pillow 可以根据色差识别需要滑动的位置。
实现数据分析的方法
使用numpy和pandas进行数据处理,使用matplotlib库实现数据可视化
数据分析项目
常见的机器学习算法有哪些
KNN算法、线性回归法、决策树算法、随机森林算法
什么是机器学习
简单的说,机器学习就是让机器从数据中学习,进而得到一个更加符合现实规律的模型,通过对模型的使用使得机器比以往表现的更好,这就是机器学习。
对上面这句话的理解:
数据:从现实生活抽象出来的一些事物或者规律的特征进行数字化得到。
学习:在数据的基础上让机器重复执行一套特定的步骤(学习算法)进行事物特征的萃取,得到一个更加逼近于现实的描述(这个描述是一个模型它的本身可能就是一个函数)。我们把大概能够描述现实的这个函数称作我们学到的模型。
更好:我们通过对模型的使用就能更好的解释世界,解决与模型相关的问题。
解释有监督和无监督机器学习之间的区别?
监督学习需要训练标记的数据。换句话说,监督学习使用了基本事实,这意味着我们对输出和样本已有知识。这里的目标是学习一个近似输入和输出之间关系的函数。
另一方面,无监督学习不使用标记的输出。此处的目标是推断数据集中的自然结构。
KNN算法介绍
邻近算法,或者说K最邻近(KNN,K-NearestNeighbor)分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是K个最近的邻居的意思,说的是每个样本都可以用它最接近的K个邻近值来代表。近邻算法就是将数据集合中每一个记录进行分类的方法 。
k近邻法是一种基本的分类和回归方法,是监督学习方法里的一种常用方法。k近邻算法假设给定一个训练数据集,其中的实例类别已定。分类时,对新的实例,根据其k个最近邻的训练实例类别,通过多数表决等方式进行预测。
k近邻法三要素:距离度量、k值的选择和分类决策规则。常用的距离度量是欧氏距离及更一般的pL距离。k值小时,k近邻模型更复杂,容易发生过拟合;k值大时,k近邻模型更简单,又容易欠拟合。因此k值得选择会对分类结果产生重大影响。k值的选择反映了对近似误差与估计误差之间的权衡,通常由交叉验证选择最优的k。
优点
简单,易于理解,易于实现,无需估计参数,无需训练;
适合对稀有事件进行分类;
特别适合于多分类问题(multi-modal,对象具有多个类别标签), kNN比SVM的表现要好。
缺点
线性回归法介绍
线性回归(Linear regression)是利用回归方程(函数)对一个或多个自变量(特征值)和因变量(目标值)之间关系进行建模的一种分析方式。
线性回归当中主要有两种模型,一种是线性关系,另一种是非线性关系。
PCA算法介绍
PCA(principal components analysis)即主成分分析技术,又称主分量分析,旨在利用降维的思想,把多指标转化为少数几个综合指标。
PCA算法优点:
1、使得数据集更易使用;
2、降低算法的计算开销;
3、去除噪声;
4、使得结果容易理解;
5、完全无参数限制。
PCA算法缺点:
1、主成分解释其含义往往具有一定的模糊性,不如原始样本完整
2、贡献率小的主成分往往可能含有对样本差异的重要信息,也就是可能对于区分样本的类别(标签)更有用
3、特征值矩阵的正交向量空间是否唯一有待讨论
4、无监督学习
PCA算法求解步骤:
去除平均值
计算协方差矩阵
计算协方差矩阵的特征值和特征向量
将特征值排序
保留前N个最大的特征值对应的特征向量
将原始特征转换到上面得到的N个特征向量构建的新空间中(最后两步,实现了特征压缩)**
PCA是一种常用的数据分析方法。PCA通过线性变换将原始数据变换为一组各维度线性无关的表示,可用于识别和提取数据的主要特征分量,通过将数据坐标轴旋转到数据角度上那些最重要的方向(方差最大);然后通过特征值分析,确定出需要保留的主成分个数,舍弃其他非主成分,从而实现数据的降维。降维使数据变得更加简单高效,从而实现提升数据处理速度的目的,节省大量的时间和成本。降维也成为了应用非常广泛的数据预处理方法。PCA算法已经被广泛的应用于高维数据集的探索与可视化,还可以用于数据压缩,数据预处理,图像,语音,通信的分析处理等领域。
支持向量机-SVM介绍
SVM(Support Vector Machine)指的是支持向量机,是常见的一种判别方法。在机器学习领域,是一个有监督的学习模型,通常用来进行模式识别、分类以及回归分析。它在解决小样本、非线性及高维模式识别中表现出许多特有的优势,并能够推广应用到函数拟合等其他其他问题中。
支持向量机(Support Vector Machine, SVM)是一类按监督学习(supervised learning)方式对数据进行二元分类(binary classification)的广义线性分类器(generalized linear classifier),其决策边界是对学习样本求解的最大边距超平面(maximum-margin hyperplane)。
随机森林算法介绍
随机森林指的是利用多棵树对样本进行训练并预测的一种分类器。
随机森林的优点有 :
1)对于很多种资料,它可以产生高准确度的分类器;
2)它可以处理大量的输入变数;
3)它可以在决定类别时,评估变数的重要性;
4)在建造森林时,它可以在内部对于一般化后的误差产生不偏差的估计;
5)它包含一个好方法可以估计遗失的资料,如果有很大一部分的资料遗失,仍可以维持准确度;
6)学习过程是很快速的。
算法过程
1、一个样本容量为N的样本,有放回的抽取N次,每次抽取1个,最终形成了N个样本。这选择好了的N个样本用来训练一个决策树,作为决策树根节点处的样本。
2、当每个样本有M个属性时,在决策树的每个节点需要分裂时,随机从这M个属性中选取出m个属性,满足条件m << M。然后从这m个属性中采用某种策略(比如说信息增益)来选择1个属性作为该节点的分裂属性。
3、决策树形成过程中每个节点都要按照步骤2来分裂(很容易理解,如果下一次该节点选出来的那一个属性是刚刚其父节点分裂时用过的属性,则该节点已经达到了叶子节点,无须继续分裂了),一直到不能够再分裂为止。注意整个决策树形成过程中没有进行剪枝。
4、按照步骤1~3建立大量的决策树,这样就构成了随机森林了。
数据的随机选取
首先,从原始的数据集中采取有放回的抽样,构造子数据集,子数据集的数据量是和原始数据集相同的。不同子数据集的元素可以重复,同一个子数据集中的元素也可以重复。第二,利用子数据集来构建子决策树,将这个数据放到每个子决策树中,每个子决策树输出一个结果。最后,如果有了新的数据需要通过随机森林得到分类结果,就可以通过对子决策树的判断结果的投票,得到随机森林的输出结果了。
待选特征的随机选取
与数据集的随机选取类似,随机森林中的子树的每一个分裂过程并未用到所有的待选特征,而是从所有的待选特征中随机选取一定的特征,之后再在随机选取的特征中选取最优的特征。这样能够使得随机森林中的决策树都能够彼此不同,提升系统的多样性,从而提升分类性能。
Python 的内存管理机制及调优手段
内存管理机制:引用计数、垃圾回收、内存池。
引用计数
引用计数是一种非常高效的内存管理手段, 当一个 Python 对象被引用时其引用计数增加 1, 当其不再被一个变量引用时则计数减 1. 当引用计数等于 0 时对象被删除。
垃圾回收
(1) 引用计数
引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集技术。当 Python 的某
个对象的引用计数降为 0 时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如某个新建对象,它被分配给某个引用,对象的引用计数变为 1。如果引用被删除,对象的引用计数为 0,那么该对象就可以被垃圾回收。不过如果出现循环引用的话,引用计数机制就不再起有效的作用了
(2)标记清除
如果两个对象的引用计数都为 1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被
回收的,也就是说,它们的引用计数虽然表现为非 0,但实际上有效的引用计数为 0。所以先将循环引用摘掉,就会得出这两个对象的有效计数。
(3) 分代回收
从前面“标记-清除”这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统
中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额外操作。
内存池
(1) Python 的内存机制呈现金字塔形状,-1,-2 层主要有操作系统进行操作
(2) 第 0 层是 C 中的 malloc,free 等内存分配和释放函数进行操作
(3)第 1 层和第 2 层是内存池,有 Python 的接口函数 PyMem_Malloc 函数实现,当对象小于
256K 时有该层直接分配内存
(4) 第 3 层是最上层,也就是我们对 Python 对象的直接操作
Python 在运行期间会大量地执行 malloc 和 free 的操作,频繁地在用户态和核心态之间进行切换,这将严重影响 Python 的执行效率。为了加速 Python 的执行效率,Python 引入了一个内存池机制,用于管理对小块内存的申请和释放。
Python 内部默认的小块内存与大块内存的分界点定在 256 个字节,当申请的内存小于 256 字节时,PyObject_Malloc 会在内存池中申请内存;当申请的内存大于 256 字节时,PyObject_Malloc 的行为将蜕化为 malloc 的行为。当然,通过修改 Python 源代码,我们可以改变这个默认值,从而改变 Python 的默认内存管理行为。
Python 函数调用的时候参数的传递方式是值传递还是引用传递
Python 的参数传递有:位置参数、默认参数、可变参数、关键字参数。函数的传值到底是值传递还是引用传递,要分情况:
不可变参数用值传递
像整数和字符串这样的不可变对象,是通过拷贝进行传递的,因为你无论如何都不可能在原处改变不可变对象
可变参数是引用传递的
比如像列表,字典这样的对象是通过引用传递、和 C 语言里面的用指针传递数组很相似,可变对象能在函数内部改变。
什么是装饰器?
「装饰器」作为 Python 高级语言特性中的重要部分,是修改函数的一种超级便捷的方式,适当使用能够有效提高代码的可读性和可维护性,非常的便利灵活。
「装饰器」本质上就是一个函数,这个函数的特点是可以接受其它的函数当作它的参数,并将其替换成一个新的函数(即返回给另一个函数)。
装饰器本质上是一个 Python 函数,它可以在让其他函数在不需要做任何代码的变动的前提下增加额外的功能。装饰器的返回值也是一个函数的对象,它经常用于有切面需求的场景。 比如:插入日志、性能测试、事务处理、缓存、权限的校验等场景 有了装饰器就可以抽离出大量的与函数功能本身无关的雷同代码并发并继续使用。
MVC和MVVM
MVVM是Model-View-ViewModel的缩写。MVVM是一种设计思想。Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
mvc和mvvm其实区别并不大,都是一种设计思想。主要就是mvc中Controller演变成mvvm中的viewModel。mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
区别:vue数据驱动,通过数据来显示视图层而不是节点操作。
场景:数据操作比较多、频繁的场景,更加便捷。
Vue 生命周期的理解
总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后。
**创建前后:**在beforeCreated阶段,vue实例的挂载元素el和数据对象data都为undefined,还未初始化。在created阶段,vue实例的数据对象data有了,el和数据对象data都为undefined,还未初始化。在created阶段,vue实例的数据对象data有了,el还没有。
**载入前后:**在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。
**更新前后:**当data变化时,会触发beforeUpdate和updated方法。
**销毁前后:**在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在
Vue的优点
kb
;angular
的特点,在数据操作方面更为简单;react
的优点,实现了 html
的封装和重用dom
操作是非常耗费性能的, 不再使用原生的 dom
操作节点,极大解放 dom
操作,但具体操作的还是 dom
不过是换了另一种方式;react
而言,同样是操作虚拟 dom
,就性能而言, vue
存在很大的优势。本项目是我校的省级创新创业项目,负责后端开发与数据库设计,对往届冬奥会数据处理与分析,前端通过可视化展示给用户,根据往届冬奥会信息进行预测模型训练,预测各个国家在下一届冬奥会所获得的奖牌情况。
从冬奥会多个官网收集数据,进行处理,存储到MySQL数据库,进行数据库表结构设计。
首页数据缓存到Redis数据库,增加系统可维护性,提高网页的响应速度,提高并发访问量。
使用随机森林算法建立预测奖牌模型,进行各个国家奖牌预测,并将结果数据缓存到Redis中。
使用Viper配置项目参数,增加项目可扩展性,使用Swagger编写注释,方便前端理解接口和代码调试。
项目使用JWT跨域认证,增加用户数据安全性,注册使用Email进行身份验证,保障了账号的真实性。
使用Zap日志进行日志收集,提高日志收集效率,能够收集程序运行多种基本信息,便于系统调试。
介绍一下项目
本项目是我校的省级创新创业项目,我负责后端开发与数据库设计,项目主要包括两部分,第一为冬奥会的数据分析与图表展示,第二为使用随机森林算法根据往届冬奥会信息进行预测模型训练,预测各个国家在下一届冬奥会所获得的奖牌情况。采用前后端分离开发,前端采用Vue+Echarts,后端采用GoLang的gin框架进行开发,预测算法机器学习中的随机森林算法,数据库采用Mysql,数据来源为2022北京冬奥组委官网、国际奥林匹克官网等网站收集奖牌及视频数据,百度百科收集运动员个人信息数据,聚会数据网收集预测因素数据。
项目重点难点有什么
预测使用的方法为随机森林。
预测模型建立时考虑了9种可能对预测产生影响的因素,分别为(1).参加冬奥会男子数(2).参加冬奥会女子数(3).是否为主办方(4).国家人均gdp(5).国家总gdp(6).国家人口总数 (7).国家社会制度 (8).获得奖牌排行 (9).获得奖牌占总奖牌数的比率。
首先用RandomForestRegressor按照不同的权重对9种因素进行模型训练,然后使用同种方法对每一种因素进行训练,并预测出2026年的值,再将2026年的数据带入总的预测模型,并得出2026年冬奥会各个国家获得的奖牌,并将2022年的奖牌进行数据检验,最后将获得的数据进行保存。
项目使用了哪些技术
本项目采用前后端分离开发,前端采用Vue+Echarts,后端采用GoLang的gin框架进行开发,预测算法机器学习中的随机森林算法,使用的语言为Python,在开发过程中我们使用了一下技术:
项目有哪些亮眼的设计
从项目中学到了什么
本项目是基于Micro框架开发的通用的账户功能系统,包括账户的基本操作如账号增删改查和权限操作,使用配置中心、链路追踪、监控等相关技术,详情查看博客:https://juejin.cn/column/7179162138446364732
使用ProtoBuf进行参数传输和定义相关接口,定义了13个相关的账户功能接口。
使用Redis缓存Email验证码和存储Token,提高用户注册效率,实现定时清除Token机制。
使用Docker部署多个微服务插件,如Consul,Jaeger,Prometheus等,降低开发难度,提高开发效率。
客户端使用负载均衡,大大提高项目的并发量,使用Prometheus进行项目监控,及时发现并解决问题。
介绍一下项目
项目是基于Micro框架开发的通用的账户功能系统,采用Mysql+Redis+Docker进行项目开发,包括账户的基本操作如账号增删改查和权限操作,使用配置中心、链路追踪、监控等相关技术,使用ProtoBuf进行数据传输和Api接口定义,相关插件使用Docker进行安装,避免花费大量精力放在安装部署方面。
项目重点难点有什么
项目使用了哪些技术
项目有哪些亮眼的设计
从项目中学到了什么
学到了微服务开发流程:
创建项目(Docker或go-micro)
编写proto文件,并生成.go文件
编写domain数据库方面,包含(model层,repository层,service层)等
编写Handle层,实现proto定义接口
编写common层,配置,mysql,公共函数,jaeger(链路追踪)等
编写main函数,完成项目闭环
Docker、Docker Compose的基本使用
Proto的使用、命令、编写
对微服务组件有了更加深入的了解
对微服务的作用、使用和开发有了更加深入的了解
对中间件整合到项目中更加熟悉