Go
Go语言优势
底层原生的多核并发技术。
Go语言比Java
1.语法简单:Go语言的语法更加简单和现代,没有像Java那样的复杂的类型系统和繁琐的语法。
2.编译速度:Go语言的编译速度通常比Java快,特别是在大型项目中。
3.性能:Go语言编译生成的机器代码性能通常比Java要高,尤其在低级的、需要高吞吐量的任务中。
4.并发与并行:Go语言从设计时就考虑了并发,提供了轻量级的线程goroutine和基于CSP(Communicating Sequential Processes)的通信机制,使得并发编程变得简单和高效。
5.内存管理:Go语言的垃圾回收机制使得内存管理变得更为自动化,减少了内存泄漏的风险。
6.网络编程:Go语言内置的网络编程特性使得编写高性能网络服务变得简单,例如内置的net/http包。
7.开发效率:Go语言的开发效率也是一大优势,它提倡的是“即编即尝”,能快速进行原型开发和迭代。
CSP并发模型
以通信的方式来共享内存,是通过Goroutine和Channel来实现的。
Channel
Channel有无缓冲的区别
无缓冲:发送和接收需要同步。
有缓冲:不要求发送和接收同步,缓冲满时发送阻塞。
Channel底层数据结构
type hchan struct {
qcount uint // 循环数组中的元素数量,长度
dataqsiz uint // 循环数组的大小,容量
// channel分为无缓冲和有缓冲channel两种
// 有缓冲的channel使用ring buffer(环形缓冲区)来缓存写入的数据,本质是循环数组
// 为什么是循环数组?普通数组容量固定、更适合指定的空间,且弹出元素时,元素需要全部前移
buf unsafe.Pointer // 指向底层循环数组的指针(环形缓冲区)
elemsize uint16 // 元素的大小
closed uint32 // 是否关闭的标志,0:未关闭,1:已关闭
elemtype *_type // channel中的元素类型
// 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和当前写的下标位置
sendx uint // 下一次写的位置
recvx uint // 下一次读的位置
// 尝试读/写channel时被阻塞的goroutine
recvq waitq // 读等待队列
sendq waitq // 写等待队列
// 互斥锁,保证读写channel时的并发安全问题
lock mutex
}
// **waitq**
type waitq struct{
first *sudog
last *sudog
}
type sudog struct{
g *g //记录哪个协程在等待
next *sudog
prev *sudog
elem unsafe.Pointer // 等待发送/接收的数据在哪里
....
c *chan //等待的是哪个channel
}
创建channel
条件
元素大小不能超过 65536 字节,也即 64K;
元素的对齐大小不能超过 maxAlign 也即 8 字节;
计算出来的所需内存不能超过限制;
策略*
如果是无缓冲channel,直接为hchan结构体分配内存并返回指针。
如果是有缓冲channel,但元素不包含指针类型,则一次性为hchan结构体和底层循环数组分配连续内存并返回指针。(需要连续内存空间)
如果是有缓冲channel,且元素包含指针类型,则分别分配hchan结构体内存和底层循环数组的内存并返回指针。(可以利用内存碎片)
发送数据
向channel中发送数据主要分为两大块:边界检查和数据发送,数据发送流程主要如下:
1.如果channel的读等待队列中存在接收者goroutine,则为同步发送:
1)无缓冲channel,不用经过channel直接将数据发送给第一个等待接收的goroutine,并将其唤醒等待调度。
2)有缓冲channel,但是元素个数为0,不用经过channel(假装经过channel)直接将数据发送给第一个等待接收的goroutine,并将其唤醒等待调度。
2.如果channel的读等待队列中不存在接收者goroutine:
1)如果底层循环数组未满,那么把发送者携带的数据入队队尾,此为异步发送
2)如果底层循环数组已满或者是无缓冲channel,那么将当前goroutine加入写等待队列,并将其挂起,等待被唤醒,此为阻塞发送
接收数据
接收数据的流程主要由两大块组成:边界检查和接收数据,其中接收数据的处理逻辑如下:
1.如果 channel 的写等待队列存在发送者 goroutine,此为同步接收:
1)如果是无缓冲 channel,直接从第一个发送者 goroutine 那里把数据拷贝给接收变量,唤醒发送的 goroutine;
2)如果是有缓冲 channel(已满),将循环数组 buf 的队首元素拷贝给接收变量,将第一个发送者 goroutine 的数据拷贝到 buf 循环数组队尾,唤醒发送的 goroutine;
2.如果 channel 的写等待队列不存在发送者 goroutine:
1)如果循环数组 buf 非空,将循环数组 buf 的队首元素拷贝给接收变量,此为异步接收
2)如果循环数组 buf 为空,将当前 goroutine 加入读等待队列,并挂起等待唤醒,此为阻塞接收
Channel为什么是线程安全的
原子性,底层实现互斥锁来保证线程安全,出队入队都加了锁。
Channel是同步还是异步的
异步。存在3种状态:
1.nil,未初始化的状态,只进行了声明,或者手动赋值为nil
2.active,正常的channel,可读或者可写
3.closed,已关闭,千万不要误认为关闭channel后,channel的值是nil
下面对channel的零值nil通道、非零值但已关闭通道、非零值且尚未关闭通道三种操作解析:
Goroutine
底层数据结构
type g struct{
goid int64 //唯一的goroutine的ID
sched gobuf // goroutine切换时,用于保存g的上下文
stack stack //栈
gopc // pc of go statement that created this goroutine
startpc uintptr// pc of goroutine function
...
}
type gobuf struct {
sp uintptr // 栈指针位置
pc uintptr // 运行到的程序位置
g guintptr //指向 goroutine
ret uintptr // 保存系统调用的返回值
}
type stack struct {
lo uintptr // 栈的下界内存地址
hi uintptr // 栈的上界内存地址
}
type m struct{ // 内核线程
g0 *g
//每个M都有一个自己的G0,不指向任何可执行的函数,在调度或系统调用时,M会切换到G0] 使用G0的栈空间来调度
curg *g
//当前正在执行的G
...
}
type p struct{ // 虚拟处理器
lock mutex
id int32
status uint32 // one of pidle/prunning/...
//Queue of runnable goroutines. Accessed without lock.
runqhead uint32 //本地队列队头
runqtail uint32 //本地队列队尾
runq [256]guintptr // 本地队列,大小256的数组,数组往往会被都读入到缓存中,对缓存友好,效率较高
runnext guintptr //下一个优先执行的goroutine(一定是最后生产出来的),为了实现局部性原理,runnext中的G永远会被最先调
...
}
type schedt struct { // 调度器
...
runq gQueue //全局队列,链表(长度无限制)
runqsize int32 //全局队列长度
...
}
Goroutine
goroutine是golang实际并发的实体,底层使用coroutine(协程)实现并发,coroutine是一种运行在用户态的用户线程,底层使用coroutine,是因为:
用户空间,避免了内核态和用户态的切换导致的成本 可以由语言和框架层进行调度。
更小的栈空间允许创建大量的实例。
特性:GMP
G:Goroutine M:线程 P:CPU
运行:
CPU开启一个线程执行Goroutine。单核情况,所有goroutine运行在同一个线程中,每个线程维护一个上下文,一个上下文只有一个goroutine,其他goroutine在runqueue中等待。多核情况,碰到goroutine阻塞,会再开启一个线程执行goroutine,以充分利用cpu资源。
m获取p本地队列的g,本地队列没有,从全局队列中获取 (加锁),全局队列没有就从其他的p本地队列中窃取一半(后一半)放到当前p本地队列中(work stealing),如果其他队列中也没有,p置于空闲状态,m放回线程池中。如果p中有不对应数量的m,获取空闲m,如果没有,创建m。如果g发生系统调用阻塞,p会和m解绑,寻找空闲m,没有就新建m去执行p其余的g(hand off)。如果g发生网络io等阻塞,会触发调度让出执行权,将g上下文信息放到sched控制器(或netword poller)中,m调用其他的g。m执行完g后,切换为g0,负责gc和调度。如果新建g,p本地队列满,会从p中获取前一半和当前g放到全局队列中。
GM模型存在的问题:加了P之后解决了存在的问题
. 全局队列的锁竞争
当 M 执行 G,必须访问全局队列,因为 M 有多个,当多线程访问同一资源需要加锁保证线程安全,导致激烈的锁竞争。
. M 转移 G 增加额外开销
当 M1 在执行 G1 的时候, G1 包含创建新协程 G2,M1 创建了 G2,为了继续执行 G1,需要把 G2 保存到全局队列中,无法保证 G2 是被 M1 处理,造成了很差的局部性。
因为 M1 原本就保存了 G2 的信息,所以 G2 最好是在 M1 上执行,这样的话也不需要转移 G 到全局队列和线程上下文切换。
加了P后解决的问题:
. 每个 P 有自己的本地队列,大幅度减轻了对全局队列的直接依赖,减少了锁竞争。而 GM 模型的性能开销大头就是锁竞争。
. 局部性较好,当 M1 在执行 G1 的时候, G1 包含创建新协程 G2,M1 创建了 G2,直接将 G2 保存到当前绑定的 P 上,G2 大概率也是被 M1 执行,不需要将 G2 转移到全局 G 队列增加额外开销
停止Goroutine
1.runtime.Goexit()。
2.通过channel传递退出信号(<-ch)(适用于单个goroutine)。
3.使用waitgroup(Add、Wait、Done)。
4.context手动cannel或超时控制(contex应用)。
g0和g,g0栈和g栈
g0是Golang运行时中的特殊goroutine,即每个M(线程)都会绑定一个g0。g0主要用于执行运行时系统的初始化和调度相关的任务,它不执行用户代码,而是作为调度器和系统调用的桥梁。g0栈用来存放m的调度栈等信息。
g指的是普通goroutine,用于执行用户代码。每个goroutine都有自己独立的栈空间,用于执行用户代码,g栈是动态扩展的,可以根据需要增大或缩小。
g栈扩容
1.3版本之前是分段栈,之后是连续栈。
分段栈:空间是不连续的,但是当前 goroutine 的多个栈空间会以双向链表的形式串联起来,运行时会通过指针找到连续的栈片段。分段栈虽然能够按需为当前 goroutine 分配内存并且及时减少内存的占用,但是它也存在一个比较大的问题:如果当前 goroutine 的栈几乎充满,那么任意的函数调用都会触发栈的扩容,当函数返回后又会触发栈的收缩,如果在一个循环中调用函数,栈的分配和释放就会造成巨大的额外开销,这被称为热分裂问题(Hot split)。
连续栈:为解决分段栈问题。每当程序的栈空间不足时,初始化一片比旧栈大两倍的新栈并将原栈中的所有值都迁移到新的栈中,新的局部变量或者函数调用就有了充足的内存空间。
goroutine生命周期
创建(go关键字创建)- 执行(执行代码) - 挂起与恢复(协程可通过chan或select语句挂起,等待条件满足后恢复)- 完成 (协程执行完成或调用close(chan)结束生命周期)
特殊协程,main协程:生命周期与程序相同,若main协程结束,其他协程也会结束。
创建(newpro1)- 休眠(gopark)- 唤醒(goready)
调度器的生命周期
1.runtime创建最初的线程m0和goroutine g0,并把两者关联
2.调度器初始化:初始化m0,栈,GC,以及创建和初始化由GOMAXPROCS个P构成的P列表
3.示例代码中的main函数是main.main,runtime中也有1个main函数(runtime.main),代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列
4.启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine
5.G拥有栈,M根据G中的栈信息和调度信息设置运行环境
6.M运行G
7.G退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序
进程、线程、协程
进程: 系统进行资源分配和调度的基本单位,拥有独立的内存空间,不同进程通过进程间通信来通信,上下文切换开销(栈、寄存器、虚拟内存、文件句柄)大,相对比较安全。
线程:轻量级进程,不拥有系统资源,共享进程全部资源,线程间通信主要通过共享内存,上下文切换开销小,相比进程容易丢失数据。
协程:一种用户态的轻量级线程,由go运行时(runtime)管理,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
从单进程到多进程提高了 CPU利用率;从进程到线程,降低了上下文切换的开销;从线程到协程,进一步降低了上下文切换的开销,使得高并发的服务可以使用简单的代码写出来。
(进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度)
协程如何调度的
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。进程和线程的操作是由程序触发系统接口,最后执行者是系统,协程的操作执行者则是用户自身程序。
协程泄露
协程泄漏(Goroutine Leakage)是指那些已经没有任何用处(不再被使用或者无法到到达其执行路径),但由于某些原因未被收回的goroutine。这些泄漏的goroutine占用内存资源,可能会随着程序运行时间的增长而累积,最终导致内存耗尽或者程序性能下降。
协程泄露原因:
. 长时间运行或未正确终止的goroutine。
. 未处理的通道(Channel)操作:发送操作未被接受(发送者阻塞)、接受操作没有数据可接受(接收者阻塞)。
. 死锁。
解决:
. 使用context控制goroutine的生命周期(ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) select case <-ctx.Done())。
. 为通道设置超时操作(发送接收select case <-time.After(1 * time.Second))。
. 避免死锁。
Slice
切片数据结构
指向底层数组的指针、长度、容量。
nil slice和空slic
nil slice:未初始化的slice,不能赋值。指针并不指向底层的数组,而是指向一个没有实际意义的地址。
空slice:初始化的slice。指针指向底层数组的地址。
数组和切片区别
长度
数组长度固定。切片长度不固定,可以追加元素,在追加时可能会发送扩容。
传参
数组是值类型,赋值时传递的是一份深拷贝,会复制整个数组数据,会占用额外的内存,修改不会影响原数组内容。切片是引用类型,赋值时传递的是一份浅拷贝,只会复制len和cap,底层共用同一个数组,不会占用额外的内存,修改会修改原数组内容。
计算数组长度方式
数组需要遍历计算数组长度,时间复杂度是O(n)。切片底层包含len字段,通过len()计算数组长度,时间复杂度是O(1)。
Slice扩容策略:
1.8版本以前:容量小于1024,双倍扩容,大于等于1024,1.25倍扩容。如果原容量是1000, 按照双倍扩容就是2000。如果容量是1024,按照1.25被扩容就是1280。这里在1024临界处,原容量大的扩容后反而小,不是一个单调递增的趋势。
1.8版本以后(为了内存的变化更加平滑):容量小于256,双倍扩容,大于等于256,扩容后容量=1.25倍的原容量+ 192。在容量较小的时候,扩容的因子更大,容量大的时候,扩容的因子相对来说比较小。如果原容量是255,双倍扩容后是510。 如果原容量是256,扩容后=1.25*256+192= 512。临界处很平滑,这样内存扩容就是单调递增的。
扩容前后的Slice是否相同?
情况一:
原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的Slice。
情况二:
原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append()操作。这种情况丝毫不影响原数组。 要复制一个Slice,最好使用Copy函数。
Slice append
当你对一个slice执行append操作时,如果slice的容量足够大(即容量大于或等于长度加上要追加的元素数量),Go会直接在slice的末尾追加元素,然后返回原始slice的引用。如果容量不足,Go会创建一个新的、更大的底层数组,并将原slice中的元素复制到这个新数组中,然后追加新的元素,最后返回这个新数组的slice引用。
new和make区别
new:new对象,分配零值内存,返回指针。
make:make slice、map、channel,分配非零值内存(并初始化成员结构),返回对象。
Map
map底层数据结构
//A header for a Go map.
type hmap struct{
count int
//代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。
flags uint8
//状态标志(是否处于正在写入的状态等)
B uint8
// buckets(桶)的对数。如果B=5,则buckets数组的长度:2^B=32,意味着有32个桶
noverflow uint16
// 溢出桶的数量
hash0 uint32
//生成hash的随机数种子
buckets unsafe.Pointer
//指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil.
oldbuckets unsafe.Pointer
//如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2;非扩容状态下,它为n11.
nevacuate uintptr
//表示扩容进度,小于此地址的buckets代表已迁完成。
extra *mapextra
//存储溢出桶,这个字段是为了优化GC扫描而设计的。
}
//A bucket for a Go map.
//bmap就是我们常说的'桶',一个桶里面会最多装8个key,这些key之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果的低B位是相同的。在桶内,又会根据 key 计算出来的 hash 值的高 8位来决定key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。
type bmap struct{
tophash [bucketCnt]uint8
//len为8的数组,存储key的hash值的高8位。用来快速定位key是否在这个bmap中,如果key所在的tophash值在tophash中,则代表该key在这个桶中。一个桶最多8个槽位。
}
//上面bmap结构是静态结构,在编译过程中runtime.bmap 会拓展成以下结构体:
type bmap struct{
tophash [8]uint8
keys [8]keytype
//keytype 由编译器编译时候确定
values [8]elemtype
//elemtype 由编译器编译时候确定
overflow uintptr
//overflow指向下一个bmap,overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
}
//mapextra结构体
//当map的key和value都不是指针类型时候,bmap将完全不包含指针,那么gc时候就不用扫描bmap,bmap指向溢出桶的字段overflow是uinpt类型,为了防止这些overflow桶被gc掉,所以需要mapextra.overflow将它保存起来。如果bmap的overfiow是"bmap类型,那么gc扫描的是一个个拉链表,效率明显不如直接扫描一段内存(hmap.mapextra.overflow)
type mapextra struct {
overflow *[]*bmap
//overflow包含的是hmap.buckets的overflow的 buckets
oldoverflow *[]*bma
//oldoverflow包含扩容时hmap.oldbuckets的overflow的 bucket
nextOverflow *bmap
//指向空闲的 overflow bucket 的指针
}
注:bmap的key和 value 是各自放在一起的,并不是 key/value/key/value/…这样的形式,当key和value类型不一样的时候,key和value占用字节大小不一样,使用key/value这种形式可能会因为内存对齐导致内存空间浪费,所以Go采用key和value分开存储的设计,更节省内存空间。


map如何查找

map扩容装载因子为什么是6.5
装载因子主要是为了平衡buckets的存储空间大小和查找元素时的性能高低。官方通过溢出率、开销数、查找数测试发现:装载因子越大,填入的元素越多,空间利用率就越高,但发生哈希冲突的几率就变大。反之,装载因子越小,填入的元素越少,冲突发生的几率减小,但空间浪费也会变得更多,而且还会提高扩容操作的次数。由此取了一个相对适中的值6.5,也意味着,当map存储的元素个数大于或等于6.5*桶个数时,就会触发扩容行为。
map扩容(渐进式)
条件:装载因子大于6.5(count > LoadFactor * 2^B)
扩容方式:创建一个原来buckets规模的2倍大的buckets,然后oldbuckets指向原buckets数组,只有当assgin和delete操作(即插入和删除)时,进行排空和迁移,直到完全迁移完,删除oldbuckets指向,原buckets数组释放。(双倍扩容)
条件:溢出的桶数量太多(noverflow >= uint16(1)<<(B&15))
扩容方式:创建一个和原来buckets一样规模的buckets,将松散的键值对重新弄排列,提高bucket利用率,也是只当assgin和delete操作(即插入和删除)时,进行排空和迁移。(等量扩容)
map并发安全
原生map并发是非安全的,是因为map是指针类型,并发写时,多个协程同时操作一个内存,类似于多线程操作同一个资源会发生竞争关系,共享资源会遭到破坏,因此golang出于安全的考虑,抛出致命错误:fatal error: concurrent map writes。
解决并发非安全:
1.sync.map(同步锁,golang自带)
2.mutex(互斥锁)
3.rwMutex(读写锁)
如何选择解决方案:
1.(一亿)压测写入效率对比(优<差):rwMutex(207.8 ns/op) < mutex( 237.1 ns/op) < sync.map(509.3 ns/op)
2.(一亿)压测查找效率对比(优<差):sync.map(53.39 ns/op) <rwMutex(60.49 ns/op) < mutex(166.7 ns/op)
3.(一亿)压测删除效率对比(优<差):sync.map(41.54 ns/op) < mutex(168.3 ns/op) < rwMutex(188.5 ns/op)
分析:读多写少用sync.map
sync.map查询效率高的原因:
结构
(无锁访问)read作为缓存层,提供了快路径(fast path)的查找。同时其结合 amended 属性,配套解决了每次读取都涉及锁的问题,实现了读这一个使用场景的高性能。
sync.map写入效率低的原因:
1.写入一定要会经过 read,无论如何都比别人多一层,后续还要查数据情况和状态,性能开销相较更大。
2.当初始化或者 dirty 被提升(需要赋值给read)后,会从 read 中复制全量的数据,若 read 中数据量大,则会影响性能。
sync.map删除效率高的原因:
先检查read是否存在元素,存在即标记为删除,不存在且dirty不为空即上互斥锁标记为删除(软删)
哈希表的高效性(O(1))原因
.哈希函数直接映射:一次计算定位桶位置,无需扫描数据。
.冲突控制策略:动态扩容降低负载因子 + 红黑树避免长链表退化。
.均摊成本优化:扩容代价分散到多次操作,维持整体 O(1) 效率。
defer
go语言会将defer后面的语句进行延迟处理。编译时将defer后的语句追加到goroutine_defer链表最前面,运行goroutine_defer时从前到后依次执行。return时执行defer。
注意:return不是一条原子指令是一条语句。return i={返回值=i;调用defer函数;return}
用途:
关闭文件句柄。
锁资源释放。
数据库连接释放。
经典例子:
func f1() (result int) {
defer func() {
result++
}()
return 0
}
fmt.Println(f1())
输出:1 (逻辑:result=0; result+;return)
func f2() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
fmt.Println(f2())
输出:5(逻辑:r=t=5;t=t+5;return)
func test() int{
i := 0
defer fmt.Println("defer:",i)
i ++
return i
}
输出:defer:0 1
func test() int{
i := 0
defer func(){
fmt.Println("defer:",i)
}()
i ++
return i
}
输出:defer:1 1
func returnValues()int {
var result int
defer func() {
result ++
fmt.Println("defer:",result)
}()
return result
}
输出:defer:1 0
func reReturnValues() (result int) {
defer func() {
result ++
fmt.Println("defer:",result)
}()
return result
}
输出:defer:1 1
捕获异常
func f(){
defer func(){
if r:=recover();r!=nil{
fmt.Println("recovered in f:",r)
}
}()
//异常
g(0)
}
注:defer recover机制只针对当前函数以及直接调用的函数可能产生的panic,无法处理调用产生的其他协程的panic,这一点和try catch机制不一样。
反射
interface一般使用场景
1.需类型安全的多类型逻辑,替代 interface{} 的弱类型约束,泛型在编译期严格校验类型,避免运行时因类型断言错误导致的崩溃。
2.消除重复代码,统一处理多类型算法。
反射
反射机制是指在程序运行时动态地捕获甚至改变变量的类型信息和值,但是在编译时并不知道这些变量的具体类型。
使用场景:
1.有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
2.有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。
IDE代码自动补全、json序列化、fmt函数、DeepEqual等缺点:
1.与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
2.Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接panic,可能会造成严重的后果。
3.反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。
interface: 存储实体的类型信息。
reflect: TypeOf()函数返回一个接口,这个接口定义了一系列方法,利用这些方法可以获取关于类型的所有信息; ValueOf()函数返回一个结构体变量,包含类型信息以及实际值。
内存
内存分配
span:golang内存管理的基本单位,每个span管理指定规格(以page为单位)的内存块,内存池分配出不同规格的内存块就是通过span体现出来的,应用程序创建对象就是通过找到对应规格的span来存储的,数据结构是mspan。
mcache:绑在并发模型的P上,也就是说每一个P都会有一个mcahe绑定,用来给协程分配对象存储空间。
内存分配规则:
tiny对象内存分配,直接向mcache的tiny对象分配器申请,如果空间不足,则向mcache的tinySpanClass规格的span链表申请,如果没有,则向mcentral申请对应规格mspan,依旧没有,则向mheap申请,最后都用光则向操作系统申请。
小对象内存分配,先向本线程mcache申请,发现mspan没有空闲的空间,向mcentral申请对应规格的mspan,如果mcentral对应规格没有,向mheap申请对应页初始化新的mspan,如果也没有,则向操作系统申请,分配页。
大对象内存分配,直接向mheap申请spanclass=0,如果没有则向操作系统申请。
内存泄漏场景
由于逻辑问题导致资源一直无法被释放,增加阻塞。
1.Goroutine
func mainI(){
ch := make(chan int)
go func(){
ch <- 1
}()
//<-ch
time.Sleep(time.Second)
}
解释:对于非缓冲管道,必须有接收者才能将数据放入管道,否则就会阻塞,从而导致内存泄漏。
2.Http
http请求resp.Body,需要defer resp.Body.Close()
GC
垃圾回收GC
垃圾回收:指内存中不再使用的内存区域,自动发现和释放这种内存区域的过程就是垃圾回收。
三色标记原理:
初始所有对象白色。
从根(root)出发(全局指针和goroutine栈上的指针)扫描所有可达对象,标记为灰色,放入待处理队列。
从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。
重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。
原理:
WB(write barrier:写屏障,go1.7Dijkstra写屏障,只监控堆上指针数据的变动(增量式垃圾回收时,新增的指针对象会被初始为白色,需要标记为黑色)re-scan。go1.8混合写屏障,只监控堆上指针数据的变动,无需re-scan)-STW(stop the world:暂停所有正在执行的用户线程/协程)-mark(三色标记,标记协程是并行的)-WB(关闭写屏障)-STW(挂起stw协程)-sweep(清除,唤醒清扫垃圾协程,该协程在应用启动后创建,创建后立即进入睡眠,被唤醒后把白色对象挨个清理掉,清扫协程和应用协程是并发进行的,清扫完成后再次进入睡眠状态)
回收方式:
非增量式:垃圾回收需要STW,在STW期间完成所有垃圾对象的标记,STW结束后慢慢的执行垃圾对象的处理。
增量式:垃圾回收需要STW,在STW期间完成部分垃圾对象的标记,然后结束STW继续执行用户线程,一段时间后再次执行STW再标记部分垃圾对象,这个过程会多次重复执行,直到所有垃圾对象标记完成。
GC触发条件:
辅助GC:在分配内存时,会判断当前的Heap内存分配量是否达到了触发一轮GC的阈值(每轮GC完成后,该阈值会被动态设置),如果超过阈值,则启动一轮GC。
调用runtime.GC()强制启动一轮GC。
sysmon是运行时的守护进程,当超过 forcegcperiod (2分钟)没有运行GC会启动一轮GC。
GC调优
。控制内存分配的速度,限制 Goroutine的数量,提高赋值器 mutator的CPU利用率(降低GC的CPU利用率)
。少量使用+连接string(每次拼接都会生成新字符串,可能导致内存临时对象过多,增加GC压力)
。slice提前分配足够的内存来降低扩容带来的拷贝
。避免map key对象过多,导致扫描时间增加
。变量复用,减少对象分配,例如使用 sync.Pool来复用需要频繁创建临时对象、使用全局变量等
。增大 GOGC 的值,降低 GC的运行频率
sync.Once和init
init:文件包首次被加载的时候执行,且只执行一次。
sync.Once:需要的时候执行,且只执行一次。
Go为什么需要指针
因为函数参数都是值传递,如果对实参进行修改,值类型只是复制了一份值,对参数没有影响,指针就可以,而且指针指向的是一个地址,内存占用小,其实go里面不存在引用类型,比如map,slice,interface底层都是指针类型。
有哪些方式安全读写共享变量
- Mutex锁
- 通过 Channel 进行安全读写共享变量。
- 通过原子性操作(atomic:CAS无锁算法,该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。本质还是不断的占用CPU 资源换取加锁带来的开销)
常用的并发模型
channle:无缓冲,同步。
sync.WaitGroup:为了防止main函数结束结束goroutine,waitgroup会等待所有goroutine完成。
context:上下文。包括程序的运行环境、现场和快照,主要处理多个goroutine之间共享数据及多个goroutine的管理。
context常用方法
context.Background() 返回空的根Context,通常用于主函数或初始化场景(无超时/取消)
context.TODO() 占位Context,当不确定用哪种Context时使用(编译器静态分析辅助)
context.WithCancel() 创建可取消的Context,返回ctx和cancel函数(调用cancel()触发取消)
context.WithDeadline() 设置绝对截止时间(time.Time),超时自动取消
context.WithTimeout() 设置相对超时时间(time.Duration),底层调用WithDeadline
context.WithValue() 附加键值对到Context中(建议用自定义类型作key,避免冲突)
逃逸分析
通过在编译器里做逃逸分析(escape analysis)来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。判断是否被外部引用,被外部引用分配在堆上,否则分配在栈上。
为什么要有堆栈:堆上动态内存分配比栈上静态内存分配,开销大很多。
堆栈回收:堆上一般手动回收,一般语言有自动垃圾回收机制, 栈上由操作系统自动(分配)回收。
深拷贝和浅拷贝
深拷贝:拷贝的是数据本身。新创建对象,新对象和原对象不共享内存,新对象在内存中开辟一个新的内存地址,新对象修改不会影响原对象值。(copy())
浅拷贝:拷贝的是数据地址,只复制指向的对象的指针,此时新对象和原对象指向的内存地址是一样的,新对象修改原对象也会变化。
Beego和Gin区别
MVC:Beego支持完整的MVC;Gin不支持完整的MVC,需要开发者自己实现MVC。
路由和Session:Beego支持正则路由,Gin不支持正则路由;Beego支持Session,Gin不支持,需要安装另外的包。
适用场景:Beego适用快速开发或者业务相对复杂的项目;Gin适用性能要求高或者业务相对简单的项目。
gozero和kratos
gozero:
领域驱动设计(DDD),业务逻辑解耦清晰。
goctl(go control)脚手架工具一键生成API、BIZ、Data代码,开发效率提高60%。
自带ORM,开箱即用。
单节点10万QPS(每秒处理查询请求数量),grpc优化。
敏捷交付,高并发,中小项目
kratos:
严格分层(API、BIZ、Data),依赖注入控制。
需集成第三方ORM(如GORM。)
约8万QPS,标准grpc。
强规范protobuf,长期维护扩展,大型系统。
Go版本
版本优化点
1.23:range语句正式支持用户定义的容器类型遍历及倒序迭代实现,提升代码灵活性与安全性。支持包内泛型类型别名定义。
1.20:增量垃圾回收算法降低内存占用,垃圾回收效率提升30%,并发性能提升显著。泛型功能优化,支持更复杂的类型约束场景。
1.19:改进ARM架构兼容性,优化内存模型描述。增强调试器对并发程序的追踪能力。
1.18:首次正式支持泛型编程,简化代码复用结构。
推荐使用版本
作为长期支持(LTS)候选版本,Go 1.20经过大规模生产验证,垃圾回收效率提升30%,适合高并发服务场景。
Mutex互斥锁


Buffer和Cache区别
Buffer:用于存放将要输出到disk(块设备)的数据,进行流量整形,把突发的大数量较小规模的IO整理成平稳的小数量较大规模的IO,以减少响应次数。
Cache:用于存放从disk上读出的数据,为了弥补高速设备和低速设备的鸿沟而引入的中间层,最终起到加速访问速度的作用。
两者都是为提高IO性能而设计的。
分布式和微服务的区别
微服务:将一个系统分别多个业务模块。
分布式:将一个系统分为多个业务模块,业务模块分别部署在不同的机器上,各个业务模块之间通过接口进行数据交互。
RabbitMQ和ActiveMQ
作用: 异步、解耦、销峰。
ActiveMQ:
基于JMS(Java Message Service),java语言。
提供P2P和Pub/Sub(发布/订阅)两种消息模型。
提供多种消息类型(TextMessage、MapMessage、BytesMessage、StreamMessage、ObjectMessage)。
跨平台性差。
RabbitMQ:
基于AMQP,erlang语言(并发强)。
提供direct exchange、fanout exchange、topic exchange、headers exchange、system exchange 五种消息模型。
只有byte[]消息类型。
AMQP具有跨平台、跨语言特性。
处理高并发
系统拆分:将一个系统拆分为多个子系统,dubbo。
缓存:读多写少情况。
MQ(消息队列):多写情况。
分库分表:一个数据库拆分为多个库,一个表拆分为多个表,每个表的数据量保持少一点,提高sql跑的性能。
读写分离:读多写少情况。主从架构,主库写入,从库读取。 SolrCloud(solr云):是Solr提供的分布式搜索方案,可以解决海量数据的分布式全文检索,因为搭建了集群,因此具备高可用的特性,同时对数据进行主从备份,避免了单点故障问题。可以做到数据的快速恢复。并且可以动态的添加新的节点,再对数据进行平衡,可以做到负载均衡。
服务器雪崩效应及解决方案
雪崩效应:由于网络或自身原因,服务不能保证100%可用,服务出现问题,调用这个服务的服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。并且由于服务与服务之间的依赖,故障传播从而导致所有服务瘫痪。其实就是某个微小服务挂了,导致整一大片服务不可用,相当于生活中由于落下的最后一片雪花引发了雪崩情况。
容错方案:
1.隔离机制:微服务架构、进程多分、线程多分。
2.超时机制:在上游服务调用下游服务的时候,设置一个最大响应时间,超过这个时间,下游未作出反应,便断开请求,释放掉线程。
3.限流机制:限制系统的输入和输出流量以达到保护系统的目的。一旦达到限制的阈值需要限制流量并采取少量措施以完成限制流量的目的。
4.熔断机制:互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用,牺牲局部,保全整体。(熔断状态:关闭状态、开启状态、半熔断状态(尝试恢复服务调用))
5.降级机制:为服务提供一个兜底方案,一旦服务无法正常调用,就使用兜底方案。
自旋锁和互斥锁的区别
互斥锁:当拿不到锁的时候,会阻塞等待,会睡眠,等待锁释放后被唤醒
自旋锁:当拿不到锁的时候,会在原地不停的看能不能拿到锁,所以叫自旋锁,不会阻塞,不会睡眠
数据库
Mysql
Mysql数据结构
B+树,采用二分查找法。在B树的基础上,将非叶节点改造为不存储数据纯索引节点,空间利用率更高,进一步降低了树的高度;将叶节点使用指针连接成链表,范围查询更加高效。 此外, B+树, 主要是查询效率高,O(logN),可以充分利用磁盘预读的特性,多叉树,深度小,叶子结点有序且存储数据。

B+树是如何进行记录检索的?
如果通过 B+ 树的索引查询行记录,首先是从 B+ 树的根开始,逐层检索,直到找到叶子节点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录中的槽(sot)采用二分查找 的方式先找到一个粗略的记录分组然后再在分组中通过 链表遍历 的方式查找记录。(槽:页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。)
存储结构


页(16kb):页a,页b,页c…页n 这些页可以 不在物理结构上相连接 ,只要通过 双向链表 相关联即可。每个数据页中的记录会按照主键值从小到大的顺序组成一个 单向链表 ,每个数据页都会为存储在它里边的记录生成一个页目录。
区(Extent) 是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB。
段(Segment) 由一个或多个区组成,不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
表空间(Tablespace) (正常表为独立表空间)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。
附 一个使用InnoDB存储引擎的表只有一个聚集索引,一个索引会生成2个段(索引段(非叶子节点)和数据段(叶子节点))。单表建议不超过2G1000w行(阿里建议500w行)。
不定义主键
表中如果没有自定义主键,InnoDB会自动生成一个6字节的隐藏RowId作为主键。
Mysql索引
Mysql为什么加了索引可以加快查询:
在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据。
索引的优缺点:
优势:可以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;
劣势:索引本身也是表,因此会占用存储空间,一般来说,索引表占用的空间的数据表的1.5倍;索引表的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表.
MySQL中加入索引可以加快查询速度的原因在于索引通过减少数据库需要扫描的数据行数来提高搜索效率。当你对表中的某一列或多列建立索引后,数据库会将这些列的值排序并保存在一个特殊的数据结构中(B+树),这样在查询时数据库可以快速定位到符合条件的记录所在的位置,而不是进行全表扫描。
类型:
UNIQUE(唯一索引):不可以出现相同的值,可以有NULL值
INDEX(普通索引):允许出现相同的索引内容
PROMARY KEY(主键索引):不允许出现相同的值
fulltext index(全文索引):可以针对值中的某个单词,但效率确实不敢恭维
组合索引:实质上是将多个字段建到一个索引里,列值的组合必须唯一
组合索引的作用:
1.减少开销。
建一个组合索引(col1,col2,col3),实际相当于建了(col1),(col1,col2),(col1,col2,col3)三个索引。每多一个索引,都会增加写操作的开销和磁盘空间的开销。 对于大量数据的表,使用组合索引会大大的减少开销。(组合索引在存储/维护时可能进行一些优化,存储空间/维护成本可能会小于或等于三单列索引的空间总和。)
2.覆盖索引。
通常指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。对组合索引(col1,col2,col3),如果有如下的sql: select col1,col2,col3 from test where col1=1 and col2=2。那么MySQL可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io操作。减少io操作,特别的随机io其实是dba主要的优化策略。所以,在真正的实际应用中,覆盖索引是主要的提升性能的优化手段之一。
3.效率高。
索引列越多,通过索引筛选出的数据越快。
组合索引列越多越好吗?
不是,组合索引的列数量超过3个之后,其优势逐渐减小,甚至可能导致查询性能下降。这是因为MySQL在使用组合索引时需要考虑“索引选择性”和“最左前缀”的原则,过长的索引会使得选择性变差,查询时可能无法有效利用索引。此外,每增加一个索引列都会增加数据库维护索引的开销。
Mysql事务的基本要素
原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。(undolog)
一致性:事务开始前和结束后,数据库的完整性约束没有被破坏。(redolog)
隔离性:同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。(mvcc)
持久性:事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。(redolog)
Mysql事务隔离级别
读未提交(Read uncommitted):一个事务可以读取另一个未提交事务的数据,最低级别,任何情况都无法保证。引发脏读(Dirty Read):读取到了未提交的数据。
读已提交(Read committed):一个事务要等另一个事务提交后才能读取数据,可避免脏读的发生。引发不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果(针对数据)。
可重复读(Repeatable read):就是在开始读取数据(事务开启)时,不再允许修改操作,可避免脏读、不可重复读的发生。引起幻读:幻读是指在一个事务中进行两次查询,但是在这两次查询之间,被另一个事务插入或删除了数据,导致两次查询结果不一致(针对行数)(InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题)。Mysql的默认隔离级别是Repeatable read。
串行(Serializable):是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。 但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。 (3)在这个级别,可能导致大量的超时现象和锁竞争。
解决幻读
MySQL 的 RR 隔离级别通过 MVCC 解决快照读的幻读,通过 Next-Key Lock 解决当前读的幻读:
快照读:依赖 Read View 实现数据版本隔离,确保事务内一致性。
当前读:通过 Next-Key Lock 锁定索引间隙,物理阻止新数据插入。
需注意事务中避免混合使用快照读与当前读,以防边界幻读。
Mysql高可用方案有哪些
主从复制:从服务器到主服务器拉取二进制日志文件,然后再将日志文件解析成相应的SQL在从服务器上重新执行一遍主服务器的操作,通过这种方式保证数据的一致性。 为了达到更高的可用性,在实际的应用环境中,一般都是采用MySQL replication技术配合高可用集群软件keepalived来实现自动failover,这种方式可以实现95.000%的SLA。
MMM/MHA:MMM提供了MySQL主主复制配置的监控、故障转移和管理的一套可伸缩的脚本套件。在MMM高可用方案中,典型的应用是双主多从架构,通过MySQL replication技术可以实现两个服务器互为主从,且在任何时候只有一个节点可以被写入,避免了多点写入的数据冲突。 同时,当可写的主节点故障时,MMM套件可以立刻监控到,然后将服务自动切换到另一个主节点,继续提供服务,从而实现MySQL的高可用。
Heartbeat/SAN:高可用集群软件Heartbeat,它监控和管理各个节点间连接的网络,并监控集群服务,当节点出现故障或者服务不可用时,自动在其他节点启动集群服务。 在数据共享方面,通过SAN(Storage Area Network)存储来共享数据,这种方案可以实现99.990%的SLA。
Heartbeat/DRBD:依旧采用Heartbeat,不同的是,在数据共享方面,采用了基于块级别的数据同步软件DRBD来实现。 DRBD是一个用软件实现的、无共享的、服务器之间镜像块设备内容的存储复制解决方案。和SAN网络不同,它并不共享存储,而是通过服务器之间的网络复制数据。
NDB CLUSTER:NDB集群不需要依赖第三方组件,全部都使用官方组件,能保证数据的一致性,某个数据节点挂掉,其他数据节点依然可以提供服务,管理节点需要做冗余以防挂掉。 缺点是:管理和配置都很复杂,而且某些SQL语句例如join语句需要避免。
Mysql中utf8和utf8mb4区别
utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。三个字节的 UTF-8 最大能编码的 Unicode 字符是 0xffff,也就是 Unicode 中的基本多文种平面(BMP)。任何不在基本多文本平面的 Unicode字符,都无法使用 Mysql 的 utf8 字符集存储。包括 Emoji 表情(Emoji 是一种特殊的 Unicode 编码,常见于 ios 和 android 手机上),和很多不常用的汉字,以及任何新增的 Unicode 字符等等。Mysql 中保存 4 字节长度的 UTF-8 字符,需要使用utf8mb4 字符集,但只有5.5.3版本以后的才支持(查看版本: select version()😉。因此呢,为了获取更好的兼容性,应该总是使用 utf8mb4 而非 utf8.为了节省空间,一般情况下使用utf8也就可以了。
Mysql InnoDB和MyIsam存储引擎区别
innoDb:
支持外键 。
聚集索引,数据文件和索引绑在一起,必须要有主键,通过主键索引效率很高。
不保存表的具体行数,不保存表的具体行数,执行select count(8) from table时需要全表扫描 。
不支持全文索引。
支持事务 。
myIsam:
不支持外键 。
非聚集索引,数据文件和索引是分离的,索引保存数据文件的指针。
保存表的具体行数,用一个变量保存了整个表的行数,执行select count(*) from table语句时只需要读出该变量即可,速度很快 。
支持全文索引,查询效率高 。
不支持事务。
MVCC
Multiversion (version) concurrency control (MCC or MVCC) 多版本并发控制 ,它是数据库管理系统一种常见的并发控制。
MVCC的实现原理:
MVCC 使用了“三个隐藏字段”来实现版本并发控制, 事务字段(DB_TRX_ID),回滚指针字段(DB_ROLL_PTR),是否删除字段(isDelete)。
UndoLog、Redolog、Binlog
UndoLog:回滚日志文件,主要用于事务中执行失败,进行回滚,以及MVCC中对于数据历史版本的查看。由引擎层的InnoDB引擎实现,是逻辑日志,记录数据修改被修改前的值,比如"把id=‘B’ 修改为id = ‘B2’ ,那么undo日志就会用来存放id ='B’的记录”。当事务提交之后,undo log并不能立马被删除,而是会被放到待清理链表中,待判断没有事物用到该版本的信息时才可以清理相应undolog。
Redolog:重做日志文件,记录数据修改之后的值,用于持久化到磁盘中。由引擎层的InnoDB引擎实现记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动”。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redoLog 在内存中,然后找个时间将redoLog的操作执行到磁盘上的文件上。 不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性。操作记录。
Binlog:由Mysql的Server层实现,是逻辑日志,记录的是sql语句的原始逻辑,比如"把id=‘B’ 修改为id = ‘B2’。Binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。用于主从复制和数据修复。
Mysql主从同步
原理:主库将变更写binlog日志,然后从库连接到主库后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中,接着从库中有一个sql线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再执行一遍sql,这样就可以保证自己跟主库的数据一致。
同步机制:
全同步复制:指的就是主库写入binlog日志后,就会将强制此时立即将数据同步到从库,所有的从库都执行完成后才返回给客户端,很显然这个方式的话性能会受到严重影响。
半同步复制:semi-sync复制,指的就是主库写入binlog日志后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库ack之后才会认为写完成。
并发复制:指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这样库级别的并行。(将主库分库也可缓解延迟问题)
Mysql如何保证一致性和持久性
Mysql为了保证ACID中的一致性和持久性,使用了WAL(Write-Ahead Logging,先写日志再写磁盘)。Redo log就是一种WAL的应用。
当数据库忽然掉电,再重新启动时,Mysql可以通过Redo log还原数据。也就是说,每次事务提交时,不用同步刷新磁盘数据文件,只需要同步刷新Redo log就足够了。
Mysql 更新数据执行流程

执行器和引擎层之间的交互
- 执行器先从引擎中找到数据,如果在内存中直接返回,如果不在内存中,查询后返回。
- 执行器拿到数据之后会先修改数据,然后调用引擎接口重新写入数据。
- 引擎将数据更新到内存,同时写数据到redolog中,此时处于Prepare阶段,并通知执行器执行完成,随时可以操作。
- 执行器生成这个操作的binlog。
- 执行器调用引擎的事务提交接口,引擎把刚刚写完的redolog改为commit状态,更新完成。
(redolog和binlog之间加了事务,保证了数据一致性)

进程内缓存(Buffer Pool)和自适应哈希索引
Buffer Pool:复制idb文件,随机写,O(lgn),定制缓存策略。
自适应哈希索引:key(值)-value(页地址),O(1)。
为什么要同时存在redolog和binlog
Redolog:
。InnoDB存储引擎
。恢复事务数据持久性
。针对buffer pool
binlog:
。整个Mysql
。恢复磁盘数据
。针对磁盘记录所有日志
。主从同步,数据误删恢复
SQL优化和索引优化
sql优化:
避免索引失效导致全表扫描。
适当使用in和exists(in先执行子查询,exists先执行外层,所以in适合外表大而内表小的情况,exitst适合外表小而内表大的情况)。
避免返回不必要的列(*)和数据(用limit)。
适当使用limit,因为当offset值很大时,需要跳过大量的记录才能获取到指定的结果集,导致性能下降。可以考虑使用id条件分页方式,即where>最新id limit 页数或子查询获取id结合join。
尽量避免子查询,使用join 用小表驱动大表,减少内循环次数。
尽量用inner join,少用left join,inner join 会自动选择小表去驱动大表,left join 一般使用场景是大表驱动小表。join 连接最好不超过2个。
索引优化:
避免索引失效:
索引列上做操作,计算、函数、类型转换。
!= 或<>,会导致后面索引失效 。
Not in
null 。
like 通配符前缀%,会导致后面索引失效。 (可以将值反过来加一个列然后like 'xx%'后缀)
字符串不加单引号。
在join操作中(需要从多个数据表提取数据时),mysql只有在主键和外键的数据类型相同时才能使用索引,否则及时建立了索引也不会使用。
非最左前缀顺序
需要回表的查询结果集过大(超过配置的范围)
SQL超时
SQL执行超时通常是由于查询效率低下或者数据库服务器资源不足导致的。以下是一些解决方法:
优化查询:检查SQL查询是否可以优化,比如使用更有效的JOIN操作、WHERE子句条件、索引等。
增加索引:确保查询中涉及的列有适当的索引,以加快查询速度。
连接数小:Mysql或服务端连接池连接数设置小。
Buffer Pool小:通过查询命中率可以体现。
分批查询:如果查询无法优化,可以考虑将一次大查询分解为多次小查询,每次查询处理少量数据。
异步处理:如果查询是异步操作,可以将查询放入队列中,分批夜间或资源空闲时执行。
分析和优化表:定期执行EXPLAIN分析查询计划,并根据结果对表进行优化,比如重建索引、整理碎片(碎片整理主要是指对数据库中的表进行优化处理,以消除由于数据的插入、删除和更新操作会在数据库中留下一些未使用并且不连续的空间(碎片)。这些空间无法被有效利用,从而导致物理存储与逻辑存储的位置顺序不一致)等。
调整超时设置:临时增加查询的超时时间,但这只是短期应对方法。
数据库配置优化:调整数据库的配置参数,比如缓存大小、并发线程数等。
增加服务器资源:如果服务器资源确实不足,可以考虑增加CPU、内存或者磁盘空间。
COUNT(*)和COUNT(1)和COUNT(column_name)
COUNT()和COUNT(1):现代数据库对COUNT()和COUNT(1)进行了优化,两者的性能基本相似。
COUNT(column_name):统计去除NULL值特定列的值,相对慢一些,特别是当列中有大量NULL值时,因为需要遍历和检查每个值。
什么情况不需要索引
。小表查询。对于非常小的表,MySQL可能会选择全表扫描(忽略索引),因为全表扫描的开销可能比通过索引逐行查找还要低。在这种情况下,索引失效不会损害性能,反而简化了查询。
。读取表中大部分或所有行。当一个查询返回表中很大比例的行(如 30% 或更多)时,使用索引查找可能会耗时更多,因为数据库必须跳回主数据页以读取完整记录。全表扫描可能更有效,因为它可以逐行顺序读取数据。
。低选择性索引。如果索引列的选择性非常低,例如一个布尔型字段,许多行有相同的值,那么依赖索引可能会产生不必要的开销。全表扫描可以避免索引的搜索和回表开销。
。频繁更新的表。对于包含大量更新操作的表,索引的维护成本可能相对较高。尤其是在频繁更新索引列时,通过避免使用或减少复杂的索引可以减轻写操作的负担。
。复杂查询的优化选择。对于复杂的多表联接查询,优化器有时可以选择执行计划中不使用某个索引(或部分失效)以提高整体联接和计算效率。
Redis
Redis数据结构
string:SET key value。底层数据结构是 int 或SDS(简单动态字符串)。
list:LPUSH key value [value …] 。底层数据结构是双向链表或压缩列表。(Redis 7.0 中,压缩列表数据结构已经废弃,交由 listpack 数据结构来实现了)
hash:HSET key field value 。底层数据结构是压缩列表或哈希表。场景:存储对象。
set:SADD key member [member …]。底层数据结构是整数集合或哈希表。场景:抽奖。
zset:ZADD key score member [[score member]…] 。底层数据结构是压缩列表或跳表。场景:排行榜。
bitmap:位图,是一串连续的二进制数组(0和1)。场景:二值统计,判断用户登录态。
hyperloglog:提供不精确的去重计数。场景:百万级网页 UV 计数。
geo:主要用于存储地理位置信息,并对存储的信息进行操作。底层数据结构是zset。
stream:消息队列,不仅支持自动生成全局唯一 ID,而且支持以消费组形式消费数据。
Redis持久化存储方式
rdb(Redis DataBase)(4.0之前默认):定时快照存储二进制压缩.rdb文件,存储的是内容。 优点是大规模数据恢复、存储内存小。缺点是可能缺失数据多。
aof(Append Of File):即时存储.aof文件,存储的是命令操作。优点是可能缺失数据少。缺点是重写数据慢。
混合持久化(4.0之后默认):结合rdb和aof优点。使用rdb和aof文件存储,定时快照存储rdb文件,aof文件存储持久化时数据操作。重启时先加载rdb,再重放aof文件。
(fsync:强制将内存数据刷新到磁盘)
单线程的Redis为什么快
1.单线程操作,避免了频繁的上下文切换
2.纯内存操作
3.合理高效的数据结构
4.采用了非阻塞I/O多路复用机制
Redis 缓存穿透、缓存击穿、缓存雪崩
缓存穿透:缓存和数据库中都没有的数据,可用户还是源源不断的发起请求,导致每次请求都会到数据库,从而压垮数据库。
解决办法:
①参数校验。
②不存在数据设置短过期时间。
③布隆过滤器。布隆过滤器是一种数据结构,利用极小的内存,可以判断大量的数据“一定不存在或者可能存在”。
缓存击穿:Redis中一个热点key在失效的同时,大量的请求过来,从而会全部到达数据库,压垮数据库。
解决办法:
①设置热点数据永不过期。
②定时更新。
③互斥锁。互斥锁简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。
缓存雪崩:Redis中缓存的数据大面积同时失效,或者Redis宕机,从而会导致大量请求直接到数据库,压垮数据库。
①设置有效期均匀分布。
②数据预热。对于即将来临的大量请求,我们可以提前走一遍系统,将数据提前缓存在Redis中,并设置不同的过期时间。
③保证Redis服务高可用。主从复制+哨兵模式;集群+分片机制+分布式投票+gossip通信。
B+树和跳表
B+树(多叉平衡二叉树):
写入慢,因为要平衡旋转。
查询快,因为树矮。
跳表(二叉树):
写入快,因为独立插入。
查询慢,因为树高。
Redis ZSET为什么用跳表而不用B+树
因为跳表更好实现,而且没有平衡树的旋转开销。至于跳表层数高的io问题,redis是内存存储,io开销小。
Redis过期时间设置
调用方设置。
缓存淘汰策略:LRU(最近最少使用)、RANDOM(随机)。
Memcached和Redis比较
memcached:数据结构单一,只能缓存数据不能持久化,适用多读少写。
redis:数据结构丰富,三个持久化方案,并可以数据恢复。
LevelDB
Leveldb
基于本地文件存储,数据存量是物理内存的3-5倍,将一部分分热区数据.log保存在内存,持久化数据.sst保存在磁盘上。
ETCD
Etcd分布式锁原理
raft算法。
Etcd分布式锁实现原理
1.在etcd系统里创建一个key。
2.如果创建失败,key存在,则监听key的变化事件,直到该key被删除,回到1。
3.如果创建成功,则认为获得了锁。
Etcd工作原理
http server接受请求并转发给store进行处理,如果涉及节点修改,则交给raft进行状态变更、日志记录,然后同步给其他节点以确认提交,最后提交数据,并再次同步。其中etcd使用wal来进行持久化存储。
组件
Prometheus和Grafana
Prometheus主要负责监控数据的收集、存储和查询,而Grafana则负责将这些数据以可视化的方式展示出来,并提供警报功能。它们通常一起使用,形成一个完整的监控系统。Prometheus为Grafana提供数据支持,而Grafana则提供了直观的可视化界面,帮助用户更好地理解和分析监控数据。
ELK
ELK是Elasticsearch、Logstash和Kibana的缩写,它们是一个开源的日志分析架构技术栈,被广泛应用于实时日志处理、全文搜索和数据分析等领域。
.Logstash:这是一个日志收集器,用于搜集各种数据源,并对数据进行过滤、分析和格式化,然后存储到Elasticsearch中。
.Elasticsearch:这是一个基于Lucene的分布式搜索引擎,具有高可伸缩性、高可靠性和易管理等特点。它能够对大容量数据进行接近实时的存储、搜索和分析操作。
.Kibana:这是一个数据分析和可视化平台,与Elasticsearch配合使用,对其中数据进行搜索、分析和图表展示。
Elasticsearch数据结构



Inverted Index(倒排索引):分词-》词项-+文档id-》字典序排序-》二分查找。数据在磁盘。
Term Index:前缀目录树+偏移量(节点)。数据在内存,加速搜索。
Stored Fields:文档原始数据。
Doc Values:字段集中存放,做排序和聚合。
Elasticsearch写入流程

Http-》协调节点-》Hash路由-》主分片-》Lucene-》Segment-》Inverted Index、Term Index、Stored Fields、Doc Values-》同步副本分片-》主分片响应ACK给协调节点-》协调节点响应写入完成。
Elasticsearch查找流程
查找文档id

Http-》协调节点-》多分片节点-》搜索多个Segment-》Inverted Index获取文档id-》Doc Values获取排序信息-》分片将结果聚合返回给协调节点-》协调节点对多个分片数据进行排序聚合。
获取数据

协调节点拿文档id请求分片-》Segment-》Stored Fields获取文档内容-》协调节点-》客户端。
Elasticsearch高效搜索的原因
倒排索引:记录每个单词出现的文档列表,查询时直接抵顾问文档ID,避免全表扫描,支持快速布尔运算。
分布式架构:分片机制和副本机制,实现并行处理和高可用(负载均衡)。
内存优化:内存缓存索引,直接走内存查询,效率高。
分层缓存:节点级缓存(缓存热点数据)和查询级缓存(缓存高频查询结果)
预计算:对聚合类查询预生成中间结果,减少实时计算量。
倒排索引效率高的原因:通过词项找文档ID的映射,跳过文档扫描。正向索引是通过文档扫描词项。
1 分词标准化生成词项
2 词汇表直接关联倒排列表
3 倒排列表返回文档ID集
K8s

K8S,全称 Kubernetes,是一个用于管理容器的开源平台。它可以让用户更加方便地部署、扩展和管理容器化应用程序,并通过自动化的方式实现负载均衡、服务发现和自动弹性伸缩等功能。Kubernetes 可以将应用程序打包成容器,并将这些容器部署到一个集群中,然后自动处理容器的生命周期管理、自动扩容等操作,让用户更加专注于应用程序的开发和业务逻辑。同时,Kubernetes 还提供了一系列的资源管理机制,如资源调度、容器网络、存储编排等,控制整个容器集群的运行状态,并保证应用程序在容器集群中的高可用性和可靠性。
K8s日志:
1.K8s集成了prometheus和Grafana,prometheus收集日志,通过Grafana可视化展示。
2.通过第三方日志收集器Logstash将数据发送到指定Elasticsearch中,通过Kibana可视化展示出来。
RPC和GRPC
RPC(Remote Procedure Call),即远程过程调用,它允许像调用本地服务一样调用远程服务。是一种客户端-服务端(Client/Server)模式。RPC 会给对应的服务接口名生成一个代理类,即客户端 Stub。使用代理类可以屏蔽掉 RPC 调用的具体底层细节,使得用户无感知的调用远程服务。客户端 Stub 会将当前调用的方法的方法名、参数类型、实参数等根据协议组装成网络传输的消息体,将其序列化成二进制流后,通过 Sockect 发送给 RPC 服务端。服务端反之。
核心功能:服务寻址(通过注册中心注册和发现服务)、数据编解码(序列化和反序列化(XML、JSON、Protocol Buffers))、网络传输(TCP协议-传输层)。
GRPC(Google Remote Procedure Call):是一个高性能,开源和通用的RPC框架,基于Protobuf序列化协议开发,且支持众多开发语言。客户端和服务器调用需要使用Protobuf进行序列化和反序列化。基于HTTP2协议–应用层。
优点:gRPC消息使用一种有效的二进制消息格式protobuf进行序列化。Protobuf在服务器和客户机上的序列化非常快。Protobuf序列化之后的消息体积很小,能够有效负载,在移动应用程序等有限宽带场景中显得很重要。与采用文本格式的json相比,采用二进制格式的protobuf在速度上可以达到前者的5倍。
缺点:虽然protobuf的发送和接收效率很高,但它的二进制格式是不可读的。
GRPC和HTTP
共同点:都是CS模式的架构设计。
区别:
(1)gRPC可以自定义消息格式,HTTP的格式是固定的,可能存在性能不足的问题;
(2)gRPC通过proto buffer进行数据交换,相比于json和XML,他的序列化和反序列化的效率都更高。
Consul和Etcd
Consul和Etcd是两种不同的分布式系统管理和服务发现工具,它们各自具有独特的特点和用途。
Consul 是一个服务发现和配置管理的工具,它提供了动态负载均衡、配置中心、服务发现等功能。Consul通过gossip协议形成动态集群,实现服务的注册与发现,并且支持健康检查和自定义命令执行。Consul的一致性读模式包括default、consistent和stale三种,其中default和consistent模式保证了一致性读,而stale模式则允许非一致性的读操作。
Etcd 是一个分布式键值存储系统,用于共享配置和服务发现。它提供了一个可靠的方式来存储配置信息,并且支持数据持久化。Etcd使用Raft协议来维护集群中的一致性,确保数据的强一致性。Etcd的设计目标是简单、安全、快速和可靠,适用于对一致性要求较高的场景。
总的来说,Consul和Etcd都是用于构建分布式系统的工具,但它们在设计理念、功能和使用场景上有所不同。Consul更侧重于服务发现和动态服务管理,而Etcd则专注于提供可靠的数据存储和一致性保证。
Kafka和RocketMQ
Kafka:17w QPS,sendfile(两次拷贝),返回字节数。
RocketMQ:10w QPS,mmap(三次拷贝),返回内容(方便二次投递)。
Kafka防止丢失和重复
一、防止数据丢失
1.生产端
。acks确认机制(acks=0不等待任何服务器的确认,acks=1等待Leader副本确认,acks=all等待所有Leader和ISR副本确认(推荐))
。Retries重试机制(retries设置大于0时自动重试发送消息)
max.in.flight.requests.per.connection(设置为 acks=all 时,建议将此值设为 1。这可以防止在重试时因网络延迟导致的消息乱序)
2.Broker端
。副本机制(为每个Topic设置多个副本)
。ISR(In-Sync Replicas)机制
。最小ISR数量:定义了成功写入所必需的最小ISR副本数(小于最小数时其发送请求会被拒绝)。
3.消费端
。防止机制:手动提交偏移量(禁用自动提交 (enable.auto.commit=false),并在业务逻辑成功处理完消息之后,再手动调用 consumer.commitSync() 来提交偏移量,这样保证了只有成功消费的消息,其偏移量才会被更新。如果处理过程中失败,你可以选择不提交,下次还能重新拉取到这条消息)
二、防止数据重复
生产者重试: 生产者可能因为未收到 Broker 的 ACK 而重试,导致 Broker 上存入了多条内容相同的消息(但可能具有不同的偏移量)。
消费者重试: 消费者处理完业务逻辑后,在提交偏移量之前失败,下次重启后会重新消费已处理过的消息。
。生产端幂等性:(enable.idempotence=true)Kafka 生产者为每个消息分配一个唯一的 PID (Producer ID) 和序列号 (Sequence Number)。Broker 会缓存每个分区最近收到消息的序列号。如果收到的新消息的序列号比缓存中的小,Broker 就会将其视为重复消息而丢弃。
。生产和消费端事务:事务允许生产者将一批消息的发送到一个或多个分区(以及外部系统)的操作原子化(要么全部成功,要么全部失败)。这主要依靠一个分布式事务协调器(Transaction Coordinator)和两阶段提交(2PC)来实现。
集群和负载均衡之间的关系
负载均衡的实现基于集群的存在,因为只有当服务器数量增加,形成集群时,负载均衡才能有效地将请求分发到多个服务器上。通过负载均衡将请求分发到不同的服务器上,可以避免单个服务器过载;而通过集群的协同工作,可以提高系统的处理能力和可靠性。这种组合使用不仅提高了系统的可用性,避免了单点故障,而且还通过动态调整实现了更加灵活和高效的请求分发。
网络
为什么进行三次握手
保证发送端和接收端发送和接收能力没问题.。
第一次握手:表示发送端的发送能力没问题。
第二次握手:表示接收端的接收能力和发送能力没问题。
第三次握手:表示发送端的接收能力没问题。
Http报文格式

// GET
GET /v3/weather/weatherInfo?city-%E9%95%BF%E6%B2%99&key-13cb58f5&key-13cb58f5884f9749287abbead9c6 HTTP/1.1 // 请求行
Host: restapi.amap.com // 请求头部
// 响应头
HTTP/1.1 200 0K
Server: Tengine
Date: Wed, 13 Feb 2019 03.19 55 GMT
Content-Type: applicatlonjson;charset=UTF-8
Content-Length: 449
Connectione: close
X-Powered-By: ring/1.0.0
gsd:011169103007155002799520700199108415000
sC:0.011
Access Control Allow Origin: *
Access-ControhAllow-Methods: *
Access-Controll-Allow-Headers: DNTx.CustomHeader Keep-Alve,User.Agen. x-Requested:withifiodifed+since.cache-Control content
Type,key,x-biz.x-nfo,plat nfo.encr,enginover.gzipped,polld
// 响应体
{"status": "1","count":"2","info": "oK","infocode":"10000","lives":[{"province":"湖南","cty","长沙市","adcode":"430100","weather":"雨夫雪"}]}
// POST
POST /v3/weather/weatherInfo HTP/1.1 请求行
// 请求头部
Host:restapi.amap.com
Content-Length:60
Content-Type:application/x-www-form-urlencoded
// 响应头
// 响应体
Socket(对tcp/ip的封装)实现http交互
- 封装http报文发送到服务器
- 解析响应报文,解析响应报文中的响应体可以根据Content-Length或者分块编码
HTTPS(443)和HTTP(80)的区别
HTTPS (HTTP over SecureSocket Layer):安全性高。身份认证(CA)和不可否认/抵赖、加密传输(SSL/TSL,非对称加密和对称加密)、保证完整(摘要hash)。HTTP+SSL/TLS(会话层)。
HTTP(Hyper Text Transfer Protocol):安全性低。身份冒充、明文传输、传输过程可能被篡改、无法保证事务真实性。
SSL(SecureSocket Layer,安全套接层)/TLS(Transport Layer Secure,安全传输层)的工作流程
CS双方通过非对称加密交换密钥,通过对称加密传输数据
1 客户端给出协议版本号、一个随机数(Random1),以及客户端支持的加密方法,
2.服务端确认使用的加密方法,并给出数字证书、以及随机数(Random2)
3 客户端确认数字证书有效,生成一个新的随机数(premaster secret),并使用数字证书中的公钥,利用RSA/ECDHE算法加密这个随机数,发给服务端。
4 服务端使用自己的私钥,获取客户端发来的随机数(premaster secret)。
5 客户端和服务端根据约定的加密方法,使用前面的三个随机数,生成对话密钥(master secret),用来加密接下来的整个对话过程。
TLS1.2和TLS1.3区别




TLS1.2:四次握手、两倍RTT(往返时延)、服务端将公钥发给客户端。
TLS1.3:三次握手、一倍RTT(往返时延)、客户端将公钥发给服务端。
HTTPS性能优化
HTTP:
。协议优化:TLS1.2->TLS1.3、密钥交换:ECDHE(椭圆曲线:x25519)、对称加密:AES 128_GCM
。证书优化:服务器证书:RSA->ECDSA、吊销机制:定期吊销CRL->在线状态OCSP
。会话复用:Session lD、Session Ticket、PSK(0-RTT)(Pre-shared Key)
其他:
。硬件:更快的CPU、SSL加速卡、SSL加速服务器
。软件升级:Linux:内核升级4.x、Nginx:1.16+、OpenSSL: 1.1.0/1.1.1
为什么现在还在普遍用http1.1而不是http2
。实际部署的复杂性。旧系统或未更新的服务器未进行http2设置。
。客户端兼容性约束。
。在低延迟网络环境下,HTTP/1.1与HTTP/2的性能差异较小。
。迁移成本。遗留系统集成成本。
。开销。HTTP2通常与HTTPS捆绑部署,增加了证书管理和加密开销。
访问CDN
域名-》DNS-》CNAME-》CDN专用的DNS调度系统-》最新的IP
持续更新…
本文深入探讨Go语言的优势,如原生多核并发技术,详细解析了指针在Go中的作用。介绍了内存泄漏场景,展示了Goroutine的并发模型、调度机制以及如何停止Goroutine。此外,讨论了Channel的线程安全、切片数据结构以及Go的垃圾回收机制。还涵盖了分布式锁、数据库优化、反射和异常处理等方面,揭示了Go语言在并发编程和系统设计中的高效实践。




906






