牛客网、leetcode面经整理
操作系统
同步、异步,阻塞、非阻塞
同步:一个同步调用发出后,调用者要等待返回结果,才能进行后续的执行
异步:一个异步过程调用发出后,调用者不能立刻得到返回结果,处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者(同步异步更关注消息通知机制)
阻塞IO:用户程序执行
read
,线程阻塞,一直等到内核数据准备好并从内核缓冲区拷贝到应用程序的缓冲区非阻塞:read 在数据未准备好的情况下立即返回,用户程序继续往下执行,但需要轮询内核,内核将数据拷贝到应用程序缓冲区后
read
调用获取到结果(最后一次 read 调用中获取数据是一个同步的过程,需要等待数据从内核态拷贝到用户程序的缓冲区)同步阻塞:用户程序执行
read
,线程阻塞,一直等到内核数据准备好并从内核缓冲区拷贝到应用程序的缓冲区小明去点火烧水(发消息),在烧水过程中,小明啥也不干,端个板凳坐着傻等(阻塞),等水开后,小明才能做下一步处理(同步)
同步非阻塞:read 在数据未准备好的情况下立即返回,用户程序继续往下执行,但需要轮询内核,内核将数据拷贝到应用程序缓冲区后
read
调用获取到结果(最后一次 read 调用中获取数据是一个同步的过程,需要等待数据从内核态拷贝到用户程序的缓冲区)小明去点火烧水(发消息),在烧水过程中,小明跑去看电视了(非阻塞),但它时不时查看一下水烧开没有,最后等水开了,小明才能做下一步处理(同步)——小明还是要亲自去看水是否烧开没有,即同步
异步阻塞:
小明换了个带有铃铛的水壶,小明去点火烧水(发消息),在烧水过程中,小明啥也不干,端个板凳坐着傻等(阻塞),等水开后,铃铛自动会响通知小明水已经烧好(异步)
异步非阻塞:「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待
小明换了个带有铃铛的水壶,小明去点火烧水(发消息),在烧水过程中,小明跑去看电视了(非阻塞),等水开后,铃铛自动会响通知小明水已经烧好(异步)
IO
- 文件系统的默认IO操作是缓存IO。OS将IO的数据缓存在文件系统的页缓存(page cache)——数据先被拷贝到内核的缓冲区中,再从内核缓存区拷贝到应用程序的地址空间——因为应用程序不能直接操作底层硬件
- IO阶段:数据准备阶段 + 内核空间复制回用户进程缓冲区阶段
BIO/NIO/AIO
同步阻塞IO:用户进程发起一个IO操作后,等待IO操作真正完成才继续运行
同步非阻塞IO:客户端与服务器通过Channel连接,多路复用器轮询注册的Channel。用户进程发起一个IO操作后,可做其它事情,但需要轮询IO操作是否完成,造成不必要的CPU资源浪费
select,poll,epoll都是IO多路复用的机制,I/O多路复用可以监视多个文件描述符(所有的I/O设备都被抽象为了文件),一旦某个描述符就绪(一般是读就绪或者写就绪),能通知程序进行相应的读写
- 多路:多个socket网络连接
- 复用:复用一个线程
select,poll,epoll本质上都是同步I/O,要在读写事件就绪后自己负责读写
select:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 类似bitmap
static __inline__ void __FD_SET(unsigned long fd, __kernel_fd_set *fdsetp)
{
unsigned long _tmp = fd / __NFDBITS;
unsigned long _rem = fd % __NFDBITS;
fdsetp->fds_bits[_tmp] |= (1UL<<_rem);
}
typedef struct {
unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;
- 将需要监控的fd(以socket为例)放到一个文件描述符集合,select将集合拷贝到内核空间,遍历监控的socket,顺次调用socket的poll检查该sk是否有可读事件。如果没有任何一个sk可读,select会调用schedule_timeout进入schedule循环,使得process进入睡眠。如果某个sk有数据可读,将整个集合拷贝回用户空间,用户态里再次遍历找可读/可写的socket
- 两次遍历集合,两次拷贝
- 问题:
- 每次select都要拷贝两次
- 能监听端口的数量有限,即单个进程所能打开的最大连接数有限——使用固定长度的 BitsMap 表示文件描述符集合,单个进程所能打开的最大连接数由FD_SETSIZE宏定义,其大小是32*整数的大小(32位机器为32,64位机器为64),默认为1024个fd
- 被监控的fds集合只要有一个有数据可读,整个socket集合会遍历一次,以收集可读事件
- 将需要监控的fd(以socket为例)放到一个文件描述符集合,select将集合拷贝到内核空间,遍历监控的socket,顺次调用socket的poll检查该sk是否有可读事件。如果没有任何一个sk可读,select会调用schedule_timeout进入schedule循环,使得process进入睡眠。如果某个sk有数据可读,将整个集合拷贝回用户空间,用户态里再次遍历找可读/可写的socket
poll:和select相似,但描述fd集合的方式不同,使用了pollfd结构而不是select的fd_set结构。也是进行轮询,但无最大文件描述符数量的限制(基于链表组织)
1
2
3
4
5
6int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /*文件描述符*/
short events; /*监控的事件*/
short revents; /*监控事件中满足条件返回的事件*/
};epoll:创建好epoll句柄后,会占用一个fd值(查看/proc/进程id/fd/ 能够看到这个fd),使用完epoll后必须调用close()
三个函数:
- epoll_create:创建一个epoll句柄(
int epoll_create(int size);
),size可忽略,只要大于0即可。如果成功,返回 epoll 专用的文件描述符- 创建一个eventpoll结构体
- 包含等待队列链表(软中断数据就绪时通过该链表找阻塞在该epoll对象上的用户进程)、红黑树(管理主进程accept的所有socket连接)、就绪的描述符链表(应用进程只需要查找该链表就能找出就绪进程,而不用去遍历红黑树的所有节点)
- epoll_ctl:向 epoll 对象添加/修改/删除要监视的连接(
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
)——epfd为create的返回值,op包括注册新fd、删除fd、修改fd的监听事件,fd为监听的描述符 - epoll_wait:等待连接上的 IO 事件(
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
),等待事件的产生,收集在 epoll 监控的事件中已经发送的事件
- epoll_create:创建一个epoll句柄(
解决方案:
使用红黑树跟踪所有监控的fd。select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 在内核维护了红黑树,可以保存所有待检测的 socket ,只需要传入一个待检测的 socket
epoll 使用事件驱动的机制,内核维护一个记录就绪事件的链表。当某个socket有事件发生,内核通过回调函数将其加入链表,用户调用
epoll_wait
时只拷贝该链表中的socket,不需要轮询整个socket集合
水平触发模式EPOLL LT:被监控的 Socket 关联的内核读(写)缓冲区非空,则一直发送可读(写)信号,直到内核缓冲区数据取完才结束——内核通知文件描述符可读写时,还可以继续去检测它的状态,看它是否依然可读或可写
边缘触发模式EPOLL ET:被监控的 Socket 关联的内核读缓冲区由空转为非空,则发出可读信号,内核写缓冲区由满转为不满,则发出可写信号,只通知一次,程序要保证一次性将内核缓冲区的数据取完(一直读)
例如,读缓冲区开始为空,写入2kb数据,此时LT和ET都发出可读信号,LT下读取1kb数据后会再次通知,而ET下读取1kb数据后不会再通知,因此ET需要一直读,直到读到EGAIN
异步非阻塞IO:非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,read和write方法均是异步方法。用户进程发起一个IO操作后立即返回,等IO操作真正完成后,应用程序会得到IO操作完成的通知
Reactor和Proactor
- 封装IO多路复用
Reactor:同步
对事件反应,即来了一个事件 Reactor 有相对应的反应/响应。收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程
Reactor 模式由 Reactor 和处理资源池组成,前者监听和分发事件(连接事件、读写事件),后者处理事件(read -> 业务逻辑 -> send)
Reactor可以是多个,处理资源池可以是多进程/线程(C为多进程,java由于有jvm通常采用多线程)
单 Reactor 单进程 / 线程
- Reactor 对象监听和分发事件,Acceptor 对象获取连接,Handler 对象处理业务
- dispatch 是分发事件操作,select、accept、read、send 是系统调用
- 过程:
- Reactor 通过 select (IO 多路复用接口) 监听事件,收到事件后根据事件类型通过 dispatch 分发给 Acceptor 对象或 Handler 对象
- 连接建立的事件交由 Acceptor,Acceptor通过 accept 获取连接并创建一个 Handler 处理后续的响应
- 不是连接建立事件则交由当前连接对应的 Handler 进行响应,Handler 通过 read -> 业务处理 -> send 完成完整的业务流程
- 如果 handler 处理业务时无法处理其他的连接事件,因此此方案只是和业务处理较为快速的场景——redis 6.0之前为此方案
单 Reactor 多线程 / 多进程(多进程比较少,因为多线程之间可以共享数据,多进程需要考虑通信问题)
过程:
- 前面步骤相同
- Handler 不负责业务处理,只负责数据的接收和发送。通过 read 读取到数据后将数据发给子线程的 Processor 处理业务
- Processor 将处理结果发给主线程中的 Handler 以响应 client
Reactor 对象只有一个,容易成为性能瓶颈
多 Reactor 多进程 / 线程
- 过程:
- 主线程的 MainReactor 通过 select 监控连接建立事件,通过 Acceptor 获取连接并分配给某个子线程
- 子线程的 SubReactor 将 MainReactor 分配的连接加入 select 继续监听,创建一个 Handler 处理响应事件
- 新的事件发生时,SubReactor 调用当前连接对应的 Handler 响应,通过 read -> 业务处理 -> send 完成完整的业务
- 主线程只需要把新连接传给子线程,子线程无须返回数据,直接将处理结果发送给客户端——Netty和Memcache采用多 Reactor 多线程
- 过程:
Thread Pool 仅用来处理非I/O操作的逻辑,I/O的accept()、read()、write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程 或 subReactor线程)中完成
Proactor:异步
Proactor 是异步网络模式, 感知已完成的读写事件。发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,系统内核自动把数据的读写工作完成,并通知应用进程直接处理数据——「来了事件操作系统来处理,处理完再通知应用进程」
过程:
- Proactor Initiator 创建 Proactor 和 Handler 对象,将 Proactor 和 Handler 通过 Asynchronous Operation Processor 注册到内核
- Asynchronous Operation Processor 处理注册请求,并处理 I/O 操作
- Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor
- Proactor 根据不同的事件类型回调不同的 Handler 处理业务
进程
进程控制块(PCB)
- 包含:
- 进程描述信息:
- 进程标识符:标识各个进程
- 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
- 进程控制和管理信息
- 进程当前状态(new、ready、running、waiting 或 blocked)
- 进程优先级:抢占 CPU 时的优先级
- 资源分配清单:内存地址空间或虚拟地址空间的信息,打开文件列表和使用的 I/O 设备信息
- CPU 相关信息:CPU 中各个寄存器的值(进程切换时,CPU 的状态信息会被保存在相应的 PCB 中,以便重新执行时,从断点处恢复)
- 进程描述信息:
- 结构:
- 通过链表的形式组织(相同状态的进程链在一起,形成队列)——就绪队列、不同等待事件的阻塞队列
- 运行队列在单核 CPU 系统中只有一个运行指针,因为一个时间只能运行一个程序
进程、线程、协程的区别
- 进程(Process)是系统进行资源分配和调度的基本单位,线程(Thread)是CPU调度和分派的基本单位
- 一个进程至少有一个线程
- 进程的地址空间独立,线程共享所属进程的地址空间
- 线程自己基本不拥有系统资源,和其他线程共享本进程的相关资源如内存、I/O、cpu等
- 线程ID
- 寄存器组的值:当线程切换时,必须将原有的线程的寄存器集合的状态保存
- 线程的函数堆栈:使得函数调用可以正常执行,不受其他线程的影响
- 线程优先级:线程调度的次序
- 进程切换涉及到当前进程CPU环境的保存和新进程CPU环境的设置(需要切换页表),线程切换只需保存和设置少量寄存器——切换进程开销远大于切换线程开销
- 线程的通信方便(通过共享全局变量数据),进程之间的通信需要以进程间通信的方式进行
- 多线程程序只要有一个线程崩溃,整个程序就崩溃了,但多进程程序中一个进程崩溃并不会对其它进程造成影响,因为进程有自己的独立地址空间,因此多进程更加健壮
- 各个线程的地址空间是共享的,某个线程对地址的非法访问会导致内存的不确定性,可能会影响到其他线程。此时会有信号发给进程,暂停当前程序并交给os控制,os根据信号找相应的信号处理函数(可以注册,也可以执行默认的处理函数)
- JVM 不会崩溃:jvm自己定义了信号处理函数,当发送 kill pid 命令(默认传 15 也就是 SIGTERM)后,JVM 在信号处理函数中先清理资源后再调用 exit 退出,或者不退出
- 协程:一个线程有多个协程
- 调度:
- 线程被调度切换时,需要保存一个用户线程的状态到内存,恢复另一个线程状态到寄存器,然后更新调度器的数据结构——内核负责
- 协程的调度完全由用户控制,拥有自己的寄存器上下文和栈,调度时将寄存器上下文和栈保存到其他地方,切回来的时候恢复先前保存的寄存器上下文和栈,直接操作用户空间栈,没有内核切换的开销
- 在同一个线程上,因此可以避免竞争关系而使用锁
- 协程的栈最小为2KB,可以动态伸缩,线程的栈一般为2MB
- 调度:
进程状态转换
三种状态:就绪态、运行态和阻塞态
- 就绪状态:进程获得除cpu外的所需资源,等待分配cpu资源
- 运行状态:占用cpu运行
- 阻塞状态: 进程等待某条件,条件满足前无法执行
通常阻塞的进程换出到硬盘,需要运行的时候再换入内存
挂起:进程没有占用实际的物理内存空间
阻塞挂起:进程在外存(硬盘)并等待某个事件的出现
就绪挂起:进程在外存(硬盘),只要进入内存就立刻运行
进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源,通常交换的信息保存在进程的 PCB,运行另外一个进程的时候 PCB 取出上下文恢复到 CPU
不同进程之间的地址隔离怎么做到——为了实现进程隔离,采用了虚拟地址空间,两个进程各自的虚拟地址不同,从逻辑上来实现彼此间的隔离
进程切换的场景
- 时间片到了:CPU 时间被划分为时间片,分配给各个进程。某个进程的时间片耗尽,进程从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行
- 等待资源:进程在资源不足时被挂起,由系统调度其他进程运行
- 主动挂起:进程通过睡眠函数 sleep 等将自己主动挂起
- 优先级抢占:有优先级更高的进程运行时,当前进程被挂起
- 中断:发生硬件中断时,CPU 上的进程会被中断挂起,执行内核的中断服务程序
进程的控制(不同阶段发生了什么)
创建进程:
- 申请一个空白的 PCB,向 PCB 填写一些控制和管理进程的信息,如:唯一标识
- 分配运行时所必需的资源,比如内存资源
- PCB 插入到就绪队列
终止进程:
- 查找需要终止的进程的 PCB
- 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程
- 如果其还有子进程,则子进程交给 1 号进程接管
- 进程所拥有的全部资源都归还给操作系统
- 将其从 PCB 所在队列中删除
阻塞进程:
- 找到对应的 PCB
- 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行
- 将该 PCB 插入到阻塞队列
唤醒进程:只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒
- 在该事件的阻塞队列中找到相应进程的 PCB
- 从阻塞队列中移出,转为就绪状态
- 把该 PCB 插入到就绪队列中,等待调度程序调度
进程间通信(IPC)
管道
- 半双工,具有固定的读端和写端
- 父子进程、兄弟进程之间通信
- 视为一种特殊的文件,读写可以用普通的 read、write 等函数,但它不属于文件系统,只存在于内存中
命名管道
- FIFO 可以在无关的进程之间交换数据
- FIFO 有路径名与之相关联,以一种特殊设备文件形式存在于文件系统
消息队列:消息的链接表,存放在内核,一个消息队列由一个标识符 ID 来标识
- 面向记录,消息具有特定的格式和特定的优先级
- 独立于发送与接收进程,进程终止时消息队列及其内容不会被删除
- 存在用户态与内核态之间的数据拷贝开销
- 消息不一定要以FIFO读取,也可以按消息的类型读取
共享内存:两个或多个进程共享一个给定的存储区
- 拿出一块虚拟地址空间,映射到相同的物理内存
- 最快的一种 IPC——进程直接对内存进行存取
信号量:是一个计数器,实现进程间的互斥与同步,不存储进程间通信数据,若要传递数据需要结合共享内存
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作;
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数;
P:信号量减 1,相减后如果信号量 < 0,表明资源已被占用,进程需阻塞等待
V:信号量加 1,相加后如果信号量 <= 0,表明当前有阻塞中的进程,将该进程唤醒运行
信号(signal/kill):通过
kill -l
查看所有的信号,kill -9 1050——给进程1050发送9号信号- 进程间通信机制中唯一的异步通信机制
- 处理方式:执行默认操作、捕捉信号、忽略信号
- 通知接收进程某个事件已经发生
套接字(Socket)
不同主机间进程通信、同一主机中进程通信
系统调用:
int socket(int domain, int type, int protocal)
- domain:指定协议族,
AF_INET
用于 IPV4、AF_INET6
用于 IPV6、AF_LOCAL/AF_UNIX
用于本机 - type:指定通信特性,
SOCK_STREAM
表示字节流(对应TCP)、SOCK_DGRAM
表示数据报(对应UDP)、SOCK_RAW
表示原始套接字 - protocal:用来指定通信协议,现在基本废弃(前两个参数指定即可),一般为0
- domain:指定协议族,
通信方式
- TCP 字节流通信
- UDP 数据报通信
- 本地进程间通信:本地字节流 socket 参数是
AF_LOCAL
和SOCK_STREAM
,本地数据报 socket 参数是AF_LOCAL
和SOCK_DGRAM
线程同步方式
- **互斥量(Mutex)**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限
- 信号量(Semaphore) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
- 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步
进程调度:抢占式调度和非抢占式调度
- 非抢占式调度
- 先来先服务
- 最短作业优先
- 最短剩余时间优先
- 最高响应比优先
- 抢占式调度
- 时间片轮转:每个进程被分配一个时间段
- 优先级调度算法
- 多级反馈队列调度:N个队列,各个队列的时间片是随着优先级的增加而减少
进程的异常控制流:陷阱、中断、异常和信号
- 陷阱:
- 同步异常,是执行一条指令的结果
- 主要作用是实现系统调用:中断当前的控制流,陷入到内核态,执行相应的系统调用,处理结束后将结果返回给进程,进程继续执行下一条指令
- 中断:在计算机运行过程中,当发生某个事件后,CPU 会停止当前程序流,转而去处理该事件,并在处理完毕后继续执行原程序流
- 处理器外部硬件产生,不是执行某条指令的结果——中断是异步事件
- 中断包括I/O中断(I/O设备)、时钟中断(各种定时器)等
- 软中断是一条 CPU 指令,由当前正在运行的进程产生,软中断模拟了硬中断的处理过程。系统调用是一种软中断处理程序,用于让程序从用户态陷入内核态,以执行相应的操作
- 异常:是执行当前指令产生的错误情况,可能被错误处理程序修正,也可能直接终止应用程序——是同步的
- 信号:
- 一种更高层的软件形式的异常,会中断进程的控制流,可以由进程处理
- 一个信号代表了一个消息,用来通知进程发生了某种系统事件
守护进程、僵尸进程和孤儿进程
- 守护进程:
- 在后台运行的,没有控制终端与之相连的进程,独立于控制终端,周期性执行某种任务
- Linux大多数服务器用守护进程的方式实现,如web服务器进程http
- 僵尸进程
- 一个子进程结束后,它的父进程没有等待它(调用wait或者waitpid),子进程将成为一个僵尸进程
- 已经死亡但没有真正被销毁的进程,没有任何可执行代码,不能被调度,占用进程表的一个位置(记载该进程的进程ID、终止状态、资源利用信息——CPU时间,内存使用量等等)供父进程收集
- 危害:占用进程号(进程号有限),占用内存
- 解决:
- 进程结束时,系统会扫描是否其存在子进程,如果有则用Init进程接管,Init成为该进程的父进程,调用wait等待其结束
- 父进程调用wait或者waitpid等待子进程结束(每隔一段时间查询子进程是否结束):
- wait系统调用会使父进程暂停执行,直到一个子进程结束为止
- waitpid系统调用可以加入WNOHANG(wait-no-hang)选项,如果没有结束的子进程则立即返回。waitpid可以选择:等待任一子进程(同wait)、等待指定pid的子进程、等待同一进程组下的任一子进程、等待组ID等于pid的任一子进程
- 子进程结束时,系统产生SIGCHLD(signal-child)信号,可以注册一个信号处理函数,在该函数中调用waitpid,等待所有结束的子进程——一般需要循环调用waitpid,因为在信号处理函数开始执行之前,可能已经有多个子进程结束;可以用
signal(SIGCLD, SIG_IGN)
(signal-ignore)通知内核,表示父进程忽略SIGCHLD信号,子进程结束后内核会进行回收
- 孤儿进程:父进程结束,其子进程还在运行,这些子进程将成为孤儿进程。会被Init(进程ID为1)接管,孤儿进程结束时由Init完成状态收集工作
死锁
- 某进程集合中,每个进程都等待只能由该进程集合的其他进程引发的事件,该进程集合是死锁的
- 资源死锁是竞争性同步问题,通信死锁是协同同步问题
- 必要条件
- 互斥:某资源仅为一个进程占用
- 占用并等待:进程因请求资源而阻塞时,不释放已有资源
- 不可抢占:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
- 环路等待:在发生死锁时,必然存在一个进程–资源的环形链
- 解决
- 预防
- 破坏请求条件:一次性分配所有资源
- 破坏请保持条件:只要一个资源不被分配,其他资源也不被分配给该进程
- 破坏不可剥夺条件:某进程获得了部分资源,但得不到其它资源,则释放已占有的资源
- 破坏环路等待条件:每类资源编号,进程按编号递增的顺序请求资源,释放则相反
- 避免:银行家算法
- 解除
- 资源剥夺:挂起某些死锁进程,剥夺其资源,分配给其他死锁进程
- 撤销进程:强制撤销部分、甚至全部死锁进程并剥夺进程的资源——按进程优先级和撤销进程代价的高低进行
- 进程回退:一个或多个进程回退到足以避免死锁的地步,系统保持进程的历史信息,设置还原点
- 预防
典型的锁
读写锁
- 多个读者同时读
- 写者互斥(一个写者写,不能读写同时进行)
- 写者优先读者(一旦有写,则后续读必须等待,唤醒时优先考虑写)
互斥锁:
- 一次只能一个线程拥有互斥锁
- 抢锁失败的线程进入睡眠状态,直到锁的状态改变时再唤醒
- 为了实现锁的状态改变时唤醒阻塞的线程或者进程,互斥锁由操作系统管理,因此在加锁操作时涉及上下文的切换
条件变量(信号量)
- 互斥锁只有两种状态:锁定和非锁定
- 条件不满足时,线程解开相应的互斥锁并阻塞线程然后等待条件变化。一旦其他线程改变了条件变量,将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程
- 互斥锁是线程间互斥的机制,条件变量是同步机制
自旋锁
- 进线程无法取得锁时,不会立刻放弃CPU,而是一直循环尝试获取锁,直到获取为止——容易浪费CPU资源
- 一般应用于加锁时间很短的场景
- 在单核 CPU 上,需要抢占式的调度器
- 互斥锁加锁失败时,会从用户态陷入到内核态,让内核切换线程地址隔离
进程中不同线程的地址隔离
内存
分页与分段的区别?
三种内存存储(虚拟地址与物理地址之间的关系)
页式存储:用户地址空间划分为大小相等的页(page),内存空间划分为同样大小的页框,分配时以页为单位,按进程需要的页数分配
多级页表:虚拟地址到物理地址的转换多了几道转换的工序
TLB: CPU 芯片中加入一个专门存放程序最常访问的页表项的 Cache,TLB(Translation Lookaside Buffer) ——页表缓存、转址旁路缓存、快表。CPU 在寻址时先查 TLB,如果没找到再查常规的页表
段式存储:用户地址空间按照自身逻辑关系划分为四个segment(代码段,数据段,堆栈段),内存空间被动态划分为长度不同的区域,分配时以段为单位,每段在内存中占据连续空间,各段可以不相邻
段页式存储:用户进程先按段划分,段内再按页划分,内存划分和分配按页。三次内存访问:访问段表,得到页表起始地址;访问页表,得到物理页号;物理页号与页内位移组合,得到物理地址
- 区别:
- 目的不同:
- 分页的目的是管理内存,用于虚拟内存以获得更大地址空间
- 分段的目的是满足用户的需要,使程序和数据可以被划分为逻辑上独立的地址空间
- 大小不同:段的大小不固定,由相应功能决定;页的大小固定,由系统决定
- 地址空间维度不同:
- 分段是二维地址空间(段号+段内偏移)
- 分页是一维地址空间(每个进程一个页表/多级页表,通过一个逻辑地址就能找到对应的物理地址)
- 分段便于信息的保护和共享;分页的共享受限
- 碎片:
- 分段没有内碎片,会产生外碎片(不属于当前进程的闲置空间)——内存交换的方式解决。将占用的内存写入磁盘,再从磁盘读到内存,放到已经被占用的内存后
- 分页没有外碎片,会产生内碎片(一个页填不满)
- 目的不同:
详细解析:为什么要有虚拟内存
物理地址、逻辑地址、虚拟内存
进程持有的虚拟地址通过 CPU 芯片中的内存管理单元(MMU)的映射关系,转换成物理地址
物理地址:地址转换的最终地址,是内存单元真正的地址
逻辑地址:计算机用户看到的地址,如一个长度为100的整型数组,OS返回一个逻辑上的连续空间——OS通过地址映射将物理地址映射成连续的逻辑地址
虚拟内存:计算机系统内存管理的一种技术,使得应用程序认为它拥有连续的可用的内存(连续完整的地址空间),实际地址空间被分隔成多个物理内存碎片,甚至部分存储在磁盘,需要时进行数据交换;每个进程的虚拟内存空间相互独立,解决了多进程之间地址冲突的问题。
地址空间到物理内存的映射:
- 内存管理单元(MMU)管理逻辑地址和物理地址的转换
- MMU中的页表(Page table)存储页(逻辑地址)和页框(物理内存空间)的映射表
- 逻辑地址:页号+页内偏移;一个进程一个页表,页表起始地址在PCB中、
如果申请的物理内存超过实际物理内存,开启了Swap机制则程序可以正常运行,没有开启则系统会提示内存溢出
OOM
- 空闲的物理内存仍然无法满足此次物理内存的申请时触发
- 根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置
- 选择标准:内核有一个
oom_badness()
函数,扫描系统中可以被杀掉的进程并打分——进程已经使用的物理内存页面数、每个进程的 OOM 校准值 oom_score_adj(通过/proc/[pid]/oom_score_adj
)。不想某个进程被首先杀掉,调整该进程的 oom_score_adj;某个进程永远不被杀掉,oom_score_adj 配置为 -1000
寄存器和内存的区别,为什么比内存快
内存离CPU比较远
寄存器的工作方式只有2步:(1)找到相关的位(2)读取这些位
内存的工作方式:
(1)找到数据的指针(指针可能存放在寄存器内,所以这一步就已经包括寄存器的全部工作)
(2)将指针送往内存管理单元(MMU),虚拟内存地址翻译成物理地址
(3)物理地址送往内存控制器(memorycontroller),由于内存控制器找出该地址在哪一根内存插槽(bank)上
(4)确定数据在哪一个内存块(chunk)上,从该块读取数据
(5)内存先送回内存控制器,再送回CPU
页面置换算法
- 先进先出置换(FIFO):淘汰最早调入的页面
- 最佳置换(OPT):淘汰未来最远将使用的页,最优的方案但不可行
- 最近最久未使用(LRU):淘汰最近最久未使用的页面
- 时钟(Clock)置换(最近未用,NRU):每个页面设置一位访问位,内存中的所有页面都通过链接指针链成一个循环队列
- 每一页设置一个访问位,将内存中所有页面都通过链接指针链接成一个循环队列
- 当某页被访问时,其访问位为1。淘汰时,检查页的访问位。若是0,则将该页换出;若是1,则重新将它置为0,暂时不换出,接着按照FIFO的算法检查下一个页面。当检查到队列中最后一个页面时,其访问位仍为1,则再放回队首,检查下一个页面
- 最不经常使用置换(NFU):淘汰访问次数最少的页面
预读失效和缓存污染
- 预读失效:额外读一些用不到的数据,命中率下降
- OS 缓存读取的文件数据行到文件系统的 Page Cache,下一次访问相同的数据不需要通过磁盘 I/O
- 预读:只想读取文件 A 的 offset 为 0-3KB 的数据,由于磁盘基本读写单位为 block(4KB),因此会读 0-4KB 的内容。同时根据局部性原理,offset [4KB,8KB)、[8KB,12KB) 都加载到内存,额外多了三个page
- 失效:提前加载的页没被访问,预读白做;LRU会将「预读页」放到链表头部,内存空间不够还淘汰末尾的页——不被访问的预读页却占用了 LRU 链表前排的位置,末尾淘汰的页可能是热点数据
- 改进:
- linux:
- 活跃 LRU 链表(active_list)和非活跃 LRU 链表(inactive_list)
- active list 存放最近被访问过(活跃)的内存页;inactive list 存放很少被访问(非活跃)的内存页
- 预读页加入 inactive list 头,当页被真正访问才插入 active list 头,如果一直没有被访问就会从 inactive list 移除
- mysql:
- 在一个 LRU 链表上划分 young 区域 和 old 区域,分别在 LRU 链表的前半部分和后半部分,有各自的头和尾节点
- 预读的页加入到 old 区域的头部,页被真正访问才将页插入 young 区域的头部
- linux:
- 缓存污染:批量读数据时热点数据被淘汰,而这些数据只被请求一次
- 提高进入到活跃 LRU 链表(或者 young 区域)的门槛,保证活跃 LRU 链表(或者 young 区域)里的热点数据不会被轻易替换掉
- Linux:内存页被访问第二次才将页从 inactive list 升级到 active list
- MySQL Innodb:内存页被访问第二次,判断停留在 old 区域的时间,第二次的访问时间与第一次访问的时间在 1 秒内(默认值),该页不会升级到 young 区域,否则升级
动态链接库和静态链接库
- 静态链接:编译链接时将需要的执行代码拷贝到调用处。优点:不需要带着库一起发布程序;缺点:体积相对大
- 动态链接:编译链接时不直接拷贝可执行代码,在程序运行或加载时OS将需要的动态库加载到内存,程序运行到指定的代码时去共享执行内存中寻找动态库。优点:多个程序共享同一段代码;缺点:影响程序的前期执行性能
用户态和内核态
操作系统的两种运行状态
- 内核态:内核态的CPU可以访问任意数据,可以从一个程序切换到另外一个程序,占用CPU不会发生抢占,一般处于特权级0
- 用户态:用户态的CPU只能访问部分内存,不允许访问外围设备,不允许独占,能够被其他程序获取。用户程序都运行在用户态,进行内核态的操作(从硬盘、键盘读数据)时,进行系统调用,使用陷阱指令CPU切换到内核态,执行相应的服务后切换回用户态并返回系统调用的结果
磁盘调度
磁盘一般有多个盘片,每个盘片有自己的磁头,每个盘片分为多个磁道,每个磁道分多个扇区,每个扇区是
512
字节。多个具有相同编号的磁道形成一个圆柱,称之为磁盘的柱面磁盘调度——优化磁盘的请求顺序,减少寻道时间
先来先服务
最短寻道时间优先:优先选择从当前磁头位置所需寻道时间最短的请求——可能存在“饥饿”
假设是一个动态的请求,如果后续来的请求都是小于 183 磁道的,那么 183 磁道可能永远不会被响应,于是就产生了饥饿现象——产生饥饿的原因是磁头在一小块区域来回移动
扫描算法:磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向
循环扫描算法:只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道(复位磁头),返回中途不处理任何请求
LOOK 和 CLOOK:对上面两个的优化,磁头在移动到「最远的请求」位置,然后立即反向移动(不是移动到磁盘的最始端或最末端)
检测下
- 说下进程的状态
- 说下进程和线程的联系与区别
- 为什么进程上下文切换比线程上下文切换代价高?
- 说下你对进程同步的理解
- 对多个相关进程在执行次序上进行协调,以使并发执行的诸进程之间能有效地共享资源和相互合作
- 同步方式:临界区、互斥锁、信号量、事件(Event)
- 进程的通信方式有哪些
- 进程调度的种类有哪些?
- 非抢占式调度与抢占式调度的区别是什么?
- 让进程运行直到结束或阻塞的调度方式
- 允许将逻辑上可继续运行的在运行过程暂停的调度方式
- 说下你知道的调度算法
- 一个程序从开始运行到结束的完整过程(四个过程)
- 死锁出现的条件?
- 如何处理死锁问题
- 什么是临界资源
- 介绍一下内存池、进程池、线程池
- 动态链接库与静态链接库的区别
- 说下对虚拟内存的理解
- 页面置换算法了解多少?
- 中断与系统调用了解吗?
- 用户态切换到内核态的方式有哪些?
- 用户态和核心态(内核态)之间的区别是什么呢?
- 内部碎片与外部碎片分别是什么?
- 系统调用与库函数的区别
- 守护、僵尸、孤儿进程的概念
计算机网络
OSI与TCP/IP体系结构
- OSI:七层,物理层、数据链路层、网络层、传输层、会话层、表示层、应用层(应用层在用户态,其他在内核态)
- TCP/IP:应用层(Telnet、FTP、SMTP)、传输层(TCP、UDP)、网络层(IP协议)、网络接口层
- TCP/IP四层是OSI七层的简化,是目前的标准
- 网络接口层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)
HTTP,HTTPS,Get和Post
HTTP:超文本传输协议,定义计算机之间交流通信的规范和各种控制和错误处理方式
状态码:
- 1xx:提示信息,协议处理的中间状态
- 2xx:成功,报文收到并正确处理
- 200:请求成功(一般用于GET与POST请求)
- 201:Created,已创建。成功请求并创建了新的资源
- 202:Accepted,已接受。已经接受请求,但未处理完成
- 204:No Content,服务器成功处理,但未返回内容(响应没有 body 数据)
- 206:Partial Content,服务器成功处理了部分GET请求(应用于 HTTP 分块下载或断点续传)
- 3xx:重定向,资源位置发生变动,需要客户端重发请求
- 301:永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI
- 302:Found,临时移动。与301类似。但资源只是临时被移动,客户端应继续使用原有URI
- 304:Not Modified,所请求的资源未修改,服务器返回此状态码时,不会返回任何资源(浏览器从缓存中检索网页的保存版本,防止浏览器重复下载相同的信息)
- 305:Use Proxy,所请求的资源必须通过代理访问,代理地址在 Response 的 Location 中
- 4xx:客户端错误,请求报文有误
- 400:Bad Request,客户端请求的语法错误,服务器无法理解
- 401:未授权,要求身份验证
- 403:Forbidden,服务器禁止访问资源(理解请求含义)
- 404:Not Found,请求的资源在服务器上不存在
- 5xx:服务器错误,服务器处理请求时内部出现问题
- 500:Internal Server Error,服务器内部错误,无法完成请求
- 501:Not Implemented,服务器不支持请求的功能
- 502:Bad Gateway,服务器作为网关或代理时返回的错误码,服务器自身工作正常但访问后端服务器发生错误
- 503:Service Unavailable,服务器正忙暂时无法处理请求
转发和重定向的区别:
转发是服务器行为,服务器向目标地址访问URL,读取相应内容并发给浏览器,转发页面和转发到的页面可以共享request里面的数据
重定向利用服务器返回的状态码实现。如果服务器返回301或302,浏览器会自动跳转到新的网址,重新请求资源,用户的地址栏URL发生改变
Get和Post区别
- get:
- 获取数据
- 安全(数据可以是任意格式的数据),幂等(多次执行相同的操作结果「相同」),可缓存
- 发送的数据或参数是 URL 的一部分,只能为ASCII码,浏览器对URL有长度限制
- RFC 规范并没有规定 GET 请求不能带 body
- post:
- 提交数据
- 不安全,无幂等性(URI是去执行创建动作的操作者,例如post一个帖子,两次post会创建两个帖子,两次响应中包含帖子的创建状态以及帖子的不同URI)
- 数据可以是任意格式的数据,放在 body 中
- 可以用 GET 方法实现新增或删除数据的请求
- get:
幂等方法:
- put(从客户端向服务器传送的数据取代指定的文档的内容,且为全部取代)、delete(请求服务器删除某个文档)
- patch(从客户端向服务器传送的数据取代指定的文档的内容,且为部分取代)不是幂等:PATCH提供的实体则需要根据程序或其它协议的定义,解析后在服务器上执行,即PATCH请求是会执行某个程序的,如果重复提交,程序可能执行多次
HTTP缓存(状态码304):
缓存静态资源:js、css、img(html和业务数据随时可能更新,不能缓存)
缓存策略:
强制缓存:
- 只要浏览器判断缓存没有过期,则文件直接从本地缓存中获取,不需要发送请求(服务器第一次发回资源时,附带
cache-control
,设置缓存的相对过期时间) - cache-control存在于响应头Response Headers中,设置了一些强制缓存参数的值
- 如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段,Cache-Control 的优先级更高
- 只要浏览器判断缓存没有过期,则文件直接从本地缓存中获取,不需要发送请求(服务器第一次发回资源时,附带
协商缓存:也称对比缓存,是服务端的缓存策略。服务端判断客户端的资源,是否和服务端资源一样,如果一致则返回
304
,反之返回200
和最新的资源和资源标识- 协商缓存用
Last-Modified
来判断:浏览器第一次发送请求时,服务器返回资源并返回一个Last-Modified
值给浏览器。浏览器通过If-Modified-Since
字段保存。再次发送请求时,请求头携带If-Modified-Since
,服务器匹配If-Modified-Since
是否和Last-Modified
相等。如果相等,则返回304
,表示资源未被修改;如果不相等,则返回200,并返回资源和新的Last-Modified
的值 - 协商缓存用
Etag
来判断:第一次发送请求时,服务器返回资源并返回一个Etag
给浏览器。浏览器通过If-None-Match
字段保存。再次发送请求时,请求头携带If-None-Match
,服务器匹配If-None-Match
是否和Etag
相等 Last-Modified
:资源的最后修改时间,秒级,资源重复生成时会改变Etag
:资源的唯一标识,本质是一个字符串- 同时存在
Last-Modified
和Etag
的值时,会优先使用Etag
,以下为原因:- 在没有修改文件内容情况下文件的最后修改时间可能也会改变
- 可能有些文件是在秒级以内修改的,
If-Modified-Since
能检查到的粒度是秒级
- 协商缓存用
协商缓存的两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求
刷新的影响:
- 正常操作:输入
url
,跳转链接,前进后退——二者均有效 - 手动刷新:
F5
,点击刷新按钮,右击菜单刷新——强制缓存失效,协商缓存有效 - 强制刷新:
ctrl
+F5
——强制缓存失效,协商缓存失效
- 正常操作:输入
HTTP请求包、响应包:HTTP 请求包和响应包 (网络篇)
- 请求包:请求行,请求头,【空行】,请求体
- 请求行:
方法 /url HTTP/版本号
,例如:GET /xinwen/2018-07/17/content_5307156.htm HTTP/1.1 - 请求头:包含了客户端的环境
- 空行:通知服务器后面不再有请求头部
- 请求体:要发送的数据,post方式会使用
- 请求行:
- 响应包:响应行,响应头,【空行】,响应体
- 响应行:
HTTP/版本号 响应码(状态码)
,例如:HTTP/1.1 200 OK
- 响应行:
- 请求包:请求行,请求头,【空行】,请求体
HTTP优缺点:
- 优点:简单、易于拓展、跨平台
- 缺点:无状态、明文传输、不安全
- 无状态:服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,但完成有关联性的操作非常麻烦(例如登录、下单)——
Cookie
通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态
- 无状态:服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,但完成有关联性的操作非常麻烦(例如登录、下单)——
HTTP短连接和长连接:
- 短连接:客户端与服务器进行一次HTTP连接,连接结束TCP关闭连接。
- 长连接:HTTP头部带有参数keep-alive,底层用于传输数据的TCP连接不会直接关闭,会根据服务器设置的保持时间保持连接,保持时间过后连接关闭
HTTPS:
HTTP明文传输,HTTPS在TCP和HTTP之间加SSL/TLS安全协议,报文加密传输——SSL:安全套接层,TLS:传输层安全协议
- 混合加密实现机密性,防止窃听
- 摘要实现完整性,防止篡改
- 服务器公钥放入数字证书实现不可抵赖性,防止冒充
证书:
- 证书信任链(确保根证书的绝对安全性):
- 向 CA 申请的证书一般不是根证书签发的,而是由中间证书签发的
- 根据服务器证书中的签发者找到该证书的颁发机构
- 若颁发机构没有再上级签发机构,说明是根证书,即自签证书。软件检查此证书有否已预载于根证书清单
- 证书信任链(确保根证书的绝对安全性):
先有SSL,之后SSL升级为TLS;TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3
HTTP进行TCP三次握手后便可进行报文传输,HTTPS在TCP三次握手后需要建立SSL连接:
- 三次握手后还需SSL/TLS的握手
- ClientHello:浏览器发起第一个HTTPS请求(tcp第三次握手中进行,因为前两次握手没有数据传输),给服务器发送支持的加密算法
- SeverHello:服务器据此选择一套加密算法(RSA和ECDHE),和CA证书一起返回给浏览器
- 客户端(SSL/TLS)解析证书,验证证书合法性,生成对称加密密钥client key,用服务器的公钥非对称加密客户端密钥;发起HTTPS中的第二个HTTP请求,发给服务器加密的client key
- 服务器用私钥解密,用解密后的客户端密钥加密数据,发回给客户端
- 客户端解密密文,得到服务器的数据。整个HTTPS传输完成
HTTP端口号是 80,HTTPS端口号是443
TLS 分为握手协议和记录协议,后者负责保护应用程序数据并验证其完整性和来源,对 HTTP 数据加密是使用记录协议
- 首先,消息被分割成多个片段分别压缩
- 经过压缩的片段加上消息认证码
- 通过对称密码进行加密
- 加密数据再加上数据类型、版本号、压缩后的长度组成的报头得到报文数据,发给TCP
https一定安全吗?
- 客户端发起 HTTPS 请求时,被「假基站」转发到「中间人服务器」完成 TLS 握手,「中间人服务器」再与真正的服务端完成 TLS 握手——期间中间人发送自己的证书给客户端,客户端验证证书的真伪,伪造证书能被浏览器识别出是非法,但用户点击接受了中间人服务器的证书
- HTTPS 协议本身没有任何漏洞,即使收到中间人攻击,本质利用客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全
- 可以通过HTTPS 双向认证避免
HTTP1.1、HTTP2、HTTP3
HTTP1.1相对于HTTP1.0的优化:
- 长连接;HTTP/1.0默认使用短连接,每次请求要重新建立一次连接;HTTP 1.1默认使用长连接
- 管道pipeline网络传输:只要第一个请求发出,就可以发第二个请求
- 错误状态响应码:HTTP1.1新增多个错误状态响应码
- 缓存处理:HTTP1.1引入更多的缓存控制策略
HTTP1.1性能瓶颈
- 请求/响应头部未经压缩就发送
- 服务器按请求顺序响应,如果服务器响应慢,会导致消息队头阻塞(客户端一直请求不到数据)
- 无没有请求优先级控制
- 请求只能从客户端开始,服务器只能被动响应
HTTP2改进(15年提出):
- 头部压缩:如果同时发出多个请求且请求头一样或相似,协议会消除重复的部分。客户端和服务器同时维护一张头信息表,所有字段存入这个表并生成索引号,只发送索引号
- 二进制格式:头信息和数据体都是二进制
- 数据流:
- 1 个 TCP 连接包含多个 Stream,Stream 包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应。Message 包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容数据流
- 不同的 HTTP 请求用唯一的 Stream ID 区分,接收端通过 Stream ID 有序组装成 HTTP 消息。不同 Stream 的帧可以乱序发送,因此 HTTP/2 可以并行交错发送请求和响应
- 客户端收到后,根据相同的 Stream ID 有序组装成 HTTP 消息
- 通过多个请求复用一个 TCP 连接解决 HTTP 层队头阻塞
HTTP2性能瓶颈:
多个 HTTP请求复用一个 TCP 连接,一旦发生丢包,一个TCP连接中的HTTP请求都必须等待丢的包被重传
TCP层队头阻塞:TCP 层必须保证字节数据完整且连续,当「前 1 个字节数据」没有到达,后收到的字节数据只能存放在内核缓冲区,只有等到这 1 个字节数据到达,应用层才能从内核中拿到数据
HTTP3改进:
- HTTP下层的TCP改成UDP,UDP不管顺序,因此不存在HTTP1.1的队头阻塞和HTTP2的丢包全部重传、TCP队头阻塞问题
- QUIC:
- 基于UDP实现类似TCP的可靠性传输,当某个流丢包,只会阻塞这个流,其他流不受影响
- HTTPS建立一个连接要6次交互(三次握手,TLS1.3三次握手),QUIC合并成3次
大文件传输:
- 压缩文件:通常浏览器在发送请求时都会带着“Accept-Encoding”头字段,里面是浏览器支持的压缩格式列表
- 分块传输:分批发给浏览器,浏览器收到后再组装复原。在响应报文里用头字段“Transfer-Encoding: chunked”来表示(“Transfer-Encoding: chunked”和“Content-Length”字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知)
- 每个分块包含分块长度和数据块两个部分
- 分块长度使用 16 进制数字表示,以
\r\n
结尾;数据块紧跟在分块长度后面,也使用\r\n
结尾,但数据不包含\r\n
;终止块是一个常规的分块,表示块的结束。不同之处在于其长度为 0,即0\r\n\r\n
- 范围请求:在一个 Range 首部中,可以一次性请求多个部分,服务器会以 multipart 文件的形式将其返回(类似断点续传)
断点传输
- 基本原理
- 对上传的文件进行分割,每次只上传一小片。服务端接收到文件后追加到原来部分,最后合并成完整的文件
- 每次上传文件片前先获取已上传的文件大小,确定本次应切割的位置
- 每次上传完成后更新已上传文件大小的记录
- 标识客户端和服务端的文件,保证不会把A文件的内容追加到B文件上
- 在上一次下载断开的位置开始继续下载——HTTP中,在请求报文头中加入Range段,来表示客户机希望从何处继续下载
- 客户端发请求时对应的是 Range ,服务器端响应时对应的是 Content-Range
- Range用于请求头中,指定第一个字节的位置和最后一个字节的位置
- Content-Range用于响应头中,服务器在 Content-Range 段返回当前发送的范围和文件总大小,响应完成后,返回的响应头为:HTTP/1.1 206 Partial Content
- 需要标识文件唯一性(可能重传时,目标文件不在该url)——Last-Modified 是由服务器往客户端发送的 HTTP 头,用于记录页面最后修改时间
- 基本原理
TCP和UDP
区别:
- TCP面向连接,提供可靠服务(传送数据无差错不丢失),适合文件传输、远程登录等;UDP无连接,不保证可靠交付
- TCP面向字节流,把数据看成一连串无结构的字节流;UDP面向报文,没有拥塞控制,网络拥塞时不会降低客户端的发送速率(适合实时应用,如IP电话,实时视频会议)
- 一条TCP连接只能是点到点;UDP支持一对一,一对多,多对一和多对多的通信
- TCP首部20字节;UDP首部8个字节(源端口号、目标端口号、包长、校验和)
- TCP会在传输层分片,UDP会在IP层分片
当传输层的数据包大小超过 MSS(TCP 最大报文段长度),要将数据包分块,称为TCP Segment
浏览器(客户端)中每个标签栏都是一个独立的进程,操作系统为这些进程分配临时的端口号
查看tcp连接状态:
netstat -napt
查看路由表:
route -n
查看端口号:lsof -i:端口号
TCP和UDP可以用同一个端口。收到数据包后在 IP 包头的「协议号」字段知道交给TCP模块还是UDP模块
TCP可靠传输:
- 编号:给发送包编号,接收方据此排序,丢弃重复的数据,把有序数据传给应用层
- 校验和:TCP保持首部和数据的检验和(端到端的检验和)
- 流量控制:TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。接收方会将自己接收缓冲区的大小放入TCP首部的字段,通过ACK通知发送方——TCP利用滑动窗口实现流量控制
- 拥塞控制:网络拥塞减少数据的发送——慢启动策略
- ARQ协议:每发完一个分组就停止发送,等待对方确认,收到确认后再发下一个分组
- 超时重传:每发完一个分组就启动一个定时器,等待目的端的ACK,如果不能及时收到ACK则重传;若尝试几次均失败,TCP会断开连接,发送RST,告知应用程序连接错误
三次握手四次挥手:
三次握手:第一二次握手没有应用层数据,第三次可以携带客户端到服务器的数据
- 开始,客户端和服务端处于CLOSED状态,之后服务端主动监听某个端口,处于LISTEN状态
- 客户端随机初始化序号(client_isn),置于TCP首部的“序号”字段中,设置SYN标志位(SYN报文),把第一个SYN报文发给服务端,发起连接,之后客户端处于SYN-SENT状态
- 服务端收到SYN报文,也随机初始化序号(server_isn),将序号填入TCP“序号”字段,并将client_isn+1填入首部的“应答序号”字段,设置SYN和ACK标志位,发送报文给客户端,之后服务端处于SYN-RECEIVED状态
- 客户端收到报文,向服务端回应最后一个应答报文:设置报文首部ACK标志位,“确认应答号”字段填入server_isn+1,发送报文给服务端,之后客户端处于ESTABLISHED状态
- 服务器收到客户端的应答报文,也进入ESTABLISHED状态
- 客户端主动打开,服务器被动打开
四次挥手:
- 过程:
- 客户端关闭连接,发送设置首部FIN标志位的报文,进入FIN_WAIT_1状态
- 服务端收到后,发送ACK应答报文,进入CLOSED_WAIT状态
- 客户端收到ACK应答报文,进入FIN_WAIT_2状态
- 服务端处理完数据,向客户端发送FIN报文,进入LAST_ACK状态
- 客户端收到FIN报文,回复ACK应答报文,进入TIME_WAIT状态
- 服务器收到ACK应答报文,进入CLOSED状态,服务端关闭连接
- 客户端经过2MSL后,进入CLOSED状态,关闭连接
- 主动关闭连接的一方才有TIME_WAIT状态
- 过程:
三次握手原因:防止历史连接的建立,减少双方不必要的资源开销,并同步初始化序列号
两次:无法可靠地同步双方序列号,只有客户端的起始序列号被确认,服务器的序列号得不到确认;服务端可能建立一个历史连接,造成资源浪费
四次:三次握手就能建立可靠连接
四次挥手原因:服务端收到客户端的FIN后,可能还有数据没发完
TIME_WAIT=2MSL:保证上一次连接的报文在网络中消失(网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应)
- 更短可能会导致具有相同四元组的”旧“数据包被收到
- 更长导致内存资源和端口资源的占用
- MSL:Maximum Segment Lifetime
服务器出现大量的 TIME_WAIT 状态的 TCP 连接,说明服务器主动断开了很多 TCP 连接——HTTP 没有使用长连接
握手时序列号不同:为了防止历史报文被下一个相同四元组(源ip源端口、目标ip目标端口)的连接接收
TCP半连接队列和全连接队列
三次握手时,Linux内核会维护两个队列:
- 半连接队列(哈希表,方便快速取出)
- 全连接队列(accept队列,链表形式)
服务端收到客户端发起的SYN请求后,内核将该连接存储到半连接队列
服务端收到第三次握手的ACK后,内核将该连接从半连接队列移除,创建新的完全的连接,添加到accept队列
进程调用accept函数取出连接
SYN攻击会导致SYN队列溢出:增大 TCP 半连接队列
服务端如果只 bind 了 IP 地址和端口,而没有调用 listen,客户端对服务端发起了连接建立,服务端会回 RST 报文
TCP拥塞控制:
慢启动:由小到大逐渐增加拥塞窗口(发送端在一个RTT内可以最多发送的数据包数)的大小——令cwnd=1,发送方只发送1个数据包,收到确认将cwnd加倍,之后能发送的数据包数量为:2、4、8 …
拥塞避免:当cwnd >= ssthresh(慢开始门限) 时,进入拥塞避免,每个轮次cwnd加1
拥塞发生:如果出现超时,令ssthresh=cwnd / 2,重新执行慢开始
快重传:接收方收到一个失序的报文段后立刻发出最后一个有序报文段的重复确认,而不是捎带确认。如果发送方连续收到三个重复确认,则立即重传对方尚未收到的报文段,而不等待重传计时器到期
快恢复:发送方连续收到三个重复确认时,ssthresh/=2,cwnd=ssthresh,执行拥塞避免
TCP滑动窗口
Sender发送一个数据包后不等待ACK消息返回,直接发送后面的数据包,提高吞吐量
发送方和接收方各有一个窗口,接收方通过Header的窗口字段告诉发送方自己的窗口大小(接收端能接收的最大字节数),发送方据此和其它信息设置自己的窗口大小
发送窗口收到发送窗口内的ACK确认,才移动左边界
接收窗口只有在前面所有段都确认的情况下才移动左边界
粘包、拆包:根源在于TCP面向字节流,消息之间没有消息保护边界(TCP只负责传输数据,碰到应用层的数据大于MSS就拆开,碰到小于MSS就攒够再发,TCP不关心数据内容)
粘包原因:一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送
拆包原因:一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送
解决:
- 发送端给每个数据包的首部添加数据包的长度
- 发送端把每个数据包设置为固定长度,接收端每次读取固定长度的数据,以拆分每个数据包
- 发送端在每个数据包之间设置边界,接收端通过这个特殊符号来拆分
IP
由四段组成,每个字段一个字节,8位
由两部分组成:网络地址和主机地址。网络地址表示属于互联网的哪一个网络,主机地址表示其属于该网络中的哪一台主机
网址划分:
分为A、B、C三类及特殊地址D、E,内部还分公有IP和私有IP
- A类:(1.0.0.0-126.0.0.0),一般用于大型网络。(第一位为1)
- B类:(128.0.0.0-191.255.0.0),用于中等规模网络。(第二位为1)
- C类:(192.0.0.0-223.255.255.0),用于小型网络。(第三位为1)
- D类:多播地址,地址的网络号取值于224~239之间,一般用于多路广播用户
- E类:保留地址。地址的网络号取值于240~255之间
CIDR:网络号+主机号,通过子网掩码进一步划分子网
数据包到达一个网络节点,通过路由算法决定下一步走哪条路径
路由器和交换机:MAC 实现「直连」的两个设备之间通信,IP 则负责在「没有直连」的两个网络之间通信;MAC 每经过一个路由器就变化一次
IP相关协议:
ARP
- 主机广播发送 ARP 请求,请求包含要知道的 MAC 地址的主机 IP
- 同个链路的设备收到 ARP 请求,查看目标 IP 与自己 IP 一致,则将自己的 MAC 放入响应包返回给主机
RARP:
- 架设一台
RARP
服务器,注册设备的 MAC 地址及其 IP 地址,将设备接入网络 - 设备发送一条「我的 MAC 地址是XXXX,我的IP地址是什么」的请求,服务器返回「MAC地址为 XXXX 的设备,IP地址为 XXXX」
- 架设一台
DHCP:客户端监听 68 端口,服务端监听 67 端口;基于UDP广播通信
- 步骤:
- 客户端没有IP也不知道DHCP服务器地址,UDP广播 DHCP 发现报文(255.255.255.255:67,源为0.0.0.0:68)
- DHCP 服务器发现报文时,广播 DHCP 提供报文(DHCP OFFER),报文携带服务器提供可租约的 IP 地址、子网掩码、默认网关、DNS 服务器以及 IP 地址租用期
- 客户端收到一个或多个 DHCP 提供报文后,选择一个服务器发送 DHCP 请求报文(DHCP REQUEST)进行响应
- 服务端响应 DHCP ACK
- 优化:DHCP 中继代理,对不同网段的 IP 地址分配可以由一个 DHCP 服务器统一进行——客户端向 DHCP 中继代理发送 DHCP 请求,DHCP 中继代理收到广播包后以单播发给 DHCP 服务器,服务器返回应答,中继代理将此响应广播给 DHCP 客户端
- 步骤:
NAT:
NAT:同个公司、家庭、教室内的主机对外部通信时,把私有 IP 地址转换成公有 IP 地址(N 个私有 IP 地址,就要 N 个公有 IP 地址)
NAPT:IP 地址 + 端口号一起进行转换
NAT穿透:应用程序主动发现自己位于 NAT 设备之后,主动获得 NAT 设备的公有 IP,并为自己建立端口映射条目,不需要 NAT 设备来进行转换——从而避免以下问题
- NAT 路由器重启导致所有 TCP 连接被重置
- 外部无法主动与 NAT 内部服务器建立连接
ICMP:互联网控制报文协议,分为两大类:查询报文、差错报文。如果某个
IP
包因为某种原因未能达到目标地址,这个具体的原因将由 ICMP 负责通知
回环地址、localhost和0.0.0.0
ping 127.0.0.1:
源码规定127 开头的都属于回环地址、127.0.0.1为本机回环
发现目标IP是回环地址时,选择本地网卡(没有ring buffer),会把数据推到
input_pkt_queue
的链表中。该链表为所有网卡共享,保存发给本机的各种消息。消息被发送到这个链表后会触发一个软中断,内核线程ksoftirqd将消息取出,顺着链路层传递给应用ping回环地址和ping本机地址没有区别
localhost是一个域名,默认被解析为
127.0.0.1
(/etc/hosts
文件下修改)启动服务器时,一般会
listen
一个 IP 和端口,如果listen 0.0.0.0
,则表示监听本机上的所有IPV4地址。例如,一台服务器有一个外放地址A和一个内网地址B,如果绑定0.0.0.0,则客户端通过A和B都能访问服务器
浏览器键入一个网址经历了哪些过程?
整体过程:
解析URL,生成HTTP请求信息(请求信息被拼接到tcp头部下)
查询服务器域名对应的IP地址(DNS解析)
获取IP后,HTTP的传输⼯作交给操作系统中的协议栈(调用 Socket 库)
TCP三次握手建立连接
TCP模块执行连接、收发、断开等操作时,需要委托IP模块将数据封装成网络包发送
生成IP头部后,在IP头部前面加上MAC头(ARP协议广播),用于在以太网通讯
- 交换机根据 MAC 地址表(mac地址和端口映射)查找 MAC 地址,然后将信号发送到相应的端口
- 地址表中找不到指定的 MAC 地址,则将包转发到除了源端口之外的所有端口上
网络包经过交换机之后到达路由器,完成包接收操作之后,路由器去掉包开头的 MAC 头部,然后通过查IP表判断包转发的目标
经过网卡、交换机和路由器到目的端,然后反向
交换机和路由器:交换机用来连接局域网,路由器用来连接互联网(也可以连接多个局域网)
在发送数据包时,如果目标主机不是本地局域网,填入的MAC地址是路由器,也就是把数据包转发给路由器,路由器一直转发下一个路由器,直到转发到目标主机的路由器,发现 IP 地址是自己局域网内的主机,就会 arp 请求获取目标主机的 MAC 地址,从而转发到这个服务器主机
DNS解析
- 本地hosts文件查找是否有网址的映射关系,有则直接调用该映射进行访问
- 没有则去本地DNS解析缓存查找
- 没有则首先找本地TCP/IP设置的DNS服务器,如果网址在本地DNS资源范围内则返回解析给主机(解析具有权威性)
- 没有,但该服务器已经存储了网址的映射关系,同样返回这个映射,完成地址解析(解析不具有权威性)
- 如果本地DNS解析失败,没有对应的映射关系:
- 本地DNS服务器开启转发模式,则向上一级请求,若上一级也不能解析就找上上一级,以此类推,最后返回给主机
- 本地DNS服务器不使用转发模式,直接找13组根DNS,根DNS返回负责这个域名的DNS,重复过程
Ping的工作原理
- Ping基于ICMP协议栈,ping某IP地址或域名,检查基本连接是否正常,以及网络延迟情况(应用层直接使用网路层)
- ICMP:告知网络包传送过程中产生的错误以及各种控制信息
- 过程:
- 源主机先构建一个ICMP回送请求消息数据包
- ICMP消息包含多个字段,最重要的是:
- 类型:对于回送请求消息,该字段为8
- 序号:用于区分连续ping的时候,发出的多个数据包
- 每发出一个请求数据包,序号自动加1
- 在报文的数据部分插⼊发送时间,以计算往返时间RTT
- 规定时间内:
- 源主机没有收到ICMP应答包,说明目标主机不可达
- 收到ICMP应答包,则源主机用当前时刻减去该数据包从源主机发出的时刻,得到ICMP数据包时间延迟
Cookie和Session
- Cookie和Session都是用来跟踪浏览器用户身份的会话方式
- Cookie:保存用户信息,不可跨域名。服务器在本地机器上存储的一小段文本,浏览器每次访问网页都会带上cookie。应用场景:
- 保存已经登录过的用户信息,下次访问网站时,页面自动填写一些登录信息
- 网站保持登录(下次访问不需要登录):用户登录的时候,在Cookie保存一个Token,下次登录时服务器根据Token值来查找用户
- Session:通过服务端记录用户的状态。客户端浏览器访问服务器,服务器把客户端信息以某种形式记录在服务器上。应用场景:
- 购物车:添加商品到购物车时,HTTP协议是无状态的,系统不知道是哪个用户操作
- 服务端给特定的用户创建特定的Session,以标识、跟踪这个用户
- Cookie:保存用户信息,不可跨域名。服务器在本地机器上存储的一小段文本,浏览器每次访问网页都会带上cookie。应用场景:
- cookie中的一些敏感信息最好加密,使用的时候发给服务器解密
- 会话cookie:浏览器关闭后失效,存放在内存中;持久cookie:存放在硬盘
- session默认被存储在服务器的一个文件里(不是内存)
- session的运行依赖session id,session id存储在cookie——如果浏览器禁用cookie,session也会失效(但可以通过其它方式实现,如在url中传递session id)——用户验证一般用session,客户端每次请求服务器的时候会发送session_id,服务器根据当前session_id判断相应的用户数据标志,以确定用户是否登录,或具有某种权限
- session真正的失效(即在服务器端被删除)是取决于服务器端设置的session过期时间
- 但由于依赖cookie存储session id,因此可能服务器的session没有过期,但cookie丢失导致session id丢失
- 关闭浏览器只会使存储在客户端浏览器内存中的session cookie失效,不会使服务器端的session对象失效
XSS攻击
- 跨站脚本攻击(Cross Site Scripting),利用站点内的新人用户
- 攻击者在web页面中插入恶意的script代码,用户浏览该页面时嵌入web页面的script代码会执行
- 分类:反射型、存储型、DOM-based型
- 反射性和DOM-baseed型为非持久性XSS攻击
- 存储型为持久性XSS攻击
为什么要RPC
纯粹TCP的问题:纯裸 TCP 收发 01 串是没有任何边界,需要在加入一些自定义的规则,用于区分——HTTP 和 RPC 协议只是定义不同消息格式的应用层协议,且RPC不一定基于TCP
RPC(Remote Procedure Call)不是一个具体的协议,而是一种调用方式。远端服务器暴露出一个方法
remoteFunc
,像调用本地方法那样去调用res = remoteFunc(req)
,屏蔽掉一些网络细节区别:
- 服务发现:
- http:向某个服务器发起请求得先建立连接,需要知道 IP 地址和端口
- RPC:会有专门的中间服务保存服务名、IP、端口信息,如 Consul 或者 Etcd,甚至是 Redis
- 底层连接形式
- http:建立 TCP 后一直保持连接,请求和响应会复用连接(http也有连接池)
- RPC:建立 TCP 长链接,但 RPC 还会建个连接池,建立多条连接放在池内,发数据就取一条
- 传输内容:
- http:使用 Json 序列化结构体数据(body);
Header
里的信息可能冗余 - RPC:定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议保存结构体数据
- http:使用 Json 序列化结构体数据(body);
- 服务发现:
杂项
- 完整聊天过程:
- 两端三次握手
- 一个数据包从聊天框里发出,消息从聊天软件所在的用户空间拷贝到内核空间的发送缓冲区(send buffer),顺着传输层、网络层,进入数据链路层,经过流量控制(qdisc),再通过RingBuffer发到物理层的网卡
- 数据经过路由器和交换机的跳转,到达目的机器的网卡
- 网卡通知DMA将数据包信息放到
RingBuffer
,触发硬中断给CPU
,CPU
触发软中断让ksoftirqd
去RingBuffer
收包,顺着物理层,数据链路层,网络层,传输层,从内核空间拷贝到用户空间里的聊天软件
- 已经建立的tcp,收到syn会怎么办?
- 如果新的syn的源端口号跟上一次连接的源端口号不一样,则服务端认为要建立新的连接。旧的连接中,服务器发送数据后会收到客户端的RST报文,从而释放旧的连接,或者不发送数据的话,keep alive会启动,检测客户端没有存活然后释放连接
- 如果新的syn的源端口号跟上一次连接的源端口号一样,会回复一个携带了正确序列号和确认号的 ACK 报文(challenge ack),客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文
- 要伪造一个能关闭 TCP 连接的 RST 报文,必须同时满足「四元组相同」和「序列号是对方期望的」这两个条件
- 有了tcp,数据一定不会丢吗?
- 如果半连接队列或者全连接队列满了,新的syn会被丢弃
- 流量控制丢包。发送数据过快,流控队列长度
txqueuelen
不够大,容易丢包 - ringbuffer丢包。接收数据时,如果ringbuffer缓冲区过小,发送的数据又过快,可能发生溢出,从而丢包
- 应用层丢包。TCP只保证传输层可靠,聊天软件还需要将数据从TCP的接收缓冲区里读出来,读出来这一刻软件崩溃,则会丢失消息
- 解决:中间加一个第三方服务器。服务器记录最近的数据,每条消息都有个id,服务器和软件每次拿最新消息的id进行对比从而知道两端消息是否一致
- n个好友的通信,有了服务器只需要和服务器建立一个连接
- 第三方服务器可以强制软件升级
- 建立连接后,如果客户端主机崩掉怎么办?服务端进程崩掉呢?
- 客户端:
- 如果在发送数据:
- 服务端报文会超时重传,重传总间隔时长达到一定阈值后,会断开 TCP 连接
- 如果不会发送数据:
- 开启有keep alive机制,一个时间段内没有任何连接相关的活动,则该机制将每隔一个时间间隔发送一个探测报文,连续几个探测报文没有响应则认为当前的 TCP 连接死亡,通知上层应用(TCP的keep alive时间较长,因此应用程序一般会有自己的keep alive)
- 若没开启keep alive机制,则TCP 连接将会一直处于 ESTABLISHED 状态直到服务端重启进程
- 如果在发送数据:
- 服务端:
- TCP 连接由内核维护,进程崩溃后,内核将回收进程的所有 TCP 连接,发出第一次挥手报文,后续挥手不需要进程参与,因此能够完成四次挥手
- 客户端:
- 客户端拔掉网线后再插回,原来的tcp是否存在?
- TCP 连接内核中是一个
struct socket
结构体,拔掉网线操作系统不会变更该结构体,TCP 连接的状态也不会改变 - 拔掉网线后,有数据传输:服务端超时重传
- 如果重传时插回网线,由于TCP状态没有改变,客户端正常回复ACK
- 如果重传时没有插回,服务端会断开TCP连接,客户端插回网线后发送数据时服务端会回复RST报文,客户端也释放该连接
- 拔掉网线后,没有数据传输
- 如果开启了TCP alive:持续一段时间后,TCP发送探测报文。发送次数达到保活探测次数后,报告该 TCP 连接已死亡
- 如果没有开启TCP alive:双方没有数据传输,TCP连接将会一直保持存在
- TCP 连接内核中是一个
MySQL
MySQL架构
MySQL主要分为 Server 层和存储引擎层:
- Server 层:负责建立连接、分析和执行 SQL,包括连接器、查询缓存、解析器、预处理器、优化器、执行器等,所有的内置函数、跨存储引擎的功能在这一层实现(存储过程、触发器、视图等,日志模块 bin log 也在 Server 层)
- 存储引擎:主要负责数据的存储和读取。Server 层通过 api 与存储引擎通信
Server 层基本组件
- 连接器:客户端连接 MySQL 时,server层对其进行身份认证,并获取该用户的权限
- 基于tcp,如果中途修改了权限,需要重连才使用新的权限设置
show processlist
查看客户端的连接、- MySQL 定义了空闲连接的最大空闲时长,默认8h,超过则自动断开
- 存在最大链接数,存在长连接和短连接
- 长连接的内存占用问题(连接对象资源只有在连接断开时才会释放):定期断开长连接,或客户端执行一个很大操作后,调用
mysql_reset_connection()
函数主动重置连接
- 查询缓存:执行查询语句时,先查询缓存,校验该 sql 是否执行过,如果有执行结果则直接返回
- key-value,key为查询语句,value为查询结果
- 只要一个表有更新操作,表的查询缓存就被清空
- 8.0后没有查询缓存,因为更新频繁的表命中率低
- 解析器:没有命中缓存的话,SQL 语句就会经过解析器(词法分析和语法分析,看 SQL 语句要做什么,检查 SQL 语句语法是否正确)
- 预处理器:检查 SQL 查询语句中的表或者字段是否存在、将
*
符号扩展为表的所有列 - 优化器:优化器对查询进行优化,包括重写查询、决定表的读写顺序、选择合适的索引等,生成执行计划
- 执行器:执行前会校验用户有没有执行权限,没有权限会返回错误信息,有权限会根据执行计划调用引擎的接口,返回结果
- 主键索引查询:通过主键索引的 B+ 树结构定位到第一条记录,判断记录是否符合查询条件,如果符合则发送给客户端
- 全表扫描:存储引擎读取表中的第一条记录,如果记录符合查询条件则发送给客户端,然后读下一条记录(这里每读到符合的记录就发送给客户端)
- 索引下推:对于联合索引(a,b),先定位满足a的记录,然后检查该索引中包含的b是否符合条件,如果不符合则查看下一个a的索引(如果不下推,会直接返回该索引的所有数据,将判断的逻辑交给server负责)
- 连接器:客户端连接 MySQL 时,server层对其进行身份认证,并获取该用户的权限
MySQL表空间
表空间由段(segment)、区(extent)、页(page)、行(row)组成
页:数据按页读写,默认16KB
区:链表中相邻的两个页的物理位置不连续,磁盘查询可能存在随机IO——为某个索引分配空间时以区(而非页)为单位,每个区1MB64页,从而链表中相邻页的物理位置也相邻,读取时可以顺序IO
段:段由多个区(extent)组成,分为数据段、索引段和回滚段等
- 索引段:存放 B + 树的非叶子节点的区的集合
- 数据段:存放 B + 树的叶子节点的区的集合
- 回滚段:存放的是回滚数据的区的集合
行格式(Compact)
记录的额外信息:
变长字段长度列表:变长字段存储的数据长度(真实数据占用的字节数)不固定,因此要存储数据大小
- 若有多个变长字段,则逆序存储(例如,name列和intro列的长度为1字节和4字节,则记录为04 01)
- 记录头信息中指向下一个记录的指针,指向的是下一条记录的记录头信息和真实数据之间的位置,此时向左读就是记录头信息,向右读就是真实数据——靠前的真实数据和字段长度信息可以在同一个cpu缓存行中
- 若没有变长字段,则没有此信息
NULL 值列表:同样逆序存放,存储值为 NULL 的列(不放到真实数据中)
每个列对应一个二进制位(逆序存放),为1则该列的值为NULL
若长度不足整数个字节,则在字节的高位补0
记录头信息:
- delete_mask :标识此条数据是否被删除
- next_record:下一条记录的位置
记录的真实数据
- row_id:建表时指定主键或者唯一约束列则没有该字段
- trx_id:事务id,表示最后一个处理该数据的事务
- roll_pointer:指向该记录的上一个版本
varchar(n)的最大n
- 一行记录最多只能存储 65535 字节数据
- n 代表的是最多存储的字符数量——字符和字节的关系取决于字符集类型,ascii中一个字符一个字节
- 取决于是否有额外信息、隐藏字段以及相应长度。要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535
- 行溢出:一些大对象如 TEXT、BLOB 可能存储的数据超过一个页——行溢出,多的数据就会存到溢出页。真实数据只会保存该列的一部分数据,剩余的数据放在溢出页中,用 20 字节存储指向溢出页的地址
- Compressed 和 Dynamic 行格式采用完全行溢出,只存储 20 字节的指针指向溢出页,实际数据存储在溢出页
数据页
I/O 最小单位是页,默认 16KB
File header:两个指针,指向上一个数据页和下一个数据页
User records:记录按「主键」顺序组成单向链表
Free space:插入一条记录就从 Free Space(尚未使用的存储空间)申请一个记录大小的空间,划分到 User Records。当Free Space被User Records替代,这个页用完
页目录:
- 记录划分成几个组,每组最后一条记录是组内最大的记录,最后一条记录的头信息存储该组的记录数目,作为 n_owned 字段(上图中粉红色字段)
页目录存储每组最后一条记录的地址偏移量,每组的地址偏移量称为槽(slot),每个槽相当于指针指向了不同组的最后一个记录
- 规定:(防止一个槽的记录太多)
- 第一个分组只有1条记录只能有
- 最后一个分组有1-8条记录
- 其余分组有4-8记录
- 规定:(防止一个槽的记录太多)
三大范式
第一范式1NF:数据库表字段是原子的,例如字段
userInfo: 广东省 10086'
必须拆分成userInfo: 广东省
和userTel: 10086
两个字段第二范式2NF:满足第一范式,并且:
- 一个表有一个主键
- 非主键列完全依赖于主键,而不能只依赖于主键的一部分
- 例如:关系表
student_course(student_no, student_name, age, course_name, grade, credit)
,主键为(student_no, course_name),学分完全依赖于课程名称,姓名年龄完全依赖学号,不符合第二范式
第三范式3NF:满足第二范式,并且:
- 非主键列直接依赖于主键,不存在传递依赖
- 例如,关系表
Student(student_no, student_name, age, academy_id, academy_telephone)
,主键为”学号”,学院id依赖于学号,学院地点和学院电话依赖于学院id,存在传递依赖
事务
四大特性
- 原子性:事务的所有操作要么全部成功,要么全部失败回滚——MySQL的InnoDB引擎是靠undo log(回滚日志)来实现的,undo log能够保证在事务回滚时,撤销所有已经执行成功的SQL
- 一致性:一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,转账之后无论成功失败,账户总和还是1000——应用层面保证
- 隔离性:多个事务彼此之间隔离、互不干扰——锁机制+MVCC
- 两个事务并发访问同一数据资源的情况主要分为
读-读
、写-写
和读-写
三种;锁机制解决写-写
,MVCC解决读-写
(读操作利用MVCC
,写操作进行加锁) - 锁机制:
- 读锁:又称“共享锁”
- 写锁:又称“排他锁”
- 表级锁:将整个表进行锁定
- 表锁
- 元数据锁(MDL):对表执行 CRUD 操作时,防止其他线程变更表结构(CRUD操作时为MDL读锁,变更表结构时为MDL写锁),在事务提交后释放
- 意向锁:一个事务已经对一个表中的某个数据加上了排他锁或共享锁,可以加上意向锁,下一个事务来锁表时发现已经存在意向锁了,否则需要遍历查看是否有数据已经被锁住
- 行级锁:将需要操作的相应行进行锁定
- 记录锁:分为S锁(共享锁)和X锁(独占锁),commit后释放
- 间隙锁:防止产生幻读,加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。保证在间隙锁执行的时候,新增的数据会阻塞,保证一个事务中的两次查询获得的记录数是一致的。两个事务可以同时持有包含共同间隙范围的间隙锁
- Next-Key Lock:行级锁和间隙锁的结合产生的锁。间隙锁不会锁住当前记录,Next-Key Lock会将当前记录也锁住
- MVCC:通过快照读和当前读保证读写隔离
- 两个事务并发访问同一数据资源的情况主要分为
- 持久性:一个事务一旦被提交,对数据库中的数据的改变是永久性的,即使数据库崩溃也可以回滚——InnoDB提供缓冲池(Buffer Pool),包含了磁盘中部分数据页的映射。在对Buffer Pool中的数据进行修改的时候通过redo log记录这次操作,事务提交时对redo log进行刷盘。 宕机时从磁盘中读取redo log进行数据的恢复,保证事务的持久性——redo log先记录日志,再更新Buffer Pool
事务隔离级别
脏读、不可重复读、幻读
- 脏读:在一个事务处理过程中,读取另一个未提交的事务的数据
- 不可重复读:一个事务多次查询某行记录却返回不同的数据值——读取的间隔中,另一个事务修改数据并提交(脏读读取未提交的数据,不可重复读读取前一个事务提交的数据)
- 幻读:某个事务在读取某个范围的记录时,另一个事务在该范围内插入新的记录。第一个事务再次读取该范围记录时,会产生幻行(不可重复读的重点是修改,幻读的重点是增加和删除)
事务隔离能够解决脏读、不可重复读、幻读
- 解决脏读:修改时加排他锁,直到事务提交后才释放。事务1读取时加上共享锁后,不允许任何事务操作该数据,只能读取,事务1如果有更新操作,共享锁转换为排他锁,其他事务更无权参与进来读写
- 解决不可重复读:MVCC的版本链
- 解决幻读:见后文
四种隔离级别:
- Serializable (串行化):强制事务排序,使之不可能冲突,解决幻读
- Repeatable read (可重复读):默认事务隔离级别,确保同一事务多个实例在并发读取数据时,看到同样的数据行,解决不可重复读
- Read committed (读已提交):一个事务只能看见已经提交事务所做的更改,解决脏读
- Read uncommitted (读未提交):事务可以看到其他未提交事务的执行结果
隔离级别的实现:
- 串行化:加读写锁,避免并行访问
- 可重复读和读已提交:MVCC中创建read view的时机,读已提交在每个语句执行前重新生成一个read view,可重复读在启动事务时生成一个read view
- 读未提交:直接读取最新的数据
查看隔离级别:
1
select @@transaction_isolation;
设置隔离级别:
1
set session transaction isolation level read uncommitted;
索引
存储引擎用于提高数据库表访问速度的一种数据结构;查询数据时如果没有索引,会加载所有的数据到内存依次进行检索,而有索引时B+树的高度一般在2-4层,最多只需要读取2-4次磁盘
优缺点:
- 优点:
- 加快数据查找的速度
- 排序或者是分组的字段添加索引,可以加快分组和排序的速度
- 加快表与表之间的连接
- 缺点:
- 占用物理空间
- 要动态维护索引,降低表的增删改效率
- 优点:
建立索引:经常用于查询的字段、经常用于连接的字段、经常需要排序的字段,需要建立索引(索引已经排好序)
不建索引:where中用不到的字段、表记录较少、经常增删改、区分度不高的字段
数据结构:B+树、哈希表,InnoDB引擎的索引类型包括B+树索引和哈希索引,默认索引类型为B+树索引
B+树索引
B+树是基于B 树和叶子节点顺序访问指针(双向链表)进行实现,具有B树的平衡性,并且通过顺序访问指针来提高区间查询的性能
每个节点里的数据按主键顺序存放。每一层父节点的索引值都出现在下层子节点的索引值中。节点中的key从左到右递增排列,如果某个指针的左右相邻 key 分别是$key_i$和$key_{i+1}$,则该指针指向节点的所有key大于等于$key_i$、小于等于$key_{i+1}$
查找时,先在根节点二分查找,找到key所在的指针,递归地在指针所指向的节点查找,直到查找到叶子节点,然后在叶子节点上二分查找,找出key对应的数据项
3 层的 B+树一般能够存储千万级别的数据,这是怎么算出的?一般有多少层?
- 非叶子节点(同样是一个数据页)内指向其他页的数量为 x,叶子节点内能容纳的数据行数为 y,B+ 数的层数为 z,则$total=x^{z-1}*y$
- x:参考数据页结构,大约能有15kb存储实际数据,在主键索引节点(索引页)中记录主键(bigint,8byte)和页号(4byte),x=15*1024/12
- y:一行数据1k,同样一页有15kb存储实际数据,y=15
- z:三层时,total为2.45kw;四层,total为百亿——因此,数据量更多会导致层数更多,磁盘IO更多
- 如果一行数据占据的空间更多(5kb),则total为500w
哈希索引:
基于哈希表实现
- 对于每一行数据,存储引擎会对索引列进行哈希计算得到哈希码(哈希算法尽量保证不同列值的哈希码不同)
哈希码作为哈希表的key,指向数据行的指针作为value
Hash索引和B+树索引的区别
- 哈希索引不支持排序、范围查找、模糊查询、多列索引的最左前缀匹配
- 哈希索引中会存在哈希冲突,因此哈希索引的性能不稳定
B+树比B树适合索引
- B+树的数据存储在叶子结点中,扫库时扫一遍叶子结点即可,而B树分支结点存储数据,扫库时要一次中序遍历按序,B+树适合区间查询的情况
- 数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,比 B 树更「矮胖」
- B+ 树删除一个节点可以直接从叶子节点中删除
- B+树的查询效率更加稳定,每一个数据的查询效率相当
不用红黑树:减少IO(B+树只有3-4层,红黑树为二叉树)
不用跳表:为了达到理论上的二分效果,下一层节点数目为上一层的两倍。如果存储2kw数据,则跳表高度在24层左右,查找一次需要24次IO——跳表的写入性能比B+树好,因为跳表不用拆分、合并索引数据页
索引分类
根据数据结构分:B+tree索引、Hash索引、Full-text索引(只在
MyISAM
引擎上使用,Innodb也支持,只在CHAR
、VARCHAR
和TEXT
类型字段上使用全文索引)根据物理存储分:
- 聚簇索引(主键索引):索引结构和数据一起存放的索引(如果为聚集索引,叶子节点直接存放一页数据,否则存放指向数据的指针)
- 以主键作为 B+ 树索引的键值而构建的 B+ 树索引,称为聚集索引;以主键以外的列值作为键值构建的 B+ 树索引,称为非聚集索引;在InnoDB中,主键索引即聚集索引
- 如果表中没有指定主键,则选择表中的第一个不允许为
NULL
的唯一索引。如果没有主键也没有唯一索引,InnoDB
内部生成一个隐藏的主键作为聚集索引,隐藏主键长6个字节,值随着数据的插入自增
- 二级索引(辅助索引):主键索引以外的索引。二级索引叶子节点存放主键值,而不是实际数据
- 聚簇索引(主键索引):索引结构和数据一起存放的索引(如果为聚集索引,叶子节点直接存放一页数据,否则存放指向数据的指针)
根据字段特性分:
主键索引:索引列中的值是唯一的,不允许有空值;建表时指定了主键,就会自动创建主键索引
- 未建立主键,则取唯一索引非空,还没有就自建一个row id列
- 主键的作用:数据库以页增删数据,主键可以防止页分裂造成数据维护成本太大
唯一索引:索引列中的值是唯一的,但允许为空值——唯一标识数据库表中的每条记录,防止数据重复插入。创建唯一索引的SQL语句如下:
1
ALTER TABLE table_name ADD CONSTRAINT constraint_name UNIQUE KEY(column_1,column_2,...);
普通索引
前缀索引:在很长的字符列上创建索引,会造成索引特别大。前缀索引是指对文本或者字符串的前几个字符建立索引,关键在于选择足够长的前缀
1
2// email列创建前缀索引
ALTER TABLE table_name ADD KEY(column_name(prefix_length));
根据字段个数分:
- 单列索引
- 组合索引:表中的多个字段组合上创建的索引,使用组合索引时需遵循最左前缀原则
- 组合索引排序:
select * from order where status = 1 order by create_time asc
- 利用索引的有序性,在 status 和 create_time 列建立联合索引,此时根据 status 筛选后的数据是按照 create_time 排序的
- 组合索引排序:
索引设计原则
- 索引列的区分度要高
- 尽量使用短索引,对于较长的字符串进行索引应该指定一个较短的前缀长度,以减少磁盘I/O,提高索引缓存可以容纳的键值数量
- 利用最左前缀原则
- 太多的索引导致表的更新费时
索引失效
- 对于组合索引,不使用组合索引最左边的字段,则不会使用索引
- 以%开头的like查询(非%开头的like查询如 abc%会使用索引)
- 判断索引列是否不等于某个值
- OR 前的列是索引列,OR 后的列不是索引列
- 对索引列进行运算(带函数的查询不使用索引)
索引优化
- 前缀索引优化:使用某个字段中字符串的前几个字符建立索引
- 覆盖索引优化:从二级索引中查询得到记录
- 主键索引最好是自增的:每次插入一条新记录都是追加操作,避免页分裂
- 防止索引失效
索引下推
- 存储引擎层进行索引遍历时,先判断索引中包含的字段,直接过滤不满足条件的记录,再返还给 Server 层,减少回表次数
最左匹配原则
如果 SQL 语句中用到组合索引中的最左边的索引,则 SQL 语句可以利用组合索引去匹配。遇到范围查询(
>
、<
)停止匹配,后面的字段不用到索引,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配对
(a,b,c)
建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引对
(a,b,c,d)
建立索引,查询条件为a = 1 and b = 2 and c > 3 and d = 4
,a、b和c三个字段能用到索引,d无法使用索引SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2
,联合索引(a, b)都用到了联合索引进行索引查询(对于符合 a=2、a=8 的二级索引记录,b 字段的值是「有序」的)如下图,对(a, b)建立索引,a在索引树全局有序,b是全局无序,局部有序(a相等时根据b[排序)。直接执行
b = 2
这种查询条件无法使用索引。当执行a = 1 and b = 2
时a和b字段能用到索引。执行a > 1 and b = 2
时,a字段能用到索引,b字段用不到索引,因为在这个范围内b值不是有序的
覆盖索引
select
的数据列只从索引中就能够取得,不需要回表进行二次查询——查询列要被该表所使用的索引覆盖(回表:对于非聚集索引,需要遍历两遍索引树,先通过非聚集索引定位到主键值,再通过聚集索引定位到行记录)覆盖索引要存储索引列的值,而哈希索引、全文索引不存储索引列的值,所以MySQL使用b+树索引做覆盖索引
对于使用了覆盖索引的查询,在查询前面使用
explain
,输出的extra列会显示为using index
比如
user_like
用户点赞表,组合索引为(user_id, blog_id)
,user_id
和blog_id
都不为null
。1
explain select blog_id from user_like where user_id = 13;
- 查询的列被索引覆盖,where筛选条件符合最左前缀原则,通过索引查找就能直接找到符合条件的数据,不需要回表查询数据
1
explain select user_id from user_like where blog_id = 1;
- 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过索引扫描找到符合条件的数据,不需要回表查询数据
存储引擎
- MySQL常用的四种存储引擎: MyISAM、InnoDB、MEMORY、ARCHIVE。默认存储引擎为
InnoDB
InnoDB
默认的事务型存储引擎,基于聚集索引建立
同一个树节点中同时存放索引和数据;非主键索引的树节点data为主键的值
优点缺点:
支持事务、具有崩溃修复能力;引入了行级锁和外键约束
占用的数据空间较大
场景:需要事务支持,有较高的并发读写频率
MyISAM
- 数据以紧密格式存储。只读数据、小表、可以容忍修复操作,使用MyISAM引擎。MyISAM将表存储在两个文件:数据文件
.MYD
、索引文件.MYI
- 使用B+Tree作为索引结构,叶节点的data存放数据记录的地址(因此不支持聚集索引)
- 写操作需要锁定整个表
- 优点缺点:
- 访问速度快(数据组织 成有固定长度的记录,按顺序存储)
- MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,不支持外键
- 场景:对事务完整性没有要求;数据只读
MEMORY(Heap)
- 数据放在内存,使用哈希索引,将键的哈希和指向数据行的指针保存在哈希索引中
- 同时支持散列索引和 B+ 树索引,B+树索引可以使用部分查询和通配查询
- 每个 MEMORY 表只实际对应 一个磁盘文件
- 优点缺点:
- 访问速度较快
- 哈希索引数据不是按照索引值顺序存储,无法用于排序;不支持部分索引匹配查找,因为哈希索引使用索引列的全部内容计算哈希值;只支持等值比较,不支持范围查询;哈希冲突时,引擎遍历链表中所有的行指针,逐行比较直到找到符合条件的行
ARCHIVE
- 适合存储大量独立的、作为历史记录的数据。提供压缩功能,有高效的插入速度,不支持索引,查询性能差
MyISAM和InnoDB的区别?
是否支持行级锁:
MyISAM
只有表级锁,InnoDB
支持行级锁和表级锁,默认为行级锁是否支持事务和崩溃后的安全恢复:
MyISAM
不提供事务支持。InnoDB
提供事务支持,具有事务、回滚和崩溃修复能力是否支持外键:
MyISAM
不支持,InnoDB
支持。是否支持MVCC:
MyISAM
不支持,InnoDB
支持。对高并发事务,MVCC比单纯的加锁更高效。是否支持聚集索引:
MyISAM
不支持聚集索引,InnoDB
支持聚集索引
MVCC
- MVCC( Multiversion concurrency control ) :同一份数据保留多版本的一种方式,实现并发控制,对于高并发场景,MVCC比行级锁更有效
- InnoDB 的 MVCC 通过 read view 和版本链实现,版本链保存有历史版本记录,通过 read view 判断当前版本的数据是否可见,如果不可见,根据版本链找上一个版本,继续判断
版本链
通过三个隐藏字段实现
DB_TRX_ID :当前事务id;通过id大小判断事务的时间顺序
DB_ROLL_PRT :回滚指针;指向当前行记录的上一个版本
DB_ROLL_ID :主键;如果表没有主键,InnoDB会自动生成自增的主键
使用事务更新行记录时会生成版本链:
用排他锁锁住该行
将该行原本的值拷贝到 undo log,作为旧版本用于回滚
修改当前行,生成一个新版本,更新事务id,回滚指针指向旧版本的记录
read view
对数据在每个时刻的状态拍照记录,获取某时刻的数据就是快照恢复
四个字段:
- m_ids :创建 Read View 时,当前数据库中活跃事务的 id 列表,——启动了但还没提交的事务
- min_trx_id :m_ids 的最小值
- max_trx_id :创建 Read View 时全局事务中最大的事务 id 值 + 1;
- creator_trx_id :创建该 Read View 事务的 id
不同隔离级别创建read view的时间不同
- 可重复读:一个事务范围内,第一次select时更新这个read_view,以后不再更新,后续的select复用之前的read_view。保证事务范围内每次读取的内容一样
- 读已提交:每次执行select创建新的read_view,保证读到其他事务已经提交的修改
read view 的记录筛选:(trix_id即为DB_TRX_ID)
- 如果 DATA_TRX_ID < min_trx_id :创建 read view 时,修改该数据行的事务已提交,该版本的数据行可被当前事务读取到
- 如果 DATA_TRX_ID >= max_trx_id :当前版本数据行的事务是在创建 read view 之后启动的,该版本的数据行不可以被当前事务访问,需要通过版本链找到上一个版本
- 如果 min_trx_id <= DATA_TRX_ID < max_trx_id :
- 在m_ids列表中查找ID为 DATA_TRX_ID 的事务
- 如果存在,所以该记录不可见(活跃事务链表中的事务是未提交的)
- 如果不存在,说明事务 trx_id 已经提交,这行记录可见
快照读和当前读
- 表记录有两种读取方式:快照读、当前读
- 快照读:读取快照版本
- 普通的
SELECT
就是快照读,通过mvcc进行并发控制的,不用加锁 - InnoDB通过
mvcc
机制避免了幻读
- 普通的
- 当前读:读取最新版本。
UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE
是当前读
MySQL如何避免幻读
- 快照读情况下,MySQL通过
mvcc
来避免幻读——事务执行过程中看到的数据,和这个事务启动时看到的数据一致,即使中途其他事务插入了新记录,该记录对当前事务不可见 - 当前读情况下,MySQL通过
next-key
(加行锁和间隙锁)来避免幻读——行锁是加在索引上的锁,间隙锁是加在索引之间的,如果其他事务在 next-key lock 锁范围内插入一条记录,插入语句会被阻塞 Serializable
隔离级别也可以避免幻读,会锁住整张表,并发性极低,一般不使用- 可重复读的级别下,不能完全避免幻读
- 快照读:事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,事务 B 向表中新插入了一条 id = 5 的记录并提交。事务 A 更新 id = 5 的记录,此时新记录的 trx_id 变成事务 A 的事务 id, A 再 select 查询时可以看到这条记录
- 当前读:事务 A 先执行快照读,B插入一个记录并提交,A再执行当前读——为避免此情况,尽量在开启事务之后,马上执行 select … for update 当前读的语句
共享锁和排他锁
SELECT 的读取锁定分为:共享锁和排他锁。
1
2select * from table where id<6 lock in share mode;--共享锁
select * from table where id<6 for update;--排他锁LOCK IN SHARE MODE
:多个事务同时更新同一个表单时,容易造成死锁排他锁:申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁。进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程只能读这些数据,直到该语句的事务被
commit
语句或rollback
语句结束为止for update
仅适用于innodb,在事务范围内才生效- 根据主键进行查询,查询条件为
like
或者不等于时,主键字段产生表锁 - 根据非索引字段进行查询,会产生表锁
MySQL是怎么加行级锁的?
- 加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是前开后闭区间,而间隙锁是前开后开区间
- 在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成退化成记录锁或间隙锁
唯一索引等值查询
- 当查询的记录存在,索引树上定位后该记录的索引中的 next-key lock 退化成记录锁
- 当查询的记录不存在,索引树找到第一条大于该查询记录的记录后,该记录的索引中的 next-key lock 退化成间隙锁
唯一索引范围查询
非唯一索引等值查询
非唯一索引范围查询
非索引查询
MySQL日志:bin log/redo log/undo log
- MySQL日志:包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等
bin log
(二进制日志)、redo log
(重做日志)和、undo log
(回滚日志)。
bin log:追加写
MySQL数据库级别的文件,记录对MySQL数据库执行修改的操作,不记录select和show
用于恢复数据库、主从复制
写入机制:
事务执行过程中,先把日志写到binlog cache,事务提交时,再把binlog cache写到bin log
系统为每个线程分配一片binlog cache内存,参数binlog_cache_size控制单个线程内binlog cache大小。超过这个大小就要暂存磁盘
事务提交时,执行器把binlog cache完整的事务写入bin log中。并清空binlog cache
write是把日志写入到文件系统的page cache内存,没有持久化到磁盘,fsync是将数据持久化到磁盘
sync_binlog = 0时,每次提交事务都只write,不fsync,系统选择刷盘时机;sync_binlog = 1时,每次提交事务都会执行fsync;sync_binlog = N(N>1)时,表示每次提交事务都write,累积N个事务后才fsync
redo log:物理日志,记录对某个数据页做了什么修改,循环写
Innodb引擎级别的文件,记录innodb存储引擎的事务日志,不管事务是否提交
用于掉电等故障恢复,InnoDB存储引擎使用
redo log
恢复到发生故障前的时刻参数
innodb_flush_log_at_tx_commit
设置为1,commit时会将redo log
同步写到磁盘,bufferpool中的数据之后由系统写入机制(参数设置同样影响数据是否丢失):
生成的redo log要先写到redo log buffer(多个线程公用),buffer的内容并不需要每次生成后都要持久化到磁盘中
事务没提交的时候,redo log buffer部分日志也是有可能被持久化到磁盘中(redo log buffer 占用到了一定程度,或者有的线程会定时(每隔 1 秒)把redo log buffer中的数据刷盘)
innodb_flush_log_at_trx_commit设置为0,每次事务提交的时候都只是把redo log留在redo log buffer中,每1s刷新一次到磁盘中;设置为1,每次事务提交的时候都只是把redo log直接持久化到磁盘;设置为2,每次事务提交时都只是把redo log写到page cache;
InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),由有 2 个 redo log 文件组成:ib_logfile0
和
ib_logfile1。一个文件满了,则写入另一个文件Buffer Pool 的脏页刷盘,redo log 对应的记录也就没用了
undo log:逻辑日志,记录一个操作过程
进行数据修改时还会记录
undo log
,用于数据的撤回操作——保留了修改前的内容,如果事务执行失败或者调用rollback,InnoDB引擎会根据undo log中的记录,将数据回滚到之前的样子可以根据
undo log
回溯到某个特定的版本的数据,保证事务原子性,实现MVCC刷盘:
- 需要通过 redo log 保证持久化——undo 写入时,也像数据一样产生对应的 redo Log
- buffer pool 中有 undo 页,对 undo 页的修改会记录到 redo log,redo log刷盘后,undo 页才刷盘,然后数据刷盘
redo log & undo log
- Redo Log 记录了此次事务「完成后」 的数据状态,记录的是更新之 「后」的值。事务提交之后发生崩溃,通过redo log恢复事务
- Undo Log 记录了此次事务「开始前」 的数据状态,记录的是更新之 「前」的值。事务提交之前发生崩溃,通过undo log回滚事务
更新过程:
- 检查buffer pool有没有这个要更新的entry,没有就从磁盘加载到内存
- buffer pool中的undo page记录undo log,记录对应的redo log
- 执行器更新内存中的数据
- 写入redo log buffer
- 写入redo log(WAL,写入数据前先写入日志,后台线程选择一个合适的时机将脏页写入到磁盘)
- 执行器写入bin log cache
- 两阶段提交
redo log 和 bin log 的一致性
保证Redo Log和Binlog在事务提交时的数据一致性,要么都存在,要么都不存在
差异:
适用对象不同:
- bin log 是 MySQL 的 Server 层实现的,所有引擎都可以使用
- 而 redo log 是 InnoDB 引擎特有的
写入内容不同:
- bin log 是逻辑日志,记录的是这个语句的原始逻辑,比如 “给 id = 1 这一行的 age 字段加 1”
- redo log 是物理日志,记录的是 “在某个数据页上做了什么修改”
写入方式不同:redo log 具有崩溃恢复的能力,bin log 不具备
bin log 追加写入,不会覆盖以前的日志——没有标志能让 InnoDB 从 bin log 中判断哪些数据已经刷入磁盘了,哪些数据还没有
bin log 记录了两条日志:
1
2记录 1:给 id = 1 这一行的 age 字段加 1
记录 2:给 id = 1 这一行的 age 字段加 1假设在记录 1 刷盘后,记录 2 未刷盘时崩溃,数据库是无法判断这两条记录哪条已经写入磁盘,哪条没有写入磁盘
redo log 是循环写,空间固定会被用完,只会记录未刷入磁盘的日志,已经刷入磁盘的数据会从 redo log 里删除。数据库重启后,直接把 redo log 中的数据都恢复至内存就可以
redo log 两阶段提交
- redo log prepare —— bin log写入磁盘 —— redo log commit
- prepare阶段,写redo log
- commit阶段,写binlog并且将redo log的状态改成commit状态;
- 恢复时的判断:
- 如果 redo log 里面的事务有了 commit 标识,直接提交
- 如果 redo log 里面的事务处于 prepare 状态,则判断对应的事务 bin log 是否存在并完整
- 如果 binlog 存在并完整,提交事务
- 否则,回滚事务
- redo log prepare —— bin log写入磁盘 —— redo log commit
原因:(背景:主从复制)
- redo log 状态修改为 commit 前发生崩溃:bin log 已经写入成功,从数据库根据主数据库的bin log同步时,已经完成这一操作,而刚恢复的主库之前没有提交这一事务,因此需要将这一事务提交,保证主从一致
- bin log还没写入时发生崩溃:binlog 还没有写入,之后从库进行同步的时候,无法执行这个操作,但是实际上主库已经完成了这个操作(redo log中有这个记录,表明之前内存或磁盘中的数据已经更新),所以为了主备一致,在主库上需要回滚这个事务(直接加载这个redo log,不执行prepare状态的内容)
无两阶段提交是否可以?
- 先bin log后redo log:bin log 写完,redo log 还没写的时候,MySQL 崩溃。binlog 已经写入成功。会被从库同步过去,但 redo log 还没写,主库没有完成这个操作(redo log没有写,因此内存数据没有刷盘,同时在重新加载redo log恢复内存数据时,没有这个操作记录),从库相比主库就会多执行一个事务
- 先redo log后bin log:redo log 写完,bin log 还没有写完的时候,MySQL 崩溃。主库中的数据已经被修改了,但 bin log 没有记录这个语句。从库根据bin log同步的时候,会丢失这个更新,和主库不一致
bin log是否完整的校验:
- bin log有完整的格式:
- statement 格式的 bin log,最后会有 COMMIT
- row 格式的 bin log,最后会有 XID event
- bin log有完整的格式:
5.6.2 版本引入
binlog-checksum
参数,验证 bin log 内容的正确性两阶段提交导致磁盘IO高,对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。解决方法:将 sync_binlog 设置为大于 1 的值(比较常见是 100~1000),每次提交事务都 write,但累积 N 个事务后才 fsync、将 innodb_flush_log_at_trx_commit 设置为 2。以上风险为,主机关机会丢失多个事务的数据
组提交
- 多个事务提交的时候,将多个 binlog 刷盘操作合并成一个,减少磁盘 I/O
- prepare 阶段不变,将 commit 阶段拆分多个阶段,每个阶段都有一个队列,每个阶段有锁进行保护,以保证事务写入的顺序。第一个进入队列的事务成为 leader,leader领导所在队列的所有事务,完成后通知队内其他事务操作结束
- flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘)
- 第一个事务成为leader
- 获取队列中的事务组,由leader将同组事务的redo log刷盘(prepare)
- 将该组事务的 binlog 写入 binlog 的page cache
- sync 阶段:对 binlog 文件做 fsync 操作(多个事务的 binlog 合并一次刷盘)
- 等待一段时间,以组合更多事务的 binlog 一起刷盘
- 如果事务的数量提前达到了参数
Binlog_group_commit_sync_no_delay_count
,马上将 binlog 刷盘
- commit 阶段:各个事务按顺序做 InnoDB commit 操作。调用引擎的提交事务接口,将 redo log 状态设置为 commit
- flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘)
MySQL 执行计划
通过 explain 命令获取 select 语句的执行计划,了解 select 语句以下信息:(
explain select ...
)- 表的加载顺序
- sql 的查询类型
- 可能用到哪些索引,实际上用到哪些索引
- 读取的行数
查看字段password的前prefixlen的区分度:
select count(distinct left(password,prefixLen))/count(*);
查SQL的执行情况(是否使用了索引)
type
:type
反应查询语句的性能system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
- 查询至少达到
range
级别,最好能达到ref
级别 - All:全表扫描
- index:全索引扫描
- range:索引范围扫描
- ref:使用非唯一性索引或者唯一索引的前缀扫描,返回匹配某个单独值的记录行
- eq_ref:唯一索引扫描(相对于ref来说就是使用的是唯一索引)
- const/system:单表中最多只有一条匹配行,查询起来非常迅速,匹配行中的其他列中的值可以被优化器在当前查询中当做常量来处理。例如根据主键或者唯一索引进行的查询
possible_keys
:SQL查询时用到的索引key
:SQL实际决定查询结果使用的键(索引)rows
:MySQL认为它执行查询时必须检查的行数
大表优化
- 单表记录数过大时,数据库的性能会明显下降,常见的优化措施:
- 限定数据的范围。比如:查询历史信息时,限制时间范围为一个月
- 读写分离。数据库拆分,主库负责写,从库负责读;
- 通过分库分表的方式进行优化——垂直拆分和水平拆分
分库分表
切分的目的在于减少数据库的负担,缩短查询的时间
参考TDDL中间件——解决分布式数据库产生的相关问题
垂直划分:根据业务进行划分。通过降低单库的大小来提高性能,但并没有解决高数据量带来的性能损耗
- 行记录变小,数据页可以存放更多记录
- 主键出现冗余,需要管理冗余列; 会引起表连接JOIN操作;依然存在单表数据量过大的问题
水平划分:根据一定规则,例如时间或id序列值等进行数据的拆分
* 单库(表)的数据量得以减少;切分出的表结构相同,程序改动较少 * 分片事务一致性难以解决,跨节点join性能差,逻辑复杂,数据分片在扩容时需要迁移
分区表
分区表是一个独立的逻辑表,底层由多个物理子表组成。当要查询的数据分布在某一个分区时,查询引擎只去某一个分区查询,而不是遍历整个表。如果需要删除某一个分区的数据,只需要删除对应的分区即可
分区方法:
按照范围分区
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19CREATE TABLE test_range_partition(
id INT auto_increment,
createdate DATETIME,
primary key (id,createdate)
)
PARTITION BY RANGE (TO_DAYS(createdate) ) (
PARTITION p201801 VALUES LESS THAN ( TO_DAYS('20180201') ),
PARTITION p201802 VALUES LESS THAN ( TO_DAYS('20180301') ),
PARTITION p201803 VALUES LESS THAN ( TO_DAYS('20180401') ),
PARTITION p201804 VALUES LESS THAN ( TO_DAYS('20180501') ),
PARTITION p201805 VALUES LESS THAN ( TO_DAYS('20180601') ),
PARTITION p201806 VALUES LESS THAN ( TO_DAYS('20180701') ),
PARTITION p201807 VALUES LESS THAN ( TO_DAYS('20180801') ),
PARTITION p201808 VALUES LESS THAN ( TO_DAYS('20180901') ),
PARTITION p201809 VALUES LESS THAN ( TO_DAYS('20181001') ),
PARTITION p201810 VALUES LESS THAN ( TO_DAYS('20181101') ),
PARTITION p201811 VALUES LESS THAN ( TO_DAYS('20181201') ),
PARTITION p201812 VALUES LESS THAN ( TO_DAYS('20190101') )
);list分区。分区字段必须是已知的,如果插入的字段不在分区时枚举值中,将无法插入
1
2
3
4
5
6
7
8
9
10create table test_list_partiotion(
id int auto_increment,
data_type tinyint,
primary key(id,data_type)
)partition by list(data_type)
(
partition p0 values in (0,1,2,3,4,5,6),
partition p1 values in (7,8,9,10,11,12),
partition p2 values in (13,14,15,16,17)
);hash分区,将数据均匀地分布到预先定义的分区中
1
2
3
4
5create table test_hash_partiotion(
id int auto_increment,
create_date datetime,
primary key(id,create_date)
)partition by hash(year(create_date)) partitions 10;
潜在问题:
- 打开和锁住所有底层表的成本可能很高。当查询访问分区表时,MySQL需要打开并锁住所有的底层表——可以通过批量操作降低此类开销,如批量插入、LOAD DATA INFILE、一次删除多行数据
- 所有分区必须使用相同的存储引擎
查询语句执行流程?
- 权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎
1 | update user set name = '大彬' where id = 1; |
- 流程:
- 检查权限,没有权限则返回错误
- 查询缓存,缓存命中则直接返回
- 词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;
- 两种执行方案:先查 id > 1 还是 name = ‘大彬’ ,优化器根据自己的优化算法选择执行效率最好的方案
- 校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果
更新语句执行过程?
- 分析器、权限校验、执行器、引擎、
redo log
(prepare
状态)、bin log
、redo log
(commit
状态)
1 | update user set name = '大彬' where id = 1; |
流程:
- 先查询 id 为 1 的记录,有缓存会使用缓存
- 拿到查询结果,name 更新为大彬,调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,记录
redo log
,此时redo log
进入prepare
状态 - 执行器收到通知后记录
bin log
,调用引擎接口,提交redo log
为commit
状态 - 更新完成
记录完
redo log
,不直接提交而是先进入prepare
状态- 假设写
redo log
后直接提交,然后写bin log
。此时如果写完redo log
,机器挂了,bin log
日志没有被写入,重启后机器会通过redo log
恢复数据。但后续进行数据库备份时,bin log
没有这个记录,会丢失这条数据——主从同步也会丢失这一条数据
- 假设写
exist和in的区别
略
truncate、delete与drop区别?
相同
truncate
、不带where
子句的delete
、drop
都会删除表内的数据drop
、truncate
都是DDL
(数据定义语言),执行后自动提交
不同
- truncate 和 delete 只删除数据不删除表;drop 删除表的结构、被依赖的约束、触发器、索引
- 执行速度:drop > truncate > delete
having和where区别
where
子句作用于表和视图,having
作用于组where
在数据分组前过滤,having
在数据分组后过滤
MySQL主从同步
数据从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(
master
),其余的服务器充当从服务器(slave
)复制是异步的,从服务器不需要一直连接着主服务器
好处
- 读写分离
- 在主服务器上生成实时数据,在从服务器上分析数据
- 数据备份,保证数据安全
过程:
- 当主库数据发生变更时,写入本地Bin Log文件
- 从库IO线程发起dump主库Bin Log文件的请求
- 主库IO线程推送Bin Log文件到从库中
- 从库IO线程把Bin Log内容写入本地的Relay Log文件中
- 从库SQL线程定时读取Relay Log文件内容
- 从库SQL线程重新执行一遍SQL语句
同步延迟
- 使用多线程复制:主从同步的最后两步使用多线程,修改配置 slave_parallel_workers=4,代表开启4个复制线程
- 修改同步模式:
- 全同步复制
当主库执行完一个事务,并且所有从库都执行完该事务后,才给客户端返回成功。 - 半同步复制
至少有一个从库执行完成后,就给客户端返回成功。 - 异步复制
主库执行完后,立即返回成功,不关心从库是否执行完成。
- 全同步复制
乐观锁和悲观锁
- 数据库的并发控制:确保多个事务同时存取数据库中同一数据时,不破坏事务的隔离性和统一性、数据库的统一性
- 悲观锁
- 乐观锁
- 悲观锁:假定发生并发冲突,查询完数据时把事务锁,直到提交事务——使用数据库中的锁机制实现
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改。给表增加
version
字段,在修改提交前检查version
与原来取到的version
是否相等,若相等,表示数据没有被修改,可以更新,否则不能更新——使用版本号或CAS算法实现
存储过程和触发器
- 存储过程:一组为了完成特定功能的 SQL 语句集,经过第一次编译后再次调用不需要再次编译,通过指定存储过程的名字并给出参数来执行
- 查找语句尽量不要放在循环内
- 利用一些 sql 语句来替代一些小循环
- 事务越短越好
- 中间结果存放于临时表
- 触发器:特殊的存储过程,对某一个表进行操作时触发——DML触发器和DDl触发器
分布式事务的两阶段提交和三阶段提交
两阶段提交
使分布式架构下的所有节点在事务提交时保持一致性
每个节点知晓自己的操作时成功或者失败,无法知道其他节点的操作成功或失败
XA: X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。接口函数由数据库厂商提供
要引入一个作为协调者的组件来统一掌控所有节点的操作结果,最终指示这些节点是否要把操作结果进行真正的提交
- 参与者将操作成败通知协调者
- 协调者根据所有参与者反馈情报,决定各参与者是否要提交操作还是中止操作
操作:
- 准备阶段:协调者给每个参与者(资源管理器)发送 Prepare,参与者要么直接返回失败(如权限验证失败),要么在本地执行事务——写 redo 和 undo 日志
- 提交阶段:协调者给每个参与者发送Rollback或Commit,参与者执行回滚或者提交
在commit过程中,发生宕机等异常,在服务重启后根据XA recover再次进行补偿
缺陷:
- 脑裂(部分参与者收到commit)
- 协调者单点故障
- 同步阻塞:所有参与节点都是同步阻塞的
三阶段提交
- 在协调者和参与者中引入超时机制
- 第一阶段和第二阶段中插入一个准备阶段。保证在最后提交阶段之前各参与节点的状态一致
- 操作:
- CanCommit:协调者向参与者发送 commit 请求,参与者如果可以提交就返回 Yes 响应,否则返回 No
- PreCommit:
- 协调者从所有的参与者获得的反馈都是 Yes 响应,执行事务的预执行
- 任何一个参与者向协调者发送 了 No 响应,或者等待超时,执行事务的中断
- DoCommit:
- 协调这发送提交请求
- 参与者提交事务
- 参与者发送 Ack
- 协调者确定完成事务
柔性事务(分布式事务)
刚性事务:
- 强一致性:各个业务操作必须在事务结束时全部成功,或者全部失败
- XA模型
- 应用程序(AP):定义事务边界(事务开始与结束),并且访问事务边界内的资源
- 资源管理器(RM):管理计算机共享的资源,资源即数据库
- 事务管理器(TM):负责管理全局事务,分配全局事务ID,监测事务的执行速度,并负责事务的提交,回滚,失败恢复等
- 满足CAP理论的CP
BASE:
- 基本可用(Basically Available)
- 柔性状态(Soft State)
- 最终一致性 (Eventual Consistency)
两阶段型:分布式事务两阶段提交
补偿型:
TCC(Try/Confirm/Cancel)——基于补偿的 long-running 的事务处理模型(每个子业务都需要实现try-confirm-cancel接口,对业务本身的侵入性较大) 分布式事务设计与实践-TCC与Saga
- try:尝试执行事务,完成业务检查,预留必要的资源
- confirm:真正执行业务,不做业务资源检查
- cancel:释放try阶段预留的业务资源
过程:
- 服务器 A 发起事务, 服务器 B 参与事务
- 服务器 A 的事务如果执行顺利,事务 A 先行提交,如果事务 B 也执行顺利,则事务 B 也提交,整个事务完成
- 如果事务 B 执行失败,事务 B 本身回滚,需要执行一个补偿操作,将已提交的事务 A 执行的操作反向,恢复到未执行前事务 A 的状态
牺牲了一定的隔离性和一致性的,提高了 long-running 事务的可用性
举例:转账500
汇款服务:
Try
1.检查A账户有效性,查看A账户是否存在或者未冻结。
2.检查账户余额是否大于等于500元
3.从A账户扣减500元,并且状态置为“转账中”
4.记录一个转账日志或者消息
Confirm
1.将“转账中”的状态改为“正常”状态
2.生成转账流水,删除转账日志
Cancel
1.账户余额加回500元
2.账户状态改为“正常”
收款服务:
Try:
1.检查B账户是否正常
Confirm:
1.从转账日志或者消息中获取账户A往账户B转账500元
2.账户B增加500元
Cancel:
不做操作
SAGA:适合用来处理时间跨度比较长的分布式事务 分布式事务系列三
由一系列的本地事务构成,每一个本地事务在更新完数据库之后,会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行
如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作
基于事件和基于命令
基于事件:以订单流程为例,Order Service、Payment Service、Stock Service、Delivery Service
- 处于当前Saga下的各个服务,会产生某类事件,或者监听其它服务产生的事件并决定是否需要针对监听到的事件做出响应
- 正常流程:
为了在异常情况下回滚整个分布式事务,需要为相关服务提供补偿操作接口(库存服务出现异常为例,需要回滚订单服务、支付服务)
此时,各参与方相互之间无直接沟通,完全解耦,但如果业务方很多,则可能产生环形监听
基于命令:定义一个新的服务,作为协调中心。协调中心通过命令/回复的方式和Saga中其它服务交互
OSO清楚一个订单处理Saga的具体流程,并在出现异常时向相关服务发送补偿命令来回滚整个分布式事务
实现协调中心的一个比较好的方式是状态机
注意,监听消息是主动监听,命令的话是被动接收。监听消息的话除了开发业务逻辑外,还需要了解整个长事务模型
异步确保型:将一系列同步的事务操作变为基于消息执行的异步操作,避免分布式事务中的同步阻塞操作的影响
最大努力通知型(多次尝试):与前面异步确保型操作不同的是,在消息由 MQ Server 投递到消费者之后,允许在达到最大重试次数之后正常结束事务
嵌套事务
优化设计
- sql 优化的思路