Go面试(高级版)
2023-06-11 09:53:33 28 举报
AI智能生成
Go面试(高级版)是一场专为具备一定Go语言基础和实际项目经验的开发者设计的面试。在这场面试中,面试官将深入探讨Go语言的高级特性、性能优化、并发编程、网络编程、内存管理等方面的问题。此外,面试者还需要展示自己在设计和实现复杂系统时的能力,如微服务架构、中间件开发、数据库优化等。通过这场面试,面试官旨在评估面试者在Go领域的专业知识、实战经验和解决问题的能力,以确保他们能够胜任高级Go开发工程师的职责。
作者其他创作
大纲/内容
网络
基础
网络层次划分
OSI七层网络模型(理论)
应用层
产生数据
表示层
对应用层来的数据进行压缩、解压缩,加解密
会话层
数据传输之前建立一个会话,传输过程中维持一个会话,传输结束终止这个会话
传输层
标明上层是哪些应用程序(流控)
网络层
寻址
源IP地址、目的IP地址、协议类型
数据链路层
承上启下
源MAC地址、目的MAC地址、上层协议类型
物理层
定义一些设备的接口以及传输速率
TCP/IP五层网络模型(实际)
应用层
为操作系统或应用程序提供访问应用的接口,基本单位为报文
传输层
提供端对端的数据传输(网关)
网关:为局域网内用户提供了一扇门,通过该门可以访问到别的网络,路由器的每个接口都代表一个不同的网络
网络层
路由选择,基本单位为IP数据报(路由器)
数据链路层
为网络层提供可靠的数据传输,基本单位为帧,主要协议为以太网协议(网桥、交换机)
物理层
确保原始数据可在各种物理媒体上传输(中继器、集线器)
IP地址
IP地址的类型
网络地址
IP地址由网络号和主机号组成,网络地址的主机号全为0,网络地址代表整个网络
广播地址
广播地址通常称为直接广播地址,是为了区分受限广播地址,广播地址的主机号全为1,向广播地址发送消息将广播给网络内所有主机
组播地址
D类地址就是组播地址
私有地址
A类私有地址
10.0.0.0~10.255.255.255
B类私有地址
172.16.0.0~172.31.255.255
C类私有地址
192.168.0.0~192.168.255.255
子网掩码
用来分割子网和区分哪些是同一网段的
子网划分:减少子网掩码长度,合并多个网络;增加子网掩码长度,将一个网络切成多个网络
无类域间路由:CIRD基于可变长子网掩码,使网络分配更加灵活
设备
交换机(数据链路层)
描述
维护MAC地址表,隔离冲突域,寻址和进行数据转发,交换机会把广播、组播和未知单播帧从所有其他端口发送出去(除了发送帧的端口)
工作流程
ARP解析:如果发送方不知道目标设备的MAC地址,它将首先使用ARP协议来解析目标设备的MAC地址,它会广播ARP请求,目标设备回复自己的MAC地址
帧封装:发送方知道目标设备MAC后,将IP报文封装到一个数据帧中,数据帧包括源MAC地址,目的MAC地址,帧类型和数据部分
MAC地址表查询;当交换机收到数据帧后,它会查询自己的MAC地址表,查找目标MAC地址对应的接口
转发数据帧:如果交换机能够找到目标MAC地址对应的接口,则从此接口发送,构否则广播到所有接口
MAC地址学习:当一个端口接收到一个数据帧时,交换机会检查源MAC地址,并更新其MAC地址表
路由器
工作过程
主机将数据包发送给TCP层,TCP将数据分成多个数据端,并为每个数据段添加TCP头
TCP层将数据段传递给IP层,IP层将数据段封装成IP数据报,并在数据报头中添加源IP地址和目标IP地址
如果目标主机不在同一子网中,IP层需要将数据包发送给默认网关,此时,IP层将使用ARP协议获取网关的MAC地址
如果ARP缓存中不存在网关的MAC地址,则主机将会向本地网络所有主机发送ARP请求
源主机接收到ARP响应后,将使用网关的MAC地址封装IP数据报,并将其发送给网关,跨网段通信,其他网段IP地址的MAC地址均为网关MAC地址
网关接收到数据包,根据目标IP地址查找路由表,将源MAC地址设置为入口接口的MAC地址,目的MAC地址设置为下一跳的MAC地址,并将数据包转发出去
MAC地址是一个局域网内才有效的地址,因而,MAC地址只要过网关,就必定会改变,因为已经换了局域网
集线器
信号放大,解决往下只能短距离传输问题
无脑广播,从一个接口收到数据会从所有其他接口发送出去
半双工,同一时间只能接收或发送数据,会使网络造成冲突,所波及的范围叫做冲突域
无线AP就是hub
协议
TCP/IP协议栈
TCP
描述:在不可靠的网络上,为host-to-host的通信双方提供可靠的逻辑链路
特点
基本的数据传输
连接是双向通信的,TCP决定何时停止和发送数据
流量控制
通过滑动窗口算法实现流量控制,滑动窗口可以动态调整发送方和接收方的数据传输速率,避免接收方因处理能力不足而被发送方发送的大量数据淹没
可靠传输
为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是回应答之前的某个ID,称为累计应答或累计确认
重传
自适应重传算法
TCP通过采样RTT(往返时延)的时间,然后进行加权平均,算出一个值,这个值还是不断变化的
快速重传算法
当发送方收到3个连续的重复确认时,它会立即重传丢失的报文段,而不需要等待重传计时器超时
拥塞控制
TCP的拥塞控制主要来避免两种现象,包丢失和超时重传,一旦出现这些现象就说明发送速度太快,要慢一点
BBR拥塞算法
BBR主要思想是利用瓶颈链路的带宽和往返时间(RTT)来调整发送速率,而不是依赖丢包信号
BBR通过测量链路的带宽和最小往返时间来评估网络的传输能力 ,然后根据这些信息调整发送速率
TCP报文头部
FLAGS
紧急比特(URG)
当URG=1时,表明紧急指针有效,它告诉系统此报文段中有紧急数据,应当尽快传送(相当于高优先级数据)
确认比特(ACK)
只有当ACK=1时确认序号字段才有效
复位比特(RST)
当RST=1时,表明TCP连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后重新建立连接
同步比特(SYN)
表示一个连接请求或连接接受请求
终止比特(FIN)
用于释放一个连接,表明此报文段的发送端数据已经发送完毕,并要求释放连接
推送比特(PSH)
接收方TCP收到推送比特置1的报文段,就尽快地交付给接收应用程序,而不再等到整个缓存都填满了后再向上交付
窗口大小
16bit,窗口字段用来控制对方发送的数据量,单位为字节,用来通知对方以确定发送窗口上限
TCP连接的状态
LISTEN:等待远程的TCP连接请求
SYN-SEND:发送了建立连接的请求
SYN-RECIVED:收到对方建立连接请求,且回复了建立连接请求,等待对方确认
ESTABLISHED:连接已经建立,可以正常进行数据传输
FIN-WAIT-1:等待对方确认刚刚发送的关闭连接请求
FIN-WAIT-2:收到关闭连接请求的确认,等待对方发送关闭连接的请求
CLOSE-WAIT:确认了对方的关闭连接请求,等待本地用户关闭连接指令
LAST-ACK:被动关闭的一方,在CLOSE-WAIT状态下收到本地用户关闭连接的指令,发送关闭连接请求,等待确认
TIME-WAIT:主动关闭连接的一方收到对方发送的关闭连接请求的确认消息后,等待2MSL以确认对方接收到ACK包,最后回到CLOSE状态
CLOSE:关闭状态
三次握手
客户端向服务端发送连接请求包,标识位SYN=1,序号Seq=X
服务器收到客户端发过来的报文,由SYN=1知道客户端要建立连接,想客户端发送一个SYN和ACK都置为1的TCP报文,Seq=Y,将确认序号Ack=X+1
客户端收到服务端发来的包后,检查确认Ack是否正确,如果正确,客户端再次发送确认包ACK=1,Seq=X+1,Ack=Y+1
四次挥手
服务端发出FIN、ACK,用来断开服务端到客户端的数据传送,进入FIN-WAIT-1状态
客户端收到服务器端的FIN后,发送ACK确认报文,进入CLOSE-WAIT状态
客户端发出FIN、ACK,用来断开客户端到服务端的数据传送,进入LAST-ACK状态
服务器收到客户端的FIN后,发送ACK确认报文,进入TIME-WAIT状态,服务器等待两个最长报文过期时间后进入CLOSE,客户端收到ACK立即进入CLOSE
问题
为什么是四次挥手
TCP是全双工模式,需要两边的连接全部关闭,此TCP会话才算完全关闭
当收到对方FIN报文时,仅仅表示对方不再发送数据但是还能接收数据,己方是否现在关闭数据通道,需要上层应用来决定,因此己方ACK和FIN会分开发送
为什么等待2MSL(报文最大生存时间)
当TCP的一端发起主动关闭,在发出最后一个ACK包后就进入TIME-WAIT状态,必须在此状态上停留两倍MSL时间
等待2MSL时间主要是怕最后一个ACK包对方没有收到,那么对方在超时后将重发第三次挥手的FIN包,主动关闭端接到重发的FIN可以再次发送ACK应答包
UDP
描述:用户数据报协议,面向数据报的传输层协议,不建立连接,不提供可靠性,传输速度快
特点
无连接
相比于TCP协议,UDP协议在传输数据前不需要建立连接
尽最大努力交付
也就是说UDP协议无法保证数据能准确交付到目的主机
面向报文的
UDP协议将应用层传输下来的数据封装到一个UDP包中,不进行拆分或合并,因此,传输层在收到对方UDP包后,去掉首部后将数据原封不动交给应用层
没有拥塞控制
UDP协议的发送速率不受网络的拥塞影响
IP
特点
IP协议提供了数据报在源和目的地之间的透明传输机制
IP协议提供了对长数据报的分段和重组机制
路由选择协议
描述:路由协议是用于计算机网络中的路由器之间的通信协议,用于确定如何将数据包从源地址转发到目标地址
RIP(路由信息协议)
RIP是一种距离矢量类型的路由选择协议,它使用跳数作为衡量标准,
OSPF(开放最短路径优先)
OSPF是一种链路状态类型的路由选择协议,基于路径成本,可以应对更大规模网络,使用洪泛法广播链路状态信息
操作系统
Linux的IO原理
描述:指在计算机执行操作时,CPU不需要先将数据从一个内存区域复制到另一个内存区域,从而减少上下文切换以及CPU的拷贝时间
作用:它的作用是,在数据包从网络设备到用户程序空间的传递过程中,减少数据拷贝次数,减少系统调用,实现CPU零参与(DMA、内存映射技术)
网络数据传输过程
当http服务器收到来自浏览器的请求时,负责处理请求的线程发起系统调用,让内核将所需数据从存储设备加载到内核缓冲区kernel buffer
由于内存设备和存储设备之间没有数据传输,没有CPU参与,所以这次时DMA操作
当数据准备好后,内核唤醒处理请求的线程,让它使用read()函数把数据复制到应用缓冲区,到了应用缓冲区就属于该线程独有,可以对其进行读写操作
当数据修改完成(也可能没做任何操作),通过write()函数将应用缓冲区中的数据复制到socket缓冲区的send buffer
非本机的数据最终还是通过网卡传输出去的,所以再使用send()函数就可以将send buffer的数据发送给网卡
由于内存设备和存储设备之间没有数据传输,没有CPU参与,所以这次时DMA操作
Linux IO数据传输过程
第一阶段;等待数据放入内核空间
第二阶段:将数据从内核空间复制到用户空间
Linux IO读写方式
轮询
基于循环对IO端口不断检测
IO中断
当数据到达时,磁盘主动向CPU发起中断请求,由CPU自身负责数据传输过程
在DMA技术出现之前,应用程序与磁盘之间的IO请求都是通过CPU完成的,CPU中断然后发起IO请求,等待数据拷贝完成
DMA
直接内存存取,在IO中断的基础上引入DMA磁盘控制器,由DMA磁盘控制器负责数据的传输,降低CPU的资源消耗
Linux IO模型
描述:所谓IO模型,描述的是出现IO等待时,进程的状态以及处理数据的方式
阻塞式IO(BIO)
程序想要在缓冲区读数据时,缓冲区并不一定会有数据,这回造成陷入系统调用,只能等待数据可以读取,没有数据读取时会阻塞住进程
非阻塞式IO(NIO)
应用进程在发起IO系统调用(读取数据)后会立即返回,应用进程可以轮询发起IO系统调用,直到内核返回可以读取标识
多路IO复用
将多个应用进程的socket注册到多路复用器上,使用一个进程来监听该多路复用器,只要有一个socket的数据准备好就会返回该socket
再由应用进程发起IO系统调用,来完成数据读取(使用多路复用器来统一轮询,而不是每个进程进行轮询)
信号驱动IO
应用进程向内核注册一个信号处理程序,当内核中有数据准备好,会发送一个信号给应用进程
应用进程便可以在信号处理程序中发起IO系统调用,来完成数据读取(通过信号通知而不是轮询,避免了大量无效的数据状态轮询操作)
异步IO
应用进程发起IO系统调用后,会立即返回,当内核中数据准备好并复制到用户空间后,会产生一个信号来通知应用进程
只需发起一次系统调用,便可以完成对数据的读取
零拷贝技术
用户态直接IO
运行在用户态的库函数直接访问硬件设备,数据直接跨过内核进行传输,适用于不需要内核缓冲区处理的应用程序
mmap+write
mmap是Linux提供的一种内存映射方法,即将一个进程的地址空间中的一段虚拟地址映射到磁盘文件地址
使用mmap的目的是将内核中读缓冲区的地址与用户空间的缓冲区进行映射,从而实现内核缓冲区与应用程序内存共享
sendfile
sendfile系统调用可以将数据从文件描述符发送到另一个文件描述符,这个过程始终在内核空间,因此可以实现零拷贝
sendfile+DMA gather copy
DMA引入了gather操作,使其可以根据内存地址、地址偏移量将数据批量从内核缓冲区拷贝至网卡
sendfile只需将读缓冲区的文件描述符和数据长度拷贝至socket buffer,DMA通过gather操作直接读取内核缓冲区数据
splice
splice系统调用在内核空间的读缓冲区和socket buffer之间建立管道,避免两者之间CPU拷贝操作
CPU
中断
cpu运行过程中,当发生其他事件,cpu停止当前程序流,转而处理该事件
软中断
cpu指令,由当前运行程序产生
系统调用
程序从用户态转为内核态
执行系统函数
程序从内核态返回用户态
硬中断
外设、磁盘、网卡、时钟
时钟周期:时钟频率的倒数,是计算机中最小的事件单位
cpu架构
控制器,运算器,寄存器,L1(数据缓存,指令缓存,L2)
寄存器
寄存器是cpu内部用于存放数据的小型存储区域,用来暂时存放参与运算的数据及结果,和一些cpu运行时需要的信息
通用寄存器、标志寄存器、指令寄存器、控制寄存器
L3,总线、主存、磁盘
缓存
局部性原理
cpu在访问存储设备的时候,都趋于聚集在一片连续的区域中
时间局部性:
如果一个信息项正在被访问,那么近期它可能倍再次访问
空间局部性:
如果一个存储器的位置被引用,那么它附近的位置也可能将来被引用
缓存一致性协议
MESI
M(修改)
表示缓存块最近被修改过,与内存中不一致,同时在所有私有缓存中只有一份数据
E(独享)
与在所有私有缓存中只有一份数据,该缓存块没有被修改过,与内存中一致,所以该cpu要修改缓存块时不需要通知其他cpu
S(共享)
表示至少在其他cpu中存在一份副本,所以当cpu修改该缓存块状态时,需要通知其他cpu
I(无效)
表示该缓存无效,当读取时,发送缓存未命中
协议消息
Read
该消息包含了所读取的缓存块的物理地址,表示cpu读取某个缓存块
Read Message
该消息表示Read消息的响应
Invalidate
该消息包含了要失效的缓存块的物理地址
Invalidate acknowledge
表示Invalidate消息的响应
Read Invalidate
该消息时Read和Invalidate的组合,其他cpu必须让其私有缓存块中对应缓存失效
Writeback
该消息表示缓存内的数据写回主存
工作流程
缓存一致性协议通过监控独立的loads和stores指令来监控缓存同步冲突的,并确保不同处理器对于共享内存的状态有一致性的看法
当一个处理器loads或stores一个内存地址时,它会在bus总线上广播该请求,其他的处理器都会监听总线
注意
MESI协议只对汇编指令中执行加锁操作的变量有效
如果变量超过一个缓存行的大小,则只能用总线锁。因为MESI是针对单个缓存进行加锁的
MOESI:增加了允许CPU cache间同步数据
store buffers
要修改一个当前缓存块中不存在的变量,那么需要发出Read Invalidate消息并等待其他cpu响应,为了解决这个问题,引入了store buffers
为了不让cpu等待,指令A1将修改的值存入store buffers并给其他cpu发Read Invalidate消息,cpu继续执行其他命令,此时cacheline为修改前的值
若在其他cpu响应之前,当前cpu指令A2使用了cacheline中修改前的值,则会产生类似指令重排的现象,即A2在A1之前执行
内存屏障
由于引入store buffers会导致指令重排问题,这种重排会导致可见性问题,所以引入内存屏障来解决这个问题(解决数据只更新到store buffers而没有更新到cacheline导致的可见性问题)
将store buffers中所有已有变量做标记,后续的write操作,必须等待store buffers中所有被标记数据被清空后才更新cache line
缓存行cache line
cpu缓存中可分配的最小单元,与结构有关,常用64字节
伪共享
what:共享数据被修改,需从内存中重新加载,极大降低了cpu效率
how:缓存行填充(缓存行对齐)
多路组相联 Set Associativity
将缓存划分为若干个cacheline,将指定数量的cacheline关联成一个组
缓存淘汰机制
FIFO、LRU
Linux的CPU上下文切换
上下文:CPU在运行每个任务之前,都必须知道任务从哪里加载以及从哪里开始运行,这些都依赖于系统事先帮他设置好寄存器和程序计数器
CPU寄存器:CPU内置容量小、速度快的内存空间
程序计数器:用来存储即将执行的下一跳指令位置
上下文切换
将当前任务的上下文保存起来,然后加载新任务的上下文,运行新任务,这些保存的上下文,会存储在内核中,并在任务调度执行时再次加载进来
CPU上下文切换类型
系统调用上下文切换
内核空间(Ring0):具有最高权限,可以直接访问所有资源
用户空间(Ring3):只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中才能访问这些特权资源
CPU按照特权等级,把进程运行空间分为内核空间和用户空间,分别对应CPU特权等级的RIng0和Ring3
进程既可以在用户空间运行也可以在内核空间运行,进程在用户空间运行称为用户态,而陷入内核空间时,被称为内核态
从用户态到内核态的转变,需要通过系统调用来完成
系统调用过程中对用户的资源没有任何影响,也不会切换进程,所以也称为特权模式切换
系统调用会产生两次上下文切换,由于内核对用户的不信任,因此内核需要对用户进行一些额外的检查,这就耗费了更多的工作了
进程上下文切换
进程的切换只能发送在内核态,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户资源,还包括了内核堆栈,寄存器等内核空间的状态
虚拟内存
进程上下文切换时机
CPU时间片耗尽、系统资源不足、sleep主动挂起、高优先级进程调度、中断
线程上下文切换
同进程中两个线程切换:因为虚拟内存共享,所以只需要切换线程的私有数据,寄存器等不共享的数据
不同进程的两个线程切换:应为资源不共享,所以和进程上下文切换一样
中断上下文切换
与进程上下文不同,中断上下文切换并不设计进程的用户态,只包括内核态中断服务程序执行所必须的状态,包括寄存器、内核堆栈、硬件中断参数等
消息队列
Kafka
原理
消费者组
相同的数据会被不同的消费则组消费多次
同一组内一个消费者只能消费一个分区,消费者大于分区数将会被空闲
消费者
消费者会记录消费的物理偏移量
一个分片对应一个消费者成员,如果消费者组中成员太多会有空闲成员
顺序
kafka每个partition中的消息在写入时都是有序的,消费时,每个partition只能被一个group中的一个消费者消费,保证了消费时也是顺序的
整个topic不保证有序,如果保证topic整个有序,那么将partition调整为1
Rebalance
Rebalance本质上是一种协议,规定了一个消费者组下所有消费者如何达成一致,来分配订阅topic的每个分区
Rebalance重平衡,发生时机
组成员数量发生变化
订阅主题数量发生变化
订阅主题的分区发生变化
消息存储结构
kafka每个partition对应唯一一个文件夹,消息采用Segment File的存储方式
Segment File的意思是:将大文件拆分成小文件存储,这样大文件就变成一段一段(Segment段),这样的好处是IO加载速度快
稀疏索引
并不为每条数据建立索引,而是隔几条建立一个索引,找到位置再遍历(需要有序),减少了内存消耗
正常使用场景
削峰
为什么能削峰?
因为它性能好
分布式:
kafka是一个分布式系统,由多个broker组成,每个topic可以划分为多个partition,这些partition可以分布在不同broker上,提高了系统的处理能力
分区
通过多个partition,topic上的数据被分散到多个broker,kafka可以在多个broker上并行处理
kafka中的消费者使用偏移量来跟踪每个分区的读取进度
复制
通过存储topic的多个副本,以保证数据的持久性和可靠性
持久化:
kafka内部是顺序IO加缓存提高消息存储速度,不用处理业务,能很快让接口返回
零拷贝:
数据传输过程中使用零拷贝技术(sendfile和mmap),减少数据在内核空间与用户空间之间的拷贝
两种方式
网络数据持久化到磁盘 (Producer 到 Broker) 使用mmap
mmap将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件
不可靠
写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘
Kafka提供了一个参数producer.type来控制是不是主动flush
如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)
磁盘文件通过网络发送(Broker 到 Consumer)使用sendfile
sendfile系统调用可以将数据从文件描述符发送到另一个文件描述符,这个过程始终在内核空间,因此可以实现零拷贝
kafka发送数据时,使用sendfile系统调用将数据直接从磁盘发送到网络Socket
批处理:
支持将消息打包再一起发送,减少网络请求次数,提高处理效率
消息压缩:
支持消息压缩,有效减少网络传输和磁盘存储的开销
解耦
服务间解耦用消息队列
服务内解耦用事件总线
消息丢失场景
生产者发送丢失
业务上使用消息发送回调处理发送失败问题
消息幂等
解决重试导致的消息重复问题
实现
ProducerID
在每个Producer初始化时,会被分配一个唯一的ProducerID
SequenceNumber
对于每个ProducerID,Producer发送数据的每个Topic和Prrtition都对应一个从0开始单调递增的SequenceNumber值
broker消息丢失
kafka为了得到更高的性能和吞吐量,将数据异步批量的存储在磁盘中
消费者消息丢失
手动提交offset
如何保证消息不丢
副本
topic创建时,可以设定这个主题要创建多少个副本,每个副本在不同的broker上
ack机制
0: kafka不会等待broker的ack,延迟低,但当server挂掉可能会丢数据
1: 等待leader确认收到发送的ack消息,如果此时leader挂掉,不保证新leader上存在此数据
-1: 等待所有follower收到数据才发送ack
重试
kafka生产者配置中,有一个retries参数,用于设置生产者发送消息失败后的重试次数
参考资料
https://tobebetterjavaer.com/home.html
https://github.com/ZhongFuCheng3y/athena
kafka 尚硅谷
Go
资料
Go专家编程
原理
Go语言原本
源码
Go编程之旅
实战
Go语言设计与实现
源码·原理
刘丹冰
Golang修养之路
原理
基础
const
iota
const声明块中每新增一行iota值自增1
iota在const关键字出现时被重置为0
iota代表了const声名块的行索引
常量未赋值将继承上一行的表达式,必须是在同一个const声明块内
零值
数值类型
0
int、uint、float、complex
布尔类型
false
bool
字符串
""
string
数组
所有元素都是其元素类型的零值
[3]int、[4]MyStruct
结构体
所有元素都是其字段类型的零值
MyStruct{}
其他类型
nil
map、channel、函数、指针
指向无效内存地址的指针
slice
内部数据结构为空,底层数组指针为nil,长度和容量都是0,size=24
接口
内部的类型指针和值指针都为空的值
当你声明但不初始化一个变量时,编译器会自动插入代码来设置对应类型的零值
make与new的区别?
new返回一个指向新分配的零值的该类型的指针,可作用于所有类型
*new([]string) == nil
make返回初始化后的值,作用于slice、map、channel
make([]string, 0)!=nil
数据结构
channel
零值
不能收发数据
任何尝试从nil的channel收发数据都将永久阻塞
向channel写数据
如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把G唤醒
如果缓冲区有空余位置,则将数据写入缓冲区
如果缓冲区没有空余位置,则将当前goroutine加入sendq,等待被唤醒
从channel读数据
如果等待发送队列sendq不为空,先读缓冲区,如果没有则直接从sendq取出G,把G中数据读出,最后把G唤醒
如果sendq为空,则将当前goroutine加入recvq,进入睡眠,等待被唤醒
关闭channel
将recvq中的G全部唤醒,本该写入G的数据位置为零值,把sendq全部唤醒,但这些G会panic
panic场景
关闭值为nil的channel,会panic
关闭已经关闭的channel,会panic
向已经关闭的channel写数据,会panic
slice
零值
可以读写,以及调用append、len、cap
cap
cap为slice首元素到底层数组尾元素的长度
扩容
如果slice容量小于1024,则容量扩大为原来的2倍
如果slice容量大于等于1024,则容量扩大为原来的1.25倍
copy
切片copy,拷贝数量取两个切片长度最小值,从头开始覆盖,不会发生扩容
map
零值
可以读,返回对应类型的零值
不能写,写会导致运行时错误
map使用哈希表作为底层实现,一个哈希表里可以有多个bucket,每个bucket保存了map中的多个键值对
bucket
哈希冲突
两个或多个键被hash到同一个bucket,我们称这些键发生了冲突
Go使用链地址法解决键冲突,由于每个bucket可以存储8个键值对,超过8个时,创建一个bucket,使用类似链表的方式将bucket链起来,被称为overflow
负载因子
负载因子=键数量/bucket数量
哈希表需要将负载因子控制在合适大小,超过其阈值需要进行rehash
数据结构
tophash是个长度为8的数组,哈希值低位相同的键存入bucket时会将hash值的高8位存储在该数组中
data区存放的时key-value数据,存放顺序是key/key/key...value/value/value,如此存放是为了节省字节对齐带来的空间浪费
overflow指针指向下一个bucket,据此将所有冲突的键连接起来
渐进式扩容
扩容条件
负载因子>6.5
overflow数量>2^15
增量扩容
新建一个buckets长度是原来的两倍,oldbuckets指向原来的buckets
逐步搬迁,每次访问map都会触发一次搬迁,每次搬迁2个键值对
等量扩容
buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次
查找过程
根据key算出hash,取hash低位与hmap.B取模确定bucket位置,取hash高位在top数组中查询
如果tophash[i]中存储的hash高8位相等,则与data区的key进行比较
如果没找到,则继续从下个overflow的bucket中查找
如果当前处于搬迁过程,则优先从oldbuckets查找
string
string与[]byte的相互转换需要进行内存拷贝
string对象不可修改
字节切片可以被修改
[]byte临时转换为string不需要内存拷贝
编译器会识别以下场景
使用m[string(b)]来查找map
字符串比较string(b)=="foo"
uintptr
用于指针运算
不能存储在临时变量中
从GC的角度看,uintptr类型的变量只是一个无符号整数,并不知道他是一个指针地址,当满足一定条件,这个临时变量将被回收
interface
作用
反射可以将interface类型变量转换成反射对象
反射可以将反射对象还原成interface对象
反射对象可修改,value值必须时可设置的,比如指针
控制结构
return
若为命名返回值,return将后面的表达式的值赋给返回值,然后执行defer(defer可以修改命名返回值),最后返回结果
若为匿名返回值,return将后面的表达式的值赋给临时变量,然后执行defer(defer无法修改临时变量),最后再将临时变量返回
defer
基本用法
执行顺序:压栈,先进后出
return->defer->返回
panic后依然执行defer,主动os.Exit不会执行defer
defer后的函数参数会先被执行
实现机制
defer的实现机制是以栈的形式存储defer函数,这是因为go的执行环境是以栈的形式进行函数调用的
因此,在函数调用时,每一个defer函数都会被压入当前函数的defer栈中,当函数开始返回时,会从deger栈中逐一取出并执行每个defer函数
源码实现
每个go函数在运行时都有一个关联的 `_defer` 结构体,这个结构体记录了函数defer的调用
当一个新的defer语句被执行时,Go运行时会执行 newdefer 函数创建一个新的 `_defer` 实例,并将其链接到当前goroutine的defer链表上
在函数返回之前,deferreturn函数会被调用,它会遍历执行defer链表上的所有deger函数
deferreturn和newdefer函数时怎样的
函数栈帧
每次函数调用时都会为该函数创建一个栈帧,这个栈帧保存了函数的状态,包括局部变量,返回地址,以及函数调用的上下文等信息
局部变量
当函数被调用时,Go会为函数中的每个局部变量分配空间,这个空间存储在栈帧上,所以局部变量只在函数执行期间存在,函数返回后局部变量就会被销毁
返回地址
当一个函数被另一个函数调用时,调用者需要知道哪里可以找到结果,为此,调用者会在调用前保存自己的程序计数器到栈帧中,这个地址就是返回地址
当被调函数执行完毕后,可以通过这个地址找到调用者并将控制权返回
函数上下文
Go的函数可能会引用或修改调用者的环境,比如闭包的实现,这些环境信息会被保存在栈帧中,以便函数执行过程中使用
panic
实现机制
panic是一种内建函数,可以用来触发一个运行时错误
它会中断常规的控制流程,导致程序立即停止当前函数的执行,并开始执行defer函数,然后控制权返回到调用当前函数的地方,并再次触发panic
这个过程会一直持续,直到所有函数都被返回,然后持续会崩溃
源码实现
当函数panic时,它会创建一个 `_panic` 结构体,将其插入当前goroutine的panic链表头部,并在goroutine中设置一个panic标志,标识当前处于panic状态
然后运行时会开始执行当前goroutine的defer链表中所有函数
如果在执行这些defer函数时遇到 recover 函数,则会清楚panic标志,停止panic传播,并返回至调用 recover 的函数
range
slice、array
先将要遍历的数据赋值给一个临时变量,并读取数据的长度作为循环次数
开始遍历数据,k为从0开始的循环次数索引,v每次从临时变量取值
slice赋值给临时变量后,不发生扩容的情况下,共享底层数组
array赋值给临时变量后,将会发生拷贝
map
随机选取一个起始位置进行遍历
map扩容会导致key位置改变,插入数据位置是随机的,为了不耽误新手,直接从随机的bucket开始遍历
channel
channel关闭退出循环,否则阻塞
select
select的case语句读channel不会阻塞,尽管channel中没有数据。这是由于case语句编译后调用读channel时会明确传入不阻塞的参数,此时读不到数据时不会将当前goroutine加入到等待队列,而是直接返回。
锁定scase语句中所有的channel
按随机顺序检测scase中的channel是否ready
有ready,则取出数据,解锁所有channel
若都不ready,且无default,则将当前goroutine加入所有channel的等待队列,转入阻塞,等待被唤醒
若被唤醒,则返回对应case index
并发控制
mutex
数据结构
state表示互斥锁的状态,比如是否被锁定等,内部实现将该变量分成四份
Waiter(29bit)
表示阻塞等待锁的协程个数,协程解锁根据此值来判断是否需要释放信号量
Starving(1bit)
表示该mutex是否处于饥饿状态,0:没有饥饿,1:饥饿状态,说明协程阻塞超过了1ms
Woken(1bit)
表示是否有协程已经被唤醒,0:没有协程唤醒,1:已有协程唤醒,正在加锁过程中,不用释放信号量唤醒其他g了
Locked(1bit)
表示该mutex是否已被锁定,0:没有锁定,1:已被锁定
sema表示信号量,协程阻塞等待该信号量,解锁的协程释放信号量从而唤醒等待信号量的协程
加解锁
加锁
如果Locked位为1,则阻塞等待,如果Locked不为1,则CompareAndSwap尝试给Locked赋值
解锁
将Locked置为0,如果Waiter大于0,则释放信号量,唤醒一个阻塞的协程
两种模式
normal:
满足自旋条件进行自旋抢锁,不满足则阻塞
自旋条件
最大四次自旋,cpu核心数大于1
当前P可运行队列为空
gomaxprocs > 空闲P数量 + 自旋M数量 + 1
自旋过程
自旋对应CPU的PAUSE指令,相当于CPU空转,对程序而言相当于sleep了一小段时间
自旋过程中会持续探测Locked是否变为0,连续两次探测间隔就是执行这些PAUSE指令,它不同于sleep,不需要将协程转为睡眠状态
starvation:
释放锁如果有等待的协程,则会释放一个信号量来唤醒一个等待的协程
如果此时锁被自旋协程抢占,被唤醒协程再次阻塞,若阻塞时间超过1ms则进入饥饿模式,不再启动自旋,此时再次被唤醒将直接获取到锁
饥饿模式下,被唤醒的goroutine不饥饿或者waiter=1,则清除饥饿状态位,恢复普通模式
如何降低锁的消耗
切片锁
减小锁粒度,减少竞争
乐观锁CAS
CompareAndSwap
通过硬件级别的原子指令 CMPXCHG
原子操作
atomic包提供了对简单类型和any的原子操作
读写锁
读频率远大于写频率的场景
rwmutex
实现逻辑
Lock():写锁定,等待之前所有的读锁定完成
Unlock():写解锁,释放读阻塞等待的信号量,唤醒所有阻塞的读锁定
Rlock():读锁定,增加读操作计数器readerCounter++,所有写锁定阻塞等待
RUnlick():读解锁,减少读操作计数器readerCounter--,最后一个读解锁,释放写阻塞等待的信号量唤醒写锁定
写操作如何阻止读操作的
写锁定时,通过将readerCounter-2^30,将其变为负数,读锁定检测到负数只好阻塞等待
读操作如何阻止写操作的
读操作时,通过readerCounter++,写锁定时没,检测到大于0,则阻塞等待
为什么写锁定不会被饿死
写操作要等待读操作结束才可以获得锁,期间可能不断有读操作到来,岂不是一只等待?
写锁定时会将readerCounter的值拷贝到readerWait中,用于标记排在写操作前的读操作个数,读操作结束后readerWait也会递减,到0则唤醒写操作
waitgroup
数据结构
counter(32bit)
当前还未执行结束的goroutine计数器
Add()让counter+1,当counter为负数会panic
Done()==Add(-1)
waiter count(32bit)
等待goroutine结束的waiter数量
Wait() 让waiter+1,并阻塞等待信号量
semaphore(32bit)
信号量
一种保护共享变量的机制,防止多个线程同时访问某个资源
context
cancelCtx
创建过程
创建一个cancelCtx结构体,将父上下文包在结构体内
如果父上下文不具备取消功能(done==nil),就直接返回cancelCtx和cancel函数
如果父上下文已经被取消,就取消掉当前上下文,然后返回
找到父上下文底层cancelCtx
如果存在,则将当前创建的cancelCtx保存到父上下文底层cancelCtx的children集合里,父上下文取消时会将所有children取消
如果不存在,则创建一个协程监听父上下文和当前创建的cancelCtx的取消事件,父上下文取消时会取消掉当前创建的cancelCtx
cancel过程
关闭done通道,此时所有阻塞等待Done的协程都将得到释放
调用children集合所有子上下文的cancel函数
主动调用cancel方法会将自己从自己的父上下文children集合中删除掉
timerCtx
在cancelCtx的基础加上time.Ticker实现
valueCtx
通过在context中包裹空、v键值对实现
首先查找当前上下文的key,然后逐层查找父上下文的key,直到最底层
实现原理
当context关闭后,Done()返回一个被关闭的管道,所有阻塞等待的goroutine将读取到零值并退出
Go语言中context携带截止日期、取消信号以及跨API边界和进程之间的其他请求作用域值,用来同步信号、设置超时时间、传递请求相关值等
内存管理
虚拟内存布局
虚拟内存区域由三个区域组成
spans(512M)
spans区域存储了指向内存管理单元runtime.mspan的指针,每个内存管理单元mspan会管理几页的空间,每页大小为8KB
spans切片的索引是页码,值对应的mspan,当一个mspan管理多个页时,多个span指向同一个mspan
每一个span指针对应一页,spans的 8B 对应arena的 8KB
bitmap(16G)
bitmap用于标识arena区域中所有地址保存的GC标记信息和是否指针信息,位图中的每个字节(8bit)都会表示堆区中的32字节(4*8B)是否空闲
bitmap中一个byte大小的内存对应arena区域中4个指针(4*8B)的内存,其中4bit表示这4个位置是否扫描,4bit表示这4个位置是否是指针
arena(512G)
arena区域时真正的堆区,运行时会将8KB看作一页,这些内存页中存储了所有堆上初始化的对象
二维稀疏内存
使用稀疏的内存布局,移除了线性内存堆大小的上线
mspan如何管理地址
计算给定地址相对于arena基地址的偏移量,然后除以页的大小,得到这个地址所在的页码,在sapns切片中通过索引查找这个页码,得到对应的mspan
内存管理单元
mspan
Go中内存管理的基本单元,一个mspan可以管理多个页(不是操作系统的页)
每个mspan按照它自身的属性将所管理的内存分割成若干个object,每个object可存储一个对象,并且会使用一个位图来标记其尚未使用的object
spanClass
Go内存管理模块有67种跨度类,每一个跨度类都指定类对象的大小,以及对应的页数
mspan只会分配给object大小接近spanClass size的对象
内存管理组件
mcache
每个工作线程都会绑定一个mcache,包含本地缓存可用的mspan资源,这样就可以直接给goroutine分配,不存在竞争
包含所有67种规格的mspan链表,为了加速GC,数组里存在包含指针的mspn和不包含指针的mspan两套
mcache初始化时没有任何mspan资源,使用过程中会向mcentral动态申请,之后会缓存下来
mcentral
为所有mcache提供mspan资源,每个mcentral保存一种特定大小的全局mspan列表,包括以分配可未分配的,每个mcentral对应一种mspan
mcentral内所有工作线程共同享有
当mcentral没有空闲的mspan时,向mheap申请
mheap
代表Go程序持有的所有堆空间的全局变量
对应着虚拟内存区域
mheap主要用于大对象的分配,以及管理未切割的mspan,mheap包含所有规格的mcentral
mheap没有资源时,向操作系统申请
内存分配流程
Go分配对象时,根据对象的大小分为三类:小对象(小于等于16B)、一般对象(大于16B,小于32KB)、大对象(大于32KB)
大对象直接从mheap分配
小对象使用mcache的tiny分配器分配
一般对象选取合适的mpan规格
内存对齐
why:从处理器的角度,cpu以字长(32位、64位)位单位访问数据,需要尽可能减少对内存访问的次数,以实现对数据结构的高效操作
规则:结构体的第一个成员偏移量为0,以后每个成员相对于结构体首地址的偏移量为 min(该成员大小,编译器默认对齐长度) 的整数倍
注意:结构体本身也需要对齐,对齐值为 min(最长成员size,编译器默认对齐长度)
空值
struct{},[0]byte 不占任何存储空间
空值放在结构体最后一个成员时,需要内存对齐
深挖
指针代表内存单元的编号,一个内存单元时一个字节8bit
64位(8Byte)对齐:数据地址(单位位Byte)需为8的整数倍
逃逸分析
逃逸分析是在编译阶段进行的一种优化技术,它试图确定哪些变量只能在栈上存活,哪些变量需要在堆上分配
如果一个变量的生命周期只限于其定义的函数,则这个变量就可以在栈上分配
如果一个变量的引用被外部函数或者goroutine捕获,或者其生命周期超过了函数的生命周期,那么这个变量就会逃逸到堆上
栈上分配内存比堆上分配内存效率更高,栈上不需要GC,堆上需要GC
逃逸分析是在编译阶段完成的
垃圾回收
三色标记法
Go 1.5 的三色并发标记法
过程
每次创建新对象对标记为白色
每次GC开始,会从根节点开始遍历(非递归,只遍历一次)所有对象,把遍历到的对象从白色集合放入灰色集合
遍历灰色集合,将灰色对象引用的对象从白色集合移入灰色集合,将此灰色对象加入黑色集合。循环直至灰色集合无任何对象
回收白色集合中的对象
问题:
执行并发流程的内存可能相互依赖,为保证GC过程数据安全,开始三色标记之前就会加上STW,扫描完再放开STW
很明显这样GC扫描的性能太低,如果三色标记法没有STW会怎样?
当一个白色对象被挂在黑色对象下,并且灰色同时丢了该白色,就会导致该白色对象被误删
如何防止误删?
引入强-弱三色不变式,破坏这两个条件
屏障机制
强三色不变式
不存在黑色对象引用到白色对象的指针
弱三色不变式
所有被黑色对象引用的白色对象都处于灰色保护状态(能向上追溯到灰色,因为灰色能继续向下扫描)
为了遵循上述两个方式,GC算法演进到两种屏障方式,插入屏障、删除屏障
插入屏障
在A对象引用B对象的时候,B对象被标记为灰色
满足:强三色不变式
扫描完准备回收白色对象前,重新扫描一次栈空间,此时加入STW防止干扰
缺点:需要STW重新扫描栈
删除屏障
被删除的对象,如果自身为灰色或者白色,那么被标记为灰色
满足:弱三色不变式
缺点:回收精度低
混合写屏障
Go 1.8 混合写屏障
过程
GC开始将栈上的对象全部扫描,将可达对象全部标记为黑色
GC主要在堆上运行,GC需要知道哪些变量引用了堆上的变量,因此也需要扫描栈,以跟踪栈上的变量对堆上的引用
GC不会清理栈上的数据
GC期间,任何在栈上创建的新对象均为黑色
被删除和被添加的对象标记为灰色
满足:变形的弱三色不变式
注意
GC主要在堆上工作
为了正确地确定哪些内存块在堆上不再使用,GC需要了解哪些内存仍然被栈上的变量引用,这是因为Go允许对象在堆上分配,然后通过栈上的变量引用它
GC标记准备阶段和GC标记结束节点还是需要STW,来通知所有goroutine我进入垃圾回收阶段了,然后启用/关闭屏障机制
goroutine
调度器的由来
单进程时代不需要调度器
单一的执行流程,进程阻塞带来CPU浪费
多进程/线程时代有了调度器需求
调度器调度进程上下文切换任然消耗很多CPU
引入协程提高CPU利用率
线程由CPU调度时抢占式的,协程由用户态调度是协作式的
协程在用户态线程即完成切换,不会陷入内核态,这种切换非常轻量快速
Go语言的协程goroutine
goroutine来自协程的概念,让一组函数运行在一组线程之上,即使有goroutine阻塞,该线程的其他goroutine也可以被runtime调度到其他可运行的线程上
Go中协程被称为goroutine,轻量,只占几KB,也就支持更多并发
GMP
模型
G
goroutine协程
全局队列存放所有等待运行的G
M
thread线程
线程是运行goroutine的实体,调度器的功能就是把可运行的goroutine分配到工作线程上
线程想运行任务就得获取P,从P的本地队列获取G
P本地队列为空时,M也会尝试从全局队列拿一批G放到自己P本地队列,或从其他P本地队列偷一半放到自己P本地队列
M何时创建
一个M阻塞了,会从休眠队列取一个M接管解绑的P,没有则会创建新的M
没有足够的M来关联P并运行其中可运行的G,就会创建新的M
自旋线程
为了新goroutine能立刻有M运行它,M在没有可以执行的G时进入自旋状态,不断寻找goroutine,最多有GOMAXPROCS个自旋线程
P
processor处理器
P包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G本地队列
所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS个
任意时刻,最多只有P个goroutine在同时执行
调度器设计策略
复用线程
避免频繁的创建、销毁现场,而是对线程的复用
work stealing机制:当本线程无可运行的G时,尝试从其他线程绑定的P本地队列中偷G,而不是销毁线程
hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行
协作与抢占
协作式调度
主动用户弃权
Gosched 用户主动放弃执行
主动调度弃权
在每个函数调用的最前方插入抢占检测指令,当检测到当前g被标记为应该被抢占时,则主动中断执行,让出执行权力
抢占式调度
被动监控抢占
当G阻塞在M上时,系统监控会将P从M上抢夺并分配给其他M,而位于被抢夺P的本地G队列则被偷取到其他M中
sysmon的retake方法,处理了两种抢占情况,一是抢占阻塞在系统调用上的P,二是抢占运行时间过长的G
被动GC抢占
当需要进行垃圾回收时,为了保证不具备主动抢占处理的函数执行过长,导致垃圾回收迟迟不得执行而导致的高延迟,而强制停止G转为执行垃圾回收
goroutine调度流程
创建goroutine
新创建的G会先保存在P的本地队列中,如果本地队列已满,则会保存在全局队列中
M从P的本地队列取一个G执行,如果本地队列为空,则先取一批全局队列中的G,没有就偷其他P的本地队列的一半,循环往复
没有可以执行的G时进入自旋状态,不断寻找goroutine,最多有GOMAXPROCS个自旋线程
当M执行某一个G时发生了syscall或其余阻塞操作,M会阻塞,runtime会把这个M的P摘除,然后创建一个新的M(如果有空闲线程,则复用)来服务这个P
当M系统调用结束时,这个G会尝试获取一个空闲的P执行,就放入到这个P的本地队列。如果获取不到P,M变成休眠状态加入空闲线程队列,G放入全局队列
调度器的生命周期
名词解释
M0
启动程序后编号为0的主线程,M0负责执行初始化操作和启动第一个G,在之后就和其他M一样了
G0
每启动一个M都会第一个创建的goroutine,每个M都有自己的G0,G0负责调度时协程的切换(函数schedule),在调度或系统调用时,会使用G0的栈空间
生命周期
runtime创建最初的m0和g0,并把二者关联
调度初始化:初始化m0、栈、垃圾回收,P列表
启动m0,绑定P,运行G
G运行完后,M且切换为G0,G0负责调度时协程的切换,取出G1,从G0切换到G1
源码
Go网络轮询器netpoll
描述
Go内部使用IO多路复用结合NIO实现了一个异步IO模型
将监听fd的事件交由runtime管理,当协程读取fd数据但是没有数据的时候,park住该协程(阻塞IO)
gopark
解除当前g与m的绑定关系,将当前g置为等待状态
切换当前线程的堆栈从g的堆栈切换为g0,保存当前协程信息,当后续对当前g调用goready时能够恢复现场
内核监听fd,runtime调用epoll_wait阻塞等待是否有fd就绪(epoll IO多路复用)
在执行协程调度时会检查fd是否就绪,如果就绪,调度器再使用goready通知该park住的协程处理fd,在用户层面实现了一个(异步IO)
goready
将协程转换为runnable状态,并将其放入p的local queue,等待调度
核心
epoll
epoll api
int epoll_create(int size); // 创建一个epoll实例,返回epfd句柄
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event); // 用于向epoll实例添加、删除、修改要监听的fd等待的IO事件
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout); // 阻塞监听epoll实例上的IO事件
工作原理
epoll采用红黑树来存储所有监听的fd,通过epoll_ctl将fd添加到红黑树,该fd会与相应的设备(网卡)建立回调关系,也就是在内核中断处理程序为它注册一个回调函数,该回调函数被称为ep_poll_callback(将这个fd添加到rdlist就绪链表中)
epoll_wait实际上就是去检查rdlist就绪链表是否有就绪的fd,就绪链表为空时挂起,直到就绪链表非空才被唤醒返回
netpoller
client连接server的时候,listener通过accept接收新connection,每个新connection都启动一个goroutine处理
accept会把该connection的fd连带所在的goroutine上下文信息封装注册到epoll的监听列表里
当goroutine嗲用conn.Read或者conn.Write等需要阻塞等待的函数时,会被gopark打包找个地方存起来,直到被唤醒
scheduler会在循环调度runtime.schedule()函数以及sysmon监控线程中调用runtime.netpoll获取可以运行的goroutine列表并执行
标准库
unsafe
unsafe.SliceData/unsafe.StringData
获取切片/字符串底层数组指针
unsafe.Slice/unsafe.String
通过指针创建一个切片/字符串
unsafe.Add
指针运算
unsafe.Alignof
返回给定类型的对齐值
unsafe.Sizeof
返回给定类型的大小
unsafe.Offsetof
返回给定类型在结构体内的偏移量
reflect
go命令
go build
-gcflags=-m
打印编译器优化信息
打印逃逸分析信息
-gcflags=-l
禁用函数内联
-gcflags=-N
禁用编译器优化
go vet
代码错误检查
指令
go:linkname
引导编译器将当前私有方法或变量,在编译时链接到指定位置的方法或变量
go:noescape
指定一个有声明没主题的函数,不允许编译器做逃逸分析
go:nosplit
指定该函数跳过堆栈溢出检查
go:noinline
禁止内联
go:norace
禁止竟态检测
go:generate
自动生成代码
数据库
Redis
基本架构
两大维度
应用维度
缓存应用、集群应用、数据结构应用
系统维度
处理层、内存层、存储层、网络层
三大主线
高性能主线
线程模型、数据结构、网络框架
高可靠主线
主从复制、哨兵机制、持久化
高可扩展主线
切片集群、数据分片、负载均衡
特点
内存数据库,速度快,支持数据持久化
丰富的数据类型
支持数据备份,集群,哨兵,事务
数据结构
字符串(string)
底层数据结构
简单动态字符串
列表(list)
底层数据结构
双向链表
压缩列表
list-max-ziplist-size -2(8kb)最大元素数量
哈希表(hash)
底层数据结构
哈希表
压缩列表
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
hash-max-ziplist-value 64
集合(set)
底层数据结构
整数数组
set-max-intset-entries 512
哈希表
有序集合(zset)
底层数据结构
跳表
压缩列表
zset-max-ziplist-entries 128 最大元素个数
zset-max-ziplist-value 64 元素最大长度
zset-max-ziplist-value 64 元素最大长度
扩展类型
Bitmap
用string实现的一种统计二值状态的数据类型
使用场景
用户行为分析
实时统计
使用
设置位:SETBIT user:10000:logins 1 1
获取位:SETBIT user:10000:logins 1
统计位:BITCOUNT
HyperLogLog
用于统计基数的数据集合类型,占用空间小,统计结果有误差
使用场景
流量统计
使用
添加值:PFADD user:visits:20230529 user_id
统计值:PFCOUNT user:visits:20230529
GEO
GeoHash
二分区间,区间编码
基于Sort Set,通过范围查询得到相近的编码值,在实际地理位置上也是相邻的
自定义数据类型
Redis基本对象结构RedisObject
定义新类型和底层数据结构,在RedisObject中增加新类型的定义,开发新类型创建和释放函数,开发新类型的命令操作
底层数据结构
简单动态字符串(SDS)
空间预分配
SDS在分配内存时会预分配一些额外的空间,分配策略是,若字符串长度小于1M,则分配len长度空间,否则分配1M空间
好处在于后续追加可以减少内存分配次数
惰性空间释放
当SDS释放不需要的内存空间时,会保留一部分预留空间,而不是立即释放
双向链表
添加、删除、插入元素速度快
额外空间存储前后指针,空间效率低
压缩列表
原理
压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块联系的内存区域,目的是节省内存
内存布局
zlbytes(4字节)
压缩列表所占用的字节数,重新分配内存时使用,不需要便利整个列表来计算内存大小
zltail(4字节)
列表尾部的偏移量
zllen(2字节)
压缩列表包含的节点个数
entry(节点)
previous_length(1、5字节)
存储上一个节点的长度,因此压缩列表可以从尾部向头部遍历
encoding
保存content的内容类型以及长度
类型一共两种:字节数组、整数
content
用于保存节点的内容
zlend(1字节)
固定值为255,用于表示列表结束
跳表
在链表的基础上增加了多级索引
跳表以有序的方式在层次化的链表中保存数据,效率和平衡树媲美
跳表节点
包含一个前进指针和后退指针
每插入一个元素,先通过随机算法告诉我们这个元素需要插入到几级索引中,然后插入底层链表,维护索引
高可用
主从
描述:一个主库多个从库,实现读写分离、数据备份
主从同步(先全量、再增量)
第一阶段,主库从库建立连接,协商同步过程,为全量复制做准备
第二阶段,主库将所有数据(RDB)同步给从库,从库收到数据后,在本地完成数据加载
第三阶段,主库吧第二阶段执行过程中新收到的写命令(repl buffer),发送给从库
增量复制
repl_backlog_buffer环形缓冲区
主库记录自己写到的位置,从库记录自己已经读到的位置
缓冲区满了怎么办
适当增加repl_backlog_size
使用切片集群,减少每个节点的key数量
主从级联模式
如果从库数量过多,会导致主库忙于fork子进程,进行数据全量同步,使用主从级联模式分担主库全量复制时的压力
哨兵
主从库自动切换,解决主从复制模式下故障转移问题
主从切换时,如何防止数据丢失
配置min-slaves-to-write,写操作所需要的从节点确认数量
减小主节点与从节点之间的网络延迟
选主库
筛选:去掉已下线和历史网络连接状态不好的从库
第一轮:优先级最高的从库(slave-priority配置项)
第二轮;和旧主库同步程度最接近的从库(通过slave_repl_offset和master_repl_offset来决定)
第三轮;ID号最小的从库
切片集群
描述:通过切片(sharding)技术,将数据分散到多个节点,从而提高了系统的可扩展性
原理
哈希槽(hash slot)
将kv映射到哈希槽中,再将16384个slot均分到mater实例中
重定向机制
如果当前实例没有这个键值对映射的哈希槽,那么实例返回moved包含新实例的访问地址
问题
键值用什么数据结构组织?
全局哈希表
哈希表其实就是一个数组,数组的每一个元素被称为哈希桶,哈希桶保存kv实体的指针
哈希冲突
链式哈希,同一个哈希桶中的多个元素用一个链表来保存
哈希冲突链过长
rehash
分配一个更大的空间哈希表2,重新映射,释放哈希表1
由于设计数据拷贝,采用渐进式rehash
每处理一个请求,按序从哈希表1 rehash一个桶到哈希表2
没有新请求时,定时执行rehash
装载因子=哈希表大小/元素数量 ,装载因子大于1时允许rehash,大于等于5时立即rehash
Redis为什么这么快?
IO多路复用
实现同时对多个文件描述符进行监控,一旦某个描述符就绪,就通知程序进行操作
服务端采用单线程,当accept一个请求后,在recv或send调用阻塞时,将无法accept其他请求(必须等上一个请求处理recv或send完)
epoll
int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程
Redis如何避免数据丢失?
(记录指令)AOF(Append Only File)日志
执行命令后记录,记录redis收到的每一条命令
AOF日志是在主线程中执行,日志文件写会磁盘存在风险
配置项 appendfsync
Always(同步写会),Everysec(每秒写回),No(操作系统控制写回)
AOF文件过于臃肿?
AOF重写机制
Redis fork子进程bgrewriteaof,根据Redis现状创建一个新的AOF文件,每个键值对用一条命令记录它的写入,以缩小日志文件
AOF故障恢复
将AOF日志中所有命令执行一遍
(存储快照)RDB(Redis DataBase)文件
内存快照,将内存中所有数据记录到磁盘
Redis fork子进程bgsave,共享主进程内存数据,借助操作系统提供的写时复制(COW),在执行快照的同时,正常处理写操作
缓存击穿如何解决?
描述:查询一个不存在的数据,这个请求会直接击穿到数据库
解决
空值缓存
把空值也缓存起来,加上过期时间
布隆过滤器
布隆过滤器可以判断一个元素是否在一个集合里,查询一个key时,可以先查询布隆过滤器(哈希冲突严重时,会导致误报已存在)
数据预热
业务启动时加载到缓存中,避免真正请求到来时,缓存中没有数据,请求穿透到数据库
分布式锁
没查到缓存时,先加分布式锁,避免大量请求击穿数据库
多级缓存
加一个内存缓存,减少数据库访问
架构设计
熔断(直接返回错误)、限流(限制访问量)、请求降级(返回精简数据)
TODO
Redis核心技术与实战(10/40)
Mysql
基础架构
连接器
负责维持和管理连接,获取权限
show processlist查看连接
如果连接长时间没有数据,连接器就会自动断开,参数wait_timeout控制,默认8小时
使用长连接,会导致mysql内存涨得很快,这是因为mysql执行过程中临时使用的内存是管理在连接对象里,这些资源会在断开时释放
定时断开长连接
通过执行mysql_reset_connection重新初始化连接资源
查询缓存
查询结果缓存一段时间,下次执行先查询缓存,Mysql8.0删掉了这个功能
分析器
对sql语句进行词法分析和语法分析
优化器
优化器是在表里面有多个索引的时候,决定使用哪个索引,或者在一个语句有多表关联的时候,决定各个表的连接顺序
执行器
执行器会根据表的存储引擎,使用这个存储引擎提供的接口
过程:第一次调用“满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口
rows_examined表示这个语句执行过程中扫描了多少行,这个值就是在执行器每次调用存储引擎获取数据行的时候累加的
日志系统
redo log
有了redo log,InnoDB就可以保证即使数据库发生重启,之前提交的记录都不会丢失,这个能力称作crash-safe
redo log记录了每一次更新操作,页做了什么改动
WAL(write-ahead logging):先写日志,再写磁盘
日志是一直添加,是顺序IO,效率高;写磁盘得先查询再更新,IO成本高
redo log是InnoDB特有的;物理日志,记录某个数据页上做了什么修改;循环写,空间固定用完从头写
binlog
binlog是MySQL的Server层实现,所有引擎都可以使用;逻辑日志,记录这个语句的原始逻辑,比如”给某字段加1“;追加写,文件写满会切换到下一个
binlog日志记录sql或sql对数据的更改,主要用于复制和恢复操作
InnoDB
更新执行过程
执行器先找引擎取出要更新的行
执行器修改数据得到新行,调用引擎写入新行
引擎将新行更新到内存,同时将更新操作记录到redo log,此时redo log处于prepare转态,然后告知执行器,随时可以提交事务
执行器生成这个操作的binlog,并把binlog写入磁盘
执行器调用引擎的事务提交接口,引擎把刚刚写入的redo log改成commit状态,更新完成
两阶段提交
redo log的写入拆成了两个步骤:prepare、commit
为了让两份日志之间的逻辑一致
崩溃恢复
redo log
在系统崩溃后,恢复那些已经提交但可能还没真正写入磁盘的修改
undo log
用于在事务失败或被中断的情况下,恢复数据库到事务开始前的状态
当事务开始执行时,系统会在undo log中记录该事务所做所有修改的反操作
崩溃恢复过程
检查:InnoDB启动后会检查redo log,确定是否需要进行崩溃恢复
恢复:如果存在未写入磁盘的修改,则会根据redo log中的记录恢复数据
清除未完成的事务:在完成数据恢复后,检查如果在系统崩溃时有些事务还没有完成,那么InnoDB会根据undo log撤销这些事务的修改
这个过程是自动的,也可以通过binlog手动从备份恢复数据
事务隔离
隔离级别
读未提交
一个事务还没提交,它做的变更就能被其他事务看到
解决更新丢失问题,导致脏读
事务未提交,可能会发生回滚,其他事务读取到的是脏数据
原理:如果一个事务已经开始写操作,那么其他事务则不允许同时进行写操作,但允许其他事务读
实现:通过排他锁实现
读已提交
一个事务提交后,它做的变更才会被其他事务看到
解决脏读问题,导致不可重复读问题
一个事务中查询两次同一条数据,如果中间有其他事务更新并提交了这条数据,则两次查询内容会不一致
可重复读
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的
解决不可重复读问题,导致幻读
不可重复读问题是两次select读结果不一致,幻读是两次写(update、insert、delete)的视图不一致
原因
select是快照读
update、delete、insert是当前读(类似读已提交)
更新数据都是先读后写,这个读就是当前读
如果当前记录的行锁被其他事务占用的话,需要进入锁等待
如何解决幻读
串行化
间隙锁
主要由于新行的插入导致幻读
锁定行之间的间隙,防止其他事务在该间隙中插入新行
达到了Serializable级别,所以可重复读是MySQL的默认隔离级别
串行化
事务串行处理,加读写锁
事务隔离的实现
读写锁
排他锁
又称为X锁,写锁
共享锁
又称S锁,读锁
MVCC(多版本并发控制)
描述:MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读
实现
系统版本号:一个递增的数字,每开始一个新的事务,系统版本号就会自增
事务版本号:事务开始时的系统版本号
MySQL中会在表中每一条数据后面添加两个字段
创建版本号:数据创建时的事务版本号
删除版本号:数据删除时的事务版本号
可重复读
select
查询条件,创建版本号<=当前事务版本号,删除版本号>当前事务版本号
insert
将当前系统版本号赋值给创建版本号字段
delete
将当前的系统版本号赋值给即将删除行的删除版本号字段
update
插入一条新数据,保存当前事务版本号为新行的创建版本号,同时将原来行的删除版本号修改为当前事务版本号(实际上是通过insert和delete来实现的)
一致性视图
启动事务的方式
start transaction with consistent snapshot
一致性视图是在启动事务时创建的
如何实现
InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前未提交的所有事物ID
这些事物的事物ID都比当前事物ID小,所以这些事物的修改对当前事物必须时不可见的,否则就是脏读了
start transaction/begin
一致性视图是在执行第一个快照读语句时创建的
描述
对于一个事务来说,它看到的数据状态就是它开始时的状态,无论此后发生什么
索引
索引类型
聚簇索引(表)
主键索引:叶子结点存储整行数据
如果没有自增主键,InnoDB会自动加一个隐藏的主键
为什么一定要有自增主键
分裂
B+树为了索引有序性,在插入新值的时候需要做必要的维护,如果插入位置所在页已经满了,则会申请一个新的数据页,然后挪动部分数据过去,这个过程称为页分裂
页分裂操作还影响数据页的利用率
合并
删除一些数据后,页利用率很低,会将数据页做合并
也可以通过重建索引来解决页利用率问题
自增主键的插入数据模式,每次插入一条新记录都是追加操作,不会触发叶子节点的分裂
非聚簇索引
非主键索引:叶子节点存储的是主键的值,需要进行回表
(MyISAM采用非聚簇索引,叶子节点存储指向数据的指针)
索引数据结构
平衡二叉树
基于二分法,提高数据查询速度,任何两个子树高度差不超过1
B树(B-Tree)
B-Tree为多叉树,又名平衡多路查找树
查询稳定,高效
排序方式:左小右大
所有叶子节点均在同一层,非叶子节点保存关键字记录的指针
B+Tree
B+Tree的非叶子节点不保存关键字记录的指针,能保存更多关键字
N叉树
叶子节点保存了所有关键字记录的指针
每一个叶子节点包含指向下一个叶子节点的指针,链表
数据读取
InnoDB在把磁盘数据读入到内存时会以页为基本单位,查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘IO次数
树高是由行数决定的,N叉树,N是由页大小和索引大小决定的,一个页存储N个节点,每个节点包含一个关键字和一个指针
优化
索引优化
最左前缀原则
查询必须从索引最左边开始,且不能跳过
不能使用范围条件右边的列
联合索引
可以理解为按索引顺序拼接成一个二进制字节数组,按字节数组逐字节比较
联合索引的最左N个字段,也可以是字符串索引的最左M个字符
覆盖索引
所有需要查询的数据都在一个索引中
不用回表
索引设计
索引字段尽量使用简单数据类型
引擎处理查询和连接时会逐个比较字符串中每一个字符
尽量不要出现NULL字段
null值会导致唯一索引失效
索引字段太长时应考虑前缀索引指定前缀长度
让索引更小更快,但无法使用前缀索引做order by和group by以及覆盖扫描
查询优化
like语句不要以通配符开头
通配符开头无法使用索引,不满足最左前缀原则
不要在列上计算
索引列不能是表达式的一部分,也不能是函数的参数
避免使用not in、<>、!=
引擎将可能放弃使用索引而进行全表扫描
not in可用not exists替换
MySQL会在选择索引的时候进行优化,如果它认为全表扫描比走索引加回表效率高,则会选择全表扫描
避免or条件
如果or前的条件中的列有索引,后面的列没有索引,那么引擎将会放弃所有索引
explain
system:表只有一行
const:表中的一个记录的最大值能够匹配这个查询(索引可以是主键或唯一索引)
eq_ref:查询使用了索引为主键或唯一键
ref:查询使用了索引
range:查询使用了索引,查询出来一个范围
index:查询全部索引
ALL:全部扫描
排序
索引排序
描述
可以利用同一索引同时进行查找和排序操作
条件
索引顺序与order by相同,且为同一方向(全为升序或全为降序)
order by字句与查询字句的限制是一样的,满足最左前缀原则
当前导列为常量时,where子句或join子句对前导列指定了常量(where a=1),可以不满足最左前缀原则
如果查询连接多个表,仅当order by中所有列都是第一个表的列时才会使用索引进行排序,其他情况都会使用filesort文件排序
文件排序 (filesort)
描述
当MySQL不能使用索引进行排序时,就会使用filesort
排序
内存中排序,使用快排算法
内存装载不下,外部排序(使用临时表)
将磁盘上的数据进行分块,在对各个数据进行排序,然后将各个块合并成有序的结果集
临时表
临时表常用于存储复杂查询的中间结果,或大量的数据修改时
临时表只在创建它的数据库会话中可见,会话结束会被自动删除
锁
表锁
表结构修改会使用表锁
行锁
在InnoDB事务中,行锁是在需要的时候才加上的,事务结束时才释放
行锁通过锁索引记录实现,没有使用索引会使用表锁
for update
锁定查询到的一行或多行数据,以便进行更新,使用写锁实现,只在事务中有效(当前读)
死锁
当并发系统中不同线程出现循环资源依赖,就会导致死锁
死锁检测
发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务
额外的资源消耗
参数
innodb_flush_log_at_trx_commit
值为1时,表示每次事务的redo log都直接持久化到磁盘,保证异常重启后数据不丢失
sync_binlog
值为1时,表示每次事务的binlog都持久化到磁盘,保证异常重启后binlog不丢失
transaction_isolation
值为READ-COMMITTED,设置隔离级别为读已提交
innodb_page_size
默认16KB,表示存储引擎每个页的大小
innodb_lock_wait_timeout
默认50秒,表示锁等待超时时间
innodb_deadlock_detect
表示是否开启死锁检测
基础
基础用法
起别名
as或空格
去重
distinct
+号
若两个操作数都为数值型,则做加法运算
若其中一个为字符型,则试图将字符型转换成数值型,如果失败则字符型数值转换为0
只要其中一方为null,则结果为null
=、<> 不能用于判断null值
常用命令
desc `table_name`
show tables
show databases
use `database_name`
常见函数
单行函数
字符函数
length(str)
获取参数值得字节数
concat(str1...)
拼接字符串
upper(str)
字符串全大写
lower(str)
字符串全小写
substr(str ,start_index [,length])
str索引从1开始截取指定长度字符串,length默认为字符串结尾
instr(str,substr)
返回子串第一次出现的索引
trim(str)
去除字符串收尾空格
lpad(str,count,pad_str)
左填充
rpad(str,count,pad_str)
右填充
replace(str,old_str,new_str)
替换
ifnull(str,default)
给空值赋默认值
数学函数
round(float_num [,len])
四舍五入
ceil(float_num)
向上取整,返回>=该参数的最小整数
floor(float_num)
向下取整,返回<=该参数的最大整数
truncate(float_num,len)
截断
mod(num1,num2)
取余
日期函数
now()
当前日期+时间
curdate()
当前日期
curtime()
当前时间
year(date)
年
month(date)
月
str_to_date(date_str,format)
字符串转日期
date_format(date,format)
格式化日期
datediff(date1,date2)
日期相差的天数
其他函数
流程控制函数
if(expr,true,false)
case [field] when [value1] then [value2] else [value3] end
case when [condition] then [val/sql] else end
分组函数
count
count(field)
统计字段不为空的行数
count(*)
统计所有字段不为空的行数
count(1)
统计所有记录行数
sum max min avg
分组查询
group by
having
连接
from 多表 ?
视图 ?
TODO
NewSQL
提供关系型数据库的ACID,同时兼具NoSQL的水平可扩展性,比如TiDB
MySQL实战45讲(8/45)
0 条评论
下一页