1.相当int32,用来区分字符值和整数值,用来处
理unicode或utf-8字符
2.byte相当int8,用来处理ascii字符
3.golang中的字符串底层是通过byte数组实现,
中文字符在unicode下占2个字节,在utf-8占3个
字节,golang默认编码是utf-8
func main() {
str := "米虫 is cool"
fmt.Println("STR LEN - ", len([]rune(str)))
}
7、 golang 中解析 tag 是怎么实现的?反射原理是什么?
计算机程序在运行时(Run time)可以访问、检测
和修改它本身状态或行为的一种能力或动态知道给
定数据对象的类型和结构,并有机会修改它。
8、调用函数传入结构体时,应该传值还是指针?
1.结构体的大小:如果结构体非常大,使用指针传
递会更有效率,因为这样只复制指针值(一般是8字
节)而不是复制整个结构体。
2.是否需要修改原始结构体:如果需要在函数中修
改原始结构体,应该使用指针传递。
9、Slice 介绍
type slice struct {
array unsafe.Pointer
len int
cap int
}
*array 指向底层数组的指针
len 切片长度
cap 切片容量
2.make创建slice时可以指定其长度和容量,底
层会分配一个数组,数组的长度即容量.
slice = make([]int,5,10)表示该slice长
度为5,容量为10.使用数组创建slice时slice
与原数组共用一部分内存。
3.使用append向slice追加元素时,若slice空
间不足则发生扩容,扩容会重新分配一块更大的内
存,将原slice拷贝到新slice,再将数据追加进
去然后返回新slice.扩容操作只针对容量,扩容
后的slice长度不变
4.使用copy拷贝切片时,会将源切片的数据逐个
拷贝到目的切片指向的数组中,拷贝数量取两个切
片长度的最小值.copy不会发生扩容.
5.slice := array[start:end],该写法新生
成的切片没指定容量,新切片的容量从start开始
到array结束(注意不是到end)
slice := array[start:end:cap],其中的
cap为新切片的容量
6.创建切片时根据实际需要预分配容量,尽量避免
追加元素过程中发生扩容,这样有利于提升性能.
10、slice扩容
1.切片扩容的条件:
使用append函数向切片追加元素时,如果追加元
素后的切片长度超过了容量,切片就会进行扩容
2.切片扩容机制:
go1.18之前,若原切片容量小于1024的时候,切
片先两倍扩容,若两倍扩容后的容量不够,直接以
切片需要的容量作为新切片容量,大于1024时,
会反复地增加25%,直到新容量超过所需要的容量
go1.18之后,临界值换成了256,原切片容量小于
256和前面相同,大于256时切片的容量每次增加
(oldcap+3*256)/4,相对于1.18之前可以更
平滑过渡
10、go struct 能不能比较?
结构体(Struct)是否可以比较取决于其字段。
如果结构体的所有字段都是可比较的那么这个结
构体也是可比较的.意味着可以使用==或!=运算
符来比较两个结构体变量。
11、context 结构是什么样的?
type Context interface {
Done() <-chan struct{}
Err() error
Deadline()(deadline time.Time,ok bool)
Value(key interface{}) interface{}
}
4个方法,是幂等的.即连续多次调用同一个方法,
得到的结果都是相同的。
1.Done()返回一个channel,表示context是
否被取消或超时的信号:当该channel被关闭时,
说明context被取消了.该channel是只读类型,
另外读一个关闭的channel会读出对应类型的零
值.而源码中并没有向这个channel里发送值.因
此在子协程里读这个channel,除非被关闭,否则
读不出任何数据.利用这一点,子协程从channel
里读出值(零值)后,则通知context相关函数停
止工作,然后返回
2.Err()返回一个错误,表示channel被关闭的
原因例如是被取消,还是超时。
3.Deadline()方法返回一个time.Time和ok
分别表示当前Context截止时间,是否有结束时间,
4.Value()获取之前设置的key对应的value。
12、context 使用场景和用途?(基本必问)
context提供在不同goroutine之间传递取消信
号,截止时间,截止日期等元数据的方法.context
主要用于在大规模的并发或分布式系统中进行取消
操作、截止时间管理、跟踪请求以及传递其他请求
范围的数据。context的常见使用场景和用途:
1.控制请求的超时和取消:context包允许在请求
超时或取消时通知Goroutine停止执行,以避免资
源泄漏和长时间阻塞.通过使用
context.WithTimeout或
context.WithCancel创建一个带有超时或取消
功能的上下文,可以在超时或取消时终止相关操作
2.传递请求范围的值:context包允许在请求范
围内传递值.这对于在请求处理流程中共享上下文
信息非常有用,通过使用 context.WithValue
创建一个带有键值对的上下文,可以将该值与上
下文关联,并在整个请求处理过程中访问这些值。
3.多个Goroutine间传递上下文信息:context
包允许在多个Goroutine间传递上下文信息.当
启动一个Goroutine并希望它能访问请求的上下
文时,可将上下文作为参数传递给Goroutine或
使用context.WithValue创建一个新的上下文。
4.中止并发操作:当需要中止一组并发操作时,可
使用context包实现.通过创建一个带取消功能
的上下文,并将该上下文传递给多个Goroutine,
可在需要时取消或终止这些Goroutine的执行
13、channel 是否线程安全?锁用在什么地方?
channel本身是线程安全.在Go的并发模型中,
channel用于不同Goroutine之间数据通信,
当一个goroutine向channel发送数据时,直
到另一个goroutine接收这个数据之前,该
goroutine将会被一直阻塞.这种机制保证了
channel的数据在Goroutine间传递时的安全
性.在对buf中的数据进行入队和出队操作时,为
当前channel使用了互斥锁,防止多个线程并发
修改数据
14、go channel 的底层实现原理 (数据结构)
type hchan struct {
//channel 中元素个数
qcount uint
//channel 缓冲区的大小
dataqsiz uint
//channel 缓冲区数据指针
buf unsafe.Pointer
//channel 发送操作处理到的位置
sendx uint
//channel 接收操作处理到的位置
recvx uint
//channel 中操作的元素大小
elemsize uint16
//channel 中操作的元素类型
elemtype \*_type
// 是否已close
closed uint32
//缓冲区不足而阻塞等待接受数据的
//goroutine队列
recvq waitq
//缓冲区不足而阻塞等待发送数据的
//goroutine队列
sendq waitq
// 互斥锁,保护所有字段
lock mutex
}
1.dataqsiz(缓冲区大小)对于无缓冲的
channel,dataqsiz字段的值为0,表示没有分
配数据缓冲区,数据的发送和接收是直接进行的,
需要发送和接收Goroutine同时准备好,否则会
阻塞.对于有缓冲的channel,dataqsiz字段的
值表示分配的数据缓冲区的大小.缓冲区的大小
决定了channel可以存储的元素数量.当缓冲区
填满时,继续向channel发送数据会阻塞发送
Goroutine,当缓冲区为空时,尝试从channel
接收数据会阻塞接收 Goroutine。
2.buf(缓存区指针)存储channel实际数据的
缓冲区.对于无缓冲的channel,buf字段为nil,
表示没有分配缓冲区,数据发送和接收操作是直
接阻塞的.对于有缓冲的channel,buf字段指向
分配的内存地址,用于存储元素值
3.recvq(接收队列)用于存储等待接收数据的
Goroutine队列.当一个Goroutine尝试从空
channel接收数据时,它会被放置在recvq中等
待其他Goroutine向channel发送数据.一旦
有数据可被接收,被阻塞的Goroutine就会从
recvq中被唤醒,并继续执行接收动作
4.sendq(发送队列)用于存储等待发送数据的
Goroutine队列.当一个Goroutine尝试向已
满的channel发送数据时,它会被放置在sendq
中等待其他Goroutine从channel接收数据.
一旦channel有足够的空间可以接收数据,被阻
塞的Goroutine就会从sendq中被唤醒,并继续
执行发送动作。
5.waitq(等待队列)是一个用于存储等待在
channel上进行发送或接收操作的Goroutine
队列.当一个Goroutine尝试向已满的channel
发送数据或从空的channel接收数据时,它会被
放置在waitq中等待其他Goroutine执行相反
的操作,从而使发送或接收操作能够进行.一旦
有对应的发送或接收操作完成,等待的
Goroutine就会从waitq中被唤醒,并继续执行
15、 nil、关闭的 channel 再进行读、写、关闭会怎么样
1.nil channel:未初始化channel,未经make
2.closed channel:执行了closed的channel
3.对nil channel的读写会永久block
4.关闭nil channel,发生panic
5.向closed channel写入会发生panic
6.从closed channel读取仍然可以读取剩余的
数据,直到数据全部读取完成立即读出零值
16、channel的主要用途?
1.向通道(channel)发送数据和从通道读取数据
是Go语言实现并发通信的重要机制。
2.通道阻塞的机制确保了goroutine之间的同步
和通信
3.同步机制,数据共享,并发模式简化,信号通知,
流程控制,优雅地处理结束场景
17、map 使用注意的点,并发安全?
1.map是引用类型,如果两个map同时指向一个底
层,那么一个map的变动会影响到另一个map。
2.map的零值是nil,对nil map进行任何添加元
素的操作都会触发运行时错误(panic)因此,使用
前必须使用make先创建map
m := make(map[string]int)。
3.map在遍历时结果顺序可能不一样
4.map进行的所有操作,读取,写入,删除,都是不
安全的
5.map不是并发安全的.并发情况下,对map的读
和写操作需要加锁,否则可能因为并发操导致程
序崩溃.并发环境下使用map,可使用sync包中
的sync.RWMutex读写锁,或者使用sync.Map
6.map哈希函数的选择:在程序启动时,会检测
cpu是否支持aes,如果支持,则使用aes hash
否则使用 memhash
18、map 循环是有序的还是无序的?
for range循环map是无序的,因为map扩容⽽
重新哈希时,各键值项存储位置都可能会发生改
变,顺序自然也没法保证了,所以官方为了避免依
赖顺序,直接打乱处理.在进行循环遍历的时候,
生成了一个随机数作为遍历开始的位置,可以for
range循环map取出所有的key,sort.Strings
(keys)排序所有的keys再循环所有的keys.
19、 map 中删除一个 key,它的内存会释放么?
1.如果删除的元素是值类型,如int,float,
bool,string以及数组和struct,map的内
存不会自动释放
2.如果删除的元素是引用类型,如指针,slice,
map,chan等,map的内存会自动释放,但释放的
内存是子元素应用类型的内存占用
3.将map设置为nil后,内存被回收
20、怎么处理对 map 进行并发访问?有没有其他方案?
1.对整个map加上读写锁sync.RWMutex虽然
解决问题但是锁粒度大,影响性能
2.1操作会导致整个map被锁住,导致性能降低
所以提出了分片思想,将一个map分成几个片,
按片加锁。第三方包实现:
github.com/orcaman/concurrent-map
3.标准库中的sync.Map是专为append-only
场景设计的。sync.Map在读多写少性能比较好
否则并发性能很差。
21、 nil map 和空 map 有何不同?
1.初始化状态:
Nil Map:没有初始化分配内存的map,默认值为
nil `var m map[keyType]valueType`
Empty Map:已分配内存但没有任何键值对的map
`m := make(map[keyType]valueType)`
2.写入操作:
Nil Map:不能向nil map写入键值对.否则会
导致运行时错误(panic)
Empty Map:可以向空map中安全地写入键值对.
3.读取操作:
Nil Map 和 Empty Map:都可以安全地进行
读取操作.如果键不存在,都会返回值类型的零值
4.长度:
Nil Map 和 Empty Map:两者的长度都是 0
22、map 的数据结构是什么?是怎么实现扩容?
type hmap struct {
// 元素数量
count int
//状态标识,比如被写,被遍历等
flags uint8
//桶(bmap)数量-2^B个
B uint8
//溢出的bucket个数
noverflow uint16
//哈希种子,增加哈希函数的随机性
hash0 uint32
//指向数组buckets的指针
buckets unsafe.Pointer
//扩容时保存原buckets的指针
oldbuckets unsafe.Pointer
//表示扩容进度-已迁移的个数
nevacuate uintptr
extra \*mapextra
}
1.触发扩容有两种情况:
第一种:负载因子超过了默认值6.5,就是正常元素
过多造成的扩容-增量扩容
负载因子=元素数量除桶数量
第二种:overflow的bucket数量过多,就是因为
元素不断的进行增删造成溢出桶很多元素很少,没
有满足负载因子的默认值,但效率很低--等量扩容
2.golang通过拉链发解决哈希冲突(开放寻址法)
3.map扩容是渐进式的,即整个扩容过程拆散在每
一次的写操作里面,这样的好处是保证每一次map
的读写操作时间复杂度都是稳定的。
23、map 取一个 key,然后修改这个值,原 map 数据的值会不会变化
在Go语言中,map为引用类型,所以在修改map内
的值时,原数据也会随之变化,因此当你通过一个
key 取出一个值并对其进行修改时,将会影响到
原map 中该key 对应的值。
24、什么是 GMP?(必问)调度过程是什么样的?
1.G - Goroutine也就是协程,是用户态
轻量级线程,一个goroutine大概需要2k
内存,在 64 位的机器上,可以轻松创建
几百万goroutine
2.M - Machine Thread,也就是操作系
统线程,go runtime 最多允许创建1万个
操作系统线程,超过了就会抛出异常
3.P - Processor逻辑处理器,默认数量
等于cpu核心数,通过环境变量GOMAXPROCS
改变其数量
4.全局队列(GlobalQueue)存放等待运行
的G,P还有个本地队列(也称为LRQ):存放等
待运行的G,但G的数量不超过256个。新建的
goroutine优先保存在P的本地队列中,如果
P的本地队列已满,则会保存到全局队列中
5.P包含了运行G所需要的资源,M想要运行
goroutine必须先获取P然后从P的本地队列
获取Goroutine,P队列为空的时候,M也会
尝试从全局队列拿一批Goroutine放到P的
本地队列,或者从其他P的本地队列偷取一半
放到自己P的本地队列,M不断通过P获取
Goroutine并执行,不断重复下去。如果M
阻塞或者不够了,会创建新的M来支持。比如
所有的M都被阻塞住了,而P中还有很多的就
绪任务,调度器就会去寻找空闲的M。找不
到的话,就会去创建新的M,总而言之一个M
阻塞P就会去创建或者切换另一个M,
25、进程、线程、协程有什么区别?(必问)
1.进程:是应用程序的启动实例,每个进程
都有独立的内存空间,不同的进程通过管道,
信号量,共享内存等方式来通信。
2.线程:从属于进程,每个进程至少包含一
个线程,线程是CPU调度的基本单位,多线程
之间可以共享进程的资源,并通过共享内存
等方式来通信。
3.协程:轻量级线程,与线程相比,协程不受
操作系统的调度,协程的调度器由用户程序
提供,协程调度器按照调度策略把协程调度
到线程中运行
26、抢占式调度是如何抢占的?
操作系统负责线程的调度,Go的runtime负
责goroutine的调度.现代操作系统调度线
程都是抢占式,不依赖代码主动让出CPU,或
者因为IO,锁等待而让出,这样会造成调度的
不公平.基于经典时间片算法,当线程的时间
片用完之后,会被时钟中断,调度器会将当前
线程的执行上下文进行保存,然后恢复下一
个线程的上下文,分配新的时间片令其开始
执行.这种抢占对于线程本身是无感知的,系
统底层支持,不需要开发人员特殊处理.基于
时间片的抢占式调度有个明显的优点,能够
避免CPU资源持续被少数线程占用,从而使其
他线程长时间处于饥饿状态.goroutine的
调度器也用到了时间片算法.只是整个Go程序
都是运行在用户态的,所以不能像操作系统那
样利用时钟中断来打断运行中的goroutine.
也得益于完全在用户态实现,goroutine的调
度切换更加轻量。
27、怎么控制并发数?如何优雅的实现一个 goroutine 池
1.信号量(Semaphore),通道(Channel),
上下文(Context)
2.协程池的实现:
1>定义池结构:需要定义一个结构体来表示
goroutine池,该结构体通常会包含一个任
务队列(用于存储待执行的任务)和一个信号
量(用于控制同时运行的goroutine数量).
2>定义任务类型:通常是一个函数签名,例如
type Task func().
3>创建池:实现一个函数来创建新的
goroutine池,包括初始化任务队列和启动
一定数量的goroutine来处理任务.
4>任务分发:编写逻辑来接收新任务并将它
们加入任务队列.
5>执行任务:goroutine从队列中获取任
务并执行.
6>优雅关闭:提供一种方式来优雅地关闭
goroutine池,等待所有正在执行的任务
完成后再退出.
3.go-playground/pool, ants(推荐)
29、Go 如何实现原子操作?
1.原子操作就是不可中断的操作.原子操作执行时,
CPU绝对不会再去执行其他针对该值的操作
2.包sync/atomic提供了原子操作(Load读取
Store写入)
原子操作与互斥锁的区别
1)互斥锁是一种数据结构,用来让一个线程执行
程序的关键部分,完成互斥的多个操作。
2)原子操作是针对某个值的单个互斥操作。
30、悲观锁、乐观锁是什么?
1.悲观锁:当要对数据库中的一条数据进行修改
时,为了避免同时被其他人修改,直接对该数据进
行加锁以防止并发.行锁,表锁等,读锁,写锁等,
都是在做操作之前先上锁.适合多写场景.Mutex
2.乐观锁:认为更新数据不会造成冲突,但更新的
时候会检测数据是否被修改,可以使用版本号机制
和CAS算法实现.乐观锁适用于读多写少的场景,
可以提高程序的吞吐量
31、Mutex 有几种模式?
1.正常模式:sync.Mutex的默认模式.等待获
取锁的goroutines会形成一个队列.当锁释放
时,队列中的第一个goroutine不一定获得锁,
其他新到的goroutines也有可能先获得锁.
这种情况可能导致某些goroutines长时间得
不到锁产生"饥饿"现象
2.饥饿模式
在饥饿模式下Mutex的拥有者将直接把锁交给队
列第一个goroutine,新来的goroutine不会参
与获取锁,它会加入到等待队列的尾部.
3.下面情况,会把这个Mutex转换成正常模式:
(1)当一个goroutine等待锁时间小于1毫秒.
(2)当前队列只剩下一个goroutine的时候.
32、goroutine 的自旋
协程加锁时,如果当前mutex的state字段的
locked位为1,说明已有其他协程持有该锁,尝
试加锁的协程并不是马上转入阻塞,而是持续探
测locked位是否变为0,
即为自旋.自旋的条件如下:
1)还没自旋超过 4 次,
2)多核处理器,
3)GOMAXPROCS > 1,
4)p 上本地 goroutine 队列为空。
mutex会让当前的goroutine去空转CPU,在空转
完后再次调用CAS方法去尝试性的占有锁资源,直
到不满足自旋条件,则最终会加入到等待队列里。
33、go三色标记法(必问)
1.起初所有的对象都是白色的;
2.从根对象出发扫描所有可达对象,标记为灰色,
放入待处理队列;
3.从待处理队列中取出灰色对象,将其引用的对
象标记为灰色并放入待处理队列中,自身标记为
黑色;
4.重复步骤(3),直到待处理队列为空,此时
白色对象即为不可达的“垃圾”,回收白色对象;
5.屏障机制-STW垃圾回收过程中为了保证准确
性防止无止境的内存增长等问题,需要停止赋值
器进一步操作对象图以完成垃圾回收。STW时间
越长,对用户代码造成的影响越大
34、知道哪些 sync 同步原语?各有什么作用?
1.sync.Mutex
它允许在共享资源上互斥访问(不能同时访问)
2.sync.RWMutex
是一个读写互斥锁
3.sync.WaitGroup
sync.WaitGroup拥有一个内部计数器.当计数
器等于0时,则Wait()方法会立即返回.否则它
将阻塞执行Wait()方法的goroutine直到计数
器等于0时为止.要增加计数器,使用Add().使
用Done()减少,也可以传递负数给Add方法把计
数器减少指定大小,Done()方法底层就是通过
Add(-1)实现的.
4.sync.Map
是并发的map.Store-添加元素.Load-检索元素
Delete-删除元素.LoadOrStore检索或添加之
前不存在的元素.如果键之前在map中存在,则返
回的布尔值为true.使用Range遍历元素.
5.sync.Pool
是一个并发池,负责安全地保存一组对象.Get()
用来从并发池中取出元素.Put将一个对象加入并
发池。
6.sync.Once
可确保一个函数仅执行一次
7.sync.Cond
它用于发出信号(一对一)或广播信号(一对多)到
goroutine
35、介绍golang的内存, 什么情况下会发生内存逃逸?
1)本该分配到栈上的变量,跑到了堆上,这就导
致了内存逃逸。
2)栈上的变量,函数结束后会被回收掉,不会有
额外性能的开销.堆上变量的回收,需要进行gc,
gc存在性能开销.变量逃逸会导致性能开销变大.
内存逃逸的情况如下:
1)方法内返回局部变量指针。
2)在闭包中引用包外的值
3)向 channel 发送指针数据。
4)在 slice 或 map 中存储指针。
5)切片(扩容后)长度太大。
36、K8s 含有哪些重要组成部分
1.Master节点:
API Server(API服务器):提供了Kubernetes
API,用于与Kubernetes集群进行通信,
接受和处理各种命令。
Controller Manager(控制器管理器):负责运
行控制器(如ReplicaSet Controller、
DeploymentController),监控集群状态
并根据所需状态进行调整。
Scheduler(调度器):负责将新创建的Pod调度
到集群中的Node上,考虑资源需求,约束条
件等因素。
2.Node节点:
Kubelet:负责在Node上管理容器,包括启动、
停止、重启容器等。
Container Runtime(容器运行时):实际运行
容器的软件,如Docker,containerd等。
Kube Proxy:负责维护网络规则并将网络流量
转发到正确的容器服务。
3.Etcd:
分布式键值存储,用于保存集群的配置信息,状
态信息等,被Master和Node节点共同使用。
4.Pod:
最小部署单元,包含一个或多个容器.Pod中的容
器共享网络和存储,通常是紧密耦合的应用组件
5.Service:
提供了一种抽象,用于定义一组Pod的访问方式.
Service可以保证一组Pod的稳定网络访问。
6.Volume:
用于在Pod中持久化数据.Volume可以连接到
Node上的物理存储、网络存储等。
7.Namespace:
用于将Kubernetes集群划分为多个虚拟集群,
以便在同一集群中运行多个不同的应用,环境等
8.ConfigMap 和 Secret:
用于保存配置信息和敏感数据,可以在Pod中被
挂载为文件或环境变量。
9.Ingress:
提供HTTP和HTTPS路由到服务的规则,允许外部
流量访问Kubernetes集群中的服务。
10.StatefulSet:
用于部署有状态应用,确保Pod有唯一的标识和
稳定的网络标识标题
37、
38、内存泄露和解决方法
内存泄漏是由于程序中的数据结构不再需要时未
能被垃圾回收器(GC)回收造成的
1.全局变量的过度使用:
全局变量会一直占用内存除非显式地将它们设
置为nil
解决方法:避免不必要的全局变量或在不再需要
时将其设置为 nil
2.协程泄漏:
未正确结束的协程可能导致内存泄漏,特别是那
些无限循环或阻塞等待的协程.
解决方法:确保协程能正确退出,使用context
来管理和取消协程.
3.未关闭的通道和其他资源:
未关闭的通道,文件句柄,数据库连接等资源
可能导致内存泄漏.
解决方法:使用defer关键字确保资源被释放
4.循环引用:
特别是在使用指针和接口时,循环引用可能导
致GC无法回收相关对象.
解决方法:避免创建循环引用,如果必须使用,
考虑使用弱引用。
5.大量未释放的临时对象:
频繁创建且长时间不释放的临时对象可能导
致内存泄漏.
解决方法:优化代码逻辑,避免不必要的临时
对象创建,或及时释放不再需要的对象.
6.内部数据结构过大:
巨大的map或slice在使用后未被缩减或清理。
解决方法:在不再需要大量数据时,及时清理或
重新分配这些数据结构.
7.缓存未及时清理:
长期运行的缓存如果没有适当的过期策略,可
能会不断增长.
解决方法:实现缓存清理策略,例如使用过期
时间或大小限制.
解决内存泄漏的通用步骤包括:
1.性能监控:定期监控应用程序的内存使用情况。
分析和调试:使用pprof工具进行内存分析,找出
内存占用异常的部分.
2.代码审查:定期审查代码,找出潜在的内存泄漏
39、CAS算法
CAS(Compare-And-Swap)算法:是重要的并发
编程算法,用于实现无锁编程和原子操作.是实现
多线程同步的关键技术之一,尤其是在构建锁和其
他并发控制结构时.CAS操作包含三个主要参数:
1.内存位置(Memory Location):需要更新的
变量的内存地址.
2.预期原值(Expected Value):变量预期的值
3.新值(New Value):希望设置的新值。
CAS 的基本工作原理如下:
它首先检查目标内存位置的当前值是否与预期原
值相同.若相同,CAS会自动将该位置的值更新为
新值若不同,说明在检查和操作之间有其他线程
修改了该变量,CAS操作失败.CAS操作是原子性
的,意味着在执行过程中不会被其他线程中断。
CAS的优点是避免了使用传统锁所带来的开销和
潜在的死锁问题,因为它不会阻塞线程.但是CAS
可能出现活锁问题和ABA问题:
1.活锁:线程不断尝试更新操作,但总是因为其他
线程的干扰而失败,导致无限循环.
2.ABA问题:一个值原先是A,被另一个线程改为B,
然后又改回A,CAS会错误地认为该值没有被更改过
40、协程池
1.在文生图项目中,处理从redis中循环出来的数据,使用了字节开源的
协程池库
1.资源复用,控制并发数量,避免内存泄漏,任务调度和均衡
41、 MySQL 中的 B 树和 B+ 树区别
1.存储方式: B树和B+树的存储方式不同.在B
树中,每个节点存储键和对应的值,而在B+树中,
只有叶子节点存储键和指向数据的指针,非叶子
节点只存储键.这意味着B+树的内部节点可以容
纳更多的键,减少了树的高度,从而减少了磁盘访
问次数,提高了查询性能
2.叶子节点结构:B树和B+树的叶子节点结构也
不同.在B树中,叶子节点包含了数据的实际值,
而在B+树中,叶子节点只包含键和指向数据的
指针.这使得B+树的叶子节点可以形成一个有
序链表,通过在链表上进行顺序遍历,可以高效
地获取范围内的数据.而B树则需要在不同的层
级进行跳跃,性能相对较低
4.适用场景:B树适用于需要随机访问的场景,
例如数据库索引.而B+树更适合范围查询和顺
序访问的场景,例如文件系统索引
42、Mysql 常见sql优化
1、在where及order by涉及的列上建立索引。
2、不要在where子句中进行的操作:
1>使用!=或<>操作符,
2>对字段进行null值判断
3>使用or来连接条件
4>使用参数
5>对字段进行表达式操作,函数操作
6>对“=”左边进行函数、算术运算或其他表达式运算
7>in和not in的使用,用between或exists代替in
导致索引失效,扫描全表
3、like '%abc%' 导致索引失效
4、不要使用 select * from t
6、不要在有大量数据重复的列上建立索引
7、一个表的索引数不要超过6个,索引提高
select 的效率,同时也降低了insert 及
update 的效率,因为可能会重建索引
8、使用varchar代替char,因为变长字段存
储空间小,可以节省存储空间,其次对于查询
来说,在一个相对较小的字段内搜索效率
显然要高些。
9、在使用索引字段作为条件时,如果该索引是
复合索引,那么必须使用到该索引中的第一个字
段作为条件时才能保证系统使用该索引,否则该
索引将不会被使用,并且应尽可能的让字段顺序
与索引顺序相一致。
10、尽量使用数字型字段,若只含数值信息的
字段尽量不要设计为字符型
11、尽量使用表变量来代替临时表。如果表变量
包含大量数据,请注意索引非常有限(只有主键索引)。
12、避免频繁创建和删除临时表,以减少系统
表资源的消耗。
13、临时表并不是不可使用,适当地使用它们
可以使某些例程更有效,例如,当需要重复引
用大型表或常用表中的某个数据集时。但是,
对于一次性事件,最好使用导出表。
14、在新建临时表时,如果一次性插入数据量
很大,那么可以使用 select into 代替
create table,避免造成大量 log ,以
提高速度;如果数据量不大,为了缓和系统表
的资源,应先create table,然后insert。
15、如果使用到了临时表,在存储过程的最后
务必将所有的临时表显式删除,先 truncate
table ,然后 drop table ,这样可以避免
系统表的较长时间锁定。
16、尽量避免使用游标,因为游标的效率较差,
如果游标操作的数据超过1万行,那么就应该考
虑改写。
43、mysql 事务实现原理
通过日志的记录和重做、回滚日志的使用、锁的
管理和控制,并发控制机制的应用实现。这些机
制共同确保了事务的原子性、一致性、隔离性和
持久性,保证了数据库的数据完整性和并发操作
的正确性
1. 日志(Log):
重做日志(Redo Log)**: 在事务进行修改操
作时,MySQL 将修改的内容记录到重做日志中,
以确保事务的修改可以持久化到磁盘。重做日志
是顺序写入的,因此具有较高的写入性能。
回滚日志(Undo Log)**: 在事务进行修改操
作之前,MySQL 将当前数据的副本记录到回滚日
志中。如果事务需要回滚,MySQL 可以使用回滚
日志将数据恢复到事务开始之前的状态。
2. 锁(Lock):
行级锁(Row-Level Locking)**: MySQL
支持行级锁,允许在事务中对数据行进行加锁。
这样可以控制并发事务对同一数据的访问,确保
数据的一致性和隔离性。
锁粒度(Lock Granularity)**: MySQL
的锁粒度可以根据具体情况调整,包括表级锁、
页级锁和行级锁。较小的锁粒度可以提高并发
性能,但也会增加锁管理的开销。
3. 隔离级别(Isolation Level)**:
MySQL 支持多个隔离级别,例如:
读未提交(Read Uncommitted)、
读已提交(Read Committed)、
可重复读(Repeatable Read)
串行化(Serializable)。
隔离级别定义了事务之间的隔离程度,
决定了事务读取数据的一致性和并发性。
4. 并发控制(Concurrency Control)**:
MySQL 使用并发控制机制来处理多个并发
事务对数据库的访问。这包括锁的获取与释
放、冲突检测和解决,以及事务的调度和执行。
44、mysql中 explain工具的使用
type 字段可能的值
ALL < index < range ~ index_merge < ref < eq_ref < const < system
45、Go 的 select 特性
1.select提供了多路IO复用机制,用于检测
是否有读写事件ready.select结构组成是
case语句和default语句和对应的执行函数
2.case中的表达式必须是Channel收发操作
3.多个case同时被触发,会随机执行其中一个
4.select同时监听多个case的channel是
否可执行,如果都不能执行,则运行default
5.要判断一个channel是否写满数据或为空,
可以使用select语句结合default分支.当
channel满或者为空时,执行default分支,
否则执行对应的case分支
46、单引号,双引号,反引号的区别?
1.单引号:表示rune类型,对应int32类型
2.双引号:是字符串,实际上是字符数组。
3.反引号:表示字符串字面量,但不支持任何转义
序列
47、redis 常见的数据类型
1、string(字符串);
2、hash(哈希);
3、list(列表);
4、set(集合);
5、sort set (有序集合)
48、Redis 的pipeline
管道(pipeline)可以一次性发送多条命令给
服务端,处理完毕后,再通过一条响应一次性将
结果返回,pipeline通过减少客户端与服务端
的通信次数来降低网络往返时延,减少read和
write的系统调用以及进程上下文切换次数,以
提升程序的执行效率与性能,Pipeline实现的
原理是队列,而队列原理是先进先出,这样就保
证数据的顺序性
49、 Redis 的雪崩,击穿,穿透
1.缓存雪崩:大量缓存数据在同一时间过期或者
Redis故障宕机时,如果此时有大量的用户请求,
都无法在Redis中处理,于是全部请求都直接访
问数据库,从而导致数据库的压力骤增,严重的会
造成数据库宕机,从而形成一系列连锁反应,造成
整个系统崩溃
2.缓存击穿:如果缓存中的某个热点数据过期了,
此时大量的请求访问了该热点数据,就无法从缓
存中读取,直接访问数据库,数据库很容易就被高
并发的请求冲垮
3.缓存穿透:当用户访问的数据,既不在缓存中,
也不在数据库中,导致请求在访问缓存时,发现
缓存缺失,再去访问数据库时,发现数据库中也
没有要访问的数据,没办法构建缓存数据,来服
务后续的请求.那么当有大量这样的请求到来时,
数据库的压力骤增
50、基于Redis实现分布式锁
SET lock_key unique_value NX PX 10000
1.lock_key 就是 key 键;
2.unique_value 是客户端生成的唯一的标识,
区分来自不同客户端的锁操作;
3.NX 代表lock_key不存在时,对lock_key设置
才能成功(返回1,失败返回0)
4.PX 10000表示设置lock_key过期时间为10s
避免客户端发生异常而无法释放锁。
51、Redis 获取指定前缀
1.keys feng*该命令会阻塞redis执行其他
命令,禁止生产使用,因为它采用的是遍历的形
式,而且redis是单线程的,顺序执行指令,当
查找的key的量特别多的时候,会一直在查找,
其他的命令就无法执行,导致阻塞或者超时报
错等
2.scan 0 match feng* count 10
游标从0开始扫描,匹配以feng开头的key,
遍历槽位10个.返回下一次遍历的游标开始
值,如果最后结果返回0,表示遍历完了
52、Redis对于多数据库的支持
Redis支持多数据库,在Redis中数据库的概念
是通过数据库索引(Database Index)来实现
的.默认情况下,Redis有16个数据库,索引从0
到15.通过SELECT命令来切换当前使用的数据库
53、Innodb与Myisam引擎的区别
1.InnoDB 支持事务,外键 MyISAM 不支持
2.innodb使用聚簇索引存储数据,表数据和索引
数据就存储在一个索引树结构中.查询数据时直接
通过索引就可以找到数据行。
myisam采用非聚簇索引存储数据,表数据和索引
数据分离存储.索引中保存的是数据在磁盘上的位
置,即指针.聚簇索引的叶子节点就是数据节点,而
非聚簇索引的叶子节点是指向对应数据块的指针
3.InnoDB 不保存表的具体行数,MyISAM保存了
整个表的行数
4.InnoDB 最小的锁粒度是行锁,
MyISAM是表锁
54、什么时候触发行级锁,表级锁
1.执行DDL语句去修改表结构时,会使用表级锁。
alter table,drop table,trunchte table
或者对应的SQL就没有使用索引,没指定查询列
2.当需要修改或读取表中某一行数据,当增删改查
匹配到索引时,Innodb会使用行级锁
55、mysql 中JOIN操作类型
1.INNER JOIN:它返回两个表中匹配的行.如
果在一个表中没有匹配的行,那么这些行就不会
出现在结果集中
2.LEFT JOIN:返回左表中的所有行,以及右表
中与左表中匹配的行.如果在右表中没有匹配的
行,结果集中将包含NULL值
![img](https://img-blog.csdnimg.cn/img_convert/eaf1b18a4f12b2f8aed1ab3e20a81210.png)
![img](https://img-blog.csdnimg.cn/img_convert/54124e741c787d0ea1e212c4a3d354ec.png)
![img](https://img-blog.csdnimg.cn/img_convert/dbb4d29d34a30ef1ed78fc656ac55bc4.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
指令,当
查找的key的量特别多的时候,会一直在查找,
其他的命令就无法执行,导致阻塞或者超时报
错等
2.scan 0 match feng* count 10
游标从0开始扫描,匹配以feng开头的key,
遍历槽位10个.返回下一次遍历的游标开始
值,如果最后结果返回0,表示遍历完了
52、Redis对于多数据库的支持
Redis支持多数据库,在Redis中数据库的概念
是通过数据库索引(Database Index)来实现
的.默认情况下,Redis有16个数据库,索引从0
到15.通过SELECT命令来切换当前使用的数据库
53、Innodb与Myisam引擎的区别
1.InnoDB 支持事务,外键 MyISAM 不支持
2.innodb使用聚簇索引存储数据,表数据和索引
数据就存储在一个索引树结构中.查询数据时直接
通过索引就可以找到数据行。
myisam采用非聚簇索引存储数据,表数据和索引
数据分离存储.索引中保存的是数据在磁盘上的位
置,即指针.聚簇索引的叶子节点就是数据节点,而
非聚簇索引的叶子节点是指向对应数据块的指针
3.InnoDB 不保存表的具体行数,MyISAM保存了
整个表的行数
4.InnoDB 最小的锁粒度是行锁,
MyISAM是表锁
54、什么时候触发行级锁,表级锁
1.执行DDL语句去修改表结构时,会使用表级锁。
alter table,drop table,trunchte table
或者对应的SQL就没有使用索引,没指定查询列
2.当需要修改或读取表中某一行数据,当增删改查
匹配到索引时,Innodb会使用行级锁
55、mysql 中JOIN操作类型
1.INNER JOIN:它返回两个表中匹配的行.如
果在一个表中没有匹配的行,那么这些行就不会
出现在结果集中
2.LEFT JOIN:返回左表中的所有行,以及右表
中与左表中匹配的行.如果在右表中没有匹配的
行,结果集中将包含NULL值
[外链图片转存中...(img-rpvegcOQ-1725475068916)]
[外链图片转存中...(img-0V8Ecqj6-1725475068917)]
[外链图片转存中...(img-ygyECWRY-1725475068917)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**