├── Go基础 ├── Go代码输出.md ├── Go基础应用.md ├── Go基础类.md ├── Go基础语法.md ├── Go基础语法2.md └── Go实现原理.md ├── Go并发 ├── GO并发sync.md ├── Go GC.md └── Go并发基础.md ├── LICENSE ├── MySQL ├── MySQL常问.md ├── 基础、锁、事务、分库分表、优化.md └── 索引连环18问.md ├── README.md ├── Redis └── Redis.md ├── 操作系统 └── 操作系统.md └── 计算机网络 ├── 网络总结-小林coding.md ├── 计算机网络上.md └── 计算机网络下.md /Go基础/Go代码输出.md: -------------------------------------------------------------------------------- 1 | ## 常量与变量 2 | 3 | 1. 下列代码的输出是: 4 | 5 | ``` 6 | func main() { 7 | const ( 8 | a, b = "golang", 100 9 | d, e 10 | f bool = true 11 | g 12 | ) 13 | fmt.Println(d, e, g) 14 | } 15 | ``` 16 | 17 |
答案

golang 100 true

在同一个 const group 中,如果常量定义与前一行的定义一致,则可以省略类型和值。编译时,会按照前一行的定义自动补全。即等价于

func main() {
const (
a, b = "golang", 100
d, e = "golang", 100
f bool = true
g bool = true
)
fmt.Println(d, e, g)
}
18 | 19 | 1. 下列代码的输出是: 20 | 21 | ``` 22 | func main() { 23 | const N = 100 24 | var x int = N 25 | 26 | const M int32 = 100 27 | var y int = M 28 | fmt.Println(x, y) 29 | } 30 | ``` 31 | 32 |
答案

编译失败:cannot use M (type int32) as type int in assignment

Go 语言中,常量分为无类型常量和有类型常量两种,const N = 100,属于无类型常量,赋值给其他变量时,如果字面量能够转换为对应类型的变量,则赋值成功,例如,var x int = N。但是对于有类型的常量 const M int32 = 100,赋值给其他变量时,需要类型匹配才能成功,所以显示地类型转换:

var y int = int(M)
33 | 34 | 1. 下列代码的输出是: 35 | 36 | ``` 37 | func main() { 38 | var a int8 = -1 39 | var b int8 = -128 / a 40 | fmt.Println(b) 41 | } 42 | ``` 43 | 44 |
答案

-128

int8 能表示的数字的范围是 [-2^7, 2^7-1],即 [-128, 127]。-128 是无类型常量,转换为 int8,再除以变量 -1,结果为 128,常量除以变量,结果是一个变量。变量转换时允许溢出,符号位变为1,转为补码后恰好等于 -128。

对于有符号整型,最高位是是符号位,计算机用补码表示负数。补码 = 原码取反加一。

例如:

-1 :  11111111
00000001(原码) 11111110(取反) 11111111(加一)
-128:
10000000(原码) 01111111(取反) 10000000(加一)

-1 + 1 = 0
11111111 + 00000001 = 00000000(最高位溢出省略)
-128 + 127 = -1
10000000 + 01111111 = 11111111
45 | 46 | 1. 下列代码的输出是: 47 | 48 | ``` 49 | func main() { 50 | const a int8 = -1 51 | var b int8 = -128 / a 52 | fmt.Println(b) 53 | } 54 | ``` 55 | 56 |
答案

编译失败:constant 128 overflows int8

-128 和 a 都是常量,在编译时求值,-128 / a = 128,两个常量相除,结果也是一个常量,常量类型转换时不允许溢出,因而编译失败。

57 | 58 | ## 作用域 59 | 60 | 1. 下列代码的输出是: 61 | 62 | ``` 63 | func main() { 64 | var err error 65 | if err == nil { 66 | err := fmt.Errorf("err") 67 | fmt.Println(1, err) 68 | } 69 | if err != nil { 70 | fmt.Println(2, err) 71 | } 72 | } 73 | ``` 74 | 75 |
答案

1 err

:= 表示声明并赋值,= 表示仅赋值。

变量的作用域是大括号,因此在第一个 if 语句 if err == nil 内部重新声明且赋值了与外部变量同名的局部变量 err。对该局部变量的赋值不会影响到外部的 err。因此第二个 if 语句 if err != nil 不成立。所以只打印了 1 err

76 | 77 | ## defer 延迟调用 78 | 79 | 1. 下列代码的输出是: 80 | 81 | ``` 82 | type T struct{} 83 | 84 | func (t T) f(n int) T { 85 | fmt.Print(n) 86 | return t 87 | } 88 | 89 | func main() { 90 | var t T 91 | defer t.f(1).f(2) 92 | fmt.Print(3) 93 | } 94 | ``` 95 | 96 |
答案

132

defer 延迟调用时,需要保存函数指针和参数,因此链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行。也就是说 t.f(1) 直接执行,然后执行 fmt.Print(3),最后函数返回时再执行 .f(2),因此输出是 132。

97 | 98 | 1. 下列代码的输出是: 99 | 100 | ``` 101 | func f(n int) { 102 | defer fmt.Println(n) 103 | n += 100 104 | } 105 | 106 | func main() { 107 | f(1) 108 | } 109 | ``` 110 | 111 |
答案

1

打印 1 而不是 101。defer 语句执行时,会将需要延迟调用的函数和参数保存起来,也就是说,执行到 defer 时,参数 n(此时等于1) 已经被保存了。因此后面对 n 的改动并不会影响延迟函数调用的结果。

112 | 113 | 1. 下列代码的输出是: 114 | 115 | ``` 116 | func main() { 117 | n := 1 118 | defer func() { 119 | fmt.Println(n) 120 | }() 121 | n += 100 122 | } 123 | ``` 124 | 125 |
答案

101

匿名函数没有通过传参的方式将 n 传入,因此匿名函数内的 n 和函数外部的 n 是同一个,延迟执行时,已经被改变为 101。

126 | 127 | 1. 下列代码的输出是: 128 | 129 | ``` 130 | func main() { 131 | n := 1 132 | if n == 1 { 133 | defer fmt.Println(n) 134 | n += 100 135 | } 136 | fmt.Println(n) 137 | } 138 | ``` 139 | 140 |
答案
101
1

先打印 101,再打印 1。defer 的作用域是函数,而不是代码块,因此 if 语句退出时,defer 不会执行,而是等 101 打印后,整个函数返回时,才会执行。

141 | 142 | ------ 143 | 144 | 参考:[Go 语言笔试面试题(代码输出) | 极客面试 | 极客兔兔 (geektutu.com)](https://geektutu.com/post/qa-golang-c1.html) -------------------------------------------------------------------------------- /Go基础/Go基础应用.md: -------------------------------------------------------------------------------- 1 | # 基础应用 2 | 3 | ### 1.你是如何关闭 HTTP 的响应体的 4 | 5 | 直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体; 6 | 7 | 手动调用 defer 来关闭响应体。 8 | 9 | ```go 10 | // 正确示例 11 | func main() { 12 | resp, err := http.Get("http://www.baidu.com") // 发出请求并返回请求结果 13 | 14 | // 关闭 resp.Body 的正确姿势 15 | if resp != nil { 16 | defer resp.Body.Close() 17 | } 18 | 19 | checkError(err) // 检查错误,省略写法 20 | defer resp.Body.Close() // 手动调用defer来关闭响应体 21 | 22 | body, err := ioutil.ReadAll(resp.Body) // 一次性读写文件的全部数据 23 | checkError(err) 24 | 25 | fmt.Println(string(body)) 26 | } 27 | ``` 28 | 29 | ### 2.你是否主动关闭过http连接,为啥要这样做 30 | 31 | 有关闭,不关闭会程序可能**会消耗完 socket 描述符**。有如下2种关闭方式: 32 | 33 | - 直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。 34 | - 设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接 35 | 36 | ```go 37 | // 主动关闭连接 38 | func main() { 39 | req, err := http.NewRequest("GET", "http://golang.org", nil) 40 | checkError(err) 41 | 42 | req.Close = true // 直接设置请求变量的Close字段值为true,每次请求结束后主动关闭连接 43 | //req.Header.Add("Connection", "close") // 等效的关闭方式 44 | 45 | resp, err := http.DefaultClient.Do(req) 46 | if resp != nil { 47 | defer resp.Body.Close() 48 | } 49 | checkError(err) 50 | 51 | body, err := ioutil.ReadAll(resp.Body) 52 | checkError(err) 53 | 54 | fmt.Println(string(body)) 55 | } 56 | ``` 57 | 58 | 你可以创建一个自定义配置的 HTTP transport(传输) 客户端,用来取消 HTTP 全局的**复用连接**。 59 | 60 | ```go 61 | func main() { 62 | tr := http.Transport{DisableKeepAlives: true} // 自定义配置传输客户端,用来取消HTTP全部的复用连接。 63 | client := http.Client{Transport: &tr} 64 | 65 | resp, err := client.Get("https://golang.google.cn/") 66 | if resp != nil { 67 | defer resp.Body.Close() 68 | } 69 | checkError(err) 70 | 71 | fmt.Println(resp.StatusCode) // 200 72 | 73 | body, err := ioutil.ReadAll(resp.Body) 74 | checkError(err) 75 | 76 | fmt.Println(len(string(body))) 77 | } 78 | ``` 79 | 80 | ### 3.解析 JSON 数据时,默认将数值当做哪种类型 81 | 82 | 在 encode/decode JSON 数据时,Go **默认会将数值当做 float64 处理**。 83 | 84 | ```go 85 | func main() { 86 | var data = []byte(`{"status": 200}`) 87 | var result map[string]interface{} 88 | 89 | if err := json.Unmarshal(data, &result); err != nil { 90 | log.Fatalln(err) 91 | } 92 | } 93 | ``` 94 | 95 | 解析出来的 200 是 float 类型。 96 | 97 | ### 4.说说go语言的beego框架 98 | 99 | - beego 是一个 golang 实现的**轻量级HTTP框架** 100 | - beego 可以**通过注释路由、正则路由等多种方式完成 url 路由注入** 101 | - 可以使用 bee new 工具生成空工程,然后使用 bee run 命令自动热编译 102 | 103 | ### 5.说说go语言的goconvey框架 104 | 105 | - goconvey 是一个**支持 golang 的单元测试框架** 106 | - goconvey **能够自动监控文件修改并启动测试**,并可以将测试结果实时输出到web界面 107 | - goconvey 提供了丰富的断言简化测试用例的编写 108 | 109 | ### 6.GoStub的作用是什么 110 | 111 | GoStub也是一种测试框架: 112 | 113 | - GoStub 可以对全局变量打桩 114 | - GoStub 可以对函数打桩 115 | - GoStub 不可以对类的成员方法打桩 116 | - GoStub 可以打动态桩,比如对一个函数打桩后,多次调用该函数会有不同的行为 117 | 118 | ### 7. JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗 119 | 120 | 首先 JSON 标准库对 nil slice 和 空 slice 的处理是不一致。 121 | 122 | 通常错误的用法,**会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。** 123 | 124 | ```go 125 | var slice []int // nil slice 126 | slice[1] = 0 127 | ``` 128 | 129 | 此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。 130 | 131 | empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下: 132 | 133 | ```go 134 | slice := make([]int,0)// 空slice,没有值,空间也是空的 135 | slice := []int{} 136 | ``` 137 | 138 | 当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。总之,nil slice 和 empty slice是不同的东西,需要我们加以区分的。 139 | 140 | -------------------------------------------------------------------------------- /Go基础/Go基础类.md: -------------------------------------------------------------------------------- 1 | # go基础类 2 | 3 | ### 1. go优势 4 | 5 | ```markdown 6 | * 天生支持并发,性能高 7 | * 单一的标准代码格式,比其它语言更具可读性 8 | * 自动垃圾收集比java和python更有效,因为它与程序同时执行 9 | ``` 10 | 11 | ### 2. go数据类型 12 | 13 | ```go 14 | int string float bool array slice map channel pointer struct interface method 15 | ``` 16 | 17 | ### 3. go程序中的包是什么 18 | 19 | ```go 20 | * 项目中包含go源文件以及其它包的目录,源文件中的函数、变量、类型都存储在该包中 21 | * 每个源文件都属于一个包,该包在文件顶部使用 package packageName 声明 22 | * 我们在源文件中需要导入第三方包时需要使用 import packageName 23 | ``` 24 | 25 | ### 4. go支持什么形式的类型转换?将整数转换为浮点数 26 | 27 | ```css 28 | * go支持显示类型转换,以满足严格的类型要 29 | * a := 15 30 | * b := float64(a) 31 | * fmt.Println(b, reflect.TypeOf(b)) 32 | ``` 33 | 34 | ### 5. 什么是 goroutine,你如何停止它? 35 | 36 | ~~~csharp 37 | * goroutine是协程/轻量级线程/用户态线程,不同于传统的内核态线程 38 | * 占用资源特别少,创建和销毁只在用户态执行不会到内核态,节省时间 39 | * 创建goroutine需要使用go关键字 40 | * 可以向goroutine发送一个信号通道来停止它,goroutine内部需要检查信号通道 41 | 例子: 42 | ``` 43 | func main() { 44 | var wg sync.WaitGroup // 等待组进行多个任务的同步,可以保证并发环境中完成指定数量的任务,每个sync.WaitGroup值在内部维护着一个计数,此计数的初始默认值为0 45 | var exit = make(chan bool) 46 | wg.Add(1) // 等待组的计数器+1 47 | go func() { 48 | for { 49 | select { 50 | case <-exit: // 接收到信号后return退出当前goroutine 51 | fmt.Println("goroutine接收到信号退出了!") 52 | wg.Done() // 等待组的计数器-1 53 | return 54 | default: 55 | fmt.Println("还没有接收到信号") 56 | } 57 | } 58 | }() 59 | exit <- true 60 | wg.Wait() // 当等待组计数器不等于0时阻塞,直到变为0 61 | } 62 | ``` 63 | ~~~ 64 | 65 | ### 6. 如何在运行时检查变量类型 66 | 67 | ```dart 68 | * 类型开关(Type Switch)是在运行时检查变量类型的最佳方式。 69 | * 类型开关按类型而不是值来评估变量。每个 Switch 至少包含一个 case 用作条件语句 70 | * 如果没有一个 case 为真,则执行 default。 71 | ``` 72 | 73 | ### 7. go两个接口之间可以存在什么关系 74 | 75 | ```css 76 | * 如果两个接口有相同的方法列表,那么他俩就是等价的,可以相互赋值 77 | * 接口A可以嵌套到接口B里面,那么接口B就有了自己的方法列表+接口A的方法列表 78 | ``` 79 | 80 | ### 8. go中同步锁(也叫互斥锁)有什么特点,作用是什么?何时使用互斥锁,何时使用读写锁? 81 | 82 | ~~~scss 83 | * 当一个goroutine获得了Mutex(互斥锁)后,其它goroutine就只能乖乖等待,除非该goroutine释放Mutex 84 | * RWMutext(读写互斥锁)在读锁占用的情况下会阻止写,但不会阻止读,在写锁占用的情况下,会阻止任何其它goroutine进来 85 | * 无论是读还是写,整个锁相当于由该goroutine独占 86 | * 作用:保证资源在使用时的独有性,不会因为并发导致数据错乱,保证系统稳定性 87 | * 案例: 88 | ``` 89 | package main 90 | import ( 91 | "fmt" 92 | "sync" 93 | "time" 94 | ) 95 | var ( 96 | num = 0 97 | lock = sync.RWMutex{} // 耗时:100+毫秒 98 | //lock = sync.Mutex{} // 耗时:50+毫秒 99 | ) 100 | func main() { 101 | start := time.Now() 102 | go func() { 103 | for i := 0; i < 100000; i++{ 104 | lock.Lock() 105 | //fmt.Println(num) 106 | num++ 107 | lock.Unlock() 108 | } 109 | }() 110 | for i := 0; i < 100000; i++{ 111 | lock.Lock() 112 | //fmt.Println(num) 113 | num++ 114 | lock.Unlock() 115 | } 116 | fmt.Println(num) 117 | fmt.Println(time.Now().Sub(start)) 118 | } 119 | ``` 120 | // 结论: 121 | // 1. 如果对数据写的比较多,使用Mutex同步锁/互斥锁性能更高 122 | // 2. 如果对数据读的比较多,使用RWMutex读写锁性能更高 123 | ~~~ 124 | 125 | ### 9. goroutine案例(两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh ... yz) 126 | 127 | ```go 128 | package main 129 | import ( 130 | "fmt" 131 | "sync" 132 | "unicode/utf8" 133 | ) 134 | // 案例:两个goroutine,一个负责输出数字,另一个负责输出26个英文字母,格式如下:12ab34cd56ef78gh ... yz 135 | var ( 136 | wg = sync.WaitGroup{} // 和第五题很相关。申明等待组 137 | chNum = make(chan bool) 138 | chAlpha = make(chan bool) 139 | ) 140 | func main() { 141 | go func() { 142 | i := 1 143 | for { 144 | <-chNum // 接到信号,运行该goroutine 145 | fmt.Printf("%v%v", i, i + 1) 146 | i += 2 147 | chAlpha <- true // 发送信号 148 | } 149 | }() 150 | wg.Add(1) // 等待组的计数器+1 151 | go func() { 152 | str := "abcdefghigklmnopqrstuvwxyz" 153 | i := 0 154 | for { 155 | <-chAlpha // 接到信号,运行该goroutine 156 | fmt.Printf("%v", str[i:i+2]) 157 | i += 2 158 | if i >= utf8.RuneCountInString(str){ 159 | wg.Done() // 等待组的计数器-1 160 | return 161 | } 162 | chNum <- true // 发送信号 163 | } 164 | }() 165 | chNum <- true // 发送信号 166 | wg.Wait() // 等待组的计数器不为0时,阻塞main进程,直到等待组的计数器为0 167 | } 168 | ``` 169 | 170 | ### 10. go语言中,channel通道有什么特点,需要注意什么? 171 | 172 | - 结论: 173 | 1. 给一个nil channel发送数据时会一直堵塞 174 | 2. 从一个nil channel接收数据时会一直阻塞 175 | 3. 给一个已关闭的channel发送数据时会panic 176 | 4. 从一个已关闭的channel中读取数据时,如果channel为空,则返回通道中类型的零值 177 | - 案例 178 | 179 | ```go 180 | package main 181 | import ( 182 | "fmt" 183 | "sync" 184 | ) 185 | func main() { 186 | var wg sync.WaitGroup // 等待组 187 | var ch chan int // nil channel 188 | var ch1 = make(chan int) // 创建channel 189 | fmt.Println(ch, ch1) // 0xc000086060 190 | wg.Add(1) // 等待组的计数器+1 191 | go func() { 192 | //ch <- 15 // 如果给一个nil的channel发送数据会造成永久阻塞 193 | //<-ch // 如果从一个nil的channel中接收数据也会造成永久阻塞 194 | ret := <-ch1 195 | fmt.Println(ret) 196 | ret = <-ch1 // 从一个已关闭的通道中接收数据,如果缓冲区中为空,则返回该类型的零值 197 | fmt.Println(ret) 198 | wg.Done() // 等待组的计数器-1 199 | }() 200 | go func() { 201 | //close(ch1) 202 | ch1 <- 15 // 给一个已关闭通道发送数据就会包panic错误 203 | close(ch1) 204 | }() 205 | wg.Wait() // 等待组的计数器不为0时阻塞 206 | } 207 | ``` 208 | 209 | ### 11. go中channel缓冲有什么特点? 210 | 211 | - 无缓冲的通道是同步的,有缓冲的通道是异步的 212 | 213 | ### 12. go中的cap函数可以作用于哪些内容? 214 | 215 | - 可作用于的类型有: 216 | 1. 数组(array) 217 | 2. 切片(slice) 218 | 3. 通道(channel) 219 | - 查看他们的容量大小,而不是装的数据大小 220 | 221 | ### 13. go convey是什么,一般用来做什么? 222 | 223 | 1. go convey是一个**支持golang的单元测试框架** 224 | 2. 能够自动监控文件修改并启动测试,并可以将测试结果实时输出到web界面 225 | 3. 提供了丰富的断言简化测试用例的编写 226 | 227 | ### 14. go语言中new的作用是什么? 228 | 229 | 1. 使用new函数来**分配内存空间** 230 | 2. **传递给new函数的是一个类型,而不是一个值** 231 | 3. **返回值是指向这个新分配的地址的指针** 232 | 233 | ### 15. go语言中的make作用是什么? 234 | 235 | - 分配**内存空间并进行初始化**, 返回值是**该类型的实例而不是指针** 236 | - make**只能接收三种类型当做参数:slice、map、channel** 237 | 238 | ### 16. 总结new和make的区别? 239 | 240 | 1. new可以**接收任意内置类型当做参数**,返回的是**对应类型的指针** 241 | 2. make**只能接收slice、map、channel当做参数**,返回值是**对应类型的实例** 242 | 243 | ### 17. Printf、Sprintf、FprintF都是格式化输出,有什么不同? 244 | 245 | - 虽然这三个函数都是格式化输出,但是输出的目标不一样 246 | 247 | 1. Printf输出到控制台 248 | 2. Sprintf结果赋值给返回值 249 | 3. FprintF输出到指定的io.Writer接口中 250 | 例如: 251 | 252 | ```go 253 | func main() { 254 | var a int = 15 255 | file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND, 0644) 256 | // 格式化字符串并输出到文件 257 | n, _ := fmt.Fprintf(file, "%T:%v:%p", a, a, &a) 258 | fmt.Println(n) 259 | } 260 | ``` 261 | 262 | ### 18. go语言中的数组和切片的区别是什么? 263 | 264 | - 数组: 265 | 1. 数组**固定长度**,数组长度是数组类型的一部分,所以[3]int和[4]int是两种不同的数组类型 266 | 2. 数组类型**需要指定大小**,不指定也会根据初始化,自动推算出大小,大小不可改变,数组是通过值传递的 267 | - 切片: 268 | 1. 切片的**长度可改变**,切片是轻量级的数据结构,三个属性:指针、长度、容量 269 | 2. **不要指定切片的大小**,切片也是值传递只不过切片的一个属性指针指向的数据不变,所以看起来像引用传递 270 | 3. 切片**可以通过数组来初始化也可以通过make函数来初始化**,**初始化时的len和cap相等,然后进行扩容** 271 | 4. 切片**扩容的时候会导致底层的数组复制**,也就是切片中的指针属性会发生变化 272 | 5. 切片也是拷贝,在不发生扩容时,底层使用的是同一个数组,当对其中一个切片append的时候, 该切片长度会增加 273 | 但是不会影响另外一个切片的长度 274 | 6. copy函数将原切片拷贝到目标切片,会导致底层数组复制,因为目标切片需要通过make函数来声明初始化内存,然后 275 | 将原切片指向的数组元素拷贝到新切片指向的数组元素 276 | - 重点:**数组保存真正的数据**,**切片值保存数组的指针和该切片的长度和容量** 277 | - append函数如果切片容量足够的话,只会影响当前切片的长度,数组底层不会复制,不会影响与数组关联的其它切片的长度 278 | - copy直接会导致数组底层复制 279 | 280 | ### 19. go语言中值传递和地址传递(引用传递)如何运行?有什么区别?举例说明 281 | 282 | 1. 值传递会把参数的值复制一份放到对应的函数里,两个变量的地址不同,不可互相修改 283 | 2. 地址传递会把参数的地址复制一份放到对应的函数里,两个变量的地址相同,可以互相修改 284 | 3. 例如:**数组传递就是值传递**,而**切片传递就是数组的地址传递**(本质上切片值传递,只不过是保存的数据地址相同) 285 | 286 | ### 20. go中数组和切片在传递时有什么区别? 287 | 288 | 1. 数组是值传递 289 | 2. 切片地址传递(引用传递) 290 | 291 | ### 21. go中是如何实现切片扩容的? 292 | 293 | 1. 当容量小于1024时,每次扩容容量翻倍,当容量大于1024时,每次扩容加25% 294 | 295 | ```go 296 | func main() { 297 | s1 := make([]int, 0) 298 | for i := 0; i < 3000; i++{ 299 | fmt.Println("len =", len(s1), "cap = ", cap(s1)) 300 | s1 = append(s1, i) 301 | } 302 | } 303 | ``` 304 | 305 | ### 22. 看下面代码defer的执行顺序是什么?defer的作用和特点是什么? 306 | 307 | 1. 在普通函数或方法前加上defer关键字,就完成了defer所需要的语法,**当defer语句被执行时,跟在defer语句后的函数会被延迟执行** 308 | 2. 知道**包含该defer语句的函数执行完毕,defer语句后的函数才会执行**,无论包含defer语句的函数是通过return正常结束,还是通过panic导致的异常结束 309 | 3. 可以在一个函数中执行多条defer语句,由于在栈中存储,所以它的**执行顺序和声明顺序相反** 310 | 311 | ### 23. defer语句中通过recover捕获panic例子 312 | 313 | 注意要在defer后函数里的recover() 314 | 315 | ```go 316 | func main() { 317 | defer func() { 318 | err := recover() 319 | fmt.Println(err) 320 | }() 321 | defer fmt.Println("first defer") 322 | defer fmt.Println("second defer") 323 | defer fmt.Println("third defer") 324 | fmt.Println("哈哈哈哈") 325 | panic("abc is an error") 326 | } 327 | ``` 328 | 329 | ### 24. go中的25个关键字 330 | 331 | - 程序声明2个: 332 | package import 333 | - 程序实体声明和定义8个: 334 | var const type func struct map chan interface 335 | - 程序流程控制15个: 336 | for range continue break select switch case default if else fallthrough defer go goto return 337 | 338 | ### 25. 写一个定时任务,每秒执行一次 339 | 340 | ```go 341 | func main() { 342 | t1 := time.NewTicker(time.Second * 1) // 创建一个周期定时器 343 | var i = 1 344 | for { 345 | if i == 10{ 346 | break 347 | } 348 | select { 349 | case <-t1.C: // 一秒执行一次的定时任务 350 | task1(i) 351 | i++ 352 | } 353 | } 354 | } 355 | func task1(i int) { 356 | fmt.Println("task1执行了---", i) 357 | } 358 | ``` 359 | 360 | ### 26. switch case fallthrough default使用场景 361 | 362 | ```go 363 | func main() { 364 | var a int 365 | for i := 0; i < 10; i++{ 366 | a = rand.Intn(100) 367 | switch { 368 | case a >= 80: 369 | fmt.Println("优秀", a) 370 | fallthrough // 强制执行下一个case 371 | case a >= 60: 372 | fmt.Println("及格", a) 373 | fallthrough 374 | default: 375 | fmt.Println("不及格", a) 376 | } 377 | } 378 | } 379 | ``` 380 | 381 | ### 27. defer的常用场景 382 | 383 | - defer语句经常被**用于处理成对的操作打开/关闭,链接/断开连接,加锁/释放锁** 384 | - 通过defer机制,不论函数逻辑多复杂,都**能保证在任何执行路径下,资源被释放** 385 | - 释放资源的defer语句应该直接跟在请求资源处理错误之后 386 | - 注意:defer一定要放在请求资源处理错误之后 387 | 388 | ### 28. go中slice的底层实现 389 | 390 | 1. 切片是基于数组实现的,它的底层是数组,它本身非常小,它可以理解为对底层数组的抽闲 391 | 2. 因为基于数组实现,所以它的底层内存是连续分配的,效率非常高,还可以通过索引获取数据 392 | 3. 切片本身并不是动态数组或数组指针,它内部实现的数据结构体通过指针引用底层数组 393 | 4. 设定相关属性将读写操作限定在指定的区域内,切片本身是一个只读对象,其工作机制类似于数组指针的一种封装 394 | 5. 切片对象非常小,因为它只有三个字段的数据结构:指向底层数组的指针、切片的长度、切片的容量 395 | 396 | ### 29. go中slice的扩容机制,有什么注意点? 397 | 398 | 1. 首先判断,如果新申请的容量大于2倍的旧容量,最终容量就是新申请的容量 399 | 2. 否则判断,如果旧切片的长度小于1024,最终容量就是旧容量的两倍 400 | 3. 否则判断,如果旧切片的长度大于等于1024,则最终容量从旧容量开始循环增加原来的1/4,直到最终容量大于新申请的容量 401 | 4. 如果最终容量计算值溢出,则最终容量就是新申请的容量 402 | 403 | ### 30. 扩容前后的slice是否相同? 404 | 405 | - 情况一: 406 | 1. **原来数组还有容量可以扩容(实际容量没有填充完)**,这种情况下,扩容之后的切片还是指向原来的数组 407 | 2. 对一个切片的操作可能影响多个指针指向相同地址的切片 408 | - 情况二: 409 | 1. 原来数组的容量已经达到了最大值,在扩容,go默认会先开辟一块内存区域,把原来的值拷贝过来 410 | 2. 然后再执行append操作,这种情况丝毫不影响原数组 411 | - 注意:要复制一个slice最好使用copy函数 412 | 413 | ### 31. go中的参数传递、引用传递 414 | 415 | 1. go语言中的**所有的传参都是值传递(传值),都是一个副本,一个拷贝**, 416 | 2. 因为拷贝的内容有时候是非引用类型(int, string, struct)等,这样在函数中就无法修改原内容数据 417 | 3. 有的是引用类型(指针、slice、map、chan),这样就可以修改原内容数据 418 | 419 | - go中的引用类型包含slice、map、chan,它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性 420 | - 内置函数new计算类型大小,为其分配零值内存,返回指针。 421 | - 而make会被编译器翻译成具体的创建函数,由其分配内存并初始化成员结构,返回对象而非指针 422 | 423 | ### 32. 哈希概念讲解 424 | 425 | 1. 哈希表又称为**散列表,由一个直接寻址表和一个哈希函数组成** 426 | 427 | 2. 由于哈希表的大小是有限的而要存储的数值是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到相同位置的情况,这种情况叫做哈希冲突 428 | 429 | 4. 通过**拉链法解决哈希冲突**: 430 | \* 哈希表每个位置都连接一个链表,当冲突发生是,冲突的元素将会被加到该位置链表的最后 431 | 432 | 4. 哈希表的查找速度起决定性作用的就是**哈希函数**: **除法哈希发、乘法哈希法、全域哈希法** 433 | 434 | 5. 哈希表的应用? 435 | 436 | 字典与集合都是通过哈希表来实现的 437 | 438 | md5曾经是密码学中常用的哈希函数,可以把任意长度的数据映射为128位的哈希值 439 | 440 | ### 33. go中的map底层实现 441 | 442 | 1. go中map的底层实现就是一个散列表,因此实现map的过程实际上就是实现散列表的过程 443 | 2. 在这个散列表中,**主要出现的结构体由两个,一个是hmap、一个是bmap** 444 | 3. go中也有一个哈希函数,用来对map中的键生成哈希值 445 | 4. hash结果的低位用于把k/v放到bmap数组中的哪个bmap中 446 | 5. 高位用于key的快速预览,快速试错 447 | 448 | ### 34. go中的map如何扩容 449 | 450 | 1. 翻倍扩容:如果map中的键值对个数/桶的个数>6.5,就会引发翻倍扩容 451 | 2. 等量扩容:当B<=15时,如果溢出桶的个数>=2的B次方就会引发等量扩容 452 | 3. 当B>15时,如果溢出桶的个数>=2的15次方时就会引发等量扩容 453 | 454 | ### 35. go中map的查找 455 | 456 | 1. go中的map采用的是哈希查找表,由哈希函数通过key和哈希因此计算出哈希值, 457 | 2. 根据hamp中的B来确定放到哪个桶中,如果B=5,那么就根据哈希值的后5位确定放到哪个桶中 458 | 3. 在用哈希值的高8位确定桶中的位置,如果当前的bmap中未找到,则去对应的overflow bucket中查找 459 | 4. 如果当前map处于数据搬迁状态,则优先从oldbuckets中查找 460 | 461 | ### 36. 介绍一下channel 462 | 463 | 1. go中不要通过共享内存来通信,而要**通过通信实现共享内存** 464 | 2. go中的**csp并发模型,中文名通信顺序进程,就是通过goroutine和channel实现的** 465 | 3. **channel收发遵循先进先出,分为有缓冲通道(异步通道),无缓冲通道(同步通道)** 466 | 467 | ### 37. go中channel的特性 468 | 469 | 1. 给一个nil的channel发送数据,会造成永久阻塞 470 | 2. 从一个nil的channel接收数据,会造成永久阻塞 471 | 3. 给一个已经关闭的channel发送数据,会造成panic 472 | 4. 从一个已经关闭的channel接收数据,如果缓冲区为空,会返回零值 473 | 5. 无缓冲的channel是同步的,有缓冲的channel是异步的 474 | 6. 关闭一个nil channel会造成panic 475 | 476 | ### channel中ring buffer的实现 477 | 478 | 1. channel中使用了ring buffer(环形缓冲区)来缓存写入数据, 479 | 2. ring buffer有很多好处,而且非常适合实现FiFo的固定长度队列 480 | 3. channel中包含buffer、sendx、recvx 481 | 4. recvx指向最早被读取的位置,sendx指向再次写入时插入的位置 482 | 483 | 来源:[go面试题-基础类 - 专职 - 博客园 (cnblogs.com)](https://www.cnblogs.com/mayanan/p/15836710.html) -------------------------------------------------------------------------------- /Go基础/Go基础语法.md: -------------------------------------------------------------------------------- 1 | # 基础语法 2 | 3 | ### 1.使用值为 nil 的 slice、map会发生啥 4 | 5 | 允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map 添加元素,则会造成运行时 panic。 6 | 7 | ```GO 8 | // map 错误示例 9 | func main() { 10 | var m map[string]int 11 | m["one"] = 1 // error: panic: assignment to entry in nil map 12 | // m := make(map[string]int)// map 的正确声明,分配了实际的内存,这样添加元素就不会错 13 | } 14 | 15 | // slice 正确示例 16 | func main() { 17 | var s []int 18 | s = append(s, 1) 19 | } 20 | ``` 21 | 22 | ### 2.访问 map 中的 key,需要注意啥 23 | 24 | 当访问 map 中不存在的 key 时,Go 则会返回元素对应数据类型的零值,比如 nil、’’ 、false 和 0,取值操作总有值返回,故**不能通过取出来的值,来判断 key 是不是在 map 中**。 25 | 26 | 检查 key 是否存在可以**用 map 直接访问,检查返回的第二个参数**即可。 27 | 28 | ```go 29 | // 错误的 key 检测方式 30 | func main() { 31 | x := map[string]string{"one": "2", "two": "", "three": "3"} 32 | if v := x["two"]; v == "" { 33 | fmt.Println("key two is no entry") // 键 two 存不存在都会返回的空字符串 34 | } 35 | } 36 | 37 | // 正确示例 38 | func main() { 39 | x := map[string]string{"one": "2", "two": "", "three": "3"} 40 | if _, ok := x["two"]; !ok { 41 | fmt.Println("key two is no entry") 42 | } 43 | } 44 | ``` 45 | 46 | ### 3.string 类型的值可以修改吗 47 | 48 | 不能,**尝试使用索引遍历字符串,来更新字符串中的个别字符,是不允许的**。 49 | 50 | string 类型的值是只读的二进制 byte slice,如果**真要修改字符串中的字符,将 string 转为 []byte 修改后,再转为 string 即可**。 51 | 52 | ```go 53 | // 修改字符串的错误示例 54 | func main() { 55 | x := "text" 56 | x[0] = "T" // error: cannot assign to x[0] 57 | fmt.Println(x) 58 | } 59 | 60 | 61 | // 修改示例 62 | func main() { 63 | x := "text" 64 | xBytes := []byte(x) 65 | xBytes[0] = 'T' // 注意此时的 T 是 rune 类型 66 | x = string(xBytes) 67 | fmt.Println(x) // Text 68 | } 69 | ``` 70 | 71 | ### 4.switch 中如何强制执行下一个 case 代码块 72 | 73 | switch 语句中的 case 代码块会默认带上 break,但可以**使用 fallthrough 来强制执行下一个 case 代码**块。 74 | 75 | ```go 76 | func main() { 77 | isSpace := func(char byte) bool { 78 | switch char { 79 | case ' ': // 空格符会直接 break,返回 false // 和其他语言不一样 80 | // fallthrough // 返回 true 81 | case '\t': 82 | return true 83 | } 84 | return false 85 | } 86 | fmt.Println(isSpace('\t')) // true 87 | fmt.Println(isSpace(' ')) // false 88 | } 89 | ``` 90 | 91 | ### 5.如何从 panic 中恢复 92 | 93 | 在一个 **defer 延迟执行的函数中调用 recover** ,它便能捕捉/中断 panic。这是因为即使panic,也会继续执行完goroutine,而defer延迟执行的函数中含有recover,所以会恢复。 94 | 95 | ```go 96 | // 错误的 recover 调用示例 97 | func main() { 98 | recover() // 什么都不会捕捉 99 | panic("not good") // 发生 panic,主程序退出 100 | recover() // 不会被执行 101 | println("ok") 102 | } 103 | 104 | // 正确的 recover 调用示例 105 | func main() { 106 | defer func() { 107 | fmt.Println("recovered: ", recover()) 108 | }() 109 | panic("not good") 110 | } 111 | ``` 112 | 113 | ### 6.简短声明(:=)的变量需要注意啥 114 | 115 | - 简短声明的变量**只能在函数内部使用** 116 | - **struct 的变量字段不能使用 := 来赋值** 117 | - 不能用简短声明方式来单独为一个变量重复声明, **:= 左侧至少有一个新变量**,才允许多变量的重复声明 118 | 119 | ### 7.range 迭代 map是有序的吗 120 | 121 | 无序的。Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的。 122 | 123 | 若**想有序遍历map,将 `Map` 中的 key 拿出来,放入 `slice` 中做排序.** 124 | 125 | ### 8.recover的执行时机 126 | 127 | 无,recover 必须在 defer 函数中运行。recover 捕获的是祖父级调用时的异常,直接调用时无效。 128 | 129 | ```go 130 | func main() { // 无效 131 | recover() 132 | panic(1) 133 | } 134 | ``` 135 | 136 | 直接 defer 调用也是无效。 137 | 138 | ```go 139 | func main() { 140 | defer recover() // 直接调用,无效 141 | panic(1) 142 | } 143 | ``` 144 | 145 | defer 调用时多层嵌套依然无效。 146 | 147 | ```go 148 | func main() { 149 | defer func() { // 多层嵌套无效 150 | func() { recover() }() 151 | }() 152 | panic(1) 153 | } 154 | ``` 155 | 156 | 必须在 defer 函数中直接调用才有效。 157 | 158 | ```go 159 | func main() { 160 | defer func() { // 执行中的函数调用,有效 161 | recover() 162 | }() 163 | panic(1) 164 | } 165 | ``` 166 | 167 | ### 9.闭包错误引用同一个变量问题怎么处理 168 | 169 | 在**每轮迭代中生成一个局部变量 i** 。如果没有 i := i 这行,将会打印同一个变量。 170 | 171 | ```go 172 | func main() { 173 | for i := 0; i < 5; i++ { 174 | i := i // 再次新的局部变量 175 | defer func() { 176 | println(i) 177 | }() 178 | } 179 | } 180 | ``` 181 | 182 | 或者是**通过函数参数传入 i** 。 183 | 184 | ```go 185 | func main() { 186 | for i := 0; i < 5; i++ { 187 | defer func(i int) { // 参数传递 188 | println(i) 189 | }(i) 190 | } 191 | } 192 | ``` 193 | 194 | ### 10.在循环内部执行defer语句会发生啥 195 | 196 | defer 在函数退出时才能执行,在 for 执行 defer 会**导致资源延迟释放**。 197 | 198 | ```go 199 | func main() { 200 | for i := 0; i < 5; i++ { 201 | func() { 202 | f, err := os.Open("/path/to/file") 203 | if err != nil { 204 | log.Fatal(err) 205 | } 206 | defer f.Close() 207 | }() 208 | } 209 | } 210 | ``` 211 | 212 | **func 是一个局部函数,在局部函数里面执行 defer 将不会有问题**。因为当**函数执行结束后,defer 语句出栈,遵循先入后出**。 213 | 214 | ### 11.如何跳出for select 循环 215 | 216 | 通常在for循环中,使用break可以跳出循环,但是注意在go语言中,**for select配合时,break 并不能跳出循环**。因为**默认在select中break是只跳脱了select体**,而不是结束for循环。需要设置标签,**break标签或goto便签即可跳出循环**!但要注意标签和便签位置不一样!还可以return,适合退出goroutine的场景。 217 | 218 | ```go 219 | func testSelectFor2(chExit chan bool){ 220 | EXIT:// 标签 221 | for { 222 | select { 223 | case v, ok := <-chExit: 224 | if !ok { 225 | fmt.Println("close channel 2", v) 226 | break EXIT//goto EXIT2 227 | } 228 | 229 | fmt.Println("ch2 val =", v) 230 | } 231 | } 232 | 233 | //EXIT2://goto的便签,注意位置不同,如果在标签位置,goto仍然不能退出for循环! 234 | fmt.Println("exit testSelectFor2") 235 | } 236 | ``` 237 | 238 | ### 12.如何在切片中查找 239 | 240 | go中使用 sort.searchXXX 方法,**在排序好的切片**中查找指定的方法,但是其**返回是对应的查找元素不存在时,待插入的位置下标(元素插入在返回下标前)**。 241 | 242 | 可以通过封装如下函数,达到目的。 243 | 244 | ```go 245 | func IsExist(s []string, t string) (int, bool) { 246 | iIndex := sort.SearchStrings(s, t) 247 | bExist := iIndex!=len(s) && s[iIndex]==t // 待插入索引不等于长度且待插入位置正好存在要查找的元素,则返回存在。 248 | 249 | return iIndex, bExist 250 | } 251 | ``` 252 | 253 | ### 13.如何初始化带嵌套结构的结构体 254 | 255 | go 的哲学是**组合优于继承,使用 struct 嵌套即可完成组合**,内嵌的结构体属性就像外层结构的属性即可,可以直接调用。 256 | 257 | 注意初始化外层结构体时,**必须指定内嵌结构体名称的结构体初始化**,如下看到 s1方式报错,s2 方式正确。 258 | 259 | ```go 260 | type stPeople struct { 261 | Gender bool 262 | Name string 263 | } 264 | 265 | type stStudent struct { 266 | stPeople 267 | Class int 268 | } 269 | 270 | //尝试4 嵌套结构的初始化表达式 271 | //var s1 = stStudent{false, "JimWen", 3} 272 | var s2 = stStudent{stPeople{false, "JimWen"}, 3} // 指定内嵌结构体的名称 273 | fmt.Println(s2.Gender, s2.Name, s2.Class) 274 | ``` 275 | 276 | ### 14.切片和数组的区别 277 | 278 | **数组是具有固定长度,且拥有零个或者多个,相同数据类型元素的序列。**数组的长度是数组类型的一部分,所以[3]int 和 [4]int 是两种不同的数组类型。数组**需要指定大小**,不指定也会根据初始化的自动推算出大小,不可改变;**数组是值传递**。数组是内置类型,是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。 279 | 280 | 当**作为方法的参数传入时将复制一份数组而不是引用同一指针。**数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。数组定义: 281 | 282 | ```go 283 | var array [10]int 284 | 285 | var array =[5]int{1,2,3,4,5} 286 | ``` 287 | 288 | **切片表示一个拥有相同类型元素的可变长度的序列**。切片是一种轻量级的数据结构,它有三个属性:指针、长度和容量。**切片不需要指定大小;切片是地址传递;切片可以通过数组来初始化,也可以通过内置函数make()初始化 。**初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容。切片定义: 289 | 290 | ```go 291 | var slice []type = make([]type, len) 292 | ``` 293 | 294 | ### 15.new和make的区别 295 | 296 | **new 的作用是初始化一个指向类型的指针 (*T)** 。new 函数是内建函数,函数定义:func new(Type) *Type。使用 new 函数来分配空间。传递给 new 函数的是一个类型,不是一个值。**返回值是指向这个新分配的零值的指针**。 297 | 298 | **make 的作用是为 slice,map 或 chan 初始化并返回引用 (T)**。make 函数是内建函数,函数定义:func make(Type, size IntegerType) Type;第一个参数是一个类型,第二个参数是长度;**返回值是一个类型**。 299 | 300 | make(T, args) 函数的目的与 new(T) 不同。它仅仅用于创建 Slice, Map 和 Channel,并且返回类型是 T(不是T*)的一个初始化的(不是零值)的实例。 301 | 302 | ### 16.Printf()、Sprintf()、Fprintf()函数的区别用法是什么 303 | 304 | 都是把格式好的字符串输出,只是输出的目标不一样。 305 | 306 | Printf(),是**把格式字符串输出到标准输出**(一般是屏幕,可以重定向)。Printf() 是和标准输出文件 (stdout) 关联的,Fprintf 则没有这个限制。 307 | Sprintf(),是**把格式字符串输出到指定字符串中**,所以参数比printf多一个char*。那就是目标字符串地址。 308 | 309 | Fprintf(),是**把格式字符串输出到指定文件设备中**,所以参数比 printf 多一个文件指针 FILE*。主要用于文件操作。Fprintf() 是格式化输出到一个stream,通常是到文件。 310 | 311 | ### 17.说说go语言中的for循环 312 | 313 | for 循环**支持 continue 和 break 来控制循环**,但是它提供了一个更高级的break,可以选择中断哪一个循环。[配合标签] 314 | 315 | for 循环**不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量**。 316 | 317 | ### 18.Array 类型的值作为函数参数 318 | 319 | 在 C/C++ 中,数组(名)是指针。将数组作为参数传进函数时,相当于传递了数组内存地址的引用,在函数内部会改变该数组的值。 320 | 321 | 在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的。 322 | 323 | ```go 324 | // 数组使用值拷贝传参 325 | func main() { 326 | x := [3]int{1,2,3} 327 | 328 | func(arr [3]int) { 329 | arr[0] = 7 330 | fmt.Println(arr) // [7 2 3] 331 | }(x) 332 | fmt.Println(x) // [1 2 3] // 并不是你以为的 [7 2 3] 333 | } 334 | ``` 335 | 336 | 想改变数组,直接传递指向这个数组的指针类型。 337 | 338 | ```go 339 | // 传址会修改原数据 340 | func main() { 341 | x := [3]int{1,2,3} 342 | 343 | func(arr *[3]int) { // 匿名函数 344 | (*arr)[0] = 7 345 | fmt.Println(arr) // &[7 2 3] 346 | }(&x) 347 | fmt.Println(x) // [7 2 3] 348 | } 349 | ``` 350 | 351 | 直接使用 slice:即使函数内部得到的是 slice 的值拷贝,但依旧会更新 slice 的原始数据(底层 array) 352 | 353 | ```go 354 | // 错误示例 355 | func main() { 356 | x := []string{"a", "b", "c"} 357 | for v := range x { // range返回类型,索引遗漏 358 | fmt.Println(v) // 1 2 3 359 | } 360 | } 361 | 362 | 363 | // 正确示例 364 | func main() { 365 | x := []string{"a", "b", "c"} 366 | for _, v := range x { // 使用 _ 丢弃索引 367 | fmt.Println(v) 368 | } 369 | } 370 | ``` 371 | 372 | ### 19.说说go语言中的switch语句 373 | 374 | 单个 case 中,可以出现多个变量或结果选项,但最终结果要为相同类型的表达式。多个可能复合条件的值,要使用逗号分隔。case默认带有break,自动退出,只有在 case 中明确添加 fallthrough关键字,才会继续执行紧跟的下一个 case。 375 | 376 | ### 20.说说go语言中有没有隐藏的this指针 377 | 378 | Go语言中没有隐藏的this指针。 379 | 380 | 方法施加的对象**显式传递**,没有被隐藏起来。(指的是接收器。需要给结构体增加方法时,需要使用 func (a 结构体名) 方法名(参数列表) (返回值列表) {函数体} 这种形式,在函数体里面,调用结构体成员的时候使用的就是 a.xxx,用 c 语言的方式来解释,就是将对象作为参数传入了函数,函数调用这个参数从而访问对象的成员,当然这个函数是友联函数,可以访问任意访问权限的成员) 381 | 382 | golang 的面向对象表达更直观,对于面向过程只是换了一种语法形式来表达。 383 | 384 | 方法施加的对象不需要非得是指针,也不用非得叫 this。(可以穿对象,不一定要传对象指针,至于名字随意!) 385 | 386 | ### 21.go语言中的引用类型包含哪些 387 | 388 | 数组切片(slice)、字典(map)、通道(channel)、接口(interface)。 389 | 390 | ### 22.go语言中指针运算有哪些 391 | 392 | 可以通过“&”取指针的地址;可以通过“*”取指针指向的数据。 393 | 394 | ### 23.说说go语言的main函数 395 | 396 | main 函数**不能带参数**;main 函数**不能定义返回值**。main 函数**所在的包必须为 main 包**;main 函数中**可以使用 flag 包来获取和解析命令行参数**。 397 | 398 | ### 24.go语言触发异常的场景有哪些 399 | 400 | - 空指针解析 401 | - 下标越界 402 | - 除数为0 403 | - 调用 panic 函数 404 | 405 | ### 25.go语言编程的好处是什么 406 | 407 | - 编译和运行都很快。 408 | - 在语言层级支持并行操作。 409 | - 有垃圾处理器GC。 410 | - 内置字符串和 maps。 411 | - 函数是 go 语言的最基本编程单位。 412 | 413 | ### 26.说说go语言的select机制 414 | 415 | - select 机制用来处理异步 IO 问题 416 | 417 | - select 机制最大的一条限制就是每个 case 语句里必须是一个 IO 操作 418 | 419 | - golang 在语言级别支持 select 关键字 420 | 421 | - 1.select+case是用于阻塞监听goroutine的,如果没有case,就单单一个select{},则为监听当前程序中的goroutine,此时注意,需要有真实的goroutine在跑,否则select{}会报panic 422 | 423 | 2.select底下有多个可执行的case,则随机执行一个。 424 | 425 | 3.select常配合for循环来监听channel有没有故事发生。需要注意的是在这个场景下,break只是退出当前select而不会退出for,需要用break TIP / goto的方式。 426 | 427 | 4.无缓冲的通道,则传值后立马close,则会在close之前阻塞,有缓冲的通道则即使close了也会继续让接收后面的值【!】 428 | 429 | 5.同个通道多个goroutine进行关闭,可用recover panic的方式来判断通道关闭问题【!】 430 | 431 | ### 27.解释一下go语言中的静态类型声明 432 | 433 | 静态类型声明是**告诉编译器不需要太多的关注这个变量的细节**。 434 | 435 | 静态变量的声明,**只是针对于编译的时候, 在连接程序的时候,编译器还要对这个变量进行实际的声明。** 436 | 437 | ### 28.go的接口是什么 438 | 439 | - 在 go 语言中,**interface 也就是接口**,被**用来指定一个对象**。接口具有下面的要素: 440 | - 一系列的方法 441 | - 具体应用中用来表示某个数据类型 442 | - 在 go 中使用 interface 来**实现多态** 443 | 444 | ### 29.Go语言里面的类型断言是怎么回事 445 | 446 | 类型断言是**用来从一个接口里面读取数值给一个具体的类型变量**。 447 | 448 | 类型断言的语法格式如下: 449 | 450 | value, ok := x.(T) 451 | 452 | 其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。 453 | 454 | 该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型: 455 | 456 | - 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。 457 | - 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。 458 | - 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。 459 | 460 | 示例代码如下: 461 | 462 | ```go 463 | package main 464 | import ("fmt") 465 | func main() { 466 | var x interface{} 467 | x = 10 468 | value, ok := x.(int) 469 | fmt.Print(value, ",", ok) 470 | } 471 | ``` 472 | 473 | 运行结果如下: 474 | 475 | 10,true 476 | 477 | 需要注意如果不接收第二个参数也就是上面代码中的 ok,断言失败时会直接造成一个 panic。如果 x 为 nil 同样也会 panic。 478 | 479 | **类型转换**是指转换两个不相同的数据类型。 480 | 481 | ### 30.go语言中局部变量和全局变量的缺省值是什么 482 | 483 | 全局变量的缺省值是与这个类型相关的零值。 484 | 485 | ### 31.模块化编程是怎么回事 486 | 487 | 模块化编程是指**把一个大的程序分解成几个小的程序。这么做的目的是为了减少程序的复杂度,易于维护,并且达到最高的效率。** 488 | 489 | ### 32.Golang的方法有什么特别之处 490 | 491 | **函数的定义声明没有接收者。** 492 | 方法的声明和函数类似,他们的区别是:**方法在定义的时候,会在func和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法。** 493 | Go语言里有两种类型的接收者:值接收者和指针接收者。使用值类型接收者定义的方法,在调用的时候,使用的其实是值接收者的一个副本,所以对该值的任何操作,不会影响原来的类型变量。——-相当于形式参数。 494 | 495 | 如果我们使用一个指针作为接收者,那么就会其作用了,因为指针接收者传递的是一个指向原值指针的副本,指针的副本,指向的还是原来类型的值,所以修改时,同时也会影响原来类型变量的值。 496 | 497 | ### 33.Golang可变参数 498 | 499 | **函数方法的参数,可以是任意多个,这种我们称之为可以变参数**。比如我们常用的fmt.Println()这类函数,可以接收一个可变的参数。可以变参数,可以是任意多个。我们自己也可以定义可以变参数,**可变参数的定义,在类型前加上省略号…**即可。 500 | 501 | ```go 502 | func main() { 503 | print("1","2","3") 504 | } 505 | 506 | 507 | func print (a ...interface{}){ 508 | for _,v := range a{ 509 | fmt.Print(v) 510 | } 511 | fmt.Println() 512 | } 513 | ``` 514 | 515 | 例子中我们自己定义了一个接受可变参数的函数,效果和fmt.Println()一样。可变参数本质上是一个数组,所以我们向使用数组一样使用它,比如例子中的 for range 循环。 516 | 517 | ### 34.Golang Slice的底层实现 518 | 519 | **切片是基于数组实现的,它的底层是数组**,它自己本身非常小,可以理解为对底层数组的抽象。**因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。** 520 | 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。 521 | 切片对象非常小,是因为它是只有3个字段的数据结构: 522 | 523 | - 指向底层数组的指针 524 | - 切片的长度 525 | - 切片的容量 526 | 527 | 这3个字段,就是Go语言操作底层数组的元数据。 528 | 529 | null 530 | 531 | ### 35.Golang Slice的扩容机制,有什么注意点 532 | 533 | Go 中切片扩容的策略是这样的: 534 | 535 | **首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量。否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍。** 536 | 537 | **否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环增加原来的 1/4 , 直到最终容量大于等于新申请的容量。如果最终容量计算值溢出,则最终容量就是新申请容量。** 538 | 539 | 情况一:原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的Slice。 540 | 541 | 情况二:原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。 542 | 543 | 要复制一个Slice,最好使用Copy函数。 544 | 545 | ### 36.Golang Map底层实现 546 | 547 | Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。 548 | 在这个散列表中,主要出现的结构体有两个,**一个叫hmap(a header for a go map),一个叫bmap(a bucket for a Go map,通常叫其bucket)**。 549 | 550 | hmap如下所示: 551 | 552 | null 553 | 554 | 图中有很多字段,但是便于理解 map 的架构,你只需要关心的只有一个,就是标红的字段:buckets 数组。Golang 的 map 中用于存储的结构是 bucket数组。而 bucket(即bmap)的结构是怎样的呢? 555 | bucket: 556 | 557 | null 558 | 559 | 相比于 hmap,bucket 的结构显得简单一些,标橙的字段依然是“核心”,我们使用的 map 中的 key 和 value 就存储在这里。 560 | 561 | “高位哈希值”数组记录的是当前 bucket 中 key 相关的”索引”,稍后会详细叙述。还有一个字段是一个指向扩容后的 bucket 的指针,使得 bucket 会形成一个链表结构。 562 | 整体的结构应该是这样的: 563 | 564 | null 565 | 566 | Golang 把求得的哈希值按照用途一分为二:高位和低位。低位用于寻找当前 key属于 hmap 中的哪个 bucket,而高位用于寻找 bucket 中的哪个 key。 567 | 需要特别指出的一点是:map中的key/value值都是存到同一个数组中的。这样做的好处是:在key和value的长度不同的时候,可以消除padding带来的空间浪费。 568 | 569 | ![null](https://topgoer.cn/uploads/blog/202104/attach_16778bc2077dd9d7.png) 570 | 571 | Map 的扩容:当 Go 的 map 长度增长到大于加载因子所需的 map 长度时,Go 语言就会将产生一个新的 bucket 数组,然后把旧的 bucket 数组移到一个属性字段 oldbucket中。 572 | 573 | 注意:并不是立刻把旧的数组中的元素转义到新的 bucket 当中,而是,只有当访问到具体的某个 bucket 的时候,会把 bucket 中的数据转移到新的 bucket 中。 574 | 575 | ### 37.Golang的内存模型,为什么小对象多了会造成gc压力 576 | 577 | 通常**小对象过多会导致 GC 三色法消耗过多的GPU**。优化思路是,减少对象分配。 578 | 579 | ### 38.Data Race问题怎么解决?能不能不加锁解决这个问题 580 | 581 | data race 译作数据竞争,比如不同的goroutine并发读写同一个变量,可能会发生数据竞争。 582 | 583 | **同步访问共享数据**是处理**数据竞争**的一种有效的方法。 584 | 585 | golang在 1.1 之后引入了竞争检测机制,可以使用 go run -race 或者 go build -race来进行静态检测。其在内部的实现是,开启多个协程执行同一个命令, 并且记录下每个变量的状态。 586 | 587 | 竞争检测器基于C/C++的ThreadSanitizer 运行时库,该库在Google内部代码基地和Chromium找到许多错误。这个技术在2012年九月集成到Go中,从那时开始,它已经在标准库中检测到42个竞争条件。现在,它已经是我们持续构建过程的一部分,当竞争条件出现时,它会继续捕捉到这些错误。 588 | 589 | 竞争检测器已经完全集成到Go工具链中,仅仅添加-race标志到命令行就使用了检测器。 590 | 591 | ``` 592 | $ go test -race mypkg // 测试包 593 | $ go run -race mysrc.go // 编译和运行程序 $ go build -race mycmd 594 | // 构建程序 $ go install -race mypkg // 安装程序 595 | ``` 596 | 597 | 要想解决数据竞争的问题**可以使用互斥锁sync.Mutex,解决数据竞争(Data race)**,也**可以使用管道解决,使用管道的效率要比互斥锁高**。 598 | 599 | ### 39.在 range 迭代 slice 时,你怎么修改值的 600 | 601 | 在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址。 602 | 603 | ```go 604 | func main() { 605 | data := []int{1, 2, 3} 606 | for _, v := range data { 607 | v *= 10 // data 中原有元素是不会被修改的 608 | } 609 | fmt.Println("data: ", data) // data: [1 2 3] 610 | } 611 | ``` 612 | 613 | **如果要修改原有元素的值,应该**使用索引直接访问。 614 | 615 | ```go 616 | func main() { 617 | data := []int{1, 2, 3} 618 | for i, v := range data { 619 | data[i] = v * 10 620 | } 621 | fmt.Println("data: ", data) // data: [10 20 30] 622 | } 623 | ``` 624 | 625 | **如果你的集合保存的是指向值的指针,需稍作修改。依旧需要使用索引访问元素,不过可以使用 range 出来的元素直接更新原有值。** 626 | 627 | ```go 628 | func main() { 629 | data := []*struct{ num int }{{1}, {2}, {3},} 630 | for _, v := range data { 631 | v.num *= 10 // 直接使用指针更新 632 | } 633 | fmt.Println(data[0], data[1], data[2]) // &{10} &{20} &{30} 634 | } 635 | ``` 636 | 637 | ### 40.nil 和 nil interface 的区别 638 | 639 | 虽然 interface 看起来像指针类型,但它不是。interface 类型的变量只有在类型和值均为 nil 时才为 nil.如果你的 interface 变量的值是跟随其他变量变化的,与 nil 比较相等时小心。如果你的函数返回值类型是 interface,更要小心这个坑: 640 | 641 | ```go 642 | func main() { 643 | var data *byte 644 | var in interface{} 645 | 646 | fmt.Println(data, data == nil) // true 647 | fmt.Println(in, in == nil) // true 648 | 649 | in = data 650 | fmt.Println(in, in == nil) // false // data 值为 nil,但 in 值不为 nil 651 | } 652 | 653 | // 正确示例 654 | func main() { 655 | doIt := func(arg int) interface{} { 656 | var result *struct{} = nil 657 | 658 | if arg > 0 { 659 | result = &struct{}{} 660 | } else { 661 | return nil // 明确指明返回 nil 662 | } 663 | 664 | return result 665 | } 666 | 667 | 668 | if res := doIt(-1); res != nil { 669 | fmt.Println("Good result: ", res) 670 | } else { 671 | fmt.Println("Bad result: ", res) // Bad result: 672 | } 673 | } 674 | ``` 675 | 676 | ### 41.select可以用于什么【可看26】 677 | 678 | 常用语gorotine的完美退出。 679 | 680 | golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作。 681 | 682 | ### 42. 指针数据坑 683 | 684 | range到底有什么坑呢,我们先来运行一个例子吧。 685 | 686 | ```go 687 | package main 688 | 689 | import ( 690 | "fmt" 691 | ) 692 | 693 | type user struct { 694 | name string 695 | age uint64 696 | } 697 | 698 | func main() { 699 | u := []user{ 700 | {"asong",23}, 701 | {"song",19}, 702 | {"asong2020",18}, 703 | } 704 | n := make([]*user,0,len(u)) 705 | for _,v := range u{ 706 | n = append(n, &v) 707 | } 708 | fmt.Println(n) 709 | for _,v := range n{ 710 | fmt.Println(v) 711 | } 712 | } 713 | ``` 714 | 715 | 这个例子的目的是,通过u这个slice构造成新的slice。我们预期应该是显示uslice的内容,但是运行结果如下: 716 | 717 | ``` 718 | [0xc0000a6040 0xc0000a6040 0xc0000a6040] 719 | &{asong2020 18} 720 | &{asong2020 18} 721 | &{asong2020 18} 722 | ``` 723 | 724 | 这里我们看到n这个slice打印出来的三个同样的数据,并且他们的内存地址相同。这是什么原因呢?先别着急,再来看这一段代码,我给他改正确他,对比之后我们再来分析,你们才会恍然大悟。 725 | 726 | ```go 727 | package main 728 | 729 | import ( 730 | "fmt" 731 | ) 732 | 733 | type user struct { 734 | name string 735 | age uint64 736 | } 737 | 738 | func main() { 739 | u := []user{ 740 | {"asong",23}, 741 | {"song",19}, 742 | {"asong2020",18}, 743 | } 744 | n := make([]*user,0,len(u)) 745 | for _,v := range u{ 746 | o := v // 多了这一步! 747 | n = append(n, &o) 748 | } 749 | fmt.Println(n) 750 | for _,v := range n{ 751 | fmt.Println(v) 752 | } 753 | } 754 | ``` 755 | 756 | 细心的你们看到,我改动了哪一部分代码了嘛?对,没错,我就加了一句话,他就成功了,我在for range里面引入了一个中间变量,每次迭代都重新声明一个变量o,赋值后再将v的地址添加n切片中,这样成功解决了刚才的问题。 757 | 758 | 现在来解释一下原因:在**for range中,变量v是用来保存迭代切片所得的值,因为v只被声明了一次,每次迭代的值都是赋值给v,该变量的内存地址始终未变,这样讲他的地址追加到新的切片中,该切片保存的都是同一个地址**,这肯定无法达到预期效果的。这里还需要注意一点,变量v的地址也并不是指向原来切片u[2]的,因我在使用range迭代的时候,变量v的数据是切片的拷贝数据,所以直接copy了结构体数据。 759 | 760 | 上面的问题**还有一种解决方法,直接引用数据的内存**,这个方法比较好,不需要开辟新的内存空间,看代码: 761 | 762 | ```go 763 | ......略 764 | for k,_ := range u{ 765 | n = append(n, &u[k]) 766 | } 767 | ......略 768 | ``` 769 | 770 | ### 43. 是否会造成死循环 771 | 772 | 来看一段代码: 773 | 774 | ```go 775 | func main() { 776 | v := []int{1, 2, 3} 777 | for i := range v { // i 为索引。range会对最初的v拷贝,所以后面v变化和range的无关! 778 | v = append(v, i) 779 | } 780 | } 781 | ``` 782 | 783 | 这一段代码会造成死循环吗?答案:当然不会,**前面都说了range会对切片做拷贝,新增的数据并不在拷贝内容中,并不会发生死循环。**这种题一般会在面试中问,可以留意下的。 784 | 785 | ### 你不知道的range用法 786 | 787 | #### delete 788 | 789 | 没看错,删除,在range迭代时,可以删除map中的数据,第一次见到这么使用的,我刚听到确实不太相信,所以我就去查了一下官方文档,确实有这个写法: 790 | 791 | ```go 792 | for key := range m { 793 | if key.expired() { 794 | delete(m, key) 795 | } 796 | } 797 | ``` 798 | 799 | 看看官方的解释: 800 | 801 | ``` 802 | The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If map entries that have not yet been reached are removed during iteration, the corresponding iteration values will not be produced. If map entries are created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0. 803 | 804 | 翻译: 805 | 未指定`map`的迭代顺序,并且不能保证每次迭代之间都相同。 如果在迭代过程中删除了尚未到达的映射条目,则不会生成相应的迭代值。 如果映射条目是在迭代过程中创建的,则该条目可能在迭代过程中产生或可以被跳过。 对于创建的每个条目以及从一个迭代到下一个迭代,选择可能有所不同。 如果映射为nil,则迭代次数为0。 806 | ``` 807 | 808 | 看这个代码: 809 | 810 | ```go 811 | func main() { 812 | d := map[string]string{ 813 | "asong": "帅", 814 | "song": "太帅了", 815 | } 816 | for k := range d{ 817 | if k == "asong"{ 818 | delete(d,k) 819 | } 820 | } 821 | fmt.Println(d) 822 | } 823 | 824 | # 运行结果 825 | map[song:太帅了] 826 | ``` 827 | 828 | 从运行结果我们可以看出,key为asong的这位帅哥被从帅哥map中删掉了,哇哦,可气呀。这个方法,相信很多小伙伴都不知道,今天教给你们了,以后可以用起来了。 829 | 830 | #### add 831 | 832 | 上面是删除,那肯定会有新增呀,直接看代码吧。 833 | 834 | ```go 835 | func main() { 836 | d := map[string]string{ 837 | "asong": "帅", 838 | "song": "太帅了", 839 | } 840 | for k,v := range d{ 841 | d[v] = k 842 | fmt.Println(d) 843 | } 844 | } 845 | ``` 846 | 847 | 这里我把打印放到了range里,你们思考一下,新增的元素,在遍历时能够遍历到呢。我们来验证一下。 848 | 849 | ```go 850 | func main() { 851 | var addTomap = func() { 852 | var t = map[string]string{ 853 | "asong": "太帅", 854 | "song": "好帅", 855 | "asong1": "非常帅", 856 | } 857 | for k := range t { 858 | t["song2020"] = "真帅" 859 | fmt.Printf("%s%s ", k, t[k]) 860 | } 861 | } 862 | for i := 0; i < 10; i++ { 863 | addTomap() 864 | fmt.Println() 865 | } 866 | } 867 | ``` 868 | 869 | 运行结果: 870 | 871 | ``` 872 | asong太帅 song好帅 asong1非常帅 song2020真帅 873 | asong太帅 song好帅 asong1非常帅 874 | asong太帅 song好帅 asong1非常帅 song2020真帅 875 | asong1非常帅 song2020真帅 asong太帅 song好帅 876 | asong太帅 song好帅 asong1非常帅 song2020真帅 877 | asong太帅 song好帅 asong1非常帅 song2020真帅 878 | asong太帅 song好帅 asong1非常帅 879 | asong1非常帅 song2020真帅 asong太帅 song好帅 880 | asong太帅 song好帅 asong1非常帅 song2020真帅 881 | asong太帅 song好帅 asong1非常帅 song2020真帅 882 | ``` 883 | 884 | 从运行结果,我们可以看出来,**每一次的结果并不是确定的**。这是为什么呢?这就来揭秘,**map内部实现是一个链式hash表,为了保证无顺序,初始化时会随机一个遍历开始的位置,所以新增的元素被遍历到就变的不确定了**,同样删除也是一个道理,但是删除元素后边就不会出现,所以一定不会被遍历到。 885 | 886 | ### 44. 拷贝大切片一定比拷贝小切片代价大吗? 887 | 888 | 这道题比较有意思,原文地址:Are large slices more expensive than smaller ones? 889 | 890 | 这道题本质是考察对切片本质的理解,Go语言中只有值传递,所以我们以传递切片为例子: 891 | 892 | ```go 893 | func main() { 894 | param1 := make([]int, 100) 895 | param2 := make([]int, 100000000) 896 | smallSlice(param1) 897 | largeSlice(param2) 898 | } 899 | 900 | func smallSlice(params []int) { 901 | // .... 902 | } 903 | 904 | func largeSlice(params []int) { 905 | // .... 906 | } 907 | ``` 908 | 909 | 切片param2要比param1大1000000个数量级,在进行值拷贝的时候,是否需要更昂贵的操作呢? 910 | 911 | **实际上不会**,因为切片本质内部结构如下: 912 | 913 | ```go 914 | type SliceHeader struct { 915 | Data uintptr 916 | Len int 917 | Cap int 918 | } 919 | ``` 920 | 921 | 切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。**将一个切片变量分配给另一个变量只会复制三个机器字,大切片跟小切片的区别无非就是 Len 和 Cap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。** 922 | 923 | ### 44. 切片的深浅拷贝 924 | 925 | 深浅拷贝都是进行复制,区别在于复制出来的新对象与原来的对象在它们发生改变时,是否会相互影响,**本质区别就是复制出来的对象与原对象是否会指向同一个地址。**在Go语言,切片拷贝有三种方式: 926 | 927 | - 使用=操作符拷贝切片,这种就是浅拷贝 928 | - 使用[:]下标的方式复制切片,这种也是浅拷贝 929 | - 使用**Go语言的内置函数copy()进行切片拷贝,这种就是深拷贝** 930 | 931 | ### 45. 零切片、空切片、nil切片是什么 932 | 933 | 为什么问题中这么多种切片呢?因为在Go语言中切片的创建方式有五种,不同方式创建出来的切片也不一样; 934 | 935 | - 零切片 936 | 937 | 我们**把切片内部数组的元素都是零值或者底层数组的内容就全是 nil的切片叫做零切片,使用make创建的、长度、容量都不为0的切片就是零值切片**: 938 | 939 | ``` 940 | slice := make([]int,5) // 0 0 0 0 0 941 | slice := make([]*int,5) // nil nil nil nil nil 942 | ``` 943 | 944 | - nil切片 945 | 946 | **nil切片的长度和容量都为0,并且和nil比较的结果为true**,**采用直接创建切片的方式、new创建切片的方式都可以创建nil切片**: 947 | 948 | ``` 949 | var slice []int 950 | var slice = *new([]int) 951 | ``` 952 | 953 | - 空切片 954 | 955 | **空切片的长度和容量也都为0,但是和nil的比较结果为false**,因为所有的空切片的数据指针都指向同一个地址 0xc42003bda0;**使用字面量、make可以创建空切片**: 956 | 957 | ``` 958 | var slice = []int{} 959 | var slice = make([]int, 0) 960 | ``` 961 | 962 | 空切片指向的 zerobase 内存地址是一个神奇的地址,从 Go 语言的源代码中可以看到它的定义: 963 | 964 | ``` 965 | // base address for all 0-byte allocations 966 | var zerobase uintptr 967 | 968 | // 分配对象内存 969 | func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 970 | ... 971 | if size == 0 { 972 | return unsafe.Pointer(&zerobase) 973 | } 974 | ... 975 | } 976 | ``` 977 | 978 | ### 46. 切片的扩容策略 979 | 980 | 这个问题是一个高频考点,我们通过源码来解析一下切片的扩容策略,切片的扩容都是调用growslice方法,截取部分重要源代码: 981 | 982 | ```go 983 | // runtime/slice.go 984 | // et:表示slice的一个元素;old:表示旧的slice;cap:表示新切片需要的容量; 985 | func growslice(et *_type, old slice, cap int) slice { 986 | if cap < old.cap { 987 | panic(errorString("growslice: cap out of range")) 988 | } 989 | 990 | if et.size == 0 { 991 | // append should not create a slice with nil pointer but non-zero len. 992 | // We assume that append doesn't need to preserve old.array in this case. 993 | return slice{unsafe.Pointer(&zerobase), old.len, cap} 994 | } 995 | 996 | newcap := old.cap 997 | // 两倍扩容 998 | doublecap := newcap + newcap 999 | // 新切片需要的容量大于两倍扩容的容量,则直接按照新切片需要的容量扩容 1000 | if cap > doublecap { 1001 | newcap = cap 1002 | } else { 1003 | // 原 slice 容量小于 1024 的时候,新 slice 容量按2倍扩容 1004 | if old.cap < 1024 { 1005 | newcap = doublecap 1006 | } else { // 原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。 1007 | // Check 0 < newcap to detect overflow 1008 | // and prevent an infinite loop. 1009 | for 0 < newcap && newcap < cap { 1010 | newcap += newcap / 4 1011 | } 1012 | // Set newcap to the requested cap when 1013 | // the newcap calculation overflowed. 1014 | if newcap <= 0 { 1015 | newcap = cap 1016 | } 1017 | } 1018 | } 1019 | 1020 | // 后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。 1021 | var overflow bool 1022 | var lenmem, newlenmem, capmem uintptr 1023 | // Specialize for common values of et.size. 1024 | // For 1 we don't need any division/multiplication. 1025 | // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant. 1026 | // For powers of 2, use a variable shift. 1027 | switch { 1028 | case et.size == 1: 1029 | lenmem = uintptr(old.len) 1030 | newlenmem = uintptr(cap) 1031 | capmem = roundupsize(uintptr(newcap)) 1032 | overflow = uintptr(newcap) > maxAlloc 1033 | newcap = int(capmem) 1034 | case et.size == sys.PtrSize: 1035 | lenmem = uintptr(old.len) * sys.PtrSize 1036 | newlenmem = uintptr(cap) * sys.PtrSize 1037 | capmem = roundupsize(uintptr(newcap) * sys.PtrSize) 1038 | overflow = uintptr(newcap) > maxAlloc/sys.PtrSize 1039 | newcap = int(capmem / sys.PtrSize) 1040 | case isPowerOfTwo(et.size): 1041 | var shift uintptr 1042 | if sys.PtrSize == 8 { 1043 | // Mask shift for better code generation. 1044 | shift = uintptr(sys.Ctz64(uint64(et.size))) & 63 1045 | } else { 1046 | shift = uintptr(sys.Ctz32(uint32(et.size))) & 31 1047 | } 1048 | lenmem = uintptr(old.len) << shift 1049 | newlenmem = uintptr(cap) << shift 1050 | capmem = roundupsize(uintptr(newcap) << shift) 1051 | overflow = uintptr(newcap) > (maxAlloc >> shift) 1052 | newcap = int(capmem >> shift) 1053 | default: 1054 | lenmem = uintptr(old.len) * et.size 1055 | newlenmem = uintptr(cap) * et.size 1056 | capmem, overflow = math.MulUintptr(et.size, uintptr(newcap)) 1057 | capmem = roundupsize(capmem) 1058 | newcap = int(capmem / et.size) 1059 | } 1060 | } 1061 | ``` 1062 | 1063 | 通过源代码可以总结切片扩容策略: 1064 | 1065 | > 切片**在扩容时会进行内存对齐**,这个和内存分配策略相关。**进行内存对齐之后,新 slice 的容量是要 大于等于老 slice 容量的 2倍或者1.25倍,当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。** 1066 | 1067 | ### 48. 参数传递切片和切片指针有什么区别? 1068 | 1069 | 我们都知道切片底层就是一个结构体,里面有三个元素: 1070 | 1071 | ```go 1072 | type SliceHeader struct { 1073 | Data uintptr 1074 | Len int 1075 | Cap int 1076 | } 1077 | ``` 1078 | 1079 | 分别表示切片底层数据的地址,切片长度,切片容量。 1080 | 1081 | **当切片作为参数传递时,其实就是一个结构体的传递,因为Go语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为底层数据的地址没有变**,所以在函数内对切片的修改,也将会影响到函数外的切片,举例: 1082 | 1083 | ```go 1084 | func modifySlice(s []string) { 1085 | s[0] = "song" 1086 | s[1] = "Golang" 1087 | fmt.Println("out slice: ", s) 1088 | } 1089 | 1090 | func main() { 1091 | s := []string{"asong", "Golang梦工厂"} 1092 | modifySlice(s) 1093 | fmt.Println("inner slice: ", s) 1094 | } 1095 | // 运行结果 1096 | out slice: [song Golang] 1097 | inner slice: [song Golang] 1098 | ``` 1099 | 1100 | 不过这也有一个特例,先看一个例子: 1101 | 1102 | ```go 1103 | func appendSlice(s []string) { 1104 | s = append(s, "快关注!!") 1105 | fmt.Println("out slice: ", s) 1106 | } 1107 | 1108 | func main() { 1109 | s := []string{"asong", "Golang梦工厂"} 1110 | appendSlice(s) 1111 | fmt.Println("inner slice: ", s) 1112 | } 1113 | // 运行结果 1114 | out slice: [asong Golang梦工厂 快关注!!] 1115 | inner slice: [asong Golang梦工厂] 1116 | ``` 1117 | 1118 | 因为切片发生了扩容,函数外的切片指向了一个新的底层数组,所以函数内外不会相互影响,因此可以得出一个结论,**当参数直接传递切片时,如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。** 1119 | 1120 | 参数传递切片指针就很容易理解了,**如果你想修改切片中元素的值,并且更改切片的容量和底层数组,则应该按指针传递。** 1121 | 1122 | ### 49. range遍历切片有什么要注意的? 1123 | 1124 | Go语言提供了range关键字用于for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素,有两种使用方式: 1125 | 1126 | ``` 1127 | for k,v := range _ { } 1128 | for k := range _ { } 1129 | ``` 1130 | 1131 | **第一种是遍历下标和对应值,第二种是只遍历下标,使用range遍历切片时会先拷贝一份,然后在遍历拷贝数据**: 1132 | 1133 | ```go 1134 | s := []int{1, 2} 1135 | for k, v := range s { 1136 | 1137 | } 1138 | 会被编译器认为是 1139 | for_temp := s 1140 | len_temp := len(for_temp) 1141 | for index_temp := 0; index_temp < len_temp; index_temp++ { 1142 | value_temp := for_temp[index_temp] 1143 | _ = index_temp 1144 | value := value_temp 1145 | 1146 | } 1147 | ``` 1148 | 1149 | 不知道这个知识点的情况下很容易踩坑,例如下面这个例子: 1150 | 1151 | ```go 1152 | package main 1153 | 1154 | import ( 1155 | "fmt" 1156 | ) 1157 | 1158 | type user struct { 1159 | name string 1160 | age uint64 1161 | } 1162 | 1163 | func main() { 1164 | u := []user{ 1165 | {"asong",23}, 1166 | {"song",19}, 1167 | {"asong2020",18}, 1168 | } 1169 | for _,v := range u{ 1170 | if v.age != 18{ 1171 | v.age = 20 1172 | } 1173 | } 1174 | fmt.Println(u) 1175 | } 1176 | // 运行结果 1177 | [{asong 23} {song 19} {asong2020 18}] 1178 | ``` 1179 | 1180 | 因为使用range遍历切片u,变量v是拷贝切片中的数据,修改拷贝数据不会对原切片有影响。 1181 | 1182 | ##### 参考:[Golang 50题 笔记 - 格罗玛什·地狱咆哮 - 博客园 (cnblogs.com)](https://www.cnblogs.com/arvin-an/p/14666978.html) 1183 | 1184 | -------------------------------------------------------------------------------- /Go基础/Go实现原理.md: -------------------------------------------------------------------------------- 1 | ## 1. init() 函数是什么时候执行的? 2 | 3 |
答案

init() 函数是 Go 程序初始化的一部分。Go 程序初始化先于 main 函数,由 runtime 初始化每个导入的包,初始化顺序不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。

每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init() 函数。同一个包,甚至是同一个源文件可以有多个 init() 函数。init() 函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init() 函数的执行顺序不作保证。

一句话总结: import –> const –> var –> init() –> main()

示例:

package main

import "fmt"

func init() {
fmt.Println("init1:", a)
}

func init() {
fmt.Println("init2:", a)
}

var a = 10
const b = 100

func main() {
fmt.Println("main:", a)
}
// 执行结果
// init1: 10
// init2: 10
// main: 10
4 | 5 | ## 2. Go 语言的局部变量分配在栈上还是堆上? 6 | 7 |
答案

由编译器决定。Go 语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。

func foo() *int {
v := 11
return &v
}

func main() {
m := foo()
println(*m) // 11
}

foo() 函数中,如果 v 分配在栈上,foo 函数返回时,&v 就不存在了,但是这段函数是能够正常运行的。Go 编译器发现 v 的引用脱离了 foo 的作用域,会将其分配在堆上。因此,main 函数中仍能够正常访问该值。

8 | 9 | ## 3. 2 个 interface 可以比较吗? 10 | 11 |
答案

Go 语言中,interface 的内部实现包含了 2 个字段,类型 T 和 值 V,interface 可以使用 ==  != 比较。2 个 interface 相等有以下 2 种情况

  1. 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
  2. 类型 T 相同,且对应的值 V 相等。

看下面的例子:

type Stu struct {
Name string
}

type StuInt interface{}

func main() {
var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"}
var stu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"}
fmt.Println(stu1 == stu2) // false
fmt.Println(stu3 == stu4) // true
}

stu1  stu2 对应的类型是 *Stu,值是 Stu 结构体的地址,两个地址不同,因此结果为 false。
stu3  stu4 对应的类型是 Stu,值是 Stu 结构体,且各字段相等,因此结果为 true。

12 | 13 | ## 4. 两个 nil 可能不相等吗? 14 | 15 |
答案

可能。

接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。

  • 两个接口值比较时,会先比较 T,再比较 V。
  • 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
func main() {
var p *int = nil
var i interface{} = p
fmt.Println(i == p) // true
fmt.Println(p == nil) // true
fmt.Println(i == nil) // false
}

上面这个例子中,将一个 nil 非接口值 p 赋值给接口 i,此时,i 的内部字段为(T=*int, V=nil),i 与 p 作比较时,将 p 转换为接口后再比较,因此 i == p,p 与 nil 比较,直接比较值,所以 p == nil

但是当 i 与 nil 比较时,会将 nil 转换为接口 (T=nil, V=nil),与i (T=*int, V=nil) 不相等,因此 i != nil。因此 V 为 nil ,但 T 不为 nil 的接口不等于 nil。

16 | 17 | ## 5. 简述 Go 语言GC(垃圾回收)的工作原理 18 | 19 |
答案

最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),Go 语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。

标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:

  • 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象;
  • 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表。

标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束之后,用户程序才可以继续执行。为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法。

三色标记算法将程序中的对象分成白色、黑色和灰色三类。

  • 白色:不确定对象。
  • 灰色:存活对象,子对象待处理。
  • 黑色:存活对象。

标记开始时,所有对象加入白色集合(这一步需 STW )。首先将根对象标记为灰色,加入灰色集合,垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。重复这个过程,直到灰色集合为空为止,标记阶段结束。那么白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。

三色标记法因为多了一个白色的状态来存放不确定对象,所以后续的标记阶段可以并发地执行。当然并发执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。所以三色标记法是一个 false negative(假阴性)的算法。

三色标记法并发执行仍存在一个问题,即在 GC 过程中,对象指针发生了改变。比如下面的例子:

A (黑) -> B (灰) -> C (白) -> D (白)

正常情况下,D 对象最终会被标记为黑色,不应被回收。但在标记和用户程序并发执行过程中,用户程序删除了 C 对 D 的引用,而 A 获得了 D 的引用。标记继续进行,D 就没有机会被标记为黑色了(A 已经处理过,这一轮不会再被处理)。

A (黑) -> B (灰) -> C (白) 

D (白)

为了解决这个问题,Go 使用了内存屏障技术,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,类似于一个钩子。垃圾收集器使用了写屏障(Write Barrier)技术,当对象新增或更新时,会将其着色为灰色。这样即使与用户程序并发执行,对象的引用发生改变时,垃圾收集器也能正确处理了。

一次完整的 GC 分为四个阶段:

  • 1)标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier)
  • 2)使用三色标记法标记(Marking, 并发)
  • 3)标记结束(Mark Termination,需 STW),关闭写屏障。
  • 4)清理(Sweeping, 并发)
20 | 21 | ## 6. 函数返回局部变量的指针是否安全? 22 | 23 |
答案

这在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上。

24 | 25 | ## 7. 非接口非接口的任意类型 T() 都能够调用 `*T` 的方法吗?反过来呢? 26 | 27 |
答案
  • 一个T类型的值可以调用为*T类型声明的方法,但是仅当此T的值是可寻址(addressable) 的情况下。编译器在调用指针属主方法前,会自动取此T值的地址。因为不是任何T值都是可寻址的,所以并非任何T值都能够调用为类型*T声明的方法。
  • 反过来,一个*T类型的值可以调用为类型T声明的方法,这是因为解引用指针总是合法的。事实上,你可以认为对于每一个为类型 T 声明的方法,编译器都会为类型*T自动隐式声明一个同名和同签名的方法。

哪些值是不可寻址的呢?

  • 字符串中的字节;
  • map 对象中的元素(slice 对象中的元素是可寻址的,slice的底层是数组);
  • 常量;
  • 包级别的函数等。

举一个例子,定义类型 T,并为类型 *T 声明一个方法 hello(),变量 t1 可以调用该方法,但是常量 t2 调用该方法时,会产生编译错误。

type T string

func (t *T) hello() {
fmt.Println("hello")
}

func main() {
var t1 T = "ABC"
t1.hello() // hello
const t2 T = "ABC"
t2.hello() // error: cannot call pointer method on t
}
28 | 29 | ------ 30 | 31 | #### 来源[Go 语言笔试面试题(实现原理) | 极客面试 | 极客兔兔 (geektutu.com)](https://geektutu.com/post/qa-golang-2.html) 32 | 33 | -------------------------------------------------------------------------------- /Go并发/GO并发sync.md: -------------------------------------------------------------------------------- 1 | # go并发编程 2 | 3 | ### Mutex几种状态 4 | 5 | - mutexLocked 表示互斥锁的锁定状态 6 | - mutexWoken 唤醒锁 7 | - mutexStarving 当前互斥锁进入饥饿状态 8 | - mutexWaiterShift 统计阻塞在这个互斥锁上的goroutine的数目 9 | 10 | 互斥锁无冲突是最简单的情况了,有冲突时,首先进行自旋,因为Mutex保护的代码段都很短 11 | 12 | 经过短暂的自旋就可以获得,如果自旋等待无果,就只好通过信号量让当前goroutine进入Gwaitting状态 13 | 14 | ### Mutex的正常模式和饥饿模式 15 | 16 | - 正常模式(非公平锁) 17 | 1. 正常模式下,所有等待的goroutine按照FIFO(先进先出)的顺序等待 18 | 2. 唤醒的goroutine不会直接拥有锁而是会和新进来的请求goroutine竞争锁 19 | 3. 新请求的goroutine更容易抢占,因为它正在CPU上执行,所以刚刚唤醒的goroutine很大可能会竞争失败 20 | 4. 在这种情况下这个被唤醒的goroutine会被加入到等待队列的前面 21 | - 饥饿模式(公平锁) 22 | 1. 为了解决等待goroutine队列的长尾问题,饥饿模式下 23 | 2. 直接由unlock把锁交给等待队列中第一个goroutine(队头),同时,饥饿模式下 24 | 3. 新进来的goroutine不会进行抢锁,也不会进入自旋状态,会直接进入等待队列的尾部 25 | 4. 这样很好的解决了老的goroutine一直抢不到锁的情况 26 | - 饥饿模式的触发条件 27 | 1. 当一个goroutine等待时间超过1毫秒时,或者当前队列剩下一个goroutine的时候,Mutex切换到饥饿模式 28 | - 总结 29 | 1. 对于两种模式,正常模式下性能是最好的,因为goroutine可以连续多次获取锁 30 | 2. 饥饿模式解决了取锁公平的问题,但是性能会下降,这其实是性能和公平的一个平衡模式 31 | 32 | ### Mutex允许自旋的条件 33 | 34 | 1. 锁已被占用,且锁不处于饥饿模式 35 | 2. 积累的自旋次数小于最大自旋次数 36 | 3. CPU核数>1 37 | 4. 有空闲的P 38 | 5. 当前的goroutine所挂载的P下,本地待运行队列为空 39 | 40 | ### RWMutex实现 41 | 42 | 1. 通过记录readerCount读锁的数量来进行限制,当有一个写锁的时候,会将读锁的数量设置为负数1<<30 43 | 2. 目的是让新进来的读锁等待之前的写锁释放通知读锁 44 | 3. 同样当有写锁进行抢占时,也会等待之前的读锁都释放完毕,才进行后续的操作 45 | 4. 而等写锁释放完成之后,会将值重新加上1<<30,并通知刚才新进入的读锁(rw.readerSem)两者互相限制 46 | 47 | ### RWMutex注意事项 48 | 49 | 1. RWMutex是个单写多读锁,可以加多个读锁或者一个写锁 50 | 2. 读锁占用的情况下会阻止写,不会阻止读,多个goroutine可以同时获取读锁 51 | 3. 写锁会阻止其它goroutine(无论读写锁)进来,整个锁由该goroutine独占 52 | 4. 适用于读多写少的场景 53 | 5. RWMutex的零值是一个未锁定状态的互斥锁 54 | 6. RWMutex在首次使用之后就不能再被拷贝 55 | 7. RWMutex的读锁或写锁在未锁定的状态下进行解锁都会引发panic 56 | 8. RWMutex的一个写锁去锁定临界区的共享资源,如果临界区的资源已被(读锁或写锁)锁定,这个写锁的goroutine会被阻塞直到解锁 57 | 9. RWMutex的读锁不要用于递归调用,容易产生死锁 58 | 10. RWMutex的锁定状态与特定的goroutine没有关联,一个goroutine可以RLock(Lock),另外一个goroutine可以RUnlock(Unlock) 59 | 11. 写锁被解锁后,所有因操作锁定读锁的goroutine会被唤醒,并都可以成功锁定读锁 60 | 12. 读锁被解锁后,在没有其它读锁锁定的情况下,所有因操作锁定写锁而被阻塞的goroutine中,其中等待时间最长的goroutine会被唤醒 61 | 62 | ### cond是什么 63 | 64 | 1. Cond实现了一种条件变量,可以使用在多个reader等待共享资源ready的场景,(如果只有一读一写,一个锁或者channel就搞定了) 65 | 2. 每个Cond都会关联一个Lock(*sync.Mutex or *sync.RWMutex), 当修改条件或者调用Wait方法时,必须加锁以保护condition 66 | 3. 案例: 67 | 68 | ```go 69 | var ( 70 | c = sync.Cond{L: &sync.Mutex{}} // 条件变量,多个reader等待共享资源ready的场景 71 | maxNum = 15 72 | ) 73 | func main(){ 74 | for i := 0; i < maxNum; i++{ 75 | go func(i int) { 76 | c.L.Lock() 77 | defer c.L.Unlock() 78 | // Wait会释放c.L锁,并挂起调用者的goroutine,之后恢复执行 79 | c.Wait() // Wait会在返回时对c.L加锁 80 | // 除非被Broadcast或Signal唤醒,否则Wait不会返回 81 | fmt.Println("goroutine:", i) 82 | time.Sleep(time.Millisecond * 100) 83 | }(i) 84 | } 85 | c.L.Lock() 86 | maxNum = 16 87 | c.L.Unlock() 88 | c.Broadcast() // 广播,所有等待的goroutine都会执行 89 | //c.Signal() // 单播,从所有等待的goroutine中随机找一个去执行 90 | time.Sleep(time.Second * 2) 91 | } 92 | ``` 93 | 94 | ### Broadcast和Signal的区别 95 | 96 | - Broadcast会唤醒所有等待c的goroutine,调用Broadcast的时候,可以加锁也可以不加锁 97 | - Signal只唤醒一个等待c的goroutine,调用Signal的时候,可以加锁也可以不加锁 98 | 99 | ### Cond中Wait使用 100 | 101 | 1. Wait会自动释放c.L锁,并挂起调用者的goroutine,之后恢复执行 102 | 2. Wait会在返回时对c.L加锁 103 | 3. 除非被Broadcast或Signal唤醒,否则Wait不会返回 104 | 4. 由于Wait第一次恢复是,c.L并没有加锁,所以当Wait返回时,调用者通常不能假设条件为真 105 | 5. 简单来说,只要想使用condition就必须加锁 106 | 107 | ### WaitGroup用法 108 | 109 | - 一个WaitGroup对象可以等待一组协程结束,使用方法是 110 | 111 | 1. main协程通过调用 wg.add(delta int) 来设置worker协程的个数,然后创建worker协程 112 | 2. worker协程结束以后,都要调用wg.Done() 113 | 3. main协程调用wg.Wait()而被block, 知道所有worker协程全部执行结束后返回 114 | 4. 如果不确定要创建的worker协程数量,就不要一次性wg.add(),而是在每个创建worker协程之前调用一次wg.add(1) 115 | 5. 首次使用后不得复制wg 116 | 117 | ### WaitGroup实现原理 118 | 119 | 1. WaitGroup主要维护了两个计数器,一个请求计数器v,一个等待计数器w 120 | 2. 二者组成了一个64bit的值,请求计数器占高32bit,等待计数器占低32bit 121 | 3. 每次Add执行,请求计数器 v+1, 每次Done,等待计数器 w-1 122 | 4. 当v为0时,通过信号量唤醒Wait 123 | 124 | ### 什么是sync.Once 125 | 126 | 1. Once可以用来执行且仅仅执行一次的动作,常常用于单例对象的初始化场景 127 | 2. Once常常用来初始化单例资源,或者并发访问只需要初始化一次的共享资源 128 | 3. 或者在测试的时候初始化一次测试资源 129 | 4. sync.Once只暴露了一个方法Do,你可以多次调用Do方法 130 | 5. 但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数 131 | 132 | ### 什么操作叫原子操作 133 | 134 | 1. 原子操作即是进程过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中 135 | 2. CPU绝不会再去进行其它针对值的操作,为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成 136 | 3. 原子操作是无锁的,常常直接通过CPU指令直接实现, 137 | 4. 事实上,其它同步技术的实现常常依赖于原子操作 138 | 139 | ### 原子操作和锁的区别 140 | 141 | 1. 原子操作由底层硬件支持,而锁由操作系统调度器实现 142 | 2. 锁应当用来保护一段逻辑,对于一个变量更新的保护 143 | 3. 原子操作通常执行上会更有效率,并且更能利用计算机多核资源 144 | 4. 如果要更新的是一个复合对象,则应当使用automic.Value封装好的实现 145 | 146 | ### sync.Pool有什么用 147 | 148 | 1. 对于很多需要重复分配、回收内存的地方,sync.Pool是一个很好的选择 149 | 2. 频繁的分配回收内存会给GC带来一定负担,严重的时候会引起CPU的毛刺 150 | 3. 而sync.Pool可以将暂时不使用的对象缓存起来,待下次需要的时候直接使用 151 | 4. 不用再次经过内存分配,复用对象的内存,减轻GC的压力,提升系统性能 152 | 5. 案例 153 | 154 | ```go 155 | func main(){ 156 | var p sync.Pool 157 | var a = "jdfakljfdalfaskdj阿加加加金灯送福卡萨发几块爱神的箭快疯了金阿奎" 158 | p.Put(a) 159 | ret := p.Get() 160 | fmt.Println(ret) 161 | ``` 162 | 163 | # Go Runtime 164 | 165 | ### 1. goroutine定义 166 | 167 | ```x86asm 168 | golang在语言级别支持协程,称之为goroutine; golang标准库提供的所有系统调用操作(包括所有同步I/O操作) 169 | 都会让出CPU给其它goroutine, 这让goroutine的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量 170 | 而是交给Golang的运行时统一调度 171 | ``` 172 | 173 | ### 2. GMP指的是什么 174 | 175 | ```markdown 176 | * G(goroutine),我们所说的协程,用户级的轻量级线程,每个goroutine对象中的sched保存着其上下文信息 177 | * M(machine),对内核级线程的封装,数量对应真实的CPU数量(真正干活的对象) 178 | * P(processor),即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS来设置,默认是CPU核心数量 179 | ``` 180 | 181 | ### 3. 1.0之前GM调度模型 182 | 183 | ```css 184 | 调度器把不同的G分配到M上,不同的G在不同的M并发运行时,都需要向系统申请资源,比如堆栈内存,因为资源是全局的 185 | 就会因为资源竞争造成很多性能损耗,为了解决这一问题go从1.1版本引入,在运行时系统的时候加入p对象, 186 | 让P去管理G对象,M想要运行G,必须先绑定P,然后才能运行P下面的G对象 187 | 188 | GM调度存在的问题: 189 | 1. 单一全局互斥锁(Sched.Lock)和集中状态存储 190 | 2. groutine传递问题(M经常在M之间传递可运行的goroutine) 191 | 3. 每个M做内存缓存,导致内存占用过高,数据局部性较差 192 | 4. 频繁的syscall调用, 导致严重的线程阻塞/解锁,家具额外的性能损耗_ 193 | ``` 194 | 195 | ### 4. GMP调度流程 196 | 197 | ```css 198 | 1. 每个p有个局部队列,局部队列放的是待执行的goroutine,当M绑定的P的局部队列满了以后就会把G放到全局队列 199 | 2. 每个P和一个M绑定,M是真正执行P中goroutine的实体,M从绑定的P的局部队列中获取G来执行 200 | 3. 当M绑定的P的局部队列为空时,M就会从全局队列获取到本地队列来执行,当全局队列也为空时, 201 | M就会从其它P队列偷取G来执行,这种从其它P偷的方式称之为 work stealing 202 | 4. 当G因系统调用(syscall)阻塞时会阻塞M,此时P会和M解绑即hand off,并寻找新的M,如果没有空闲的M就会新建一个M 203 | 5. 当G因channel或者network I/O操作阻塞时,不会阻塞M,M会寻找其它runable的G,当阻塞的G恢复后重新进入runable进入P队列等待执行 204 | ``` 205 | 206 | ### 5. GMP中的work stealing机制 207 | 208 | ```css 209 | 先获取p本地队列,如果为空时,去全局队列里取G运行,如果全局队列为空时从netpoll和事件池里拿 210 | 如果在拿不到从其它P队列里偷 211 | ``` 212 | 213 | ### 6. GMP中的handoff机制 214 | 215 | ```css 216 | 当本地线程M因为G进行的系统调用阻塞时,会释放绑定的P,把P转移给其它空闲的M执行 217 | ``` 218 | 219 | ### 7. 协作式的抢占式调度 220 | 221 | ```markdown 222 | 在1.14之前,程序只能依靠goroutine主动让出CPU资源才能触发调用,这种方式存在问题有: 223 | 1. goroutine长时间占用线程,造成其它goroutine的饥饿 224 | 2. 垃圾回收需要暂停整个程序,最长可能几分钟,导致整个程序无法工作 225 | ``` 226 | 227 | ### 8. 基于信号的抢占式调度 228 | 229 | ```css 230 | 1. 在任何情况下,go运行时并行执行的goroutine数量要小于等于p的数量 231 | 2. 为了提高性能,p的数量肯定不是越小越好,官方给出默认是CPU的核心数量 232 | 3. 如果设置过小的,当M绑定的P执行的G执行系统调用阻塞,导致M也阻塞时,GO的调度器是迟钝的,他有可能什么都不做 233 | 它有可能什么都不做,知道M阻塞了相当长时间以后,才会发现一个P/M被syscall阻塞了,然后才会用空闲的M来抢这个P 234 | 所以P也不建议设置太小,通过sysmon监控实现的抢占式调度,最快在20us,最慢在10-20ms才会发现一个M持有P并阻塞了, 235 | 而操作系统1ms可以完成几十次系统调度 236 | 所以P适当的比CPU核心数多一些最好 237 | ``` 238 | 239 | ### 9. GMP调度过程中存在哪些阻塞 240 | 241 | ```markdown 242 | 1. I/O, select 243 | 2. block on syscall 244 | 3. channel 245 | 4. 等待所 246 | 5. runtime.Gosched() 247 | ``` 248 | 249 | ### 10. sysmon有什么用 250 | 251 | ```markdown 252 | sysmon也叫监控线程,变动的周期性检查,好处 253 | 1. 释放超过5分钟的span物理内存 254 | 2. 如果超过两分钟没有垃圾回收,强制执行 255 | 3. 将长时间未处理的netpoll加入到全局队列 256 | 4. 向长时间运行的g任务发出抢占调度(超过10ms的g,会进行retake) 257 | 5. 收回因syscall阻塞的P 258 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MySQL/MySQL常问.md: -------------------------------------------------------------------------------- 1 | ## 1. MySQL中myisam与innodb的区别? 2 | 3 | Mysql5.5版本后默认引擎有myisam修改为innodb. 4 | 5 | * InnoDB支持事务,而MyISAM不支持事务 6 | 7 | * InnoDB支持行级锁,而MyISAM支持表级锁 8 | 9 | * InnoDB支持MVCC, 而MyISAM不支持 10 | 11 | * InnoDB支持外键约束,而MyISAM不支持 12 | 13 | * InnoDB不支持全文索引,而MyISAM支持。 14 | 15 | * 更详细可看:[MySQL InnoDB存储引擎 (biancheng.net)](http://c.biancheng.net/view/8021.html)、 16 | 17 | 额外:MVCC(Multi-Version Concurrency Control), undo日志中保存了多版本的记录,undo支持事务回滚的同时,也支持数据的一致性读。undo日志保存在回滚段中,undo日志的回收由purge操作进行。 18 | 19 | InnoDB行记录中保存了事务相关信息如事务id,roll_ptr。id用于可见性判断,roll_ptr用于从undo中回溯历史版本。一致性读会开启一个ReadView,ReadView包含当前正在执行的事务信息,通过此ReadView来获取一致性的记录。 20 | 21 | ## 2. 事务的特性 22 | 23 | - 原子性:是指**事务包含所有操作要么全部成功,要么全部失败回滚**。 24 | - 一致性:指事务必须使数据库从一个一致性状态变换成另一个一致性状态,也就是说**一个事务执行之前和执行之后都必须处于一致性状态**。 25 | 拿转账来说,假设用户 A 和用户 B 两者的钱加起来一共是 5000,那么不管 A 和 B 之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是 5000,这就是事务的一致性。 26 | - 隔离性:是当多个用户并发访问数据库时,比如操作同一张表时,数据表为每个用户开启的事务,不能被其他事务所干扰,**多个并发事务之间要相互隔离**。 27 | - 持久性:持久性是指**一个事务一旦被提交,那么对数据库中的数据的改变就是永久的**,即便是在数据库系统遇到故障的性况下也不会丢失提交事务的操作。 28 | 29 | ## 3. 并发操作问题 30 | 31 | - 脏读:指**在一个事务处理过程中读取到了另外一个未提交事务中的数据**。 32 | - 不可重复读:指**在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值**,这是由于在查询间隔,被另一个事务修改并提交了。 33 | - 虚读(幻读):幻**读发生在当两个完全相同的查询执行时,第二次查询所返回的结果集跟第一个查询不相同。** 34 | 比如两个事务操作,A 事务查询状态为 1 的记录时,这时 B 事务插入了一条状态为 1 的记录,A 事务再次查询返回的结果不一样。 35 | 36 | ## 4. 事务的隔离级别 37 | 38 | - Serializable(串行化):可避免脏读、不可重复读、幻读。(就是串行化读数据) 39 | - Repeatable read(可重复读):可避免脏读、不可重复读的发生。 40 | - Read committed(读已提交):可避免脏读的发生。 41 | - Read uncommitted(读未提交):最低级别,任何情况都无法保证。 42 | 43 | 在 MySQL 数据库中,支持上面四种隔离级别,默认的为 Repeatable read (可重复读);而在 Oracle 数据库中,只支持 Serializable (串行化)级别和 Read committed (读已提交)这两种级别,其中默认的为 Read committed 级别。 44 | 45 | ## 5. 索引是什么? 46 | 47 | **索引是表的目录,在查找内容之前可以先在目录中查找索引位置**,以此快速定位查询数据。对于索引,会保存在额外的文件中。 48 | 49 | 索引是帮助MySQL高效获取数据的数据结构。 50 | 51 | ## 6. 索引能干什么?有什么好处? 52 | 53 | 当表中的数据量越来越大时,索引对于性能的影响愈发重要。索引**能够轻易将查询性能提高好几个数量级,总的来说就是可以明显的提高查询效率**。 54 | 55 | ## 7. 索引的种类有哪些? 56 | 57 | 1、从存储结构上来划分:BTree索引(B-Tree或B+Tree索引),Hash索引,full-index全文索引,R-Tree索引。这里所描述的是索引存储时保存的形式, 58 | 59 | 2、从应用层次来分:普通索引,唯一索引,复合索引 60 | 61 | 3、根据中数据的物理顺序与键值的逻辑(索引)顺序关系:聚集索引,非聚集索引。 62 | 63 | 平时讲的索引类型一般是指在应用层次的划分。 64 | 65 | * 普通索引:即**一个索引只包含单个列**,一个表可以有多个单列索引 66 | * 复合索引:**多列值组成一个索引**,专门用于组合搜索,其效率大于索引合并 67 | * 唯一索引:**索引列的值必须唯一,但允许有空值** 68 | 69 | ## 8. 为什么 MySQL 的索引要使用 B+树而不是其它树形结构?比如 B 树? 70 | 71 | 终极目的:为了**减少磁盘IO** 72 | **1.b+树,非叶子节点不存数据,节点数据小,每次磁盘IO的时候,数据就多,相同区域,b+树有更多的key** 73 | **2.b+树,子节点链表,磁盘读取预读原理,多读数据,可以减少磁盘io,同时可以进行范围查询** 74 | 75 | 76 | B-树,这里的 B 表示 balance( 平衡的意思),B-树是一种多路自平衡的搜索树(B树是一颗多路平衡查找树) 77 | 它类似普通的平衡二叉树,不同的一点是B-树允许每个节点有更多的子节点。下图是 B-树的简化图. 78 | 79 | ![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNDQ2MDg3LWJjMDIzZTQ3YmM3NGNmYTEuanBn?x-oss-process=image/format,png) 80 | 81 | B-树有如下特点: 82 | 83 | ​ 所有键值分布在整颗树中(索引值和具体data都在每个节点里); 84 | ​ 任何一个关键字出现且只出现在一个结点中; 85 | ​ 搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据); 86 | ​ 在关键字全集内做一次查找,性能逼近二分查找; 87 | 88 | B+树是B-树的变体,也是一种多路搜索树, 它与 B- 树的不同之处在于: 89 | 90 | 1. 所有关键字存储在叶子节点出现,内部节点(非叶子节点并不存储真正的 data) 91 | 2. 为所有叶子结点增加了一个链指针 92 | 93 | 简化 B+树 如下图 94 | 95 | ![B+树 1](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xNDQ2MDg3LTMwYjcwYWFhMjg0MDM4MDMuanBn?x-oss-process=image/format,png) 96 | 97 | B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。** 98 | 99 | B-tree:因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低。 100 | 101 | Hash:虽然可以快速定位,但是没有顺序,IO复杂度高。 102 | 103 | 二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。 104 | 105 | 红黑树:树的高度随着数据量增加而增加,IO代价高。 106 | 107 | **不使用平衡二叉树的原因如下**: 108 | 109 | 最大原因:深度太大(因为一个节点最多只有2个子节点),一次查询需要的I/O复杂度为O(lgN),而b+tree只需要O(log_mN),而其出度m非常大,其深度一般不会超过4 110 | 平衡二叉树逻辑上很近的父子节点,物理上可能很远,无法充分发挥磁盘顺序读和预读的高效特性。 111 | 112 | ## 9. MyISAM和InnoDB实现BTree索引方式的区别 113 | 114 | ### MyISAM 115 | 116 | B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。 117 | 索引文件和数据文件是分离的。 118 | 119 | ### InnoDB 120 | 121 | - InnoDB 的 B+Tree 索引分为主索引(聚集索引)和辅助索引(非聚集索引)。一张表一定包含一个聚集索引构成的 B+ 树以及若干辅助索引的构成的 B+ 树。 122 | - 辅助索引的存在并不会影响聚集索引,因为聚集索引构成的 B+ 树是数据实际存储的形式,而辅助索引只用于加速数据的查找,所以一张表上往往有多个辅助索引以此来提升数据库的性能。 123 | - 就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。 124 | 125 | ## 10. 什么是最左匹配原则? 126 | 127 | 最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。 128 | 例如:b = 2 如果建立(a,b)顺序的索引,是匹配不到(a,b)索引的;但是如果查询条件是a = 1 and b = 2,就可以,因为**优化器会自动调整a,b的顺序**。再比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,因为c字段是一个范围查询,它之后的字段会停止匹配。 129 | 130 | **最左匹配原则的原理** 131 | 132 | MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索引.最左匹配原则都是针对联合索引来说的 133 | 134 | - 我们都知道索引的底层是一颗B+树,那么联合索引当然还是一颗B+树,只不过联合索引的健值数量不是一个,而是多个。构建一颗B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建B+树。 135 | 例子:假如创建一个(a,b)的联合索引,那么它的索引树是这样的可以看到a的值是有顺序的,1,1,2,2,3,3,而b的值是没有顺序的1,2,1,4,1,2。所以b = 2这种查询条件没有办法利用索引,因为联合索引首先是按a排序的,b是无序的。 136 | 137 | 同时我们还可以发现在a值相等的情况下,b值又是按顺序排列的,但是这种顺序是相对的。所以最左匹配原则遇上范围查询就会停止,剩下的字段都无法使用索引。例如a = 1 and b = 2 a,b字段都可以使用索引,因为在a值确定的情况下b是相对有序的,而a>1and b=2,a字段可以匹配上索引,但b值不可以,因为a的值是一个范围,在这个范围中b是无序的。 138 | 139 | 优点:最左前缀原则的利用也可以显著提高查询效率,是常见的MySQL性能优化手段。 140 | 141 | ## 11. 哪些列上适合创建索引?创建索引有哪些开销? 142 | 143 | 经常需要作为条件查询的列上适合创建索引,并且该列上也必须有一定的区分度。创建索引需要维护,在插入数据的时候会重新维护各个索引树(数据页的分裂与合并),对性能造成影响 144 | 145 | ## 12. 索引这么多优点,为什么不对表中的每一个列创建一个索引呢? 146 | 147 | 1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 148 | 2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。 149 | 3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 150 | 151 | ## 13. **MySQL建表的约束条件有哪些**? 152 | 153 | - 主键约束(Primay Key Coustraint) 唯一性,非空性 154 | - 唯一约束 (Unique Counstraint)唯一性,可以空,但只能有一个 155 | - 检查约束 (Check Counstraint) 对该列数据的范围、格式的限制 156 | - 默认约束 (Default Counstraint) 该数据的默认值 157 | - 外键约束 (Foreign Key Counstraint) 需要建立两表间的关系并引用主表的列 158 | 159 | ## 14. MySQL执行查询的过程? 160 | 161 | 1. 客户端通过TCP连接发送连接请求到mysql连接器,连接器会对该请求进行权限验证及连接资源分配 162 | 2. 查缓存。(当判断缓存是否命中时,MySQL不会进行解析查询语句,而是直接使用SQL语句和客户端发送过来的其他原始信息。所以,任何字符上的不同,例如空格、注解等都会导致缓存的不命中。) 163 | 3. 语法分析(SQL语法是否写错了)。 如何把语句给到预处理器,检查数据表和数据列是否存在,解析别名看是否存在歧义。 164 | 4. 优化。是否使用索引,生成执行计划。 165 | 5. 交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端。 166 | 167 | ![](http://blog-img.coolsen.cn/img/image-20210220120155334.png) 168 | 169 | ## 15. MySQL的binlog有有几种录入格式?分别有什么区别? 170 | 171 | 有三种格式,statement,row和mixed. 172 | 173 | - statement模式下,记录单元为语句.即每一个sql造成的影响会记录.由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制. 174 | - row级别下,记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。 175 | - mixed. 一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row. 此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录. -------------------------------------------------------------------------------- /MySQL/基础、锁、事务、分库分表、优化.md: -------------------------------------------------------------------------------- 1 | # 基础 2 | 3 | ## 1. 数据库的三范式是什么? 4 | 5 | - 第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。 6 | - 第二范式:要求实体的属性完全依赖于主关键字。所谓完全 依赖是指不能存在仅依赖主关键字一部分的属性。 7 | - 第三范式:任何非主属性不依赖于其它非主属性。 8 | 9 | ## 2. MySQL 支持哪些存储引擎? 10 | 11 | MySQL 支持多种存储引擎,比如 InnoDB,MyISAM,Memory,Archive 等等.在大多数的情况下,直接选择使用 InnoDB 引擎都是最合适的,InnoDB 也是 MySQL 的默认存储引擎。 12 | 13 | MyISAM 和 InnoDB 的区别有哪些: 14 | 15 | - InnoDB 支持事务,MyISAM 不支持 16 | - InnoDB 支持外键,而 MyISAM 不支持 17 | - InnoDB 是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高;MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。 18 | - Innodb 不支持全文索引,而 MyISAM 支持全文索引,查询效率上 MyISAM 要高; 19 | - InnoDB 不保存表的具体行数,MyISAM 用一个变量保存了整个表的行数。 20 | - MyISAM 采用表级锁(table-level locking);InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 21 | 22 | ## 3. 超键、候选键、主键、外键分别是什么? 23 | 24 | * 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。 25 | * 候选键:是最小超键,即没有冗余元素的超键。 26 | * 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。 27 | * 外键:在一个表中存在的另一个表的主键称此表的外键。 28 | 29 | ## 4. SQL 约束有哪几种? 30 | 31 | * NOT NULL: 用于控制字段的内容一定不能为空(NULL)。 32 | * UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。 33 | * PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。 34 | * FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。 35 | * CHECK: 用于控制字段的值范围。 36 | 37 | ## 5. MySQL 中的 varchar 和 char 有什么区别? 38 | 39 | char 是一个定长字段,假如申请了`char(10)`的空间,那么无论实际存储多少内容.该字段都占用 10 个字符,而 varchar 是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间. 40 | 41 | 在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用 char,否则应该尽量使用 varchar.例如存储用户 MD5 加密后的密码,则应该使用 char。 42 | 43 | ## 6. MySQL中 in 和 exists 区别 44 | 45 | MySQL中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。 46 | 47 | 如果查询的两个表大小相当,那么用in和exists差别不大。 48 | 如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。 49 | not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。 50 | 51 | ## 7. drop、delete与truncate的区别 52 | 53 | 三者都表示删除,但是三者有一些差别: 54 | 55 | ![image-20210822203927822](http://blog-img.coolsen.cn/img/image-20210822203927822.png) 56 | 57 | ## 8. 什么是存储过程?有哪些优缺点? 58 | 59 | 存储过程是一些预编译的 SQL 语句。 60 | 61 | 1、更加直白的理解:存储过程可以说是一个记录集,它是由一些 T-SQL 语句组成的代码块,这些 T-SQL 语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。 62 | 63 | 2、存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量 T_SQL 语句 ,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全 64 | 65 | 但是,在互联网项目中,其实是不太推荐存储过程的,比较出名的就是阿里的《Java 开发手册》中禁止使用存储过程,我个人的理解是,在互联网项目中,迭代太快,项目的生命周期也比较短,人员流动相比于传统的项目也更加频繁,在这样的情况下,存储过程的管理确实是没有那么方便,同时,复用性也没有写在服务层那么好。 66 | 67 | ## 9. MySQL 执行查询的过程 68 | 69 | 1. 客户端通过 TCP 连接发送连接请求到 MySQL 连接器,连接器会对该请求进行权限验证及连接资源分配 70 | 2. 查缓存。(当判断缓存是否命中时,MySQL 不会进行解析查询语句,而是直接使用 SQL 语句和客户端发送过来的其他原始信息。所以,任何字符上的不同,例如空格、注解等都会导致缓存的不命中。) 71 | 3. 语法分析(SQL 语法是否写错了)。 如何把语句给到预处理器,检查数据表和数据列是否存在,解析别名看是否存在歧义。 72 | 4. 优化。是否使用索引,生成执行计划。 73 | 5. 交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端。 74 | 75 | 76 | 77 | ![img](https://static001.geekbang.org/infoq/41/4102b7d60fa20a0caabb127ecbb4d2f3.jpeg?x-oss-process=image/resize,p_80/auto-orient,1) 78 | 79 | 80 | 81 | 更新语句执行会复杂一点。需要检查表是否有排它锁,写 binlog,刷盘,是否执行 commit。 82 | 83 | # 事务 84 | 85 | ## 1. 什么是数据库事务? 86 | 事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。 87 | 88 | 事务最经典也经常被拿出来说例子就是转账了。 89 | 90 | 假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。 91 | 92 | ## 2. 介绍一下事务具有的四个特征 93 | 94 | 事务就是一组原子性的操作,这些操作要么全部发生,要么全部不发生。事务把数据库从一种一致性状态转换成另一种一致性状态。 95 | 96 | - 原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做 97 | - 一致性。事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。 98 | - 隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的//操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 99 | - 持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。 100 | 101 | ## 3. 说一下MySQL 的四种隔离级别 102 | 103 | - Read Uncommitted(读取未提交内容) 104 | 105 | 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。 106 | 107 | - Read Committed(读取提交内容) 108 | 109 | 这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓 的 不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。 110 | 111 | - Repeatable Read(可重读) 112 | 113 | 这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。 114 | 115 | - Serializable(可串行化) 116 | 117 | 通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。 118 | 119 | ![image-20210822180308501](http://blog-img.coolsen.cn/img/image-20210822180308501.png) 120 | 121 | MySQL 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别 122 | 123 | 事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。 124 | 125 | 因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。 126 | 127 | InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。 128 | 129 | ## 4. 什么是脏读?幻读?不可重复读? 130 | 131 | 1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据 132 | 133 | 2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果 不一致。 134 | 135 | 3、幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。 136 | 137 | 不可重复读侧重于修改,幻读侧重于新增或删除(多了或少量行),脏读是一个事务回滚影响另外一个事务。 138 | 139 | ## 5. 事务的实现原理 140 | 141 | 事务是基于重做日志文件(redo log)和回滚日志(undo log)实现的。 142 | 143 | 每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。 144 | 145 | 每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。 146 | 147 | ## 6. MySQL事务日志介绍下? 148 | 149 | innodb 事务日志包括 redo log 和 undo log。 150 | 151 | undo log 指事务开始之前,在操作任何数据之前,首先将需操作的数据备份到一个地方。redo log 指事务中操作的任何数据,将最新的数据备份到一个地方。 152 | 153 | 事务日志的目的:实例或者介质失败,事务日志文件就能派上用场。 154 | 155 | ### redo log 156 | 157 | redo log 不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo 中。具体的落盘策略可以进行配置 。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 MySQL 服务的时候,根据 redo log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。RedoLog 是为了实现事务的持久性而出现的产物。 158 | 159 | ![image-20210822181340692](http://blog-img.coolsen.cn/img/image-20210822181340692.png) 160 | 161 | ### undo log 162 | 163 | undo log 用来回滚行记录到某个版本。事务未提交之前,Undo 保存了未提交之前的版本数据,Undo 中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在 MySQL innodb 存储引擎中用来实现多版本并发控制。 164 | 165 | ![image-20210822181416382](http://blog-img.coolsen.cn/img/image-20210822181416382.png) 166 | 167 | ## 7. 什么是MySQL的 binlog? 168 | 169 | MySQL的 binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE)以及表数据修改(INSERT、UPDATE、DELETE)的二进制日志。binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看 MySQL 执行过的所有语句。 170 | 171 | MySQL binlog 以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。 172 | 173 | binlog 有三种格式,各有优缺点: 174 | 175 | * **statement:** 基于 SQL 语句的模式,某些语句和函数如 UUID, LOAD DATA INFILE 等在复制过程可能导致数据不一致甚至出错。 176 | 177 | * **row:** 基于行的模式,记录的是行的变化,很安全。但是 binlog 会比其他两种模式大很多,在一些大表中清除大量数据时在 binlog 中会生成很多条语句,可能导致从库延迟变大。 178 | 179 | * **mixed:** 混合模式,根据语句来选用是 statement 还是 row 模式。 180 | 181 | ## **8. 在事务中可以混合使用存储引擎吗?** 182 | 183 | 尽量不要在同一个事务中使用多种存储引擎,MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。 184 | 185 | 如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题。 186 | 187 | 但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。 188 | 189 | ## 9. MySQL中是如何实现事务隔离的? 190 | 191 | 读未提交和串行化基本上是不需要考虑的隔离级别,前者不加锁限制,后者相当于单线程执行,效率太差。 192 | 193 | MySQL 在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。 194 | 195 | 详细原理看这篇文章:https://haicoder.net/note/MySQL-interview/MySQL-interview-MySQL-trans-level.html 196 | 197 | ## 10. 什么是 MVCC? 198 | 199 | MVCC, 即多版本并发控制。MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。 200 | 201 | ## 11. MVCC 的实现原理 202 | 203 | 对于 InnoDB ,聚簇索引记录中包含 3 个隐藏的列: 204 | 205 | - ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。 206 | - 事务 ID:记录最后一次修改该记录的事务 ID。 207 | - 回滚指针:指向这条记录的上一个版本。 208 | 209 | 我们拿上面的例子,对应解释下 MVCC 的实现原理,如下图: 210 | 211 | ![img](http://blog-img.coolsen.cn/img/modb_95751916-225c-11eb-b0bb-5254001c05fe.png) 212 | 213 | 如图,首先 insert 语句向表 t1 中插入了一条数据,a 字段为 1,b 字段为 1, ROW ID 也为 1 ,事务 ID 假设为 1,回滚指针假设为 null。当执行 update t1 set b=666 where a=1 时,大致步骤如下: 214 | 215 | - 数据库会先对满足 a=1 的行加排他锁; 216 | - 然后将原记录复制到 undo 表空间中; 217 | - 修改 b 字段的值为 666,修改事务 ID 为 2; 218 | - 并通过隐藏的回滚指针指向 undo log 中的历史记录; 219 | - 事务提交,释放前面对满足 a=1 的行所加的排他锁。 220 | 221 | 在前面实验的第 6 步中,session2 查询的结果是 session1 修改之前的记录,这个记录就是**来自 undolog** 中。 222 | 223 | 因此可以总结出 MVCC 实现的原理大致是: 224 | 225 | InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。 226 | 227 | MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。 228 | 229 | 230 | 231 | # 锁 232 | 233 | ## 1. 为什么要加锁? 234 | 235 | 当多个用户并发地存取数据时,在[数据库](https://cloud.tencent.com/solution/database?from=10680)中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。 236 | 237 | 保证多用户环境下保证数据库完整性和一致性。 238 | 239 | ## 2. 按照锁的粒度分数据库锁有哪些? 240 | 241 | 在关系型数据库中,可以**按照锁的粒度把数据库锁分**为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。 242 | 243 | 行级锁 244 | 245 | - 行级锁是[MySQL](https://cloud.tencent.com/product/cdb?from=10680)中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。 246 | - 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 247 | 248 | 表级锁 249 | 250 | - 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。 251 | - 开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。 252 | 253 | 页级锁 254 | 255 | - 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁 256 | - 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般 257 | 258 | **MyISAM和InnoDB存储引擎使用的锁:** 259 | 260 | - MyISAM采用表级锁(table-level locking)。 261 | - InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁 262 | 263 | ## 3. 从锁的类别上分MySQL都有哪些锁呢? 264 | 从锁的类别上来讲,有共享锁和排他锁。 265 | 266 | * 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。 267 | 268 | * 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。 269 | 270 | 用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。 271 | 272 | 锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。 273 | 274 | 他们的加锁开销从大到小,并发能力也是从大到小。 275 | 276 | ## 4. 数据库的乐观锁和悲观锁是什么?怎么实现的? 277 | 数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。 278 | 279 | * 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制 280 | 281 | * 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。 282 | 283 | **两种锁的使用场景** 284 | 285 | 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。 286 | 287 | 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。 288 | 289 | ## 5. InnoDB引擎的行锁是怎么实现的? 290 | InnoDB是基于索引来完成行锁 291 | 292 | 例: select * from tab_with_index where id = 1 for update; 293 | 294 | for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起 295 | 296 | ## 6. 什么是死锁?怎么解决? 297 | 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。 298 | 299 | 常见的解决死锁的方法 300 | 301 | 1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。 302 | 303 | 2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率; 304 | 305 | 3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率; 306 | 307 | 如果业务处理不好可以用分布式事务锁或者使用乐观锁 308 | 309 | ## 7. 隔离级别与锁的关系 310 | 在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突 311 | 312 | 在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁; 313 | 314 | 在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。 315 | 316 | SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。 317 | 318 | ## 8. 优化锁方面的意见? 319 | * 使用较低的隔离级别 320 | * 设计索引,尽量使用索引去访问数据,加锁更加精确,从而减少锁冲突 321 | * 选择合理的事务大小,给记录显示加锁时,最好一次性请求足够级别的锁。列如,修改数据的话,最好申请排他锁,而不是先申请共享锁,修改时在申请排他锁,这样会导致死锁 322 | * 不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能的固定顺序的获取表中的行。这样大大的减少死锁的机会。 323 | * 尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响 324 | * 不要申请超过实际需要的锁级别 325 | * 数据查询的时候不是必要,不要使用加锁。MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能:MVCC只在committed read(读提交)和 repeatable read (可重复读)两种隔离级别 326 | * 对于特定的事务,可以使用表锁来提高处理速度活着减少死锁的可能。 327 | 328 | # 分库分表 329 | 330 | ## 1. 为什么要分库分表? 331 | 332 | **分表** 333 | 334 | 比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。 335 | 336 | 分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。 337 | 338 | **分库** 339 | 340 | 分库就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。 341 | 342 | 这就是所谓的分库分表。 343 | 344 | ![img](https://upload-images.jianshu.io/upload_images/14266602-ae74054f45f44e3d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 345 | 346 | ## 2. 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点? 347 | 348 | 这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。 349 | 350 | 比较常见的包括: 351 | 352 | - cobar 353 | - TDDL 354 | - atlas 355 | - sharding-jdbc 356 | - mycat 357 | 358 | #### cobar 359 | 360 | 阿里 b2b 团队开发和开源的,属于 proxy 层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。 361 | 362 | #### TDDL 363 | 364 | 淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。 365 | 366 | #### atlas 367 | 368 | 360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。 369 | 370 | #### sharding-jdbc 371 | 372 | 当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也**可以选择的方案**。 373 | 374 | #### mycat 375 | 376 | 基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。 377 | 378 | ## 3. 如何对数据库如何进行垂直拆分或水平拆分的? 379 | 380 | **水平拆分**的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。 381 | 382 | ![img](https:////upload-images.jianshu.io/upload_images/10089464-0e01dfe246b5c7ac.png?imageMogr2/auto-orient/strip|imageView2/2/w/474/format/webp) 383 | 384 | **垂直拆分**的意思,就是**把一个有很多字段的表给拆分成多个表**,**或者是多个库上去**。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会**将较少的访问频率很高的字段放到一个表里去**,然后**将较多的访问频率很低的字段放到另外一个表里去**。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。 385 | 386 | ![img](https:////upload-images.jianshu.io/upload_images/10089464-ab3069913c0f097c.png?imageMogr2/auto-orient/strip|imageView2/2/w/320/format/webp) 387 | 388 | 389 | 390 | 两种**分库分表的方式**: 391 | 392 | - 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如**时间范围**来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。 393 | - 或者是按照某个字段hash一下均匀分散,这个较为常用。 394 | 395 | range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。 396 | 397 | hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表 398 | 399 | # 读写分离、主从同步(复制) 400 | 401 | ## 1. 什么是MySQL主从同步? 402 | 403 | 主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。 404 | 405 | 因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。 406 | 407 | ## 2. MySQL主从同步的目的?为什么要做主从同步? 408 | 409 | 1. 通过增加从服务器来提高数据库的性能,在主服务器上执行写入和更新,在从服务器上向外提供读功能,可以动态地调整从服务器的数量,从而调整整个数据库的性能。 410 | 2. 提高数据安全-因为数据已复制到从服务器,从服务器可以终止复制进程,所以,可以在从服务器上备份而不破坏主服务器相应数据 411 | 3. 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能 412 | 4. 数据备份。一般我们都会做数据备份,可能是写定时任务,一些特殊行业可能还需要手动备份,有些行业要求备份和原数据不能在同一个地方,所以主从就能很好的解决这个问题,不仅备份及时,而且还可以多地备份,保证数据的安全 413 | 414 | ## 3. 如何实现MySQL的读写分离? 415 | 416 | 其实很简单,就是基于主从复制架构,简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。 417 | 418 | ## 4. MySQL主从复制流程和原理? 419 | 420 | 基本原理流程,是3个线程以及之间的关联 421 | 422 | 主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中; 423 | 424 | 从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中; 425 | 426 | 从:sql执行线程——执行relay log中的语句; 427 | 428 | **复制过程如下**: 429 | 430 | ![img](http://blog-img.coolsen.cn/img/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC85LzIxLzE2NWZiNjgzMjIyMDViMmU) 431 | 432 | Binary log:主数据库的二进制日志 433 | 434 | Relay log:从服务器的中继日志 435 | 436 | 第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。 437 | 438 | 第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。 439 | 440 | 第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。 441 | 442 | 443 | ## 5. MySQL主从同步延时问题如何解决? 444 | 445 | MySQL 实际上在有两个同步机制,一个是半同步复制,用来 解决主库数据丢失问题;一个是并行复制,用来 解决主从同步延时问题。 446 | 447 | - 半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。 448 | - 并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。 449 | 450 | # MySQL优化 451 | 452 | ## 1. 如何定位及优化SQL语句的性能问题? 453 | 454 | 对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL提供了explain命令来查看语句的执行计划。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。 455 | 456 | 而执行计划,就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等。 457 | ![image-20210822204026552](http://blog-img.coolsen.cn/img/image-20210822204026552.png) 458 | 459 | ## 2. 大表数据查询,怎么优化 460 | * 优化shema、sql语句+索引; 461 | * 第二加缓存,memcached, redis; 462 | * 主从复制,读写分离; 463 | * 垂直拆分,根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统; 464 | * 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key, 为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表; 465 | 466 | ## 3. 超大分页怎么处理? 467 | 468 | 数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于`select * from table where age > 20 limit 1000000`,10 这种查询其实也是有可以优化的余地的. 这条语句需要 load1000000 数据然后基本上全部丢弃,只取 10 条当然比较慢. 当时我们可以修改为`select * from table where id in (select id from table where age > 20 limit 1000000,10)`.这样虽然也 load 了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快。 469 | 470 | 解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可. 471 | 472 | 在阿里巴巴《Java开发手册》中,对超大分页的解决办法是类似于上面提到的第一种. 473 | 474 | > 【推荐】利用延迟关联或者子查询优化超多分页场景。 475 | > 476 | > 说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。 477 | > 478 | > 正例:先快速定位需要获取的id段,然后再关联: 479 | > 480 | > SELECT a.* FROM 表1 a, (select id from 表1 where 条件 LIMIT 100000,20 ) b where a.id=b.id 481 | 482 | ## 4. 统计过慢查询吗?对慢查询都怎么优化过? 483 | 484 | 在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。 485 | 486 | 慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大? 487 | 488 | 所以优化也是针对这三个方向来的, 489 | 490 | * 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。 491 | * 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。 492 | * 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。 493 | 494 | ## 5. 如何优化查询过程中的数据访问 495 | 496 | * 访问数据太多导致查询性能下降 497 | * 确定应用程序是否在检索大量超过需要的数据,可能是太多行或列 498 | * 确认MySQL服务器是否在分析大量不必要的数据行 499 | * 查询不需要的数据。解决办法:使用limit解决 500 | * 多表关联返回全部列。解决办法:指定列名 501 | * 总是返回全部列。解决办法:避免使用SELECT * 502 | * 重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存 503 | * 是否在扫描额外的记录。解决办法: 504 | 使用explain进行分析,如果发现查询需要扫描大量的数据,但只返回少数的行,可以通过如下技巧去优化: 505 | 使用索引覆盖扫描,把所有的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果。 506 | * 改变数据库和表的结构,修改数据表范式 507 | * 重写SQL语句,让优化器可以以更优的方式执行查询。 508 | 509 | ## 6. 如何优化关联查询 510 | 511 | - 确定ON或者USING子句中是否有索引。 512 | - 确保GROUP BY和ORDER BY只有一个表中的列,这样MySQL才有可能使用索引。 513 | 514 | ## 7. 数据库结构优化 515 | 516 | 一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。 517 | 518 | 需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。 519 | 520 | 1. **将字段很多的表分解成多个表** 521 | 522 | 对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。 523 | 524 | 因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。 525 | 526 | 2. **增加中间表** 527 | 528 | 对于需要经常联合查询的表,可以建立中间表以提高查询效率。 529 | 530 | 通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。 531 | 532 | 3. **增加冗余字段** 533 | 534 | 设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。 535 | 536 | 表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。 537 | 538 | 注意: 539 | 540 | 冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。 541 | ## 8. MySQL数据库cpu飙升到500%的话他怎么处理? 542 | 当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 MySQLd 占用导致的,如果不是,找出占用高的进程,并进行相关处理。 543 | 544 | 如果是 MySQLd 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。 545 | 546 | 一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。 547 | 548 | 也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。 549 | ## 9. 大表怎么优化? 550 | 551 | 类似的问题:某个表有近千万数据,CRUD比较慢,如何优化?分库分表了是怎么做的?分表分库了有什么问题?有用到中间件么?他们的原理知道么? 552 | 553 | 当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下: 554 | 555 | * 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内; 556 | * 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读; 557 | * 缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑; 558 | * 通过分库分表的方式进行优化,主要有垂直分表和水平分表。 559 | 560 | ## 参考 561 | 562 | https://blog.csdn.net/ThinkWon/article/details/104778621 563 | 564 | https://haicoder.net/note/mysql-interview/mysql-interview-mysql-binlog.html 565 | 566 | https://www.modb.pro/db/40241 567 | 568 | https://www.jianshu.com/p/05da0fc0950e 569 | 570 | https://blog.csdn.net/ThinkWon/article/details/104778621 -------------------------------------------------------------------------------- /MySQL/索引连环18问.md: -------------------------------------------------------------------------------- 1 | ## 1. 索引是什么? 2 | 3 | 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。 4 | 5 | 索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。而且索引是一个文件,它是要占据物理空间的。 6 | 7 | MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。比如我们在查字典的时候,前面都有检索的拼音和偏旁、笔画等,然后找到对应字典页码,这样然后就打开字典的页数就可以知道我们要搜索的某一个key的全部值的信息了。 8 | 9 | ## 2. 索引有哪些优缺点? 10 | 11 | **索引的优点** 12 | 13 | * 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。 14 | * 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。 15 | 16 | **索引的缺点** 17 | 18 | * 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率; 19 | * 空间方面:索引需要占物理空间。 20 | 21 | ## 3. MySQL有哪几种索引类型? 22 | 23 | 1、从存储结构上来划分:BTree索引(B-Tree或B+Tree索引),Hash索引,full-index全文索引,R-Tree索引。这里所描述的是索引存储时保存的形式, 24 | 25 | 2、从应用层次来分:普通索引,唯一索引,复合索引。 26 | 27 | * 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引 28 | 29 | * 唯一索引:索引列的值必须唯一,但允许有空值 30 | 31 | * 复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并 32 | 33 | * 聚簇索引(聚集索引):并不是一种单独的索引类型,而是一种数据存储方式。具体细节取决于不同的实现,InnoDB的聚簇索引其实就是在同一个结构中保存了B-Tree索引(技术上来说是B+Tree)和数据行。 34 | 35 | * 非聚簇索引: 不是聚簇索引,就是非聚簇索引 36 | 37 | 3、根据中数据的物理顺序与键值的逻辑(索引)顺序关系: 聚集索引,非聚集索引。 38 | 39 | ## 4. 说一说索引的底层实现? 40 | 41 | **Hash索引** 42 | 43 | 基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且Hash索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。 44 | 45 | > 图片来源:https://www.javazhiyin.com/40232.html 46 | 47 | ![](http://blog-img.coolsen.cn/img/image-20210411215012443.png) 48 | 49 | **B-Tree索引**(MySQL使用B+Tree) 50 | 51 | B-Tree能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中。 52 | 53 | ![](http://blog-img.coolsen.cn/img/image-20210411215023820.png) 54 | 55 | **B+Tree索引** 56 | 57 | 是B-Tree的改进版本,同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比B-Tree来说,进行范围查找时只需要查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高。 58 | 59 | B+tree性质: 60 | 61 | * n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。 62 | 63 | * 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。 64 | 65 | * 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。 66 | 67 | * B+ 树中,数据对象的插入和删除仅在叶节点上进行。 68 | 69 | * B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。 70 | 71 | ![](http://blog-img.coolsen.cn/img/image-20210411215044332.png) 72 | 73 | 74 | 75 | ## 5. 为什么索引结构默认使用B+Tree,而不是B-Tree,Hash,二叉树,红黑树? 76 | 77 | B-tree: 从两个方面来回答 78 | 79 | * B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B(B-)树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对`IO读写次数就降低`了。 80 | 81 | * 由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在`区间查询`的情况,所以通常B+树用于数据库索引。 82 | 83 | Hash: 84 | 85 | * 虽然可以快速定位,但是没有顺序,IO复杂度高; 86 | 87 | - 基于Hash表实现,只有Memory存储引擎显式支持哈希索引 ; 88 | 89 | - 适合**等值查询**,如=、in()、<=>,不支持范围查询 ; 90 | 91 | - 因为不是按照索引值顺序存储的,就不能像B+Tree索引一样利用索引完成[排序]() ; 92 | 93 | - Hash索引在查询等值时非常快 ; 94 | 95 | - 因为Hash索引始终索引的**所有列的全部内容**,所以不支持部分索引列的匹配查找 ; 96 | 97 | - 如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题 。 98 | 99 | 二叉树: 树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。 100 | 101 | 红黑树: 树的高度随着数据量增加而增加,IO代价高。 102 | 103 | ## 6. 讲一讲聚簇索引与非聚簇索引? 104 | 105 | 在 InnoDB 里,索引B+ Tree的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引,即将数据存储与索引放到了一块,找到索引也就找到了数据。 106 | 107 | 而索引B+ Tree的叶子节点存储了主键的值的是非主键索引,也被称之为非聚簇索引、二级索引。 108 | 109 | 聚簇索引与非聚簇索引的区别: 110 | 111 | - 非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键(行号) 112 | 113 | - 对于InnoDB来说,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为**回表**。第一次索引一般是顺序IO,回表的操作属于随机IO。需要回表的次数越多,即随机IO次数越多,我们就越倾向于使用全表扫描 。 114 | 115 | - 通常情况下, 主键索引(聚簇索引)查询只会查一次,而非主键索引(非聚簇索引)需要回表查询多次。当然,如果是覆盖索引的话,查一次即可 116 | 117 | - 注意:MyISAM无论主键索引还是二级索引都是非聚簇索引,而InnoDB的主键索引是聚簇索引,二级索引是非聚簇索引。我们自己建的索引基本都是非聚簇索引。 118 | 119 | ## 7. 非聚簇索引一定会回表查询吗? 120 | 121 | 不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。一个索引包含(覆盖)所有需要查询字段的值,被称之为"覆盖索引"。 122 | 123 | 举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行`select score from student where score > 90`的查询时,在索引的叶子节点上,已经包含了score 信息,不会再次进行回表查询。 124 | 125 | ## 8. 联合索引是什么?为什么需要注意联合索引中的顺序? 126 | 127 | MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。 128 | 129 | 具体原因为: 130 | 131 | MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。 132 | 133 | 当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。 134 | 135 | ## 9. 讲一讲MySQL的最左前缀原则? 136 | 137 | 最左前缀原则就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。 138 | mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 139 | 140 | =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。 141 | 142 | ## 10. 讲一讲前缀索引? 143 | 144 | 因为可能我们索引的字段非常长,这既占内存空间,也不利于维护。所以我们就想,如果只把很长字段的前面的公共部分作为一个索引,就会产生超级加倍的效果。但是,我们需要注意,order by不支持前缀索引 。 145 | 146 | 流程是: 147 | 148 | 先计算完整列的选择性 :` select count(distinct col_1)/count(1) from table_1 ` 149 | 150 | 再计算不同前缀长度的选择性 :` select count(distinct left(col_1,4))/count(1) from table_1 ` 151 | 152 | 找到最优长度之后,创建前缀索引 :` create index idx_front on table_1 (col_1(4))` 153 | 154 | ## 11. 了解索引下推吗? 155 | 156 | MySQL 5.6引入了索引下推优化。默认开启,使用SET optimizer_switch = ‘index_condition_pushdown=off’;可以将其关闭。 157 | 158 | - 有了索引下推优化,可以在**减少回表次数** 159 | 160 | - 在InnoDB中只针对二级索引有效 161 | 162 | 官方文档中给的例子和解释如下: 163 | 164 | 在 people_table中有一个二级索引(zipcode,lastname,address),查询是SELECT * FROM people WHERE zipcode=’95054′ AND lastname LIKE ‘%etrunia%’ AND address LIKE ‘%Main Street%’; 165 | 166 | * 如果没有使用索引下推技术,则MySQL会通过zipcode=’95054’从存储引擎中查询对应的数据,返回到MySQL服务端,然后MySQL服务端基于lastname LIKE ‘%etrunia%’ and address LIKE ‘%Main Street%’来判断数据是否符合条件 167 | 168 | * 如果使用了索引下推技术,则MYSQL首先会返回符合zipcode=’95054’的索引,然后根据lastname LIKE ‘%etrunia%’ and address LIKE ‘%Main Street%’来判断索引是否符合条件。如果符合条件,则根据该索引来定位对应的数据,如果不符合,则直接reject掉。 169 | 170 | ## 12. 怎么查看MySQL语句有没有用到索引? 171 | 172 | 通过explain,如以下例子: 173 | 174 | ` EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26'; ` 175 | 176 | | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | filtered | rows | Extra | 177 | | ---- | ----------- | ------ | ---------- | ----- | ------------- | ------- | ------- | ----------------- | -------- | ---- | ----- | 178 | | 1 | SIMPLE | titles | null | const | PRIMARY | PRIMARY | 59 | const,const,const | 10 | 1 | | 179 | 180 | 181 | 182 | 183 | * id:在⼀个⼤的查询语句中每个**SELECT**关键字都对应⼀个唯⼀的id ,如explain select * from s1 where id = (select id from s1 where name = 'egon1');第一个select的id是1,第二个select的id是2。有时候会出现两个select,但是id却都是1,这是因为优化器把子查询变成了连接查询 。 184 | 185 | * select_type:select关键字对应的那个查询的类型,如SIMPLE,PRIMARY,SUBQUERY,DEPENDENT,SNION 。 186 | 187 | * table:每个查询对应的表名 。 188 | 189 | * type:`type` 字段比较重要, 它提供了判断查询是否高效的重要依据依据. 通过 `type` 字段, 我们判断此次查询是 `全表扫描` 还是 `索引扫描` 等。如const(主键索引或者唯一二级索引进行等值匹配的情况下),ref(普通的⼆级索引列与常量进⾏等值匹配),index(扫描全表索引的覆盖索引) 。 190 | 191 | 通常来说, 不同的 type 类型的性能关系如下: 192 | `ALL < index < range ~ index_merge < ref < eq_ref < const < system` 193 | `ALL` 类型因为是全表扫描, 因此在相同的查询条件下, 它是速度最慢的. 194 | 而 `index` 类型的查询虽然不是全表扫描, 但是它扫描了所有的索引, 因此比 ALL 类型的稍快. 195 | 196 | * possible_key:查询中可能用到的索引*(可以把用不到的删掉,降低优化器的优化时间)* 。 197 | 198 | * key:此字段是 MySQL 在当前查询时所真正使用到的索引。 199 | 200 | * filtered:查询器预测满足下一次查询条件的百分比 。 201 | 202 | * rows 也是一个重要的字段. MySQL 查询优化器根据统计信息, 估算 SQL 要查找到结果集需要扫描读取的数据行数. 203 | 这个值非常直观显示 SQL 的效率好坏, 原则上 rows 越少越好。 204 | 205 | * extra:表示额外信息,如Using where,Start temporary,End temporary,Using temporary等。 206 | 207 | ## 13. 为什么官方建议使用自增长主键作为索引? 208 | 209 | 结合B+Tree的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据的移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率。 210 | 211 | 插入连续的数据: 212 | 213 | > 图片来自:https://www.javazhiyin.com/40232.html 214 | 215 | ![](http://blog-img.coolsen.cn/img/java10-1562726251.gif) 216 | 217 | 插入非连续的数据: 218 | 219 | ![](http://blog-img.coolsen.cn/img/java8-1562726251.gif) 220 | 221 | ## 14. 如何创建索引? 222 | 223 | 创建索引有三种方式。 224 | 225 | 1、 在执行CREATE TABLE时创建索引 226 | 227 | ```sql 228 | CREATE TABLE user_index2 ( 229 | id INT auto_increment PRIMARY KEY, 230 | first_name VARCHAR (16), 231 | last_name VARCHAR (16), 232 | id_card VARCHAR (18), 233 | information text, 234 | KEY name (first_name, last_name), 235 | FULLTEXT KEY (information), 236 | UNIQUE KEY (id_card) 237 | ); 238 | 239 | ``` 240 | 241 | 2、 使用ALTER TABLE命令去增加索引。 242 | 243 | ```sql 244 | ALTER TABLE table_name ADD INDEX index_name (column_list); 245 | ``` 246 | 247 | ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。 248 | 249 | 其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。 250 | 251 | 索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。 252 | 3、 使用CREATE INDEX命令创建。 253 | 254 | ```sql 255 | CREATE INDEX index_name ON table_name (column_list); 256 | ``` 257 | 258 | ## 15. 创建索引时需要注意什么? 259 | 260 | * 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值; 261 | * 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高; 262 | * 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。 263 | 264 | ## 16. 建索引的原则有哪些? 265 | 266 | 1、最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 267 | 268 | 2、=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。 269 | 270 | 3、尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。 271 | 272 | 4、索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)。 273 | 274 | 5、尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。 275 | 276 | ## 17. 使用索引查询一定能提高查询的性能吗? 277 | 278 | 通常通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。 279 | 280 | 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的I* NSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况: 281 | 282 | * 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%。 283 | * 基于非唯一性索引的检索。 284 | 285 | ## 18. 什么情况下不走索引(索引失效)? 286 | 287 | ##### 1、使用!= 或者 <> 导致索引失效 288 | 289 | ##### 2、类型不一致导致的索引失效 290 | 291 | ##### 3、函数导致的索引失效 292 | 293 | 如: 294 | 295 | ``` 296 | SELECT * FROM `user` WHERE DATE(create_time) = '2020-09-03'; 297 | ``` 298 | 299 | 如果使用函数在索引列,这是不走索引的。 300 | 301 | ##### 4、运算符导致的索引失效 302 | 303 | ``` 304 | SELECT * FROM `user` WHERE age - 1 = 20; 305 | ``` 306 | 307 | 如果你对列进行了(+,-,*,/,!), 那么都将不会走索引。 308 | 309 | ##### 5、OR引起的索引失效 310 | 311 | ``` 312 | SELECT * FROM `user` WHERE `name` = '张三' OR height = '175'; 313 | ``` 314 | 315 | OR导致索引是在特定情况下的,并不是所有的OR都是使索引失效,如果OR连接的是同一个字段,那么索引不会失效,反之索引失效。 316 | 317 | ##### 6、模糊搜索导致的索引失效 318 | 319 | ``` 320 | SELECT * FROM `user` WHERE `name` LIKE '%冰'; 321 | ``` 322 | 323 | 当`%`放在匹配字段前是不走索引的,放在后面才会走索引。 324 | 325 | ##### 7、NOT IN、NOT EXISTS导致索引失效 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-Interview 2 | 项目整合: 3 | 4 | ​ [地鼠文档 (topgoer.cn)](https://topgoer.cn/) 5 | 6 | ​ [lifei6671/interview-go: golang面试题集合 (github.com)](https://github.com/lifei6671/interview-go) 7 | 8 | ​ [cosen1024/Java-Interview: Java面试小抄(github.com)](https://github.com/cosen1024/Java-Interview) 9 | 10 | ​ [代码随想录 (programmercarl.com)](https://www.programmercarl.com/) 11 | 12 | ​ [极客兔兔 (geektutu.com)](https://geektutu.com/) 13 | 14 | 特此申明: 15 | 16 | ​ Go初学者,为了未来的求职和更专注学习,所以特此整理以上资料。当然可能在收集过程中遗漏了参考资料。若有问题,请及时联系! 17 | 18 | ​ 喜欢的话,fork、star,一起学习Go,在大佬的资料下,争取做一个基础和进阶的开源好项目,帮助我们一起学习! 19 | 20 | ​ 一个Go学习者 21 | 22 | ​ 2022年2月14号 23 | 24 |
25 | 26 | #### 2022-3-17 27 | 28 | 整理一个月左右,梳理网上的大多数Go基础、并发、Mysql、网络、操作系统、Redis的知识。剔除一些其他仓库里的高阶知识。只是为了学生步入职场,打牢基础! 29 | 30 | 31 | 32 | ### 2022-3-18 33 | 34 | 梳理和完善: 35 | 36 | go基础——3.Go基础类:9-38题的解答注释,涉及slice、map、channel、goroutine等。 37 | 38 | 第一次上传到GitHub、Gitee,希望遇到一起努力前行的人! 39 | 40 | 41 | 42 | ### 2022-3-19 43 | 44 | 梳理: 45 | 46 | Go基础:4.Go基础应用,几种有设计几种测试框架,现在也只知道其名字,具体功能不清楚。 47 | 48 | Go并发:1.Go并发基础【梳理部分】 49 |
-------------------------------------------------------------------------------- /操作系统/操作系统.md: -------------------------------------------------------------------------------- 1 | ## 1. 进程和线程的区别? 2 | 3 | - 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。 4 | - 切换:线程上下文切换比进程上下文切换要快得多。 5 | - 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。 6 | - 系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。 7 | 8 | ## 2. 协程与线程的区别? 9 | 10 | - 线程和进程都是同步机制,而协程是异步机制。 11 | - 线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。 12 | - 一个线程可以有多个协程,一个进程也可以有多个协程。 13 | - 协程不被操作系统内核管理,而完全是由程序控制。线程是被分割的CPU资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。 14 | 15 | * 一次调用时的状态。 16 | 17 | ## 3. 并发和并行有什么区别? 18 | 19 | 并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器可以做到并发。比如有两个进程`A`和`B`,`A`运行一个时间片之后,切换到`B`,`B`运行一个时间片之后又切换到`A`。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。 20 | 21 | 并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。 22 | 23 | ## 4. 进程与线程的切换流程? 24 | 25 | 进程切换分两步: 26 | 27 | 1、切换**页表**以使用新的地址空间,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。 28 | 29 | 2、切换内核栈和硬件上下文。 30 | 31 | 对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2步是进程和线程切换都要做的。 32 | 33 | 因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。 34 | 35 | ## 5. 为什么虚拟地址空间切换会比较耗时? 36 | 37 | 进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(translation Lookaside Buffer,TLB本质上就是一个Cache,是用来加速页表查找的)。 38 | 39 | 由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么**当进程切换后页表也要进行切换,页表切换后TLB就失效了**,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。 40 | 41 | ## 6. 进程间通信方式有哪些? 42 | 43 | - 管道:管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 44 | 45 | 管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 46 | 47 | - 信号 : 信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。 48 | 49 | > **Linux系统中常用信号**: 50 | > (1)**SIGHUP**:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。 51 | > 52 | > (2)**SIGINT**:程序终止信号。程序运行过程中,按`Ctrl+C`键将产生该信号。 53 | > 54 | > (3)**SIGQUIT**:程序退出信号。程序运行过程中,按`Ctrl+\\`键将产生该信号。 55 | > 56 | > (4)**SIGBUS和SIGSEGV**:进程访问非法地址。 57 | > 58 | > (5)**SIGFPE**:运算中出现致命错误,如除零操作、数据溢出等。 59 | > 60 | > (6)**SIGKILL**:用户终止进程执行信号。shell下执行`kill -9`发送该信号。 61 | > 62 | > (7)**SIGTERM**:结束进程信号。shell下执行`kill 进程pid`发送该信号。 63 | > 64 | > (8)**SIGALRM**:定时器信号。 65 | > 66 | > (9)**SIGCLD**:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。 67 | 68 | - 信号量:信号量是一个**计数器**,可以用来控制多个进程对共享资源的访问。它常作为一种**锁机制**,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 69 | 70 | - 消息队列:消息队列是消息的链接表,包括Posix消息队列和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。 71 | 72 | - 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。 73 | 74 | - Socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。 75 | 76 | **优缺点**: 77 | 78 | * 管道:速度慢,容量有限; 79 | 80 | * Socket:任何进程间都能通讯,但速度慢; 81 | 82 | * 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题; 83 | 84 | * 信号量:不能传递复杂消息,只能用来同步; 85 | 86 | * 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。 87 | 88 | ## 7. 进程间同步的方式有哪些? 89 | 90 | 1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。 91 | 92 | 优点:保证在某一时刻只有一个线程能访问数据的简便办法。 93 | 94 | 缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。 95 | 96 | 2、互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。 97 | 98 | 优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。 99 | 100 | 缺点: 101 | 102 | * 互斥量是可以命名的,也就是说它可以跨越进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。 103 | 104 | * 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。 105 | 106 | 3、信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。 107 | 108 | 优点:适用于对Socket(套接字)程序中线程的同步。 109 | 110 | 缺点: 111 | 112 | * 信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点; 113 | 114 | * 信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担; 115 | 116 | * 核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。 117 | 118 | 4、事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。 119 | 120 | 优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。 121 | 122 | ## 8. 线程同步的方式有哪些? 123 | 124 | 1、临界区:当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操 作共享资源的目的。 125 | 126 | 2、事件:事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。 127 | 128 | 3、互斥量:互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。 129 | 130 | 4、信号量:当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。 131 | 132 | 区别: 133 | 134 | * 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说互斥量可以跨越进程使用,但创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。 135 | 136 | * 互斥量,信号量,事件都可以被跨越进程使用来进行同步数据操作。 137 | 138 | ## 9. 线程的分类? 139 | 140 | 从线程的运行空间来说,分为用户级线程(user-level thread, ULT)和内核级线程(kernel-level, KLT) 141 | 142 | **内核级线程**:这类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程,它们的创建、撤销和切换都由内核实现。比如英特尔i5-8250U是4核8线程,这里的线程就是内核级线程 143 | 144 | **用户级线程**:它仅存在于用户级中,这种线程是**不依赖于操作系统核心**的。应用进程利用**线程库来完成其创建和管理**,速度比较快,**操作系统内核无法感知用户级线程的存在**。 145 | 146 | ## 10. 什么是临界区,如何解决冲突? 147 | 148 | 每个进程中访问临界资源的那段程序称为临界区,**一次仅允许一个进程使用的资源称为临界资源。** 149 | 150 | 解决冲突的办法: 151 | 152 | - 如果有若干进程要求进入空闲的临界区,**一次仅允许一个进程进入**,如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待; 153 | - 进入临界区的进程要在**有限时间内退出**。 154 | - 如果进程不能进入自己的临界区,则应**让出CPU**,避免进程出现“忙等”现象。 155 | 156 | ## 11. 什么是死锁?死锁产生的条件? 157 | 158 | **什么是死锁**: 159 | 160 | 在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。 161 | 162 | **死锁产生的四个必要条件**:(有一个条件不成立,则不会产生死锁) 163 | 164 | - 互斥条件:一个资源一次只能被一个进程使用 165 | - 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放 166 | - 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺 167 | - 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系 168 | 169 | ### **如何处理死锁问题** 170 | 171 | 常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略。 172 | 173 | **(1)死锁的预防:**基本思想就是确保死锁发生的四个必要条件中至少有一个不成立: 174 | 175 | > - ① 破除资源互斥条件 176 | > - ② 破除“请求与保持”条件:实行资源预分配策略,进程在运行之前,必须一次性获取所有的资源。缺点:在很多情况下,无法预知进程执行前所需的全部资源,因为进程是动态执行的,同时也会降低资源利用率,导致降低了进程的并发性。 177 | > - ③ 破除“不可剥夺”条件:允许进程强行从占有者那里夺取某些资源。当一个已经保持了某些不可被抢占资源的进程,提出新的资源请求而不能得到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请。这意味着进程已经占有的资源会被暂时被释放,或者说被抢占了。 178 | > - ④ 破除“循环等待”条件:实行资源有序分配策略,对所有资源排序编号,按照顺序获取资源,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。 179 | 180 | **(2)死锁避免:** 181 | 182 | 死锁预防通过约束资源请求,防止4个必要条件中至少一个的发生,可以通过直接或间接预防方法,但是都会导致低效的资源使用和低效的进程执行。而死锁避免则允许前三个必要条件,但是通过动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。银行家算法是经典的死锁避免的算法。 183 | 184 | **(3)死锁检测:** 185 | 186 | 死锁预防策略是非常保守的,他们通过限制访问资源和在进程上强加约束来解决死锁的问题。死锁检测则是完全相反,它不限制资源访问或约束进程行为,只要有可能,被请求的资源就被授权给进程。但是操作系统会周期性地执行一个算法检测前面的循环等待的条件。死锁检测算法是通过资源分配图来检测是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有存在环,也就是检测到死锁的发生。 187 | 188 | > - (1)如果进程-资源分配图中无环路,此时系统没有死锁。 189 | > - (2)如果进程-资源分配图中有环路,且每个资源类中只有一个资源,则系统发生死锁。 190 | > - (3)如果进程-资源分配图中有环路,且所涉及的资源类有多个资源,则不一定会发生死锁。 191 | 192 | **(4)死锁解除:** 193 | 194 | 死锁解除的常用方法就是终止进程和资源抢占,回滚。所谓进程终止就是简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;所谓资源抢占就是从一个或者多个死锁进程那里抢占一个或多个资源。 195 | 196 | **(5)鸵鸟策略:** 197 | 198 | 把头埋在沙子里,假装根本没发生问题。因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任何措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。 199 | 200 | 201 | 202 | ## 12. 进程调度策略有哪几种? 203 | 204 | * **先来先服务**:非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。另外,对`I/O`密集型进程也不利,因为这种进程每次进行`I/O`操作之后又得重新排队。 205 | 206 | * **短作业优先**:非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 207 | 208 | * **最短剩余时间优先**:最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。 209 | 210 | * **时间片轮转**:将所有就绪进程按 `FCFS` 的原则排成一个队列,每次调度时,把 `CPU` 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 `CPU` 时间分配给队首的进程。 211 | 212 | 时间片轮转算法的效率和时间片的大小有很大关系:因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 而如果时间片过长,那么实时性就不能得到保证。 213 | 214 | * **优先级调度**:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。 215 | 216 | ## 13. 进程有哪些状态? 217 | 218 | 进程一共有`5`种状态,分别是创建、就绪、运行(执行)、终止、阻塞。 219 | 220 | ![进程五种状态转换图](http://blog-img.coolsen.cn/img/A61F5B5322ED49038C64BDD82D341987) 221 | 222 | - 运行状态就是进程正在`CPU`上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。 223 | - 就绪状态就是说进程已处于准备运行的状态,即进程获得了除`CPU`之外的一切所需资源,一旦得到`CPU`即可运行。 224 | - 阻塞状态就是进程正在等待某一事件而暂停运行,比如等待某资源为可用或等待`I/O`完成。即使`CPU`空闲,该进程也不能运行。 225 | 226 | **运行态→阻塞态**:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。 227 | **阻塞态→就绪态**:则是等待的条件已满足,只需分配到处理器后就能运行。 228 | **运行态→就绪态**:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。 229 | **就绪态→运行态**:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。 230 | 231 | ## 14. 什么是分页? 232 | 233 | 把内存空间划分为**大小相等且固定的块**,作为主存的基本单位。因为程序数据存储在不同的页面中,而页面又离散的分布在内存中,**因此需要一个页表来记录映射关系,以实现从页号到物理块号的映射。** 234 | 235 | 访问分页系统中内存数据需要**两次的内存访问** (一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址;第二次就是根据第一次得到的物理地址访问内存取出数据)。 236 | 237 | ![](http://blog-img.coolsen.cn/img/image-20210610173249387.png) 238 | 239 | ## 15. 什么是分段? 240 | 241 | **分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。** 242 | 243 | 分段内存管理当中,**地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的**。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。 244 | 245 | ![](http://blog-img.coolsen.cn/img/image-20210610173410509.png) 246 | 247 | ## 16. 分页和分段有什区别? 248 | 249 | - 分页对程序员是透明的,但是分段需要程序员显式划分每个段。 250 | - 分页的地址空间是一维地址空间,分段是二维的。 251 | - 页的大小不可变,段的大小可以动态改变。 252 | - 分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 253 | 254 | ## 17. 什么是交换空间? 255 | 256 | 操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为**页(page)**。当内存资源不足时,**Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间**。硬盘上的那块空间叫做**交换空间**(swap space),而这一过程被称为交换(swapping)。**物理内存和交换空间的总容量就是虚拟内存的可用容量。** 257 | 258 | 用途: 259 | 260 | - 物理内存不足时一些不常用的页可以被交换出去,腾给系统。 261 | - 程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去。 262 | 263 | ## 18. 物理地址、逻辑地址、有效地址、线性地址、虚拟地址的区别? 264 | 265 | 物理地址就是内存中真正的地址,它就相当于是你家的门牌号,你家就肯定有这个门牌号,具有唯一性。**不管哪种地址,最终都会映射为物理地址**。 266 | 267 | 在`实模式`下,段基址 + 段内偏移经过地址加法器的处理,经过地址总线传输,最终也会转换为`物理地址`。 268 | 269 | 但是在`保护模式`下,段基址 + 段内偏移被称为`线性地址`,不过此时的段基址不能称为真正的地址,而是会被称作为一个`选择子`的东西,选择子就是个索引,相当于数组的下标,通过这个索引能够在 GDT 中找到相应的段描述符,段描述符记录了**段的起始、段的大小**等信息,这样便得到了基地址。如果此时没有开启内存分页功能,那么这个线性地址可以直接当做物理地址来使用,直接访问内存。如果开启了分页功能,那么这个线性地址又多了一个名字,这个名字就是`虚拟地址`。 270 | 271 | 不论在实模式还是保护模式下,段内偏移地址都叫做`有效地址`。有效抵制也是逻辑地址。 272 | 273 | 线性地址可以看作是`虚拟地址`,虚拟地址不是真正的物理地址,但是虚拟地址会最终被映射为物理地址。下面是虚拟地址 -> 物理地址的映射。 274 | 275 | ![image-20210807152300643](http://blog-img.coolsen.cn/img/image-20210807152300643.png) 276 | 277 | ## 19. 页面替换算法有哪些? 278 | 279 | 在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 280 | 281 | ![image-20210807152232136](http://blog-img.coolsen.cn/img/image-20210807152232136.png) 282 | 283 | - `最优算法`在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,`因此实际上该算法不能使用`。然而,它可以作为衡量其他算法的标准。 284 | - `NRU` 算法根据 R 位和 M 位的状态将页面分为四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。 285 | - `FIFO` 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。 286 | - `第二次机会`算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。 287 | - `时钟` 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。 288 | - `LRU` 算法是一个非常优秀的算法,但是没有`特殊的硬件(TLB)`很难实现。如果没有硬件,就不能使用 LRU 算法。 289 | - `NFU` 算法是一种近似于 LRU 的算法,它的性能不是非常好。 290 | - `老化` 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择 291 | - 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。`WSClock` 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。 292 | 293 | **最好的算法是老化算法和WSClock算法**。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。 294 | 295 | ## 20. 什么是缓冲区溢出?有什么危害? 296 | 297 | 缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。 298 | 299 | 危害有以下两点: 300 | 301 | - 程序崩溃,导致拒绝额服务 302 | - 跳转并且执行一段恶意代码 303 | 304 | 造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。 305 | 306 | ## 21. 什么是虚拟内存? 307 | 308 | 虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。 309 | 310 | ## 22. 虚拟内存的实现方式有哪些? 311 | 312 | 虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或`永久`的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上。虚拟内存的实现有以下三种方式: 313 | 314 | - 请求分页存储管理。 315 | - 请求分段存储管理。 316 | - 请求段页式存储管理。 317 | 318 | ## 23. 讲一讲IO多路复用? 319 | 320 | **IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合**: 321 | 322 | - 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。 323 | - 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。 324 | - 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。 325 | - 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。 326 | - 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。 327 | - 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。 328 | 329 | ## 24. 硬链接和软链接有什么区别? 330 | 331 | - 硬链接就是在目录下创建一个条目,记录着文件名与 `inode` 编号,这个 `inode` 就是源文件的 `inode`。删除任意一个条目,文件还是存在,只要引用数量不为 `0`。但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接。 332 | - 符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 `Windows` 的快捷方式。当源文件被删除了,链接文件就打不开了。因为记录的是路径,所以可以为目录建立符号链接。 333 | 334 | ## 25. 中断的处理过程? 335 | 336 | 1. 保护现场:将当前执行程序的相关数据保存在寄存器中,然后入栈。 337 | 2. 开中断:以便执行中断时能响应较高级别的中断请求。 338 | 3. 中断处理 339 | 4. 关中断:保证恢复现场时不被新中断打扰 340 | 5. 恢复现场:从堆栈中按序取出程序数据,恢复中断前的执行状态。 341 | 342 | ## 26. 中断和轮询有什么区别? 343 | 344 | * 轮询:CPU对**特定设备**轮流询问。中断:通过**特定事件**提醒CPU。 345 | * 轮询:效率低等待时间长,CPU利用率不高。中断:容易遗漏问题,CPU利用率不高。 346 | 347 | ## 27. 什么是用户态和内核态? 348 | 349 | 用户态和系统态是操作系统的两种运行状态: 350 | 351 | > - 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。 352 | > - 用户态:用户态运行的程序只能受限地访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。 353 | 354 | 将操作系统的运行状态分为用户态和内核态,主要是为了对访问能力进行限制,防止随意进行一些比较危险的操作导致系统的崩溃,比如设置时钟、内存清理,这些都需要在内核态下完成 。 355 | 356 | ## 28. 用户态和内核态是如何切换的? 357 | 358 | 所有的用户进程都是运行在用户态的,但是我们上面也说了,用户程序的访问能力有限,一些比较重要的比如从硬盘读取数据,从键盘获取数据的操作则是内核态才能做的事情,而这些数据却又对用户程序来说非常重要。所以就涉及到两种模式下的转换,即**用户态 -> 内核态 -> 用户态**,而唯一能够做这些操作的只有 `系统调用`,而能够执行系统调用的就只有 `操作系统`。 359 | 360 | 一般用户态 -> 内核态的转换我们都称之为 trap 进内核,也被称之为 `陷阱指令(trap instruction)`。 361 | 362 | 他们的工作流程如下: 363 | 364 | ![image-20210807152619210](http://blog-img.coolsen.cn/img/image-20210807152619210.png) 365 | 366 | - 首先用户程序会调用 `glibc` 库,glibc 是一个标准库,同时也是一套核心库,库中定义了很多关键 API。 367 | - glibc 库知道针对不同体系结构调用`系统调用`的正确方法,它会根据体系结构应用程序的二进制接口设置用户进程传递的参数,来准备系统调用。 368 | - 然后,glibc 库调用`软件中断指令(SWI)` ,这个指令通过更新 `CPSR` 寄存器将模式改为超级用户模式,然后跳转到地址 `0x08` 处。 369 | - 到目前为止,整个过程仍处于用户态下,在执行 SWI 指令后,允许进程执行内核代码,MMU 现在允许内核虚拟内存访问 370 | - 从地址 0x08 开始,进程执行加载并跳转到中断处理程序,这个程序就是 ARM 中的 `vector_swi()`。 371 | - 在 vector_swi() 处,从 SWI 指令中提取系统调用号 SCNO,然后使用 SCNO 作为系统调用表 `sys_call_table` 的索引,调转到系统调用函数。 372 | - 执行系统调用完成后,将还原用户模式寄存器,然后再以用户模式执行。 373 | 374 | ## 29. Unix 常见的IO模型: 375 | 376 | 对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段: 377 | 378 | > - 等待数据准备就绪 (Waiting for the data to be ready) 379 | > - 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 380 | 381 | 正式因为这两个阶段,linux系统产生了下面五种网络模式的方案: 382 | 383 | > - 阻塞式IO模型(blocking IO model) 384 | > - 非阻塞式IO模型(noblocking IO model) 385 | > - IO复用式IO模型(IO multiplexing model) 386 | > - 信号驱动式IO模型(signal-driven IO model) 387 | > - 异步IO式IO模型(asynchronous IO model) 388 | 389 | 对于这几种 IO 模型的详细说明,可以参考这篇文章:https://juejin.cn/post/6942686874301857800#heading-13 390 | 391 | 其中,IO多路复用模型指的是:使用单个进程同时处理多个网络连接IO,他的原理就是select、poll、epoll 不断轮询所负责的所有 socket,当某个socket有数据到达了,就通知用户进程。该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。 392 | 393 | ## 30. select、poll 和 epoll 之间的区别? 394 | 395 | (1)select:时间复杂度 O(n) 396 | 397 | select 仅仅知道有 I/O 事件发生,但并不知道是哪几个流,所以只能无差别轮询所有流,找出能读出数据或者写入数据的流,并对其进行操作。所以 select 具有 O(n) 的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。 398 | 399 | (2)poll:时间复杂度 O(n) 400 | 401 | poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。 402 | 403 | (3)epoll:时间复杂度 O(1) 404 | 405 | epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。所以说 epoll 实际上是事件驱动(每个事件关联上 fd)的。 406 | 407 | > select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),就通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。 -------------------------------------------------------------------------------- /计算机网络/网络总结-小林coding.md: -------------------------------------------------------------------------------- 1 | ### 1、HTTP是什么?详细解释一下。 2 | 3 | HTTP是超文本传输协议,即HyperText Transfer Protocol。 4 | 5 | 可以拆分成三个部分:超文本、传输、协议。HTTP是一个计算机世界里专门在**两点**之间**传输**文字、图片、音频、视频等**超文本**数据的**约定和规范**。 6 | 7 | ### 2、http常见字段有哪些? 8 | 9 | ***Host字段***:客户端发送请求时,此字段用来指定服务器的域名。 10 | 11 | ***Content-Length字段***:服务器返回数据时,会有Content-Length字段,表明本次回应的数据长度。 12 | 13 | ***Connect字段***:最常用于客户端要求服务器使用TCP持久连接,以便其他请求服用。比如http/1.1版本默认时长连接,需要指定Connection首部字段的值为**Keep-Alive**.即Connection:keep-alive. 14 | 15 | **Content-Type**字段:用于服务器回应时,告诉客户端,本次数据是什么格式。比如:Content-Type:text/html;charset=utf-8,说明发送的是网页,编码格式是utf-8. 16 | 17 | **Content-Encoding**字段:表示服务器返回的数据使用的压缩方法。客户端在请求时,用**Accept-Encoding**字段说明可以接受哪些压缩方法。 18 | 19 | ### 3、说一下GET和POST的区别? 20 | 21 | **Get方法**是请求**从服务器获取资源**,这个资源可以是静态的文本、页面、图片视频等。 22 | 23 | **Post方法**是相反操作,它是向**URL**指定的资源提交数据,数据方法在报文的body里。 24 | 25 | **本质区别**: 26 | 27 | **Get方法是安全且幂等**。因为它是只读操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。 28 | 29 | Post因为是**新增或提交数据**的操作,会修改服务器上的资源,所以是**不安全**的,且多次提交数据就会创建多个资源,所以不是幂等的。 30 | 31 | 拓展知识: 32 | 33 | 在 HTTP 协议⾥,所谓的「安全」是指请求⽅法不会「破坏」服务器上的资源。 34 | 35 | 所谓的「幂等」,意思是多次执⾏相同的操作,结果都是「相同」的。 -------------------------------------------------------------------------------- /计算机网络/计算机网络上.md: -------------------------------------------------------------------------------- 1 | ## 1. 计算机网络的各层协议及作用? 2 | 3 | 计算机网络体系可以大致分为一下三种,OSI七层模型、TCP/IP四层模型和五层模型。 4 | 5 | * OSI七层模型:大而全,但是比较复杂、而且是先有了理论模型,没有实际应用。 6 | * TCP/IP四层模型:是由实际应用发展总结出来的,从实质上讲,TCP/IP只有最上面三层,最下面一层没有什么具体内容,TCP/IP参考模型没有真正描述这一层的实现。 7 | * 五层模型:五层模型只出现在计算机网络教学过程中,这是对七层模型和四层模型的一个折中,既简洁又能将概念阐述清楚。 8 | 9 | ![计算机网络体系结构](http://blog-img.coolsen.cn/img/image-20210519165421341.png) 10 | 11 | 七层网络体系结构各层的主要功能: 12 | 13 | - 应用层:为应用程序提供交互服务。在互联网中的应用层协议很多,如域名系统DNS,支持万维网应用的HTTP协议,支持电子邮件的SMTP协议等。 14 | - 表示层:主要负责数据格式的转换,如加密解密、转换翻译、压缩解压缩等。 15 | - 会话层:负责在网络中的两节点之间建立、维持和终止通信,如服务器验证用户登录便是由会话层完成的。 16 | - 运输层:有时也译为传输层,向主机进程提供通用的数据传输服务。该层主要有以下两种协议: 17 | - TCP:提供面向连接的、可靠的数据传输服务; 18 | - UDP:提供无连接的、尽最大努力的数据传输服务,但不保证数据传输的可靠性。 19 | 20 | - 网络层:选择合适的路由和交换结点,确保数据及时传送。主要包括IP协议。 21 | - 数据链路层:数据链路层通常简称为链路层。将网络层传下来的IP数据包组装成帧,并再相邻节点的链路上传送帧。 22 | 23 | - `物理层`:实现相邻节点间比特流的透明传输,尽可能屏蔽传输介质和通信手段的差异。 24 | 25 | ## 2. TCP和UDP的区别? 26 | 27 | **对比如下**: 28 | 29 | | | UDP | TCP | 30 | | :----------- | :----------------------------------------- | :----------------------------------------------- | 31 | | 是否连接 | 无连接 | 面向连接 | 32 | | 是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 | 33 | | 是否有序 | 无序 | 有序,消息在传输过程中可能会乱序,TCP 会重新排序 | 34 | | 传输速度 | 快 | 慢 | 35 | | 连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 | 36 | | 传输方式 | 面向报文 | 面向字节流 | 37 | | 首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 | 38 | | 适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 | 39 | 40 | **总结**: 41 | 42 | TCP 用于在传输层有必要实现可靠传输的情况,UDP 用于对高速传输和实时性有较高要求的通信。TCP 和 UDP 应该根据应用目的按需使用。 43 | 44 | ## 3. UDP 和 TCP 对应的应用场景是什么? 45 | 46 | TCP 是面向连接,能保证数据的可靠性交付,因此经常用于: 47 | 48 | - FTP文件传输 49 | - HTTP / HTTPS 50 | 51 | UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于: 52 | 53 | - 包总量较少的通信,如 DNS 、SNMP等 54 | - 视频、音频等多媒体通信 55 | - 广播通信 56 | 57 | ![image-20210519180008296](http://blog-img.coolsen.cn/img/image-20210519180008296.png) 58 | 59 | ## 4. 详细介绍一下 TCP 的三次握手机制? 60 | 61 | ![](http://blog-img.coolsen.cn/img/image-20210520161056918.png) 62 | 63 | > 图片来自:https://juejin.cn/post/6844904005315854343 64 | 65 | 三次握手机制: 66 | 67 | * 第一次握手:客户端请求建立连接,向服务端发送一个**同步报文**(SYN=1),同时选择一个随机数 seq = x 作为**初始序列号**,并进入SYN_SENT状态,等待服务器确认。 68 | 69 | * 第二次握手::服务端收到连接请求报文后,如果同意建立连接,则向客户端发送**同步确认报文**(SYN=1,ACK=1),确认号为 ack = x + 1,同时选择一个随机数 seq = y 作为初始序列号,此时服务器进入SYN_RECV状态。 70 | * 第三次握手:客户端收到服务端的确认后,向服务端发送一个**确认报文**(ACK=1),确认号为 ack = y + 1,序列号为 seq = x + 1,客户端和服务器进入ESTABLISHED状态,完成三次握手。 71 | 72 | 理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。 73 | 74 | ## 5. 为什么需要三次握手,而不是两次? 75 | 76 | 主要有三个原因: 77 | 78 | 1. 防止已过期的连接请求报文突然又传送到服务器,因而产生错误和资源浪费。 79 | 80 | 在双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段。 81 | 82 | 客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态,双方建立连接并传输数据,之后正常断开连接。 83 | 84 | 此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,但是已经进入 CLOSED 状态的客户端无法再接受确认报文段,更无法进入 ESTABLISHED 状态,这将导致服务器长时间单方面等待,造成资源浪费。 85 | 86 | 2. 三次握手才能让双方均确认自己和对方的发送和接收能力都正常。 87 | 88 | 第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常; 89 | 90 | 第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常; 91 | 92 | 第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常; 93 | 94 | 可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。 95 | 96 | 3. 告知对方自己的初始序号值,并确认收到对方的初始序号值。 97 | 98 | TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。 99 | 100 | ## 6. 为什么要三次握手,而不是四次? 101 | 102 | 因为三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需再第四次握手了。 103 | 104 | - 第一次握手:服务端确认“自己收、客户端发”报文功能正常。 105 | - 第二次握手:客户端确认“自己发、自己收、服务端收、客户端发”报文功能正常,客户端认为连接已建立。 106 | - 第三次握手:服务端确认“自己发、客户端收”报文功能正常,此时双方均建立连接,可以正常通信。 107 | 108 | ## 7. 什么是 SYN洪泛攻击?如何防范? 109 | 110 | SYN洪泛攻击属于 DOS 攻击的一种,它利用 TCP 协议缺陷,通过发送大量的半连接请求,耗费 CPU 和内存资源。 111 | 112 | 原理: 113 | 114 | - 在三次握手过程中,服务器发送 `[SYN/ACK]` 包(第二个包)之后、收到客户端的 `[ACK]` 包(第三个包)之前的 TCP 连接称为半连接(half-open connect),此时服务器处于 `SYN_RECV`(等待客户端响应)状态。如果接收到客户端的 `[ACK]`,则 TCP 连接成功,如果未接受到,则会**不断重发请求**直至成功。 115 | - SYN 攻击的攻击者在短时间内**伪造大量不存在的 IP 地址**,向服务器不断地发送 `[SYN]` 包,服务器回复 `[SYN/ACK]` 包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时。 116 | - 这些伪造的 `[SYN]` 包将长时间占用未连接队列,影响了正常的 SYN,导致目标系统运行缓慢、网络堵塞甚至系统瘫痪。 117 | 118 | 检测:当在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。 119 | 120 | 防范: 121 | 122 | * 通过防火墙、路由器等过滤网关防护。 123 | * 通过加固 TCP/IP 协议栈防范,如增加最大半连接数,缩短超时时间。 124 | * SYN cookies技术。SYN Cookies 是对 TCP 服务器端的三次握手做一些修改,专门用来防范 SYN 洪泛攻击的一种手段。 125 | 126 | ## 8. 三次握手连接阶段,最后一次ACK包丢失,会发生什么? 127 | 128 | **服务端:** 129 | 130 | * 第三次的ACK在网络中丢失,那么服务端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便客户端重新发送ACK包。 131 | * 如果重发指定次数之后,仍然未收到 客户端的ACK应答,那么一段时间后,服务端自动关闭这个连接。 132 | 133 | **客户端:** 134 | 135 | 客户端认为这个连接已经建立,如果客户端向服务端发送数据,服务端将以RST包(Reset,标示复位,用于异常的关闭连接)响应。此时,客户端知道第三次握手失败。 136 | 137 | ## 9. 详细介绍一下 TCP 的四次挥手过程? 138 | 139 | ![](http://blog-img.coolsen.cn/img/image-20210520180127547.png) 140 | 141 | > 图片来源:https://juejin.im/post/5ddd1f30e51d4532c42c5abe 142 | 143 | - 第一次挥手:客户端向服务端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待服务端的确认。 144 | 145 | - 序列号 seq = u,即客户端上次发送的报文的最后一个字节的序号 + 1 146 | - 确认号 ack = k, 即服务端上次发送的报文的最后一个字节的序号 + 1 147 | 148 | - 第二次挥手:服务端收到连接释放报文后,立即发出**确认报文**(ACK=1),序列号 seq = k,确认号 ack = u + 1。 149 | 150 | 这时 TCP 连接处于半关闭状态,即客户端到服务端的连接已经释放了,但是服务端到客户端的连接还未释放。这表示客户端已经没有数据发送了,但是服务端可能还要给客户端发送数据。 151 | 152 | - 第三次挥手:服务端向客户端发送连接释放报文(FIN=1,ACK=1),主动关闭连接,同时等待 A 的确认。 153 | 154 | - 序列号 seq = w,即服务端上次发送的报文的最后一个字节的序号 + 1。 155 | - 确认号 ack = u + 1,与第二次挥手相同,因为这段时间客户端没有发送数据 156 | 157 | - 第四次挥手:客户端收到服务端的连接释放报文后,立即发出**确认报文**(ACK=1),序列号 seq = u + 1,确认号为 ack = w + 1。 158 | 159 | 此时,客户端就进入了 `TIME-WAIT` 状态。注意此时客户端到 TCP 连接还没有释放,必须经过 2*MSL(最长报文段寿命)的时间后,才进入 `CLOSED` 状态。而服务端只要收到客户端发出的确认,就立即进入 `CLOSED` 状态。可以看到,服务端结束 TCP 连接的时间要比客户端早一些。 160 | 161 | ## 10. 为什么连接的时候是三次握手,关闭的时候却是四次握手? 162 | 163 | 服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段. 164 | 165 | 接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接。服务器的**ACK和FIN一般都会分开发送**,从而导致多了一次,因此一共需要四次挥手。 166 | 167 | ## 11. 为什么客户端的 TIME-WAIT 状态必须等待 2MSL ? 168 | 169 | 主要有两个原因: 170 | 171 | 1. 确保 ACK 报文能够到达服务端,从而使服务端正常关闭连接。 172 | 173 | 第四次挥手时,客户端第四次挥手的 ACK 报文不一定会到达服务端。服务端会超时重传 FIN/ACK 报文,此时如果客户端已经断开了连接,那么就无法响应服务端的二次请求,这样服务端迟迟收不到 FIN/ACK 报文的确认,就无法正常断开连接。 174 | 175 | MSL 是报文段在网络上存活的最长时间。客户端等待 2MSL 时间,即「客户端 ACK 报文 1MSL 超时 + 服务端 FIN 报文 1MSL 传输」,就能够收到服务端重传的 FIN/ACK 报文,然后客户端重传一次 ACK 报文,并重新启动 2MSL 计时器。如此保证服务端能够正常关闭。 176 | 177 | 如果服务端重发的 FIN 没有成功地在 2MSL 时间里传给客户端,服务端则会继续超时重试直到断开连接。 178 | 179 | 2. 防止已失效的连接请求报文段出现在之后的连接中。 180 | 181 | TCP 要求在 2MSL 内不使用相同的序列号。客户端在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以保证本连接持续的时间内产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。或者即使收到这些过时的报文,也可以不处理它。 182 | 183 | ## 12. 如果已经建立了连接,但是客户端出现故障了怎么办? 184 | 185 | 或者说,如果三次握手阶段、四次挥手阶段的包丢失了怎么办?如“服务端重发 FIN丢失”的问题。 186 | 187 | 简而言之,通过**定时器 + 超时重试机制**,尝试获取确认,直到最后会自动断开连接。 188 | 189 | 具体而言,TCP 设有一个保活计时器。服务器每收到一次客户端的数据,都会重新复位这个计时器,时间通常是设置为 2 小时。若 2 小时还没有收到客户端的任何数据,服务器就开始重试:每隔 75 分钟发送一个探测报文段,若一连发送 10 个探测报文后客户端依然没有回应,那么服务器就认为连接已经断开了。 190 | 191 | ## 13. TIME-WAIT 状态过多会产生什么后果?怎样处理? 192 | 193 | 从服务器来讲,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,严重消耗着服务器的资源,此时部分客户端就会显示连接不上。 194 | 195 | 从客户端来讲,客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接。 196 | 197 | **解决办法:** 198 | 199 | * 服务器可以设置 SO_REUSEADDR 套接字选项来避免 TIME_WAIT状态,此套接字选项告诉内核,即使此端口正忙(处于 200 | TIME_WAIT状态),也请继续并重用它。 201 | 202 | * 调整系统内核参数,修改/etc/sysctl.conf文件,即修改`net.ipv4.tcp_tw_reuse 和 tcp_timestamps` 203 | 204 | ```bash 205 | net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; 206 | net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 207 | ``` 208 | 209 | * 强制关闭,发送 RST 包越过TIME_WAIT状态,直接进入CLOSED状态。 210 | 211 | ## 14. TIME_WAIT 是服务器端的状态?还是客户端的状态? 212 | 213 | TIME_WAIT 是主动断开连接的一方会进入的状态,一般情况下,都是客户端所处的状态;服务器端一般设置不主动关闭连接。 214 | 215 | TIME_WAIT 需要等待 2MSL,在大量短连接的情况下,TIME_WAIT会太多,这也会消耗很多系统资源。对于服务器来说,在 HTTP 协议里指定 KeepAlive(浏览器重用一个 TCP 连接来处理多个 HTTP 请求),由浏览器来主动断开连接,可以一定程度上减少服务器的这个问题。 216 | 217 | ## 15. TCP协议如何保证可靠性? 218 | 219 | TCP主要提供了检验和、序列号/确认应答、超时重传、滑动窗口、拥塞控制和 流量控制等方法实现了可靠性传输。 220 | 221 | * 检验和:通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢弃TCP段,重新发送。 222 | 223 | * 序列号/确认应答: 224 | 225 | 序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。 226 | 227 | TCP传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文,这个ACK报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。 228 | 229 | * 滑动窗口:滑动窗口既提高了报文传输的效率,也避免了发送方发送过多的数据而导致接收方无法正常处理的异常。 230 | 231 | * 超时重传:超时重传是指发送出去的数据包到接收到确认包之间的时间,如果超过了这个时间会被认为是丢包了,需要重传。最大超时时间是动态计算的。 232 | 233 | * 拥塞控制:在数据传输过程中,可能由于网络状态的问题,造成网络拥堵,此时引入拥塞控制机制,在保证TCP可靠性的同时,提高性能。 234 | 235 | * 流量控制:如果主机A 一直向主机B发送数据,不考虑主机B的接受能力,则可能导致主机B的接受缓冲区满了而无法再接受数据,从而会导致大量的数据丢包,引发重传机制。而在重传的过程中,若主机B的接收缓冲区情况仍未好转,则会将大量的时间浪费在重传数据上,降低传送数据的效率。所以引入流量控制机制,主机B通过告诉主机A自己接收缓冲区的大小,来使主机A控制发送的数据量。流量控制与TCP协议报头中的窗口大小有关。 236 | 237 | ## 16. 详细讲一下TCP的滑动窗口? 238 | 239 | 在进行数据传输时,如果传输的数据比较大,就需要拆分为多个数据包进行发送。TCP 协议需要对数据进行确认后,才可以发送下一个数据包。这样一来,就会在等待确认应答包环节浪费时间。 240 | 241 | 为了避免这种情况,TCP引入了窗口概念。窗口大小指的是不需要等待确认应答包而可以继续发送数据包的最大值。 242 | 243 | ![](http://blog-img.coolsen.cn/img/image-20210520214432214.png) 244 | 245 | 从上面的图可以看到滑动窗口左边的是已发送并且被确认的分组,滑动窗口右边是还没有轮到的分组。 246 | 247 | 滑动窗口里面也分为两块,一块是已经发送但是未被确认的分组,另一块是窗口内等待发送的分组。随着已发送的分组不断被确认,窗口内等待发送的分组也会不断被发送。整个窗口就会往右移动,让还没轮到的分组进入窗口内。 248 | 249 | 可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前 TCP 发送包的速率,而滑动窗口的大小取决于拥塞控制窗口和流量控制窗口的两者间的最小值。 250 | 251 | ## 17. 详细讲一下拥塞控制? 252 | 253 | TCP 一共使用了四种算法来实现拥塞控制: 254 | 255 | * 慢开始 (slow-start); 256 | 257 | * 拥塞避免 (congestion avoidance); 258 | 259 | * 快速重传 (fast retransmit); 260 | 261 | * 快速恢复 (fast recovery)。 262 | 263 | 发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。当cwndssthresh时,改用拥塞避免算法。 264 | 265 | **慢开始:**不要一开始就发送大量的数据,由小到大逐渐增加拥塞窗口的大小。 266 | 267 | **拥塞避免:**拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1而不是加倍。这样拥塞窗口按线性规律缓慢增长。 268 | 269 | **快重传:**我们可以剔除一些不必要的拥塞报文,提高网络吞吐量。比如接收方在**收到一个失序的报文段后就立即发出重复确认,而不要等到自己发送数据时捎带确认。**快重传规定:发送方只要**一连收到三个**重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。 270 | 271 | ![](http://blog-img.coolsen.cn/img/image-20210520214123058.png) 272 | 273 | **快恢复:**主要是配合快重传。当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞),但**接下来并不执行慢开始算法**,因为如果网络出现拥塞的话就不会收到好几个重复的确认,收到三个重复确认说明网络状况还可以。 274 | 275 | ![](http://blog-img.coolsen.cn/img/image-20210520214146324.png) 276 | -------------------------------------------------------------------------------- /计算机网络/计算机网络下.md: -------------------------------------------------------------------------------- 1 | ## 1. HTTP常见的状态码有哪些? 2 | 3 | 常见状态码: 4 | 5 | * 200:服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。 6 | * 301 : (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。 7 | * 302:(临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。 8 | * 400 :客户端请求有语法错误,不能被服务器所理解。 9 | * 403 :服务器收到请求,但是拒绝提供服务。 10 | * 404 :(未找到) 服务器找不到请求的网页。 11 | * 500: (服务器内部错误) 服务器遇到错误,无法完成请求。 12 | 13 | 状态码开头代表类型: 14 | 15 | ![](http://blog-img.coolsen.cn/img/image-20210525114439748.png) 16 | 17 | ## 2. 状态码301和302的区别是什么? 18 | 19 | **共同点**:301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(**用户看到的效果就是他输入的地址A瞬间变成了另一个地址B**)。 20 | **不同点**:301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。 SEO中302好于301。 21 | 22 | **补充,重定向原因**: 23 | 24 | 1. 网站调整(如改变网页目录结构); 25 | 2. 网页被移到一个新地址; 26 | 3. 网页扩展名改变(如应用需要把.php改成.Html或.shtml)。 27 | 28 | ## 3. HTTP 常用的请求方式? 29 | 30 | | 方法 | 作用 | 31 | | ------- | ------------------------------------------------------- | 32 | | GET | 获取资源 | 33 | | POST | 传输实体主体 | 34 | | PUT | 上传文件 | 35 | | DELETE | 删除文件 | 36 | | HEAD | 和GET方法类似,但只返回报文首部,不返回报文实体主体部分 | 37 | | PATCH | 对资源进行部分修改 | 38 | | OPTIONS | 查询指定的URL支持的方法 | 39 | | CONNECT | 要求用隧道协议连接代理 | 40 | | TRACE | 服务器会将通信路径返回给客户端 | 41 | 42 | 为了方便记忆,可以将PUT、DELETE、POST、GET理解为客户端对服务端的增删改查。 43 | 44 | - PUT:上传文件,向服务器添加数据,可以看作增 45 | - DELETE:删除文件 46 | - POST:传输数据,向服务器提交数据,对服务器数据进行更新。 47 | - GET:获取资源,查询服务器资源 48 | 49 | ## 4. GET请求和POST请求的区别? 50 | 51 | **使用上的区别**: 52 | 53 | * GET使用URL或Cookie传参,而POST将数据放在BODY中”,这个是因为HTTP协议用法的约定。 54 | * GET方式提交的数据有长度限制,则POST的数据则可以非常大”,这个是因为它们使用的操作系统和浏览器设置的不同引起的区别。 55 | 56 | * POST比GET安全,因为数据在地址栏上不可见”,这个说法没毛病,但依然不是GET和POST本身的区别。 57 | 58 | **本质区别** 59 | 60 | GET和POST最大的区别主要是GET请求是幂等性的,POST请求不是。这个是它们本质区别。 61 | 62 | 幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。 63 | 64 | ## 5. 解释一下HTTP长连接和短连接? 65 | 66 | **在HTTP/1.0中,默认使用的是短连接**。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。 67 | 68 | 但从 **HTTP/1.1起,默认使用长连接**,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:`Connection:keep-alive` 69 | 70 | 在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。 71 | 72 | **HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。** 73 | 74 | ## 6. HTTP请求报文和响应报文的格式? 75 | 76 | **请求报文格式**: 77 | 78 | 1. 请求行(请求方法+URI协议+版本) 79 | 2. 请求头部 80 | 3. 空行 81 | 4. 请求主体 82 | 83 | ```html 84 | GET/sample.jspHTTP/1.1 请求行 85 | Accept:image/gif.image/jpeg, 请求头部 86 | Accept-Language:zh-cn 87 | Connection:Keep-Alive 88 | Host:localhost 89 | User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0) 90 | Accept-Encoding:gzip,deflate 91 | 92 | username=jinqiao&password=1234 请求主体 93 | ``` 94 | 95 | **响应报文**: 96 | 97 | 1. 状态行(版本+状态码+原因短语) 98 | 2. 响应首部 99 | 3. 空行 100 | 4. 响应主体 101 | 102 | ```html 103 | HTTP/1.1 200 OK 104 | Server:Apache Tomcat/5.0.12 105 | Date:Mon,6Oct2003 13:23:42 GMT 106 | Content-Length:112 107 | 108 | 109 | 110 | HTTP响应示例<title> 111 | </head> 112 | <body> 113 | Hello HTTP! 114 | </body> 115 | </html> 116 | ``` 117 | 118 | ## 7. HTTP1.0和HTTP1.1的区别? 119 | 120 | * **长连接**:HTTP 1.1支持长连接(Persistent Connection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启`Connection: keep-alive`,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。 121 | 122 | * **缓存处理**:在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略,可供选择的缓存头来控制缓存策略。 123 | 124 | * **带宽优化及网络连接的使用**:HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 125 | 126 | * **错误通知的管理**:在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。 127 | 128 | * **Host头处理**:在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。 129 | 130 | ## 8. HTTP1.1和 HTTP2.0的区别? 131 | 132 | HTTP2.0相比HTTP1.1支持的特性: 133 | 134 | * **新的二进制格式**:HTTP1.1的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。 135 | 136 | * **多路复用**,即连接共享,即每一个request都是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。 137 | 138 | * **头部压缩**,HTTP1.1的头部(header)带有大量信息,而且每次都要重复发送;HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。 139 | 140 | * **服务端推送**:服务器除了对最初请求的响应外,服务器还可以额外的向客户端推送资源,而无需客户端明确的请求。 141 | 142 | ## 9. HTTP 与 HTTPS 的区别? 143 | 144 | | | HTTP | HTTPS | 145 | | :----------: | :----------------: | --------------------------------------- | 146 | | 端口 | 80 | 443 | 147 | | 安全性 | 无加密,安全性较差 | 有加密机制,安全性较高 | 148 | | 资源消耗 | 较少 | 由于加密处理,资源消耗更多 | 149 | | 是否需要证书 | 不需要 | 需要 | 150 | | 协议 | 运行在TCP协议之上 | 运行在SSL协议之上,SSL运行在TCP协议之上 | 151 | 152 | ## 10. HTTPS 的优缺点? 153 | 154 | **优点**: 155 | 156 | * 安全性: 157 | 158 | * 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器; 159 | 160 | * HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。 161 | 162 | * HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。 163 | 164 | * SEO方面:谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。 165 | 166 | **缺点**: 167 | 168 | - 在相同网络环境中,HTTPS 相比 HTTP 无论是响应时间还是耗电量都有大幅度上升。 169 | - HTTPS 的安全是有范围的,在黑客攻击、服务器劫持等情况下几乎起不到作用。 170 | - 在现有的证书机制下,中间人攻击依然有可能发生。 171 | - HTTPS 需要更多的服务器资源,也会导致成本的升高。 172 | 173 | ## 11. 讲一讲HTTPS 的原理? 174 | 175 | ![](http://blog-img.coolsen.cn/img/image-20210525160006424.png) 176 | 177 | > 图片来源:https://segmentfault.com/a/1190000021494676 178 | 179 | 加密流程按图中的序号分为: 180 | 181 | 1. 客户端请求 HTTPS 网址,然后连接到 server 的 443 端口 (HTTPS 默认端口,类似于 HTTP 的80端口)。 182 | 183 | 2. 采用 HTTPS 协议的服务器必须要有一套数字 CA (Certification Authority)证书。颁发证书的同时会产生一个私钥和公钥。私钥由服务端自己保存,不可泄漏。公钥则是附带在证书的信息中,可以公开的。证书本身也附带一个证书电子签名,这个签名用来验证证书的完整性和真实性,可以防止证书被篡改。 184 | 185 | 3. 服务器响应客户端请求,将证书传递给客户端,证书包含公钥和大量其他信息,比如证书颁发机构信息,公司信息和证书有效期等。 186 | 187 | 4. 客户端解析证书并对其进行验证。如果证书不是可信机构颁布,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。 188 | 189 | 如果证书没有问题,客户端就会从服务器证书中取出服务器的公钥A。然后客户端还会生成一个随机码 KEY,并使用公钥A将其加密。 190 | 191 | 5. 客户端把加密后的随机码 KEY 发送给服务器,作为后面对称加密的密钥。 192 | 193 | 6. 服务器在收到随机码 KEY 之后会使用私钥B将其解密。经过以上这些步骤,客户端和服务器终于建立了安全连接,完美解决了对称加密的密钥泄露问题,接下来就可以用对称加密愉快地进行通信了。 194 | 195 | 7. 服务器使用密钥 (随机码 KEY)对数据进行对称加密并发送给客户端,客户端使用相同的密钥 (随机码 KEY)解密数据。 196 | 197 | 8. 双方使用对称加密愉快地传输所有数据。 198 | 199 | ## 12. 在浏览器中输入www.baidu.com后执行的全部过程? 200 | 201 | 1. 域名解析(域名 [www.baidu.com ](http://www.baidu.com/)变为 ip 地址)。 202 | 203 | **浏览器搜索自己的DNS缓存**(维护一张域名与IP的对应表);若没有,则搜索**操作系统的DNS缓存**(维护一张域名与IP的对应表);若没有,则搜索操作系统的**hosts文件**(维护一张域名与IP的对应表)。 204 | 205 | 若都没有,则找 tcp/ip 参数中设置的首选 dns 服务器,即**本地 dns 服务器**(递归查询),**本地域名服务器查询自己的dns缓存**,如果没有,则进行迭代查询。将本地dns服务器将IP返回给操作系统,同时缓存IP。 206 | 207 | 2. 发起 tcp 的三次握手,建立 tcp 连接。浏览器会以一个随机端口(1024-65535)向服务端的 web 程序 **80** 端口发起 tcp 的连接。 208 | 209 | 3. 建立 tcp 连接后发起 http 请求。 210 | 211 | 4. 服务器响应 http 请求,客户端得到 html 代码。服务器 web 应用程序收到 http 请求后,就开始处理请求,处理之后就返回给浏览器 html 文件。 212 | 213 | 5. 浏览器解析 html 代码,并请求 html 中的资源。 214 | 215 | 6. 浏览器对页面进行渲染,并呈现给用户。 216 | 217 | 附一张形象的图片:![](http://blog-img.coolsen.cn/img/image-20210525172545204.png) 218 | 219 | ## 13. 什么是 Cookie 和 Session ? 220 | 221 | **什么是 Cookie** 222 | 223 | HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。 224 | 225 | Cookie 主要用于以下三个方面: 226 | 227 | - 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) 228 | - 个性化设置(如用户自定义设置、主题等) 229 | - 浏览器行为跟踪(如跟踪分析用户行为等) 230 | 231 | **什么是 Session** 232 | 233 | Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当客户端关闭会话,或者 Session 超时失效时会话结束。 234 | 235 | ## 14. Cookie 和 Session 是如何配合的呢? 236 | 237 | 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session ,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。 238 | 239 | 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。 240 | 241 | 根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。 242 | 243 | ## 15. Cookie和Session的区别? 244 | 245 | - 作用范围不同,Cookie 保存在客户端(浏览器),Session 保存在服务器端。 246 | - 存取方式的不同,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。 247 | - 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。 248 | - 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。 249 | - 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie。 250 | 251 | ## 16. 如何考虑分布式 Session 问题? 252 | 253 | 在互联网公司为了可以支撑更大的流量,后端往往需要多台服务器共同来支撑前端用户请求,那如果用户在 A 服务器登录了,第二次请求跑到服务 B 就会出现登录失效问题。 254 | 255 | 分布式 Session 一般会有以下几种解决方案: 256 | 257 | * **客户端存储**:直接将信息存储在cookie中,cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息 258 | 259 | - **Nginx ip_hash 策略**:服务端使用 Nginx 代理,每个请求按访问 IP 的 hash 分配,这样来自同一 IP 固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次分发到服务器 B 的现象。 260 | - **Session 复制**:任何一个服务器上的 Session 发生改变(增删改),该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点。 261 | - **共享 Session**:服务端无状态话,将用户的 Session 等信息使用缓存中间件(如Redis)来统一管理,保障分发到每一个服务器的响应结果都一致。 262 | 263 | 建议采用共享 Session的方案。 264 | 265 | ## 17. 什么是DDos攻击? 266 | 267 | DDos全称Distributed Denial of Service,分布式拒绝服务攻击。最基本的DOS攻击过程如下: 268 | 269 | 1. 客户端向服务端发送请求链接数据包。 270 | 2. 服务端向客户端发送确认数据包。 271 | 3. 客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认 272 | 273 | DDoS则是采用分布式的方法,通过在网络上占领多台“肉鸡”,用多台计算机发起攻击。 274 | 275 | DOS攻击现在基本没啥作用了,因为服务器的性能都很好,而且是多台服务器共同作用,1V1的模式黑客无法占上风。对于DDOS攻击,预防方法有: 276 | 277 | - **减少SYN timeout时间**。在握手的第三步,服务器会等待30秒-120秒的时间,减少这个等待时间就能释放更多的资源。 278 | - **限制同时打开的SYN半连接数目。** 279 | 280 | ## 18. 什么是XSS攻击? 281 | 282 | XSS也称 cross-site scripting,**跨站脚本**。这种攻击是**由于服务器将攻击者存储的数据原原本本地显示给其他用户所致的**。比如一个存在XSS漏洞的论坛,用户发帖时就可以引入**带有<script>标签的代码**,导致恶意代码的执行。 283 | 284 | 预防措施有: 285 | 286 | - 前端:过滤。 287 | - 后端:转义,比如go自带的处理器就具有转义功能。 288 | 289 | ## 19. SQL注入是什么,如何避免SQL注入? 290 | 291 | SQL 注入就是在用户输入的字符串中加入 SQL 语句,如果在设计不良的程序中忽略了检查,那么这些注入进去的 SQL 语句就会被数据库服务器误认为是正常的 SQL 语句而运行,攻击者就可以执行计划外的命令或访问未被授权的数据。 292 | 293 | **SQL注入的原理主要有以下 4 点** 294 | 295 | * 恶意拼接查询 296 | * 利用注释执行非法命令 297 | * 传入非法参数 298 | * 添加额外条件 299 | 300 | **避免SQL注入的一些方法**: 301 | 302 | - 限制数据库权限,给用户提供仅仅能够满足其工作的最低权限。 303 | - 对进入数据库的特殊字符(’”\尖括号&*;等)转义处理。 304 | - 提供参数化查询接口,不要直接使用原生SQL。 305 | 306 | ## 20. 负载均衡算法有哪些? 307 | 308 | 多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,能互相分担负载。 309 | 310 | - 轮询法:将请求按照顺序轮流的分配到服务器上。大锅饭,不能发挥某些高性能服务器的优势。 311 | - 随机法:随机获取一台,和轮询类似。 312 | - 哈希法:通过ip地址哈希化来确定要选择的服务器编号。好处是,每次客户端访问的服务器都是同一个服务器,能很好地利用session或者cookie。 313 | - 加权轮询:根据服务器性能不同加权。 314 | 315 | 316 | 317 | --------------------------------------------------------------------------------