├── .github └── workflows │ └── build.yml ├── .gitignore ├── docs ├── .DS_Store ├── .vuepress │ ├── .DS_Store │ └── config.js ├── README.md └── handbook │ ├── 6.824.md │ ├── Golang.md │ ├── Kafka.md │ ├── MongoDB.md │ ├── Mysql.md │ ├── Redis.md │ ├── 场景题.md │ ├── 微服务.md │ ├── 操作系统.md │ ├── 算法与数据结构.md │ ├── 计算机网络.md │ └── 设计模式.md ├── package.json └── yarn.lock /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow one concurrent deployment 17 | concurrency: 18 | group: "pages" 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | - name: Setup Pages 28 | uses: actions/configure-pages@v2 29 | - name: Build Pages 30 | run: export NODE_OPTIONS=--openssl-legacy-provider && yarn install && yarn build 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v1 33 | with: 34 | path: docs/.vuepress/dist/ 35 | 36 | deploy: 37 | environment: 38 | name: github-pages 39 | url: ${{ steps.deployment.outputs.page_url }} 40 | runs-on: ubuntu-latest 41 | needs: build 42 | steps: 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vuepress/dist 2 | .idea 3 | node_modules 4 | test -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lansongxx/MyStudyNote/93b3ce1d6722607f0013b794b9390c635c4f2b14/docs/.DS_Store -------------------------------------------------------------------------------- /docs/.vuepress/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lansongxx/MyStudyNote/93b3ce1d6722607f0013b794b9390c635c4f2b14/docs/.vuepress/.DS_Store -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 站点配置 3 | lang: "zh-CN", 4 | title: '八股速记', 5 | description: '面试前快速背诵八股', 6 | base: '/MyStudyNote/', 7 | locales: { 8 | '/': { 9 | lang: 'zh-CN' 10 | } 11 | }, 12 | plugins: [ 13 | [ 14 | "vuepress-plugin-baidu-tongji-analytics", 15 | { 16 | key: "ff378ce1bb78883924b7f4fca85a70de", 17 | }, 18 | ], 19 | ], 20 | // 主题和它的配置 21 | theme: "@qcyblm/vpx", 22 | themeConfig: { 23 | subSidebar: 'auto', 24 | search: false, 25 | lastUpdated: "Last Updated", 26 | nav: [ 27 | { text: '首页', link: '/' }, 28 | { 29 | text: '关于我', 30 | items: [ 31 | { text: 'Github', link: 'https://github.com/Lansongxx' }, 32 | { text: '知乎', link: 'https://www.zhihu.com/people/icand'}, 33 | { text: 'Codeforces', link: 'https://codeforces.com/profile/Lansong'}, 34 | ] 35 | }, 36 | { 37 | text: '八股交流群', 38 | link: 'http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=6ymyoBULBCcSPpH6bDPzPJbVnBXXytc8&authKey=3mjoM4lMl8vVKqX5iyumC24j%2FT5StPSIM%2BBNB4cxWyZZ%2B4xPWWmD2Tlp4cfjVXYF&noverify=0&group_code=747928275' 39 | } 40 | ], 41 | sidebar: [ 42 | { 43 | title: "操作系统", 44 | path: '/handbook/操作系统', 45 | collapsable: true, // 不折叠 46 | }, 47 | { 48 | title: "Redis", 49 | path: '/handbook/Redis', 50 | collapsable: false, 51 | }, 52 | { 53 | title: "Mysql", 54 | path: '/handbook/Mysql', 55 | collapsable: false, 56 | }, 57 | { 58 | title: "Kafka", 59 | path: '/handbook/Kafka', 60 | collapsable: false, 61 | }, 62 | { 63 | title: "计算机网络", 64 | path: '/handbook/计算机网络', 65 | collapsable: false, 66 | }, 67 | { 68 | title: "MongoDB", 69 | path: '/handbook/MongoDB', 70 | collapsable: false, 71 | }, 72 | { 73 | title: "6.824", 74 | path: '/handbook/6.824', 75 | collapsable: false, 76 | }, 77 | { 78 | title: "Golang", 79 | path: '/handbook/Golang', 80 | collapsable: false, 81 | }, 82 | { 83 | title: "算法与数据结构", 84 | path: '/handbook/算法与数据结构', 85 | collapsable: false, 86 | }, 87 | { 88 | title: "场景题", 89 | path: '/handbook/场景题', 90 | collapsable: false, 91 | }, 92 | { 93 | title: "微服务", 94 | path: '/handbook/微服务', 95 | collapsable: false, 96 | } 97 | ], 98 | footer: { 99 | // 页脚信息 100 | createYear: "2024", // 创建年份 (可选,author、authorLink 启动时必选) 101 | author: "Lansong", // 作者 (可选) 102 | authorLink: "https://github.com/Lansongxx", // 作者链接 (可选) 103 | beianLink: "https://beian.miit.gov.cn/", // 备案链接 (可选) 104 | }, 105 | repo: { 106 | platform: "https://github.com/", // 填写 Git 服务商链接 107 | icon: "fab fa-github", // 填写 icon 图标 (可选) 108 | label: "github", 109 | owner: "Lansongxx", // 填写 Git 项目仓库所有者 110 | repositories: "MyStudyNote", // 填写 Git 项目仓库 111 | }, 112 | }, 113 | }; 114 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 欢迎来到Lansong的八股笔记! -------------------------------------------------------------------------------- /docs/handbook/6.824.md: -------------------------------------------------------------------------------- 1 | # 6.824 2 | 3 | ## CAP定理 4 | CAP是指一致性(C),可用性(A),分区容错性(P),在异步网络模型中,不存在一个系统可以同时满足上面三个属性。 5 | 如果需要保证一致性,那当出现网络分区时,就不能快速的响应客户端,不能满足高可用 6 | 如果需要保证可用性,那当出现网络分区时,只能查询到的可能不是最新的数据,就不能满足一致性 7 | 一致性:要么读到一个最新的数据,要么读取失败 8 | 可用性:任何客户端的请求都能在合理的响应时间内得到响应,不会返回失败 9 | 分区容错性:系统可以出现网络分区,仍然能够继续运行,不会完全失败 10 | 11 | ## Raft 12 | ### 1.Leader 选举 13 | #### 1.Leader 选举 14 | 每个节点启动后,初始都为 follower,会随机生成一个心跳超时时间,如果没有收到心跳则会开启一轮新的选举,成为 candidate 增加自己的任期,并投票给自己,然后发送投票请求给其他节点, 15 | 候选者如果总票数超过一半,则成为 leader,并立即发送心跳(为了防止此时有follower超时成为leader)和 "no-op 日志",然后定期向其他节点发送心跳和日志。否则成为 follower 16 | 17 | #### 2.投票处理 18 | candidate会发送投票请求给 follower 节点,follower 收到投票请求后,如果candidate的任期比自己小,则拒绝投票,如果自己在当前任期没有投票并且 candidate 的 日志至少和自己的日志 "一样新" (up to date),就投票给它,否则拒绝投票 19 | 20 | #### 3.心跳处理 21 | leader定期向follower发送心跳,follower收到心跳后,如果leader的任期比自己小,则拒绝该心跳,否则接收该心跳并同步任期和重置心跳超时时间 22 | #### 4.up to date 的解释 23 | 如果它的最后一个日志的任期比我大,那么它的日志一定比我新(对于一个leader,对于同一任期,同一索引,最多添加一个日志,并且不能改变它的位置?) 24 | 如果它的最后一个日志的任期和我一眼,并且最后一个日志的下标不小于我,那么它的日志至少跟我一样新 25 | 26 | ### 2.日志复制 27 | #### 1.日志复制 28 | leader 需要定期将 follower 需要的日志复制到 follower 上,follower 收到后首先判断 leader 任期是否比自己小,如果是则拒绝该日志,否则通过比对该日志的前一个索引的任期是否相同,相同则将其日志复制下来,并判断 leader 是否存在自己已经复制了但未提交的日志,将其提交,否则找到该任期的第一个日志的索引将其返回,用于 "快速回退"。 29 | leader 收到响应后,如果 follower 拒绝了该日志,则调整需要发给该 follower 的日志索引。如果 follower 接收了该日志,并更新该 follower 接收的最后一个日志的索引,然后尝试进行日志提交。 30 | #### 2.日志提交 31 | 当有日志复制成功后,会进行日志提交。会将过半节点已经接收了的和 leader 任期相同的("leader 提交原则")日志进行提交,并更新 leader 最后一个提交的日志索引。 32 | 33 | #### 3.日志应用 34 | 会有一个协程专门负责应用日志,定期判断是否有已提交但未应用的日志,如果有,则将其应用到状态机,当一个日志应用到状态机后,更新 leader 最后一个应用的日志索引。 35 | 36 | #### 3.快速回退 37 | 当发生日志复制失败时,如果每次只回退一个位置,效率太低,所以可以回退到冲突任期的第一个日志,因为如果当前任期的日志发生冲突,那么当前任期的日志都会发生冲突 38 | #### 4.leader 提交原则 39 | leader 只能提交自己任期的日志,否则会出现提交后的日志被覆盖的情况,因为当前 leader 不知道是否会存在比该日志任期更大的 follower 成为 leader 并将其覆盖,如果此时提交了该任期的日志,可能导致比该日志任期更大的 follower 成为 leader 后将其覆盖并提交 40 | #### 5.日志匹配原则 41 | 1.对于不同的日志中的两个条目的任期相同,索引相同,则存储的命令一定相同(因为每个leader只有一个任期,一个任期只能在相同索引添加一个日志) 42 | 2.对于不同的日志中两个条目的任期相同,索引相同,则之前的日志条目一定相同(因为日志复制时保证了只有前一个日志相同,才会将当前日志复制过去) 43 | #### 6.no-op日志 44 | 因为 leader 只能提交当前任期的日志,如果没有新的日志的到来,可能导致之前任期的日志一直不能被提交。所以当成为 leader 后需要立马发送一个 no-op日志(只有索引和任期,command未空),以实现快速提交,响应客户端请求 45 | 46 | ### 3.持久化 47 | 因为节点宕机后所有信息都会丢失,所以需要将一些必要的信息进行持久化,如任期,日志文件,投票,快照对应的任期和索引。所以当这些信息发生变化时都需要进行持久化。 48 | 当节点宕机重启后,通过读取持久化信息以快速恢复到宕机前的状态 49 | 50 | ### 4.日志压缩 51 | #### 1.日志压缩 52 | 当日志达到一定大小后,会调用快照函数,然后将已经快照的日志删除,保留暂未被快照的日志 53 | 当进行日志复制时发现 follower 需要的日志已经被快照时,需要将自己的快照发送给 follower 54 | 55 | #### 2.安装快照 56 | 当收到 leader 的快照后,如果 leader 的任期比自己小,则拒绝该快照,如果该快照的最后一个索引不大于 follower 的最后一个提交的日志索引,则不需要安装。否则将当前日志中被快照的日志全部删除,并更新最后一个提交的日志索引 57 | 58 | ## KVServer 59 | ### 1.客户端: 60 | 一开始客户端因为不知道leader是谁,会向每一个节点发起请求,当请求成功后,会记录leader的id,以便下次请求的时候可以直接请求leader。对于rpc调用失败会直接进行重试,对于请求超时,可能是leader发生变化,又需要轮询每个节点。 61 | 62 | ### 2.服务端: 63 | #### 1.命令处理 64 | 对于GET命令,如果发现该请求的版本号是该客户端的旧的版本号,则直接从状态机中查询并返回。否则将日志写入 Raft,等待接收结果并设置计时器,超时后直接返回超时。 65 | 对于PUT APPEND命令 如果该请求的版本号是该客户端的旧的版本号,则直接返回,否则将其写入日志 Raft,等待响应并设置计时器,超时后直接返回超时 66 | 67 | #### 2.执行命令: 68 | 从chan中读取命令: 69 | 如果是日志命令,则根据距离日志的类型进行执行,并更新该客户端对应的最新版本号和执行的最后一个日志的下标,并将响应通过chan发送出去 70 | 如果是快照命令,则说明是leader发来的快照,则将其进行安装,更新当前的状态机和客户端对应的最新版本号 71 | 72 | #### 3.快照: 73 | 后台协程负责检测日志的大小,达到一定大小后将快照发送给Raft 74 | 75 | ### 额外优化 76 | #### 1.read Index 77 | 在raft中,如果读请求必须写入raft日志,会造成响应速度太慢,因为我们必须试图绕过raft日志,来读取数据 78 | - 如果直接从leader读取数据,leader可能包含之前任期未提交的数据或当前节点是旧leader,因为不能直接读取数据 79 | - 如果直接从follower读取数据,因为follower的数据总是落后于leader的,所以可能读取到旧的数据 80 | 81 | 因此我们需要采用 read Index 机制 82 | - 当一个节点成为 leader 时,需要提交一个空日志,从而限制快速提交。 83 | - 当 leader 收到读请求时,首先记录此时的提交索引,然后发送心跳给其他节点,从而确认自己是否为新leader,如果大多数节点进行了确认,这等待状态机应用到时提交索引,然后进行查询,返回结果。 84 | - 当 follower 收到读请求时,首先去向 leader 获取提交索引,然后等待 leader 进行处理,当 follower 状态机应用到提交索引时,进行查询,返回结果。 85 | 86 | #### 2.lease read 87 | 如果每次读请求都需要等待一次心跳共识,那么会导致读请求的响应速度太慢,因此我们可以采用 lease read 机制 88 | leader 在发送一次心跳之后,计算出 lease 时间,为 发送心跳时间 + 选举超时时间, 在到 lease 时间之前,都是不可能出现新的 leader 的,因此在此期间的读请求不需要发送心跳共识。 89 | 90 | #### 3.PreVote 91 | 当网络发生分区时,对于少数节点的分区,因为不能选举出 leader 会导致任期不断增加,然后当网络分区结束时,会导致原来的 leader 变成 follower,然后重新进行一轮选举,因此这样效率太低,我们考虑采用 PreVote机制 92 | 当一个节点要成为 candidate 时,首先不增大任期,先进行一轮预选举,如果能够得到多半选票,则增大任期,开启正式选举,否则放弃该轮选举,继续等待。 93 | 当然,也会存在缺点,如果 leader 节点崩溃,会导致选举新的 leader 需要两轮选举 94 | 95 | #### 4.leaderShip Expiration 96 | 当网络出现分区时,对于少数节点的分区的 leader,因为无法将日志同步到多数节点,因此无法提交日志,然后持续接收请求,所以我们应该进行优化 97 | - Leader 给每个节点加上一个超时标签 98 | - 每次收到从节点对于心跳的回复时,更新标签 99 | - 当一定时间未收到回复时,标记未断连 100 | - 如果多数节点断连时,则直接阻塞请求 101 | 102 | ## shardctrler 103 | 与lab3类似, 实现Join,Move,Leave,Query操作即可 104 | Join:添加一些副本,并要进行负载均衡 105 | Move: 将某个分片分配给某个副本组 106 | Leave: 移除一些副本组,并要进行负载均衡,并进行负载均衡 107 | Query: 查询某个版本的配置信息 108 | 109 | ## ShardKV 110 | ### 1.执行命令 111 | 从 chan 取出命令 112 | 对于普通的命令,通过请求id进行去重,然后执行即可 113 | 对于配置更新命令,判断配置版本是否为当前配置的下一个版本,如果是这进行将分片的状态进行更新 114 | 对于插入分片命令,首先判断版本号是否匹配,然后将每一个处于Pulling状态的分片插入,并修改为 GCing 状态, 然后将每个客户端对应的请求id复制过来。 115 | 对于删除分片命令,首先判断版本号是否匹配,然后将每一个处于 BePulling 状态的分片删除,改成一个处于 Serving 状态的空分片。将每一个处于 GCing 状态的分片改为 Serving 状态 116 | ### 2.配置更新 117 | 后台协程定期检测分片状态,如果状态不为 Serving 则说明有任务需要处理,那就不能进行配置更新。如果都为 Service 则查询当前配置的下一个版本是否存在,存在则将配置更新命令写入到 Raft。 118 | ### 3.分片迁移 119 | 后台协程定期检测是否存在Pulling状态的分片,如果存在,则开启多个协程去通过RPC调用拉取每一个分片 120 | 副本收到拉取分片的请求时,首先判断如果需要拉取的数据的配置版本号比当前配置的版本号要大,则拒绝拉取。否则所需将分片的键值对和客户端对应的请求id复制过去 121 | ### 4.分片清理 122 | 后台协程定期检测是否存在GCing状态的分片,如果存在,则开启多个协程去通过RPC调用删除每一个分片 123 | 副本收到删除分片的请求时,首先判断如果需要拉取的数据的配置版本号比当前配置的版本号要大,则拒绝删除。否则将所需要删除的分片命令写入 Raft 124 | ### 5.分片状态 125 | Serving 服务状态,如果该分片属于该副本组,则可以提供读写服务,否则不能提供读写服务,但不影响配置更新 126 | Pulling 该分片在当前配置版本属于该副本组,暂不可提供读写服务,但是需要从上一个配置版本负责该分片的副本组将分片拉取过来 127 | BePulling 该分片在当前配置版本不属于该副本组,不可提供读写服务,但是在上一个配置版本属于该副本组,正在等待被当前配置版本负责该分片的副本组拉取 128 | GCing 该分片在当前配置版本属于该副本组,可以提供读写服务,但是需要将上一个配置版本负责该分片的副本组的分片删除 129 | 130 | ## Gossip协议 131 | Gossip协议是一种允许在分布式系统中共享状态的去中心化通信协议,通过这种协议,我们可以将信息传播给网络或集群中的所有成员。 132 | 133 | ### gossip协议消息传播模式 134 | ### 1.反熵 135 | 熵是指节点指节点之间数据的混乱程度/差异性,反熵是指消除不同节点中数据的差异,提升节点间数据的相似度。 136 | 集群中的节点,每隔一段时间就随机选择某个其他节点然后通过互相交换自己的所有数据来消除两者指将的差异,实现数据的最终一致性。 137 | 但是在节点过多或者动态变化的场景,不太适合反熵 138 | 139 | ### 2.谣言传播 140 | 集群中的节点,每隔一段时间就随机选择某个其他节点然后通过交换自己新增的数据来实现数据共享 141 | 142 | ### 3.消息传播方式 143 | 推方式:就是将自己的所有副本数据,推给对方。 144 | 拉方式:就是拉取对方的所有副本数据。 145 | 推拉方式:就是同时修复自己副本和对方副本。 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/handbook/Golang.md: -------------------------------------------------------------------------------- 1 | # Golang 2 | 3 | ## 1.chan中无缓冲和有缓冲的区别 4 |
5 | 答案 6 |

无缓冲chan在写入和读出时都会立马阻塞

7 |

有缓冲chan在缓冲区满时会写入阻塞,缓冲区为空时会读出阻塞

8 |
9 | 10 | ## 2.chan的主要作用 11 |
12 | 答案 13 |

不同goroutine之间进行通信

14 |
15 | 16 | ## 3.go如何实现面向对象 17 |
18 | 答案 19 |

面向对象包括三大特征,封装继承多态

20 |

go的结构体可以对属性进行封装,并且结构体可以通过内嵌匿名类型实现继承,通过接口和方法可以实现多态

21 |
22 | 23 | ## 4.make和new的区别 24 |
25 | 答案 26 |

1.使用对象不同:new可以用于任何类型,make只能用于slice,map,chan

27 |

2.返回值不同:new的返回值是指向该类型的指针,make返回的是原始类型

28 |

3.用途不同:new主要用于分配内存,make主要用于初始化slice,map,chan

29 |

4.使用方式不同:new不需要指定容量和长度,make需要指定容量和长度

30 |
31 | 32 | ## 5.go在main之前会执行什么函数吗 33 |
34 | 答案 35 |

会执行init函数

36 |

一个程序的执行顺序是 import -> const/var -> init -> main

37 |
38 | 39 | ## 6.go有没有异常类型 40 |
41 | 答案 42 |

go没有异常类型,只有错误类型error,一般用error表示异常

43 |
44 | 45 | ## 7.GMP是什么 46 |
47 | 答案 48 |

gmp是goroutine的一个调度模型

49 |

g是goroutine,是go对协程的一个抽象和实现,g有自己的运行栈,状态以及执行任务的函数,需要绑定到p上才能执行,在g看来,p就是cpu

50 |

m是go对线程的抽象,m不直接执行g,而是先和p绑定,然后由p代理执行,通过p的存在m不需要和g绑死,也不需要记录g的状态信息,所以g可以实现跨m运行

51 |

p是gmp的中枢,实现了g和m的动态结合,对于g而言,p就是它的cpu,对于m而言,p就是它的执行代理,为其提供信息的同时,隐藏了复杂的调度细节,p的数量决定了g的最大可并行数量,通过GOMAXPROCS进行设定(超过cpu核数没有意义 )

52 |
53 | 54 | ## 8.GMP模型为什么要有P 55 |
56 | 答案 57 |

如果只有GM会导致多个M竞争同一个全局队列,P的出现能够降低对全局队列的依赖,同时P能够使得G创建的子G在同一个M中运行,从而提高局部性,减少线程切换带来的额外开销

58 |
59 | 60 | ## 9.什么时候会发生数据竞争 61 |
62 | 答案 63 |

两个以上的协程访问和操作同一共享数据

64 |
65 | 66 | ## 10.100个协程执行了50个,51panic后面的还执行吗 - 不想退出怎么做 67 |
68 | 答案 69 |

后面的不会执行,如果不想退出需要在panic的协程处使用recover捕获错误

70 |
71 | 72 | ## 11.map的访问是有序还是无序的 73 |
74 | 答案 75 |

是无序的,因为它的代码中就是随机生成的起点,为了使得程序员不能依赖map的遍历顺序,这样设计的主要目的是因为map会动态扩容,key的位置可能发生改变,所以是无序的

76 |
77 | 78 | ## 12.channel底层 79 |
80 | 答案 81 |

channel本质上是一个环形数组,channel结构体包含了指向环形数组的指针,等待发送队列,等待接收队列,待接收的第一个元素的下标(recvx),待发送的元素下标(sendx),recvx~sendx-1之间等待接收的数据,互斥锁,并且channel是线程安全的

82 |
83 | 84 | ## 13.对已经关闭的channel进行操作会怎么样? 85 |
86 | 答案 87 |

读已经关闭的channel能够正常读出,如果channel为空,会读到零值,写和关闭已经关闭的channel会导致panic

88 |
89 | 90 | ## 15.go map 并发安全吗?为什么 91 |
92 | 答案 93 |

不安全,因为它内部维护了一个变量,如果多个协程同时访问或操作同一个map会导致panic().这样设计的原因是因为map不需要并发的场景更多,如果因为为了并发访问而加锁,会导致性能严重下降,如果需要并发安全可以使用sync.Map

94 |
95 | 96 | ## 16.go中切片和数组的区别 97 |
98 | 答案 99 |

1.长度:切片是不定长的,数组是定长的

100 |

2.内部实现:数组是基于连续的内存空间的存储结构,而切片则是存储了底层数组的指针和容量和长度的结构体

101 |

3.声明方式:数组声明时必须指定长度,而切片可以不指定长度

102 |
103 | 104 | ## 17.切片扩容 105 |
106 | 答案 107 |

当发生切片扩容时,首先判断新的容量是否超过旧容量的两倍,如果超过,则直接将切片容量赋值为新切片容量,否则判断旧切片容量是否超过256,如果不超过则直接赋值为旧切片容量的两倍,否则将切片容量增加到当前的1.25倍 再加上256的四分之三,直到超过新切片容量为止,然后再进行内存对齐

108 |
109 | 110 | ## 18.Go的GC原理 111 |
112 | 答案 113 |

go的gc采用的是三色标记法,分为白色,灰色,黑色三种。白色表示等待回收对象,灰色表示保留但未访问对象,黑色表示保留已访问对象

114 |

首先将根节点对象染成灰色,再从灰色集合中取出一个节点将其改为黑色,放入黑色集合,然后将其引用的节点改为灰色,放入灰色集合,直到灰色集合为空为止,然后开始回收白色对象

115 |

GC过程中需要STW,性能很低,但是如果并发GC的话,会导致引用对象丢失,一般会采用屏障机制来解决

116 |
117 | 118 | ## 19.屏障机制 119 | 120 |
121 | 答案 122 |

屏障机制分为插入写屏障机制,删除写屏障机制,混合屏障机制,其主要原理都是为了满足三色不变式,三色不变式有两种,强三色不变式和弱三色不变式,强三色不变式是指黑色对象不能引用白色对象,弱三色不变式是指黑色对象可以引用白色对象,但是白色对象必须存在其他灰色对象对它的引用

123 |

插入写屏障就是当增加一个对象时,将当前新增对象染成灰色,满足强三色不变式。但是由于插入写屏障是针对堆上对象而言的,栈对象无写屏障,所以会导致可能存在黑色对象引用白色对象,所以扫描结束后必须STW重新扫描栈才能不丢失对象

124 |

删除屏障机制就是在开始时STW,扫描所有根对象,使得根节点为黑色,根节点引用的对象都是灰色,满足弱三色不变式,然后结束STW。然后当一个灰色或白色对象删除引用的一个对象白色对象时,将被删除的对象改为灰色,以保持弱三色不变式

125 |

混合写屏障是优先扫描栈,将栈上所有可达对象标记为黑色,不需要STW,扫描到某个栈的时候,需要暂停当前栈的工作,栈上新添加对象直接标记为黑色,对于堆,被删除标记为灰色(可能栈对象引用该对象),被添加对象标记为灰色(保证强三色不变式)

126 |
127 | 128 | ## 20.gc触发的时机 129 |
130 | 答案 131 |

1.手动触发:通过调用runtime.GC()函数触发

132 |

2.周期性触发:程序启动的时候会创建一个监控线程,当周期性的触发GC

133 |

3.创建对象的时候触发:在创建对象的时候,会调用mallocgc函数,如果创建的大对象或mcache没找到对应的mspan,会触发gc

134 |
135 | 136 | ## 21.slice在做函数参数是的修改和添加是怎么回事,会改变实参吗 137 |
138 | 答案 139 |

slice在做函数参数的时候,是将slice的值复制了一遍,当修改slice的时候,因为修改了底层数组,所以原slice中的底层数组的值也会发生改变,但是当添加元素的时候,由于原slice的cap参数没有改变,所以虽然底层数组添加了元素,但是在原slice看不到

140 |
141 | 142 | ## 22.go中有引用传递吗?为什么? 143 |
144 | 答案 145 |

go中只有值传递,在函数的参数传递时,都是将值拷贝了一遍

146 |
147 | 148 | 149 | ## 23.逃逸分析 150 |
151 | 答案 152 |

在编译阶段,由编译器进行逃逸分析,进行逃逸分析检查,决定分配到栈上还是堆上。一般对于存在外部引用的指针,接口(因为在编译期无法确定接口的具体类型,因此会发生逃逸),大对象,闭包都会发生逃逸

153 |
154 | 155 | ## 24.根对象包括什么? 156 |
157 | 答案 158 |

全局变量,执行栈,寄存器,执行栈包括栈上变量和指向堆内存区的指针

159 |
160 | 161 | ## 25.map底层 162 |
163 | 答案 164 |

map的底层是一个hmap的结构体,其中包括指向旧的buckets的指针,指向当前的buckets的指针,B(buckets数组长度的对数),hash0(哈希参数),count(总元素数量),flag(是否有协程在操作map)等,buckets指向的是一个bmap数组,bmap的底层是一个tophash数组,key数组,value数组(长度为8),填充字段和指向溢出桶的指针。bmap中k/v的排列是k1,k2...v1,v2排列的,这样可以减少需要填充的空间

165 |
166 | 167 | ## 26.map的查找 168 |
169 | 答案 170 |

首先根据key计算出对应的hash值,然后取出hash值的低B位,表示该key在bmap数组总的下标,定位到对应的bmap后,遍历bmap中的tophash数组,然后判断是否正在扩容,如果正在扩容则判断当前的bmap的元素是否已经搬迁完毕,如果没有,则遍历旧bucket中对应的bmap,然后取出hash值中的高8位作为tophash值,然后与tophash数组中的值进行比对,如果相同则根据当前tophash数组中的位置定位到key的位置,比较key是否相同,如果相同则返回对应的value值,如果没有找到会继续沿着溢出通查找,如果还未找到目标元素则返回空值

171 |
172 | 173 | ## 27.map的遍历 174 |
175 | 答案 176 |

首先随机生成一个初始的bmap下标,然后开始遍历bmap,如果正在扩容,则去遍历对应的旧bucket的bmap,因为该bmap会分配到两个新的bmap中,所以我们只用遍历该bmap中,分配到当前bmap的元素。然后依次遍历即可

177 |
178 | 179 | ## 28.map插入元素 180 |
181 | 答案 182 |

首先根据key定位到对应的bmap,然后遍历bmap中的tophash数组,记录第一个空闲位置,如果找到相同的key则直接更新对应的value值,如果没有找到则将其插入空闲位置,如果没有空闲位置则添加一个溢出桶,将其插入,添加溢出桶时需要判断是否需要扩容,如果需要扩容,则还需要重新定位插入位置

183 |
184 | 185 | ## 29.gmp当一个g堵塞时,m、p会发生什么? g阻塞结束后会发生什么? 186 |
187 | 答案 188 |

当一个g阻塞后,执行它的m也会阻塞,然后调度器会将m的p分离,如果此时存在空闲的m,则会将p绑定到空闲的m上,阻塞结束后与原来的m会寻找空闲的p,如果找到了,则将其绑定,继续执行原来的g。如果没找到空闲的p,则会将原来的g放入全局队列,然后将原来的m放入缓冲池睡眠

189 |
190 | 191 | ## 31.有缓冲channel 发送数据和接收数据的流程 192 |
193 | 答案 194 |

发送数据:首先判断channel是否为nil,如果为nil,则根据block变量决定是否阻塞。否则加锁,然后判断channel是否关闭,如果已经关闭,则panic,然后会看等待接收队列是否有协程等待,如果有的将其从接收队列取出,直接复制给接收者,否则的话,如果缓冲区未满,则直接写入缓冲区,否则,先判断是否为阻塞发送,如果是,则阻塞,然后将协程放入等待发送队列。否则直接退出

195 |

接收数据:首先判断channel是否为nil,如果为nil,则根据block变量决定是否阻塞。否则加锁,然后判断channel是否关闭,如果已经关闭,则直接读出其中的值,如果没有则读出空值,然后会看等待发送队列是否有协程等待,如果有的将其从发送队列取出,直接从发送者复制过来,否则的话,如果缓冲区不为空,则直接从缓冲区读出,否则,先判断是否为阻塞接收,如果是,则阻塞,然后将协程放入等待接收队列。否则直接退出

196 |
197 | 198 | ## 32.对 nil chan 进行操作会发生什么? 199 |
200 | 答案 201 |

向nil chan 读出和写入数据会永久阻塞,close nil chan 会直接panic

202 |
203 | 204 | ## 33.context是什么 205 |
206 | 答案 207 |

context是一种成为类似于上下文的东西,主要用于父子节点之间同步取消信息,是一种协程调度的方式,并且context是线程安全的

208 |
209 | 210 | ## 34.开辟多个写协程向一个channel中写数据,是有序吗 211 |
212 | 答案 213 |

不是有序的,因为多个协程竞争一个channel,顺序是随机的,可以通过加锁来保证有序性

214 |
215 | 216 | ## 35.拷贝大切片一定比拷贝小切片代价大吗? 217 |
218 | 答案 219 |

对于浅拷贝来说,就是直接结构体值的复制,对于大小切片的代价都是一致的

220 |

对于深拷贝来说,会将底层数组的值全部拷贝,所以拷贝大切片代价比拷贝小切片代价大

221 |
222 | 223 | ## 36.go哪些数据类型是线程安全的 ? 224 |
225 | 答案 226 |

sync.Map,Once,WaitGroup,Pool,chan,读写锁,互斥锁

227 |
228 | 229 | ## 37.map可寻址吗 ? 230 |
231 | 答案 232 |

map本身作为一个结构体是可以寻址的,但是map中的元素是不可寻址的,因为map中的元素的地址总是变化着的,所以不可寻址

233 |
234 | 235 | ## 37.map的两种扩容方式 ? 236 |
237 | 答案 238 |

map包括等量扩容和翻倍扩容,等量扩容是为了应对map中存在大量空的溢出桶,翻倍扩容是为了应对map中大量桶都已经装满的情况

239 |
240 | 241 | ## 38.Map 的扩容机制 ? 242 |
243 | 答案 244 |

发生扩容的条件是如果有溢出,并且装载因子超过6.5或者溢出桶的数量超过桶的数量。装载因子是元素数量/桶的数量。第一种条件是为了应该大多数桶都装满了的情况,第二种是为了应该存在很多的空溢出桶的情况

245 |

第一种采用翻倍扩容,第二种采用等量扩容。然后扩容并不是原子的,而是通过搬迁函数实现的,每次搬迁两个bucket,搬迁过程中,会将需要搬迁的bucket分裂成两个bucket,将里面的元素均分到两个bucket中

246 |
247 | 248 | ## 39.sync.map底层结构 249 |
250 | 答案 251 |

sync map的底层是一个只读的map和一个可读可写的map,访问只读的map不需要加锁,实现了读写分离,

252 |

sync.Map包含read,dirty,misses,mu字段,read字段包括一个map和amend变量,amend变量表示dirty中是否存在read中不存在的元素,read表示一个只读的map,不需要加锁,dirty就是一个map,它是可读可写的,它的读写操作都要加锁,misses记录了在read中访问不到,去访问dirty的次数,如果该次数超过了dirty的长度时,会将会dirty赋值给read,此时read中被删除的key才真正被释放。mu表示互斥锁。

253 |

在读取数据的时候,会先去read中读取,如果读到了则直接返回,否则去dirty中读,并增加misses。

254 |

在写数据的时候,如果key还存在或只是被软删除,则只需要在read map上进行cas操作,实现无锁更新,因为存储的是指针,dirty map 也会同步更新。否则需要加锁插入dirty map,并增加misses,如果misses达到dirty map 的长度,则会将dirty map 和 read map进行轮换,并将dirty map 置空(但是在dirty map为空并进行写操作时又会将read中的值拷贝过来),并将其中软删除的值彻底删除

255 |

在删除数据时,如果key在read map中,则进行软删除,否则直接去dirty map中彻底删除

256 |

由此可见sync.Map适用于读多写少的场景,但是使用的时候需要注意,key被delete的时候并没有被释放,只有当misses到达dirty的长度时才会释放。

257 |
258 | 259 | ## 40.向一个 nil 的切片中 append 数据可以吗 260 |
261 | 答案 262 |

可以,因为在append内部,如果被append的切片是nil,那么它会将其初始化

263 |
264 | 265 | ## 41.结构体中的tag 有什么作用 266 |
267 | 答案 268 |

1.序列化和反序列化

269 |

2.数据库orm映射,通过sql标签获取对应数据库中的值

270 |

3.数据校验

271 |
272 | 273 | ## 42.Go里面的结构体可以进行比较吗? 274 |
275 | 答案 276 |

go中的结构体是否能比较取决于其属性中的是否都是可比较类型,如果包含map,chan,slice这些不可比较字段,那么结构体是不可比较的。但是我们也可以通过deepequal进行比较

277 |
278 | 279 | ## 43.mutex是个悲观锁还是乐观锁,乐观锁和悲观锁的区别? 280 |
281 | 答案 282 |

mutex是悲观锁 283 |

乐观锁在操作的时候,不会上锁,而是记录该数据的时间戳或版本号,在更新的时候判断版本号或时间戳是否发生改变,如果发生改变则放弃操作,否则执行操作 284 |

悲观锁在操作数据时直接上锁,直到操作结束才释放锁,上锁期间其他人不能修改数据

285 |
286 | 287 | ## 44.go引用类型 288 |
289 | 答案 290 |

引用类型是指一个变量和另一个变量地址完全一致

291 |

某种程度上,引用类型包括map,slice,chan。但是本身,map,slice,chan都是结构体,但是由于go编译器在取地址时,取的时底层data数组的地址,所以在这个角度上,可以看作引用类型。但是slice比较特殊,因为append可能会导致发生改变,从而导致传递后的地址不一致

292 |
293 | 294 | ## 45.新建一个协程会占用多少内存 295 |
296 | 答案 297 |

一般为2kb左右

298 |
299 | 300 | ## 46.golang中如何拼接字符串?哪种效率最高? 301 |
302 | 答案 303 |

1.直接通过+拼接

304 |

2.通过fmt.Sprintf拼接

305 |

3.通过strings.Builder拼接,该方式效率最高

306 |

4.strings.join拼接,它是基于strings.Builder实现的

307 |

5.通过bytes.Buffer拼接

308 |
309 | 310 | ## 47.只采用读写锁+map的形式有什么弊端? 311 |
312 | 答案 313 |

在读操作远大于写操作的时候,读写锁应能优势并不明显,因为写操作会阻塞读操作,不如sync.Map更好

314 |
315 | 316 | ## 48.map可以边遍历边删除吗? 317 |
318 | 答案 319 |

对于不同协程,一个遍历一个删除肯定是会panic的,但是对于同一个协程是可以的,但是遍历可能会包含已删除的key,这取决于删除key的时间

320 |
321 | 322 | ## 49.go的并发编程如何避免死锁? 323 |
324 | 答案 325 |

1.尽可能的顺序加锁

326 |

2.使用context控制超时时间,避免一直等待 327 |

3.使用死锁检测工具

328 |
329 | 330 | ## 50.GMP中调度机制,有了解过hand off和work-stealing机制吗 331 |
332 | 答案 333 |

hand off 机制就是指当某个M因为G系统调用时,会将M和P进行分离,如果此时存在空闲的M,则直接将P与空闲的M绑定,如果不存在空闲的M,则创建一个M,与其绑定。当G阻塞结束后,M会寻找原来那个P,如果该P已经和其他的M绑定了,就会寻找空闲的P,与其绑定,继续执行G,如果没有空闲的P,则将M放入缓冲池睡眠,将G放入全局运行队列

334 |

work-stealing机制就是M运行时,会从本地运行队列取,如果本地运行队列为空,则去全局运行队列取,如果全局运行队列为空,则会去偷取其他P本地运行队列中的G

335 |
336 | 337 | ## 52.defer的执行流程 338 |
339 | 答案 340 |

defer一般用于函数或方法的延迟执行,当其包含参数时,参数会被立马计算,对于链式调用会将除最后一个都执行掉,然后会以后进先出的顺序执行defer的函数

341 |
342 | 343 | ## 53.goroutine 什么时候会被回收 344 |
345 | 答案 346 |

1.正常退出

347 |

2.panic

348 |

3.通过context取消

349 |
350 | 351 | ## 54.是否可以无限创建 goroutine 352 |
353 | 答案 354 |

不能,无限创建协程会导致短时间内占据操作系统的资源,然后最终因为资源紧缺而被系统强制终止。所以我们需要控制协程的数量。我们可以通过有缓冲的chan或信号量来控制协程的数量

355 |
356 | 357 | ## 55.什么情况会出现 goroutine 泄漏 358 |
359 | 答案 360 |

启动的goroutine因为某些原因不能正确结束和回收,导致长时间占用内存和资源而无法得到释放

361 |

可能出现的情况包括协程死循环,channel阻塞,select所有case都阻塞。我们可以通过context来进行超时控制,执行超过一定时间自动取消,还可以通过pprof来检测协程泄露情况

362 |
363 | 364 | ## 56.for range 中赋值的变量,这个变量指向的是真实的地址吗,还是临时变量 365 |
366 | 答案 367 |

for range 本质上是在for range外面使用了一个变量保存了值,然后不断将值复制给这个变量,指向的地址都是相同的

368 |
369 | 370 | ## 54.如果在for range里面有一个函数,这个函数需要传一个指针,这时候应该怎么写? 371 |
372 | 答案 373 |

可以用过创建一个局部变量来传指针

374 |
375 | 376 | ## 55.Context了解吗,介绍一下它接口里的几个方法 377 |
378 | 答案 379 |

Context是一个接口,包含几个方法,Err,Deadline,Value,Done。Err用于返回错误,Deadline用于返回是否会被取消,以及自动取消时间。Value()获取key对应的value,Done用于返回一个只读的chan,用于判断context是否被取消

380 |

valueCtx则是包含了父亲的context并存储了一个key和对应的value

381 |

cancelCtx则是包含了父亲的context,并存储了儿子节点的信息,如果当前ctx被取消,也会将儿子节点一并取消

382 |

timeCtx包含一个定时器和超时时间,还内嵌了一个cancelCtx继承了其方法,能够在超时后调用cancelCtx的方法将其取消

383 |
384 | 385 | ## 56.waitgroup 的底层原理是什么 ? 386 |
387 | 答案 388 |

waitgroup的底层就是维护一个信号量和等待者的数量waiter和需要等待的数量counter。信号量负责唤醒协程和挂起协程。这里使用一个无锁优化,将waiter和counter合并在一个字段上,因为在修改waiter和counter的时候要保证并发安全,将其绑定在一起可以使用cas来避免加锁,提高效率

389 |
390 | 391 | ## 57.goroutine并发控制怎么做 ? 392 |
393 | 答案 394 |

1.可以通过全局变量来实现,子协程检查该变量的值来实现并发控制,但很难实现子协程之间的通信

395 |

2.通过channel发送信号来控制

396 |

3.通过context来实现

397 |
398 | 399 | ## 58.go 同步原语 400 |
401 | 答案 402 |

1.互斥锁

403 |

2.读写锁

404 |

3.WaitGroup

405 |

4.Once

406 |

5.Cond 用来协调想要访问共享资源的那些 goroutine,当共享资源的状态发生变化的时候,它可以用来通知被互斥锁阻塞的 goroutine

407 |
408 | 409 | ## 59.select底层 410 |
411 | 答案 412 |

1.首先根据select内部的语句进行优化,比如,没有case,没有default,只有一个case,都有不同的优化

413 |

2.然后将不同的case封装成一个结构体scase

414 |

3.然后生成一个随机的遍历顺序和按锁地址确定加锁顺序,以能够公平的访问每个chan,避免饥饿。按锁的地址来确定加锁顺序,以避免死锁的发生,然后按照生成的加锁顺序将所有的chan锁住

415 |

4.根据生成的遍历顺序,遍历所有的case,查看是否有可以立刻处理的chan,如果有,直接获取对应的索引并返回,否则将当前协程封装成一个sudog,然后写入对应的等待发送队列或等待接收队列.然后将当前协程挂起

416 |

5.协程被唤醒后,找到可以直接处理的case,返回对应的索引

417 |
418 | 419 | ## 60.go CSP模型 420 |
421 | 答案 422 |

go的csp模型是通过goroutine和channel实现,goroutine负责实现并发执行,而channel实现goroutine之间的协调和通信

423 |
424 | 425 | ## 61.go pool底层原理 426 |
427 | 答案 428 |

go pool 的底层是一个本地对象池和一个上一轮的本地对象池,它利用了GMP的特性,一个M绑定在一个P上,所以本地对象池的数量就是P的数量,这样可以减少并发的情况。然后记录上一轮的本地对象池,是一种牺牲者机制,因为每次gc前就会将本地对象池清空,但是如果直接清空,会导致内存波动比较大,所以需要有使用牺牲者机制来缓冲

429 |

本地对象池是一个链表+环形数组的结构,而环形数组的首尾指针的存储,使用了无锁优化,因为如果不同的P同时修改head和tail会导致并发冲突,所以我们可以将其合并到一个字段上,然后使用cas来确保只会有一个修改成功

430 |

当需要取对象时,首先需要调用pin函数,将当前的p锁定,以避免hand off机制将P与M分离,然后从本地对象池的私有对象中取,这样可以避免从共享对象池中取的繁琐操作。如果没有则去本地对象池中取,要从队头取,如果也没有,则去其他P的本地对象池中取,从队尾取,这样可以尽可能的减少并发冲突。如果还没有则去上一轮的本地对象池中取,如果还没有则New一个新的

431 |

当需要放入对象时,首先需要调用pin函数,然后尝试放入私有对象,如果已经存在,则放入本地对象池中,如果链表头的环形数组已满,则创建一个长度为上一个环形数组大小两倍的数组,从队头写入

432 |
433 | 434 | ## 62.interface 435 |
436 | 答案 437 |

接口分为iface和eface,eface是一个空接口,所有类型都实现了这个接口

438 |

iface包括data和itab,data记录了接口值的地址,itab记录了接口的相关信息,itab包括interfacetype,_type,fun,_,hash。interfacetype记录了接口的类型信息,interfacetype包括_type,pkgpath,mhdr,_type记录了接口类型,pkgpath记录了接口的包路径,mhdr记录了接口方法对应的名字和类型,_type记录了接口值的类型信息,包括记录了该类型实例所占用的内存大小,该类型中指针数据的大小,类型的哈希值,反射相关等变量,fun是一个可变数组,记录了具体的类型实现接口方法的函数地址.判断某个类型是否实现了某个接口,因为go已经将方法排好了序,所以双指针即可

439 |

eface包括data和type,data记录了接口值的地址,type记录了接口值的类型

440 |
441 | 442 | ## 63.反射原理以及那些场景会用到反射 ? 443 |
444 | 答案 445 |

反射本质上是通过读取interface读取变量内部的值和类型,reflect包含两个类型type和value,type记录通过读取_type值来实现的,value则是结合了_type和data,本质上是通过将值的地址转为unsafe.Pointer然后转成空接口获取对应的信息

446 |

反射一般用于数据库orm,访问结构体的内部字段,结构体tag的处理,自定义序列化和反序列化逻辑,动态方法的调用

447 |
448 | 449 | ## 64.值接收者和指针接收者有什么区别? 450 |
451 | 答案 452 |

一般值接收者的方法不会改变接收者的属性 指针接收者的方法一般会改变接收者的属性或接收者比较大 无论你的接收者是指针还是值,都可以调用,因为编译器会自动帮你转成对应的接收者。但是当转为指针接收者时,必须满足结构者可寻址

453 |
454 | 455 | ## 65.defer相比在函数的最后执行有什么优势 456 |
457 | 答案 458 |

避免代码逻辑混乱,防止遗漏

459 |
460 | 461 | ## 66.GMP G的数量,M的数量,P的数量受到什么的限制? 462 |
463 | 答案 464 |

G是协程,所以G的数量可能受到内存的限制,因为每个协程的数据结构都是要占用系统内存的,但是可以通过开启swap机制来解决

465 |

M的数量受到系语言设定(10000),debug.SetMaxThreads限制

466 |

P的数量runtime.GOMAXPROCS限制,一般来说P的数量就是CPU的核数

467 |
468 | 469 | ## 67.如何控制协程的生命周期? 470 |
471 | 答案 472 |

1.channel

473 |

2.context

474 |

3.waitgroup

475 |
476 | 477 | ## 68.主协程如何知道子协程是否退出 478 |
479 | 答案 480 |

1.channel

481 |

2.context

482 |

3.waitgroup

483 |
484 | 485 | ## 69.go map 删除key的时候内存会被释放吗? 486 |
487 | 答案 488 |

不会,他只是会将其设置为空,只有当map被设置为nil后,并触发gc才会被回收

489 |
490 | 491 | ## 70.map的key只能是哪些类型? 492 |
493 | 答案 494 |

可以比较的类型才能作为key

495 |
496 | 497 | ## 71.为什么浮点数作为map的key会有问题? 498 |
499 | 答案 500 |

浮点数在作为key的时候会出现精度问题

501 |
502 | 503 | ## 72.如何对切片进行扩容? 504 |
505 | 答案 506 |

1.通过append进行扩容

507 |

2.通过copy进行扩容

508 |
509 | 510 | ## 73.函数传递数组时,如何修改原数组的值? 511 |
512 | 答案 513 |

1.使用指针

514 |

2.使用切片

515 |
516 | 517 | ## 74.go的内存模型 518 |
519 | 答案 520 |

go将对象分成微对象(size<16B),小对象(16B<=size<=32KB),大对象(size>32KB),不同对象采用不同的内存分配策略

521 |

page是go最小的存储单位,为8KB。

522 |

mspan是go最小的管理单元,是连续的若干个page的集合。从8KB到80KB被划分成67种不同的规格(还有一种隐藏的0级,用于处理更大的对象,上不封顶),根据对象的大小映射到不同规格的mspan中,从中获取内存,mspan通过bitmap快速找到空闲内存块

523 |

mcache是每个P独有的缓存,因此不需要锁。mcache缓存了带指针和不带指针的所有规格的mspan,共136个。还包含一个tiny对象分配器,用于处理微对象

524 |

mcentral是中心缓存,每个mcentral存储了一种规格的mspan,并包含了两个链表,用于记录空的mspan列表和满的mspan列表。每个mcentral包含一个互斥锁

525 |

mheap是go对于堆的抽象,mheap有两个treap,scav是一个用于存放空闲的垃圾回收得到的mspan的treap,free一个用于存放空闲的向操作系统申请的mspan和不需要垃圾回收的mspan的trap(优先申请free的,因为scav中尽管已经垃圾回收了可能还是需要进行一些处理)。如果找到的mspan大于需要的内存,则将其进行分割,将多余部分重新插进treap中

526 |

对于微对象申请内存:

527 |

1.从所在P的mcache的tiny分配器取内存。

528 |

2.根据size对应的mspan规格,从所在p的mcache中取内存

529 |

3.根据size对应的mspan规格,从mcentral中取mspan填充到mcache中,然后从mspan中取内存

530 |

4.根据size对应的mspan规格,从mheap中的页分配器中取空闲页组装成mspan填充到mcache中,然后从mspan中取内存

531 |

5.mheap向操作系统申请内存更新页分配器的索引信息,并执行4

532 |

对于小对象申请内存从2开始

533 |

对于大对象的申请内存从4开始

534 |

当mcache对应的mspan中的内存不够用了的时候会触发GC或申请大对象时会触发GC

535 |
536 | 537 | ## 75.gmp的底层原理 538 |
539 | 答案 540 |

g的底层是一个结构体g,包含指向m的指针以及栈和寄存器信息

541 |

m的底层是一个结构体m,包含一个指向一类特殊goroutine的指针g0(不用于执行用户函数,负责执行g之间的切换和调度,一个m就有一个对应的g0),还包含了tls(是对线程的本地存储,只对当前线程可见,其中存在指向当前运行的g的指针),还包含当前绑定的p的指针

542 |

p的底层是一个结构体p,包含了一个本地goroutine队列,最大长度256,还保存了一个指针,指向下一个可以执行的goroutine,和指向当前代理的m的指针

543 |

sched是全局goroutine队列的分装,并且有一个lock保证并发安全

544 |

所以g一共有两类,一种用于执行固定的调度流程,与m是1对1的关系,另一种是执行用户函数的普通g

545 |

m通过p的调度执行goroutine在g0和普通g之间切换,当g0找到可以执行的g的时候会调用gogo方法,调度g执行用户自定义的任务,当g主动让渡或者被动调度的时候,会触发mcall方法,将执行权交还g0

546 |
547 | 548 | ## 76.gmp的调度类型 549 |
550 | 答案 551 |

1.主动调度:用户主动执行让渡的方式,通过调用gosched方法,此时g就会让出执行权,进入队列等待下一次调度执行(调用mcall)

552 |

2.被动调度:当g进入阻塞状态时(休眠,chan阻塞,垃圾回收等),通过go_park挂起当前协程,直到调用go_ready方法,才将g从阻塞态唤醒,重新进入等待执行状态

553 |

3.正常调度:g中的执行任务已经完成,就会设置为go_dead状态,发起新一轮的调度

554 |

4.抢占调度:如果g执行系统调用超过指定时间,并且全局的p资源比较紧缺,就会将p和m进行解除绑定,然后去寻找空闲的m,如果没有空闲的m那就创建一个新的m,如果有则直接与其绑定

555 |
556 | 557 | ## 77.sync.Cond的底层原理 558 |
559 | 答案 560 |

Cond通过checker(防止在运行期间拷贝)和noCopy(防止在编译期拷贝)实现不可复制,通过Locker保护条件变量,notify是一个阻塞链表,分别存储了等待的goroutine的数量,下一个被唤醒的goroutine的索引(不能直接从队头取是因为队头的元素可能不是第一个,在发放完票据后就解锁了),为了防止并发冲突的锁,链表头指针,链表尾指针

561 |

一般用于多个协程等待,一个协程通知的场景

562 |
563 | 564 | ## 78.sync.Once的底层原理 565 |
566 | 答案 567 |

Once的底层由一个变量记录是否已经执行过,和一个锁保证线程安全

568 |
569 | 570 | ## 79.singleFlight 571 |
572 | 答案 573 |

singleFlight是基于waitgroup实现的,使用map记录了对应key同一时间的响应和锁保证并发安全,如果执行函数时,发现已经存在于map中,则调用waitgroup的wait函数等待,然后从map中拿到返回值。如果不存在与map中则创建响应,并调用函数,完成后将响应从key从map中删除。并将结果发送给调用doChan的协程

574 |
575 | 576 | 577 | ## 80.sync.Mutex 578 |
579 | 答案 580 |

Locker的底层就是一个状态值和信号量。信号量用于挂起和唤醒协程,该状态值是一个复合字段,包括locked(该锁是否被持有),woken(是否有协程因为锁释放而被唤醒,保证在锁释放时只会唤醒一个真在等待的协程,减少上下文切换),starving(是否处于饥饿状态,饥饿状态是为了解决新的来协程先拿到锁,而导致等待的协程一致拿不到锁,当协程等待时间超过1ms会进入饥饿模式,一个协程获取到锁,并且处于队尾或等待时间少于1ms,则回到正常模式),waitercount(等待的协程的数量)

581 |
582 | 583 | ## 81.sync.RMutex 584 |
585 | 答案 586 |

基于Mutex实现,并记录了等待read的数量,写者等待完成的读者的数量(用于写者等待时,如果最后一个读者会唤醒写者),read的信号量(用于挂起和唤醒read协程),write的信号量(用于挂起和唤醒write协程)

587 |
588 | 589 | ## 82.concurrent_map底层原理 590 |
591 | 答案 592 |

concurrent_map的底层是32个map,每个map包含一个读写锁,本质上就是对key进行分片,将key分散到32个不同的map中,从而实现小粒度加锁,减少锁竞争,因为要实现分片,因此key必须是string或提供分片方法。concurrent_map可以用在任何场景,解决了sync.map写多读少性能低下的问题

593 |
594 | 595 | ## 83.如果有一个协程它是死循环,如何调度 596 |
597 | 答案 598 |

因为go的协程是协作式的,如果死循环中没有让渡或系统调用,则该协程会一直执行

599 |
600 | 601 | 602 | ## 84.go的闭包及其作用 603 |
604 | 答案 605 |

闭包是一类特殊的函数,它引用了外部的函数的变量,该函数和引用的变量组成了闭包

606 |

常用于封装和信息隐藏,实现回调函数

607 |
608 | 609 | ## 85.Go垃圾回收全流程 610 |
611 | 答案 612 |

首先必须要提到的是Go的屏障机制。目前 Go 采用的是混合写屏障,混合写屏障是插入写屏障和删除写屏障的结合,并且还要满足几点。1.新创建的对象被标记为黑色。2.栈上不启用屏障机制。3.一开始先将栈上所有对象标记为黑色,引用对象标记为灰色(不需要STW)。当满足这三点后,并在堆上启用删除写屏障和在栈扫描未结束时启用插入写屏障

613 |

关于以上的屏障机制其中的几个点做说明:首先是为什么要插入写屏障。如果没有插入写屏障,因为栈上对象的染色是没有STW的,那么可能存在栈上白色对象对堆上白色对象的引用。此时将栈上白色对象对堆上白色对象的引用复制g诶堆上黑色对象,然后栈上白色对象删除其引用,可能导致堆上白色对象被误回收,所以需要给堆上开启插入写屏障

614 |

其次是为什么在栈扫描完成后就不需要插入写屏障了,因为在栈扫描完成后,栈上根对象都是黑色,其引用的对象都是灰色,所以不会存在。

615 |

然后还有最重要的一点!!!有人可能会觉得如果一个栈上/堆上黑色对象引用了一个堆上白色对象,该白色对象会不会被误回收,但是很明确的就是,一个对象要引用另一个对象,前提是这个对象可达,如果是白色的并且不存在其他灰色对象对他的引用,那么是不可能的。如果一个栈上灰色对象引用了一个堆上白色对象,然后一个堆上黑色对象复制了其引用,然后栈上灰色对象删除了其引用,会导致堆上白色对象被误回收,但是这是不可能的,因此黑色对象要持有白色对象,除非他引用了栈上灰色对象,如果他引用了栈上灰色对象,那么该栈上对象就一定是在堆上,因为该对象和自己不在一个栈,属于外部引用,因此一定会逃逸到堆上。

616 |

然后就要将GC的执行流程了,一共分为4个阶段,清除终止阶段,标记阶段,标记终止阶段,清理阶段

617 |

清除终止阶段会开启STW,然后会处理还没被清理的内存单元(强制执行GC可能会出现该情况)

618 |

标记阶段会开启写屏障,开启用户协助程序并将根对象入队,关闭STW,此后创建的对象为黑色。然后遍历根对象,将根对象设置为灰色,加入灰色集合。遍历灰色集合中的元素,将其设置为黑色,将其引用的对象设置为灰色,加入灰色集合,直到灰色集合为空为止。

619 |

标记终止阶段此时会开启STW,并关闭标记进程和用户协助程序

620 |

清理阶段会关闭写屏障,停止STW,此后创建的对象会被标记为白色。然后后台并发清理垃圾

621 |

用户协助程序是为了帮助提高标记速度,从而避免创建的对象越来越多,标记永无止境

622 |
623 | 624 | ## 86.三色标记法为什么需要三种颜色 625 |
626 | 答案 627 |

首先,对于扫描过和没扫描过我们需要两种颜色标识。但是一个对象被扫描过,但是他的子对象没被扫描过还需要一种颜色标识。所以第三种颜色作为中间状态

628 |
629 | 630 | ## 87.GC的浮动垃圾问题和悬挂指针问题 631 |
632 | 答案 633 |

浮动垃圾问题:已经被标记为黑色或灰色的对象,其引用被删除,导致没有被回收。但是影响不大,因为下次GC会将其回收

634 |

悬挂指针问题:一个白色对象的引用由灰色对象变成黑色对象,导致其被错误回收。触发条件:1.灰色对象删除对白色对象的引用(通过弱三色不变式解决)。2.黑色对象添加对灰色对象的引用(通过强三色不变式解决)

635 |
636 | 637 | ## 88.go panic和recover的底层原理 638 |
639 | 答案 640 |

panic本质就是取出当前的协程defer列表,然后依次执行。如果调用了recover,那么则将当前协程的第一个panic的recovered字段设置为true。然后会再检测如果此时panic的recovered字段为true,则恢复堆栈

641 |
-------------------------------------------------------------------------------- /docs/handbook/Kafka.md: -------------------------------------------------------------------------------- 1 | # Kafka 2 | 3 | ## 1.什么是kafka 4 |
5 | 答案 6 |

kafka是一个分布式的发布-订阅消息系统和一个强大的队列,可以处理大量的数据,并可以将消息从一个端点传递到另一个端点。Kafka适合离线和在线消费消息。Kafka消息保存在磁盘上,并在集群内复制以防止数据丢失。

7 |
8 | 9 | ## 2.为什么要使用kafka 10 |
11 | 答案 12 |

1.缓冲和消峰:上游数据有突发流量时,下游可能抗不住,而kafka可以在中间起一个缓冲的作用,把消息暂存在kafka中,下游可以慢慢的消费kafka中的消息

13 |

2.解耦和扩展性:消息队列可以作为一个接口层,解耦重要的业务流程。

14 |

3.异步处理:有些操作并不需要立即执行,可以将其写入kafka,异步执行

15 |

4.kafka可以堆积请求,即使消费者挂掉也不影响主要业务的正常进行

16 |

5.通过kafka可以使得一个生产的消息可以被不同业务的消费者消费

17 |
18 | 19 | ## 3.kafka的架构 20 |
21 | 答案 22 |

kafka包含多个核心组件 消费者,生产者,Broker,Topic,Partition,Zookeeper,Controller,Replication

23 |

消费者从broker从取消息,生产者向broker发消息

24 |

Broker是一个kafka实例,kafka集群由多个broker组成,一个broker包含多个topic,kafka通过topic对消息进行分类,生产者和消费者向指定的topic生产和消费消息

25 |

Partition是为了实现扩展性,提高并发能力,将一个topic分成多个Partition,每个Partition都是一个有序队列,每个Partition分布在不同的broker

26 |

Replication 用于实现备份的功能,保证集群中某个节点故障,该分区的数据不会丢失并且能够正常工作,一个partition有多个副本,一个副本有一个leader和多个follower

27 |

leader 每个分区多个副本中的主副本,负责接收生产者发送的消息,负责给消费者提供消息

28 |

follower 每个分区多个副本中的从副本,负责从主副本中同步数据,当主副本宕机的时候,还会成为新的主副本

29 |

offset 表示消费者消息的位置信息

30 |

zookeeper 负责存储和管理kafka的集群信息

31 |
32 | 33 | ## 4.kafka会丢失消息吗?如何解决 34 |
35 | 答案 36 |

首先消息的传递有2个阶段,从生产者发送给主副本,消费者从主副本消费数据

37 |

在生产者发送给主副本的这个阶段,有一个配置参数ack,ack=0表示生产者发送消息之后,不等待主副本的响应直接返回,很容易造成消息丢失。ack=1表示当接收到主副本接收成功就放回,ack=-1或all时表示所有主副本和同步副本集都接收成功时才表示成功

38 |

在消费者从主副本消费数据的阶段,有两个操作,一个是处理数据,一个是提交offset,这个操作的顺序可以由系统参数解决。先处理数据,再提交offset,可能在提交offset之前消费者宕机,导致消息被重复消费。如果先提交offset,再处理数据,可能会导致数据丢失。

39 |
40 | 41 | ## 5.导致kafka消费顺序乱序的原因?如何解决 42 |
43 | 答案 44 |

1.一个主题存在多个分区,消息分散在不同的分区上,导致消息乱序

45 |

2.生产者ACK机制中开启ack=0,先发送的数据因为网络拥塞而延迟,后发送的数据先到达,导致消息乱序

46 |

3.生产者开启多批发送,同时发送两个批次的数据,前一个批次的数据超时,重试后顺序发生改变。

47 |

解决办法

48 |

1.1一个主题只设置一个分区

49 |

1.2生产者通过key指定发往的分区,从而保证有序

50 |

2.将ack参数设置为1或-1

51 |

3.将参数设置为一次只发送一批的数据,或启用幂等生产者

52 |
53 | 54 | ## 6.Kafka组消费之Rebalance机制 55 |
56 | 答案 57 |

rebalance,让所有消费者达成共识。触发Rebalance机制的条件包括消费组成员发生变化,分区数量发生变化,订阅的主题数量发生变化

58 |

当消费组刚创建时,每个消费者会创建消费者协调器实例,然后获取对应的组协调器(通过向任意一个broker发起寻找组协调器的请求),向组协调器请求加入消费组。第一个加入消费组的消费者将成为leader,然后leader将进行选择分区分配策略。包括按分区号排序进行均分,顺序轮流分配,均衡分配并且尽量保持与上次相同。分配好分配后将分区结果同步给消费者

59 |
60 | 61 | ## 7.Kafka如何保证高可用 62 |
63 | 答案 64 |

1.Kafka采用集群架构,由多个broker组成,每个broker存储一部分数据,当某个broker宕机,其他broker也可以正常工作

65 |

2.kafka通过数据冗余来保证高可用,每个主题由多个分区组成,每个分区分布在不同的broker上,并在多个broker上复制,即使某个broker故障,也可以从其他的broker获取数据

66 |

3.消费组 kafka的消费组可以保证消息的高可用,一个消费组包含多个消费者,每个消费者负责某个分区的消息,当某个消费者宕机,其他消费者会接替他的工作

67 |

4.监控和故障转移 kafka会实时监控集群的状况,当某个broker出现故障时,会进行故障转移,将该broker的分区迁移到其他的broker上。保证数据的可用性

68 |
69 | 70 | ## 8.Kafka的ISR机制 71 |
72 | 答案 73 |

ISR是指同步副本集,与leader保持同步的所有副本的集合。当某个副本,落后leader太多时,会被移除ISR列表,当落后的副本追上leader时,又会重新加入ISR列表,当leader宕机时,会从ISR列表从选取一个副本作为leader。在生产者的ACK机制中,ack=-1或all时,也需要等待所有ISR列表中的副本都收到消息时,才返回响应。从而保证kafka的可靠性和可用性

74 |
75 | 76 | ## 9.Kafka的LEO和HW机制 77 |
78 | 答案 79 |

LEO表示最新的日志偏移量,分为leader leo, follower local leo, follower remote leo, leader leo 表示主副本的最新偏移量,当有日志写入时,这个值会被更新。follower local leo是存储在follower 副本上的最新偏移量,当follower收到从leader拉取到的数据时,会更新该值。follower remote leo是指存储在leader副本上的follower的最新偏移量,当leader收到follower的拉取请求的时候,会更新该值。

80 |

HW表示高水位,表示已经被所有副本接收的最大日志偏移量,分为 leader hw, follower hw。 leader hw表示主副本的高水位,当有follower拉取数据或者副本成为leader时,会更新leader hw 值为 leader leo 和 follower remote leo 取min。follower hw表示从副本的高水位,当follower收到从leader拉取的数据时,会更新该值为follower local leo 和 leader hw的min值

81 |

Leader Epoch 表示当前主节点的版本号,通过记录版本号对应的起始偏移量,可以使得副本重启后不再以来HW来对日志进行截断,避免数据不一致和丢失。当副本重启后,根据当前副本的版本号,向leader拉取最后一个offset,然后进行截断。如果当前节点成为leader,则更新leader epoch

82 | 工作流程: 83 |

1.leader收到消息,更新leader leo

84 |

2.follower请求拉取数据

85 |

3.leader收到请求拉取数据,更新follower remote leo,更新leader hw = min(leader leo, min(follower local leo...))

86 |

4.follower 收到拉取的数据,follower 更新 follower local leo,follower 更新 follower hw = min(leader hw, follower local leo)

87 |
88 | 89 | ## 10.Kafka如何防止消息积压 90 |
91 | 答案 92 |

1.增加消费者的数量,可以提高消费的速度

93 |

2.增加分区数,提高并行能力

94 |

3.给key添加随机后缀,使得key均匀的分布到不同的分区

95 |

4.消费者批量消费消息,提高消费效率

96 |

5.开启异步提交offset或自动提交offset

97 |
98 | 99 | ## 11.Kafka吞吐量高的原理 100 |
101 | 答案 102 |

1.顺序读写磁盘,充分利用了操作系统的预读机制,因此有着较高的读写速度

103 |

2.使用了零拷贝技术,通过sendfile方法,DMA将数据从磁盘拷贝到内核缓冲区,然后将缓冲区描述符和长度传到socket缓冲区,然后DMA将数据从内核缓存区拷贝到网卡,这样避免重复复制数据,大大提高了性能

104 |

3.采用了分区分段+索引的思想 将消息按主题分类,每个主题的数据是按照一个个分区存储在不同的broker上的,每个分区的数据又是分段存储的,kafka又为每个段建立了索引,提升了读取数据的性能和操作的并行度

105 |

4.kafka采用了批量读写,在向kafka写入数据时,将会按批次写入,减少延迟和网络开销

106 |

5.kafka采用了批量压缩技术,将同一个批次的消息一起压缩,支持多种压缩协议,减少了网络IO的消耗

107 |
108 | 109 | ## 12.Kafka存储的原理 110 |
111 | 答案 112 |

kafka的消息是按主题分类的,每个主题的数据文件又是分区存储的,每个分区的数据又是分段存储的,每个分区由包含一个主副本,零到多个从副本,kafka为每个段的数据建立了稀疏索引,当需要查找一个数据时,通过二分查找找到对应的段,然后通过稀疏索引,找到他在文件中的位置,稀疏索引是每隔4KB就添加一个索引。

113 |
114 | 115 | ## 13.kafka消费者采用的是推还是拉?为什么? 116 |
117 | 答案 118 |

采用的是拉,因为如果采用推,会导致broker发送多少消息,消费者就要消费多少消息,可能会导致网络拥塞,消费者负载增加。而采用拉可以让消费者根据自己的消费能力控制拉去速度,但是可能拉取到空的消息,所以要控制拉取间隔

119 |
120 | 121 | 122 | ## 14.kafka如何判断一个节点是否存活? 123 |
124 | 答案 125 |

1.节点必须维护和Zookeeper的连接,Zookeeper通过心跳机制检查每个节点的连接

126 |

2.从节点要与主节点同步,不能落后主节点太多

127 |
128 | 129 | 130 | ## 15.Kafka 与传统消息系统之间的三个关键区别 131 |
132 | 答案 133 |

1.kafka将日志持久化到磁盘,这些日志可以被重复读取

134 |

2.kafka是一个分布式系统,以集群的方式运行,保证分区容错和高可用

135 |

3.kafka支持实时的流式处理

136 |
137 | 138 | ## 16.Kafka怎么做到最多消费一次 139 |
140 | 答案 141 |

1.在ack机制中,选择ack=0,这样可以保证不会重复收到消息

142 |

2.在提交offset的选项,选择手动提交同步提交,先提交offset,再处理数据

143 |

3.开启kafka幂等性,ack=all并且retries>1。可以避免重复接收消息(通过生产者ID和序列号来标识消息)

144 |
145 | 146 | ## 17.Kafka可靠性如何保证? 147 |
148 | 答案 149 |

1.消息确认机制:生产者向对应的topic发送消息,通过消息确认机制来保证消息的可靠性,ack=0,表示生产者将消息发送出去就认为已经成功写入kafka,ack=1表示主副本收到消息就直接放回响应,不等从副本复制完数据。ack=-1或all表示等待所有主副本和从副本都收到消息才返回响应

150 |

2.分区副本机制:kafka通过分区副本机制来保证消息的可靠性,一个分区有一个主副本和0到多个从副本,能够保证即使一个broker宕机,也不会数据丢失,从副本会定期从主副本拉取数据

151 |

3.Leader选举机制:每个分区维护一个ISR列表,表示与leader同步的副本列表,如果一个从副本落后主副本太多,将会被移除ISR列表,落后的副本追上了主副本也会被加入ISR列表,主副本宕机后,会从ISR列表中选举新leader,能够保证消息的可靠性

152 |
153 | 154 | ## 18.Kafka能否脱离zookeeper?脱离zookeeper如何管理节点 155 |
156 | 答案 157 |

可以,最新的Kafka已经使用使用KRaft来管理Kafka集群的元数据

158 |
159 | 160 | ## 19.kafka偏移量维护在哪里 161 |
162 | 答案 163 |

kafka的偏移量存储在kafka集群内的consumer_offset中,消费者可以自动提交offset,也可以手动提交offset

164 |
165 | 166 | ## 20.kafka如果有台机器挂掉会发生什么 167 |
168 | 答案 169 |

一开始,节点启动时,都会和zk维护一个连接,然后节点挂掉后,zk会通过心跳机制发现该节点离线,然后会将该节点的信息从zk中移除掉,并会重新分配分区和副本,并且将离线的副本移除ISR列表,然后重新进行leader选举

170 |
171 | 172 | ## 21.kafka中生产者发送消息的具体流程? 173 |
174 | 答案 175 |

1.主线程会先创建producer record,其中包含主题,分区,键,值和时间戳。

176 |

2.然后会将其序列化,然后如果没有指定分区号则会通过分区器选择一个分区。

177 |

3.然后将其写入Producer Accumulator。Accumulator 是一个消息缓冲池,类似于生产-消费模型,每个分区一个缓冲队列,消息发往缓冲池后会找到对应的队列写入对应的批次,如果不存在则新建一个批次,如果存在并且还可以写入则直接写入

178 |

4.sender线程会从 Producer Accumulator中拉取数据,构造请求发送到broker

179 |
180 | 181 | ## 22.kafka中消费者消费消息的具体流程? 182 |
183 | 答案 184 |

消费者首先会找到自己的组协调器,然后向组协调器发起加入消费组的请求,加入消费组后,消费者leader会为其指定分区分配方案,并同步给所有消费者。消费者根据自己负责的分区,进行拉取数据,处理数据并提交offset

185 |
186 | 187 | ## 23.Kafka LEO和HW在没有epoch的情况下,数据不一致和数据丢失的场景 188 |
189 | 答案 190 |

1.数据不一致: 当follower 拉取完数据,准备更新 hw 时 follower 和 leader 宕机,follower 先重启,成为了新的 leader,然后收到了新的消息,更新 leo 和 hw,然后旧的 leader 重启,成为了follower,旧的leader向新的leader拉取数据,发现新leader的hw和自己相同,故不发生改变,但是此时数据已经产生了不一致。

191 |

2.数据丢失: 当follower 拉取完数据,准备更新 hw 时 follower 宕机,重启后follower根据 hw 将日志进行截断,然后向 leader 拉取数据,但此时 leader 宕机,follower成为leader,然后旧的leader重启后,成为了follower,旧leader向新leader拉取数据,然后发现新leader的hw更小,故将自己的hw更新,并进行截断。从而导致数据丢失

192 |
193 | 194 | ## 24.kafka生产者发送消息的方式 195 |
196 | 答案 197 |

发送并忘记,同步发送,异步发送+回调函数

198 |
199 | 200 | ## 25.kafka于其他消息队列相比之下的优点? 201 |
202 | 答案 203 |

高吞吐量,低延迟,持久性

204 |
205 | 206 | ## 26.kafka的应用场景 207 |
208 | 答案 209 |

1.日志聚合

210 |

2.消息队列

211 |

3.实时流处理

212 |
213 | 214 | ## 27.kafka的负载均衡是怎么做的? 215 |
216 | 答案 217 |

1.每个主题有多个分区,分布在不同的broker上,每次写入数据时,如果没指定分区,会通过轮询来选择分区,从而实现负载均衡

218 |

2.消费者端会将每个分区均匀的分配到同一个消费组中的不同消费者,当消费者发生变化时,会通过rebalance机制实现重新负载均衡

219 |
220 | 221 | ## 28.kafka不适合的场景 222 |
223 | 答案 224 |

1.小规模数据处理

225 |

2.对数据安全性要求很高(会存在数据丢失)

226 |
-------------------------------------------------------------------------------- /docs/handbook/MongoDB.md: -------------------------------------------------------------------------------- 1 | # MongoDB 2 | 3 | 1.Mongo是什么? 4 |
5 | 答案 6 |

7 | Mongo是基于分布式文件存储的Nosql数据库,提供了面向文档的存储方式,并且支持无模式的建模方式,可以存储复杂的数据类型 8 |

9 |
10 | 11 | 2.Mongo适合的场景 12 |
13 | 答案 14 |

15 | 1.数据量大,数据结构不固定 16 | 2.需要高性能,高可用 17 | 3.需要大量的地理位置查询,文本查询 18 | 4.需要能够快速水平扩展 19 |

20 |
-------------------------------------------------------------------------------- /docs/handbook/Mysql.md: -------------------------------------------------------------------------------- 1 | # Mysql 2 | 3 | ## 1.Mysql的表空间结构 4 |
5 | 答案 6 |

1.表空间由行,页,区,段组成。

7 |

2.行代表Mysql中的一行记录

8 |

3.页是由多个连续的行组成的,页是Mysql的读写单位,每个页默认16KB

9 |

3.区是由多个连续的页组成的,以区为单位可以让相邻的页,在物理位置上也相邻,这样就可以使用顺序IO,提高读写性能

10 |

4.段是由多个区组成的,段一般分为数据段,索引段,回滚段。数据段用于存储B+树中叶子节点的区的集合,索引段用于存储B+树中非叶子节点的区的集合,回滚段用于存放回滚数据的区的集合

11 |
12 | 13 | ## 2.varchar(n) n最大为多少? 14 |
15 | 答案 16 |

首先要知道varchar(n)表示最多存储n个字符,然后数据库要求一行记录不能超过65535个字节,然后还要减去NULL值列表和变长字段的长度列表,然后还要根据字符集判断,在ascii字符集中,1个字符占一个字节,在UTF-8字符集中,1个字符占3个字节

17 |
18 | 19 | ## 3.Mysql的存储引擎有哪些?他们之间的区别? 20 |
21 | 答案 22 |

InnoDB,MyISAM,Memory

23 |

1.InnoDB支持事务,MyISAM,Memory不支持事务

24 |

2.InnoDB支持行级锁,MyISAM和Memory只支持表级锁

25 |

3.InnoDB具备崩溃回复能力,通过日志对数据进行恢复。MyISAM缺少崩溃恢复机制。Memory数据存储在内存中,重启或者宕机就会导致数据丢失

26 |

4.InnoDB和MyISAM都支持全文索引,Memory不支持全文索引

27 |

5.InnoDB支持外键,MyISAM和Memory不支持外键

28 |
29 | 30 | ## 4.索引的分类 31 |
32 | 答案 33 |

1.按数据结构分:B+树索引,哈希索引,全文索引

34 |

2.按物理存储分:聚簇索引,二级索引

35 |

3.按字段数量分: 单列索引,联合索引

36 |

4.按字段特性分:主键索引,唯一索引,普通索引,前缀索引

37 |
38 | 39 | ## 5.索引的优点和缺点 40 |
41 | 答案 42 |

优点

43 |

1.可以提高数据检索速度,减少磁盘IO

44 |

2.通过索引对数据进行排序,可以减少CPU的消耗

45 |

3.可以大大提高查询速度

46 |

缺点

47 |

1.占用额外的存储空间

48 |

2.增加写操作的开销,在进行数据插入,修改,删除时,需要同时更新索引,这会增加写操作的开销

49 |
50 | 51 | ## 6.索引优化的方法 52 |
53 | 答案 54 |

1.使用前缀索引进行优化,使用字符串的前几个字符作为前缀索引,提高查询速度

55 |

2.使用覆盖索引优化,使得要查询的数据能够在二级索引得到,从而避免回表

56 |

3.采用自增主键,这样可以让插入数据都是追加操作,不用移动数据

57 |

4.索引最好设置为NOT NULL,因为设置为NULL会导致优化器很难进行优化,并且设置为NOT NULL可以节省内存空间

58 |

5.防止索引失效。例如左或左右模糊搜索。联合索引非最左匹配。Where子句中 or 前是索引列,or后不是索引列。对索引列进行了计算,函数,类型转换操作。

59 |
60 | 61 | ## 7.为什么mysql采用B+树而不是B树 62 |
63 | 答案 64 |

1.B+树非叶子节点不存放实际记录的数据,仅存放索引,所以他的节点可以连接更多的子节点,从而B+树每层的节点更多,树高更低,从而磁盘IO更少

65 |

2.B+树有大量的冗余节点,B树的数据节点也用作索引,因此删除非叶子节点的时候,因为索引的丢失从而导致树节点需要发生变化。而对于B+树只有在删除

66 |

3.B+树叶子节点之间用链表连接了起来,有利于范围查询,而B树要进行范围查询只能通过树的遍历来完成,查询效率更低

67 |
68 | 69 | ## 8.索引失效的情况 70 |
71 | 答案 72 |

1.对索引列进行了计算,函数,类型转换操作

73 |

2.使用左模糊搜索(like '%x')或左右模糊搜索(like '%x%')

74 |

3.联合索引非最左匹配(a,b,c索引,查询b,c)

75 |

4.where 子句中 or 前是索引列, or 后不是索引列

76 |
77 | 78 | ## 9.COUNT(1),COUNT(*),COUNT(主键字段),COUNT(普通字段)的区别 79 |
80 | 答案 81 |

性能上 COUNT(字段) < COUNT(主键字段) < COUNT(\*) = COUNT(1)

82 |

1.COUNT(1) 由于1一定不为NULL,所以不需要读取记录中任何字段的值,直接遍历索引,每读到一条记录就给count+1

83 |

2.COUNT(\*) mysql会将\*转化为0,和COUNT(1)一样

84 |

3.COUNT(主键字段), mysql会遍历索引,然后读取记录中的主键字段,如果主键字段不为空,则count+1

85 |

4.COUNT(字段) mysql会遍历全表,然后读取记录中的该字段的值,如果不为空,则count+1

86 |
87 | 88 | ## 10.事务的特性?如何保证事务的特性? 89 |
90 | 答案 91 |

原子性:一个事务的所有操作,要么全部完成,要么全部不完成,不会结束在某个中间环节。通过undo日志保证

92 |

持久性:一个事务,一旦提交,对数据库的修改就是永久的,即使数据库故障也不会丢失。通过redo日志保证

93 |

隔离性:数据库允许多个并发事务同时对其数据进行读取和修改的能力,隔离性可以防止多个事务由于交叉执行而导致的不一致性。通过MVCC机制或锁机制保证

94 |

一致性:事务操作前后,数据满足完整性约束,数据库保持一致性状态。通过原子性,持久性,隔离性保证

95 |
96 | 97 | ## 11.脏读,不可重复读,幻读的区别 98 |
99 | 答案 100 |

脏读:一个事务读到了另一个未提交事务事务修改的数据,如何未提交的事务发生了回滚,就发生了脏读

101 |

不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样,就意味着发生了不可重复读

102 |

幻读:在一个事务内多次查询记录的数量,如果出现前后两次查询到的记录数不一样的情况,就意味着发生了幻读

103 |
104 | 105 | ## 12.事务的隔离级别 106 |
107 | 答案 108 |

读未提交:一个事务还没提交时,它做的变更就能被其他事务看到。通过每次读取最新的数据实现。

109 |

读已提交:一个事务提交后,它做的变更才能被其他事务看到。通过每个语句执行前都重新生成一个Read View实现。

110 |

可重复读:一个事务执行过程中看到的数据,一直跟事务启动时看到的数据是一致的。通过在启动事务时生成一个Read View,然后事务执行期间都使用这个Read View来实现

111 |

串行化:会对记录加上读写锁,在多个事务对这个记录进行读写操作时,如果发生了读写冲突时,后访问的事务必须等前一个事务执行完才能执行。通过加读写锁来实现。

112 |
113 | 114 | ## 13.快照读和当前读的区别 115 |
116 | 答案 117 |

快照读是指某个时刻的快照数据,通过MVCC机制+Undo log实现。用于事务执行过程中保证可重复读

118 |

当前读是指读取当前最新的数据,通过临键锁(间隙锁+行记录锁)实现。用户update,delete操作,因为他们的修改必须基于最新的修改进行,否则就会发生覆盖

119 |
120 | 121 | 122 | ## 14.MVCC机制 123 |
124 | 答案 125 |

MVCC机制通过在每个记录都维护了两个隐藏列,trx_id,roll_pointer,trx_id表示这个记录是由哪个事务生成的,roll_pointer记录了上一个版本的指针和Read View机制。Read View包含了creator_trx_id, min_trx_id,m_ids,max_trx_id。creator_trx_id表示创建该Read View的事务id,min_trx_id表示创建该Read View时最小的启动且未提交的事务id,m_ids是指启动且未提交的事务id列表,max_trx_id是指创建该Read View时,给下一个事务的id

126 |

然后在事务访问记录的过程中,如果该记录的trx_id < min_trx_id,则该记录可见,如果该记录的min_trx_id <= trx_id < max_trx_id, 并且 trx_id 位于 m_ids中,则该记录不可见,如果trx_id不位于 m_ids中,则该记录可见,如果trx_id >= max_trx_id,则该记录不可见。如果读到不可见的数据则会沿着 roll_pointer,找到旧版本的记录

127 |
128 | 129 | ## 15.幻读被完全解决了吗? 130 |
131 | 答案 132 |

并没有,还有两种场景。第一种是如果一个事务修改了另一个事务插入的数据,会导致原本不可见的数据,由于修改了该数据,使得该数据的trx_id发生改变,从而可见,导致发生了幻读

133 |

第二种是先执行快照读,然后另一个事务插入了一条数据,然后执行当前读,就会读到了另一个事务插入的数据,导致发生了幻读

134 |
135 | 136 | 137 | ## 16.Mysql有哪些锁? 138 |
139 | 答案 140 |

1.全局锁:用于数据库备份,InnoDB可以通过Read View实现数据库备份,但是MyISAM只能通过全局锁实现。加锁期间数据库处于只读状态,不能对数据进行增删改,对表结构进行更改

141 |

2.表级锁:包括表锁,元数据锁,意向锁,AUTO-INC锁。

142 |

2.1 表锁:分为独占锁和共享锁,开启了共享锁,则阻塞对该表的所有写操作,开启了独占锁,则阻塞对该表的所有读写操作

143 |

2.2 元数据锁:分为读锁和写锁,当对表进行增删改查操作时,会自动给表加上读锁,当对表结构进行修改时,会自动给表加上写锁。申请元数据锁的操作会形成一个队列,并且写锁获取的优先级更高

144 |

2.3 意向锁:用于快速检测表中是否有记录被加锁。分为意向独占锁,意向共享锁,意向锁之间不会发生冲突,只会和表级独占锁和表级共享锁发生冲突

145 |

2.4 AUTO-INC锁:用于生成自增主键,当需要插入记录时,加上改该锁,插入后就会被释放。mysql还有一个轻量级锁,插入之前申请该锁,然后给对应的字段赋一个自增的值,然后就可以释放该锁

146 |

3.行锁:包括记录锁,间隙锁,临键锁,插入意向锁

147 |

3.1 记录锁:分为共享锁和独占锁。共享锁和独占锁互斥,独占锁和独占锁互斥。

148 |

3.2 间隙锁:间隙锁之间是兼容的,用于防止插入幻影记录

149 |

3.3 临键锁:临键锁是记录锁和间隙锁的组合

150 |

3.4 插入意向锁:插入意向锁是一种特殊的间隙锁,用于判断插入位置是否有间隙锁,他会与间隙锁互斥。当需要插入数据时,会先加插入意向锁

151 |
152 | 153 | ## 17.undo log的作用? 154 |
155 | 答案 156 |

1.实现事务回滚,保证事务的原子性

157 |

2.实现MVCC机制

158 |
159 | 160 | ## 18.redo log的作用? 161 |
162 | 答案 163 |

1.实现事务的持久性,让 Mysql 有崩溃恢复能力

164 |

2.将写操作由随机写变成顺序写(每次写操作时都是追加redo log日志,所以是顺序写)

165 |
166 | 167 | 168 | ## 19.redo log 的刷盘时机 169 |
170 | 答案 171 |

redo log 会先写入 redo log buffer,而刷盘时机是Mysql 正常关闭,或 redo log buffer 记录写入量已经超过一半或每隔1s,后台线程都会将redo log buffer刷盘,还有一种由系统参数(innodb_flush_log_at_trx_commit)决定

172 |

innodb_flush_log_at_trx_commit = 0, 事务提交不写入 redo log 文件也不触发刷盘。innodb_flush_log_at_trx_commit = 1 事务提交直接写入redo log文件并刷盘。 innodb_flush_log_at_trx_commit = 2,事务提交时直接写入redo log 文件并且不刷盘。

173 |
174 | 175 | ## 20.redo log 满了怎么办? 176 |
177 | 答案 178 |

redo log 由两个redo log 文件组成,以循环写的方式工作,形成了一个环形,当将要写的位置,还有没被擦除的数据时, mysql 将会阻塞,将buffer pool 中的数据刷盘

179 |
180 | 181 | ## 21.binlog的作用 182 |
183 | 答案 184 |

用于备份恢复,主从复制

185 |
186 | 187 | ## 22.binlog和redo log的区别? 188 |
189 | 答案 190 |

1.适用对象不同:redo log是 InnoDB存储引擎中的日志,而binlog是mysql server层上的日志,所有引擎都可以使用

191 |

2.日志格式不同:binlog有三种日志格式

192 |

2.1.1 statement: 记录每一条修改数据的SQL。

193 |

2.1.2 row:记录行数据最终被修改成什么样了。

194 |

2.1.3 mixed:包含了statement和row模式,根据不同的情况自动使用statement模式和row模式。

195 |

2.2 redo log 是物理日志,记录的是某个数据页做了什么修改

196 |

3.1 写入方式不同 binlog是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。

197 |

3.2 redo log 是循环写,日志空间大小固定,写满了就从头开始写,保存未被刷盘的脏页日志

198 |

4.1 用途不同:binlog用于备份恢复,主从复制

199 |

4.2 redo log 用于掉电等故障恢复

200 |
201 | 202 | ## 23.主从复制如何实现? 203 |
204 | 答案 205 |

1.当客户端要提交事务时,发请求给主库,主库写入binlog,然后提交事务,并更新本地存储数据。

206 |

2.通过log dump线程发送binlog日志给每个从库,每个从库把binlog写到暂存日志中。

207 |

3.回放binlog,并更新存储引擎中的数据

208 |
209 | 210 | ## 24.binlog 什么时候刷盘? 211 |
212 | 答案 213 |

binlog首先会将日志写到binlog cache,等到事务提交的时候才把binlog cache中完整的事务写入binlog文件中,并清空binlog cache

214 |

binlog写入磁盘的时机由系统参数(sync_binlog)决定

215 |

sync_binlog=0,每次事务提交,只写入binlog文件,不刷盘,具体写入磁盘的时机由操作系统决定

216 |

sync_binlog=1,每次事务提交,写入binlog文件,并且刷盘

217 |

sync_binlog=n(n>1),每次事务提交,只写入binlog文件,等到积累了n个事务后,再刷盘

218 |
219 | 220 | ## 25.主从复制有哪些模型? 221 |
222 | 答案 223 |

1.同步复制:主库提交事务的线程要等待所有从库的复制成功响应,才能返回客户端结果

224 |

2.异步复制:主库提交事务的线程不会等待binlog同步到每个从库,就直接返回客户端结果

225 |

3.半同步复制:主库提交事务的线程收到只要一部分从库复制成功的响应,就返回客户端结果

226 |
227 | 228 | 229 | ## 26.如何保证redo log和binlog的一致性 230 |
231 | 答案 232 |

通过二阶段提交来实现。二阶段提交将事务提交拆分成两个阶段,准备阶段和提交阶段,使用了内部XA事务,binlog作为协调者,存储引擎作为参与者

233 |

1.在 redo log 中写入 prepare状态 和 XID,然后将 redo log 刷盘

234 |

2.在 binlog 中写入 XID,将 binlog 刷盘,再将 redo log 对应的事务状态设置为 commit(不刷盘)。

235 |

如果redo log刷盘后宕机,重启后去binlog中查找redo log中的XID是否存在,如果不存在就回滚事务,否则提交事务

236 |
237 | 238 | ## 27.二阶段提交的问题 239 |
240 | 答案 241 |

1.磁盘I/O次数高:每次事务提交都会刷盘两次

242 |

2.锁竞争激烈:为了保证两个日志的提交顺序一致,必须要加锁保证原子性(这里如果不加锁可能导致不同事务的日志顺序错乱)

243 |
244 | 245 | ## 28.MySQL 为什么要用自增 ID? 246 |
247 | 答案 248 |

使用自增ID作为主键可以让插入数据都是追加操作,不用移动数据。不使用自增ID会导致可能产生页分裂从而导致插入效率降低

249 |
250 | 251 | ## 29.Mysql 如何性能优化? 252 |
253 | 答案 254 |

1.使用索引优化查询

255 |

2.优化查询语句,select查询时尽量指定需要的列,减少数据传输和处理时间

256 |

3.尽量避免全表查询,使用合适的查询条件

257 |

4.使用批处理,减少数据库IO次数

258 |

5.进行分库分表

259 |

6.优化表的数据类型的使用

260 |

7.尽量避免用子查询,用join来代替

261 |

8.使用limit优化分页

262 |

9.binlog日志提交使用组提交,延迟binlog刷盘时机,从而减少刷盘次数

263 |

10.修改redo log刷盘参数,不需要每次事务提交都刷盘,而是只写入redo log文件

264 |
265 | 266 | ## 30.b+树和b树区别 267 |
268 | 答案 269 |

1.数据存储方式:B树非叶子节点既存储索引又存储数据。B+树的非叶子节点不存储数据,所有数据存储在叶子节点

270 |

2.叶子节点的链接:B树的叶子节点之间没有链接。B+树的叶子节点之间通过链表相连,可以支持范围遍历和顺序遍历

271 |

3.范围查询的性能:由于B树的每个节点都存储数据,范围查询需要在整颗树上进行遍历。B+树的范围查询非常高效,因为所有数据都存储在叶子节点上,并且通过链表相链接

272 |

4.内存占用和磁盘IO次数:由于B树的每个节点都存储数据,所以树高会更高,从而磁盘IO次数更多。B+树非叶子节点仅存储索引键值,内存占用较小,并且树高更低,磁盘IO次数更少

273 |

5.适用场景:B树更适合随机读写,低内存环境。B+树适合范围查询,大规模数据集场景

274 |
275 | 276 | 277 | ## 31.B+树和B树各自的优势 278 |
279 | 答案 280 |

1.1 B树更适用于随机读写:由于B树的每个节点都包含数据,它在进行随机读写操作时可能比B+树更高效,因为可以直接在节点中找到所需的数据记录。

281 |

1.2 B树更适用于低内存环境:相对于B+树,B树的节点包含了数据,因此在某些内存受限的情况下,B树可能占用较小的内存空间。

282 |

2.1 B+树更适用于范围查询:B+树的所有数据记录都存储在叶子节点上,并通过链表连接,使得范围查询操作更加高效。而B树需要在整棵树上进行遍历,效率较低。

283 |

2.2 B+树较少的磁盘IO次数:由于B+树的非叶子节点仅存储索引,数据记录存储在叶子节点上,并且通过链表连接,减少了磁盘IO次数,提高了数据访问的效率。

284 |

2.4 B+树更高的磁盘预读能力:由于B+树的叶子节点通过链表连接,相邻的数据记录在磁盘上也是相邻存储的,能够充分利用磁盘的顺序访问能力,提高了数据的读取效率。

285 |
286 | 287 | 288 | ## 32.主从复制,读写分离的情况下,主库宕机,从库会受到什么影响? 289 |
290 | 答案 291 |

1.首先,因为读写分离,主库宕机后,无法进行写操作,因此会导致整个系统不能进行任何插入,更新,删除操作,

292 |

2.读操作不会受到影响,因为读操作在从库上进行,因为主从复制有延迟,所以可能丢失主库宕机前的数据

293 |

3.主库宕机后,只能手动将某个从库提升为主库

294 |

4.如果某个从库被提升为主库后,可能导致数据的不一致性,因为原主库宕机前的一部分数据可能没有同步到从库

295 |
296 | 297 | ## 33.mysql explain命令 298 |
299 | 答案 300 |

explain命令用于查询SQL语句的执行计划。包括查询id,查询类型(简单查询,主键查询,子查询等),表名,匹配的分区,数据扫描类型(常见类型包括ALL全表查询,INDEX遍历索引树,RANGE范围查询,INDEX_MERGE索引合并,const最多有一个匹配记录等),可能使用的索引,实际使用的索引,索引长度,索引的那一列被用了,估计要扫描的行,符合查询条件的数据的百分比,附加信息

301 |

explain命令需要重点关注数据扫描类型(尽量避免全表扫描和索引全扫描),索引的那一列被用了,可能使用的索引,实际使用的索引(确保索引生效),附加字段(可能有潜在的性能问题),估计要扫描的行(扫描的行数越少越好)

302 |
303 | 304 | ## 34.聚簇索引,非聚集索引是什么? 305 |
306 | 答案 307 |

聚簇索引就是叶子节点中存放的数据就是整张表的行记录数据,主键索引就是聚簇索引

308 |

非聚集索引就是叶子节点中存放的数据只有主键和索引值

309 |
310 | 311 | ## 35.一条sql语句太慢如何处理 312 |
313 | 答案 314 |

首先要开启慢查询日志,然后可以通过explain分析执行计划,判断索引使用情况。然后尽量使用索引覆盖,联合索引遵循最左匹配,避免索引失效,

315 |
316 | 317 | 318 | ## 36.分库分表如何处理? 319 |
320 | 答案 321 |

1.确定分库分表策略:水平分表(将一张表的数据分到多张表中),垂直分表(将一些不经常使用的字段分到另一张表上),水平分库(将一个数据库中的多张表分到多个数据库中,并部署到多个服务器上,从而缓解单个数据库的压力),垂直分库(将单个表的数据分到多个数据库上)

322 |

2.制定数据切分方案:确定分片键,切分规则,数据迁移方案

323 |

3.选择合适的分库分表工具

324 |

4.进行充分的测试,监控和调优

325 |
326 | 327 | ## 37.水平分表后如何count 328 |
329 | 答案 330 |

1.代码中分别统计多个表中的count

331 |

2.使用UNION ALL将多个表的查询结果组合,然后用SUM计算总和

332 |

3.使用存储过程,遍历多个表,计算COUNT

333 |
334 | 335 | ## 38.B+树的缺点 336 |
337 | 答案 338 |

维护成本高,占用内存大,单点查询要查到叶子,单点查询效率不高,不适合随机读写

339 |
340 | 341 | ## 39.Hash索引和BTree索引的区别 342 |
343 | 答案 344 |

1.BTree支持等值查询,范围查询,前缀查询,而Hash索引只支持等值查询

345 |

2.BTree在范围查询和排序操作中性能更高,而Hash索引在单个等值查询中性能更高

346 |

3.BTree可以很好的支持联合索引,而Hash索引不支持

347 |
348 | 349 | ## 40.数据库范式 350 |
351 | 答案 352 |

1NF表示每个属性都是不可分的原子项

353 |

2NF表示每个非主属性都要完全依赖于主属性

354 |

3NF表示非主属性之间不能有依赖关系,必须直接依赖于主属性

355 |

BCNF表示主属性之间不能有依赖关系

356 |

4NF表示不能存在多对多的依赖关系

357 |

5NF表示不能存在不包含候选码的连接依赖

358 |
359 | 360 | ## 41.一条查询语句的执行过程 361 |
362 | 答案 363 |

1.通过连接器建立连接

364 |

2.查询语句如果缓存命中则直接返回否则继续执行(mysql8.0已删除缓存)

365 |

3.通过解析器对sql进行语法分析,词法分析,构建语法树

366 |

4.执行sql,首先检查表或字段时候存在,将select * 进行扩展

367 |

5.选择查询成本最小的执行计划

368 |

6.根据执行计划执行sql语句,从存储引擎读取记录,返回给客户端

369 |
370 | 371 | 372 | ## 42.索引的设计原则 373 |
374 | 答案 375 |

1.对于经常需要排序,查询的字段建立索引

376 |

2.对于区分度比较大的建立索引

377 |

3.尽量使用唯一索引,提高查询速度

378 |

4.使用数据量比较小的字段建立索引

379 |

5.遵循最左匹配原则

380 |
381 | 382 | ## 43.mysql分布式如何主从一致 383 |
384 | 答案 385 |

1.使用半同步复制或全同步复制

386 |

2.使用读写分离,降低主库的压力,从而提高同步速度

387 |

3.使用监控和自动修复工具

388 |
389 | 390 | ## 44.主从同步存在延迟,如果主库进行了修改,从库还没同步到,那从从库查到了旧的数据,应该如何解决 391 |
392 | 答案 393 |

1.开启同步复制

394 |
395 | 396 | ## 45.mysql的存储引擎有哪些?分别适合哪些应用场景? 397 |
398 | 答案 399 |

1.Innodb 一般适用于需要可靠性,支持事务,并发控制的场景

400 |

2.Myisam 适合数据仓库,日志记录场景,对于大量的插入操作(批量插入锁表会提高性能)或读远大于写操作的场景

401 |

3.Memory 适合作为缓存或临时存储使用

402 |
403 | 404 | ## 46.mysql b+树底层具体如何查询数据的? 405 |
406 | 答案 407 |

因为每个非叶子节点都存储了对应子节点数据页的最小索引,所以会从根节点出发,通过二分找到当前节点中,小于等于查询值的最后一个索引,然后找到对应的页,继续查找,到达叶子节点后,通过二分定位到当前记录所在的槽,然后进行遍历

408 |
409 | 410 | ## 47.索引下推和索引覆盖 411 |
412 | 答案 413 |

索引下推就是指有部分查询条件可以下推到存储引擎完成。即在索引查询时,索引列包含了部分查询条件,可以在存储引擎进行判断,不需要回表

414 |

索引覆盖就是指索引中包含了所有需要查询的字段,从而可以直接在索引中获取数据,不需要回表

415 |
416 | 417 | ## 48.varchar和char的区别 418 |
419 | 答案 420 |

1.char是定长的,不足的话会补充空格。varchar是不定长的

421 |

2.varchar需要使用额外的字段记录字符串长度

422 |

3.char最多存储255个字符,varchar最多存储65532个字符

423 |
424 | 425 | ## 49.varchar和char做索引的区别 426 |
427 | 答案 428 |

1.varchar比char可以占用更小的存储空间

429 |

2.char因为长度固定,作为索引查询起来性能更高

430 |
431 | 432 | ## 50.B+树,B树,红黑树 433 |
434 | 答案 435 |

红黑树就是二叉排序树的改进,因为普通的二叉排序树可能会退化成链表,所以红黑树通过一系列手段维持树的平衡,从而降低树高

436 |

B树就是多路排序树,每个节点可以有多个儿子节点(不能无限多个,不然会导致退化成有序数组,不能一次性加载进内存,因此,每个节点的大小就是操作系统的页大小),每个节点可以存储多个数据

437 |

B+树是基于B树的改进,每个节点不存储数据,只存储索引值,这使得它的树高更低,并且在叶子节点使用链表将其连接在一起

438 |

439 |
440 | 441 | 442 | ## 51.B+树和B树是如何增删节点的? 443 |
444 | 答案 445 |

对于B树的插入操作时,他会先找到他应该插入(小于等于前一个,大于后一个)的位置插入,如果该节点数量超过了阈值,则按中点进行分裂,然后将中点上移,右边部分分裂为新的节点,然后中点执行新节点。

446 |

对于B树的删除操作时,他会找到该节点将其删除,并将该节点的后一个节点移动到该为止。如果删除后节点数量不足一半,并且兄弟节点超过一半,则将父节点下移,否则将父节点下移与当前节点和兄弟节点合并。

447 |

对于B树的插入操作时,他会先找到他应该插入(小于等于前一个,大于后一个)的位置插入,如果该节点数量超过了阈值,则按中点分裂,左边部分称为新的左节点,中点和右边部分成为新的右节点。然后中点的值称为索引值,连接左右节点

448 |

对于B+树的删除操作时,他会找到该节点将其删除,如果他是索引值,则会将对应的索引值更新为该节点的后一个节点的值。如果删除后节点数量不足一半,则会向他的兄弟节点借节点,如果兄弟也只有一半,则将其和兄弟合并,并将父节点的索引值删除。否则更新索引值

449 |
450 | 451 | ## 52.如何实现悲观锁和乐观锁 452 |
453 | 答案 454 |

悲观锁就直接在操作先加锁,操作完后解锁即可

455 |

乐观锁就是通过操作后判断版本号是否发生改变,通过cas实现,为解决aba问题,可以每次都给版本号+1

456 |
457 | 458 | ## 53.为什么innodb比myisam快? 459 |
460 | 答案 461 |

1.innodb需要维护mvcc机制,而myisam不用

462 |

2.innodb在查找记录的时候需要找到当前事务下可见的记录,而myisam不用

463 |

3.innodb要缓存数据块,而myisam只用缓存索引块

464 |

4.innodb寻址需要映射到块,再到行。而myisam记录的是文件的offset,寻址更快

465 |
466 | 467 | ## 54.为什么有了binlog,还要redo log? 468 |
469 | 答案 470 |

1.binlog记录的是全量的数据,而redo log记录的是没有刷盘的数据,binlog并不知道哪些数据还没刷盘

471 |

2.redo log 记录的是物理日志,即在哪个扇区的哪个内存页进行了修改。而 binlog 记录的是sql语句。一个sql语句会涉及多个扇区数据的修改,这并不是一个原子的操作,如果修改过程中宕机了,恢复会导致之前修改过的又被修改了一次。因此得使用物理日志来恢复

472 |
473 | 474 | ## 55.为什么myisam不支持行级锁 475 |
476 | 答案 477 |

因为myisam的索引之间没有关联关系,加锁只能锁住数据,锁不住其他B+树的数据

478 |
-------------------------------------------------------------------------------- /docs/handbook/Redis.md: -------------------------------------------------------------------------------- 1 | # Redis 2 | 3 | ## 1.Redis是什么? 4 |
5 | 答案 6 |

Redis是基于内存的数据库,读写操作都在内存中完成,因此读写速度特别快。常用于缓存,消息队列

7 |
8 | 9 | ## 2.Redis和MemCached的区别 10 |
11 | 答案 12 |

1.Redis支持的数据类型跟丰富,如Hash,List,Set,ZSet等,而MemCached只支持字符串

13 |

2.Redis支持数据的持久化,可以将内存中的数据存储在磁盘中,重启的时候可以再次加载使用,而MemCached不支持持久化,数据全部存储在内存中,一旦重启或挂掉后,数据就丢失了。

14 |

3.Redis支持原生的集群模式,MemCached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据

15 |

4.Redis支持订阅,Lua脚本,事务等功能,而MemCached不支持

16 |
17 | 18 | ## 3.Redis常用数据类型及其应用场景 19 |
20 | 答案 21 |

1.String 用于缓存对象,计数器,分布式锁,共享Session

22 |

2.List 用于消息队列(但是需要自己实现全局唯一ID,不能以消费组的形式消费数据)

23 |

3.Set 用于共同好友,点赞等场景

24 |

4.ZSet 用于排行榜

25 |

5.BitMap 常用于签到,用户登陆状态判断

26 |

6.HyperLogLog 常用于大量数据的基数统计,例如网页浏览量统计

27 |

7.GEO 用于地理位置信息存储,例如滴滴叫车,附近的人

28 |

8.Stream 用于消息队列,支持消费组和自动生成全局唯一ID

29 |

9.hash 用于缓存对象,缓存购物车

30 |
31 | 32 | ## 4.Redis是单线程的吗? 33 |
34 | 答案 35 |

我们常说的Redis是单线程的是指接收客户端请求,解析请求,进行数据读写操作,发送数据给客户端这个过程是由一个线程完成。但Redis并不是单线程的,它还为关闭文件,AOF刷盘,释放内存任务创建了后台线程

36 |
37 | 38 | ## 5.Redis6.0之前为什么使用单线程 39 |
40 | 答案 41 |

单线程是无法利用多核CPU的,但是Redis仍然采用单线程模型,因为Redis是基于内存的,读写速度很快,所以性能瓶颈不在CPU,而在于内存和网络IO,并且使用单线程可以提高可维护性,多线程模型虽然在某些方面表现优秀,但是也导致了一些并发读写带来的问题,增加了系统的复杂度。

42 |
43 | 44 | ## 6.为什么Redis6.0后引入了多线程 45 |
46 | 答案 47 |

因为随着网络硬件的提升,Redis的性能瓶颈有时会出现在网络IO处理上,所以使用了多个IO线程来处理网络请求,但是对于命令的执行,仍然采用单线程来处理

48 |
49 | 50 | ## 7.AOF日志的原理 51 |
52 | 答案 53 |

先执行写操作,再将日志写入AOF缓冲区(因为这样可以避免额外的检查开销,还不会阻塞当前的写操作),通过系统调用write时再写入内核缓冲区,刷盘时机由系统参数决定

54 |
55 | 56 | ## 8.AOF日志重写的原理 57 |
58 | 答案 59 |

当AOF日志过大时,会触发AOF重写,Redis会开启一个子进程, 读取数据库中的所有键值对,然后每一个键值对用一条命令记录到新的AOF文件(AOF重写期间,主进程可以继续处理命令请求,主进程产生的AOF日志先写入AOF缓冲区,再写入AOF重写缓冲区),完成重写工作后,会向主进程发送一条命令,然后主进程会将AOF重写缓冲区的所有内容追加到新的AOF文件中,然后将新的AOF文件改名,覆盖旧的AOF文件

60 |
61 | 62 | ## 9.为什么AOF日志重写要用子进程而不是线程 63 |
64 | 答案 65 |

因为如果使用线程,多线程之间会共享内存,修改共享内存数据时需要加锁,从而会降低性能。而使用子进程,父子进程共享数据,当内存发生修改时,会发生写时复制,而不需要加锁

66 |
67 | 68 | ## 10.RDB快照原理 69 |
70 | 答案 71 |

Redis生成RDB快照有两种方式

72 |

一种是save,在主线程生成RDB文件,如果生成RDB文件时间过长就会阻塞主线程

73 |

一种是bgsave,会创建一个子进程来生成RDB文件,可以避免阻塞主线程。Redis的RDB快照是全量快照,每次执行快照会把内存中的所有数据都记录到磁盘中,所以频率不能太频繁

74 |
75 | 76 | ## 11.混合持久化 77 |
78 | 答案 79 |

开启混合持久化后,AOF重写时,子进程会将共享的AOF日志以RDB的形式写入到新的AOF文件中,重写完成后通知主进程,主进程将AOF重写缓冲区以AOF的形式追加到新的AOF文件,然后将新的AOF文件改名,覆盖旧的AOF文件

80 |
81 | 82 | ## 12.AOF,RDB,混合持久化优缺点 83 |
84 | 答案 85 |

AOF的文件体积更大,性能较差,恢复速度较慢,但是AOF丢失的数据更少

86 |

RDB的文件体积更小,性能较高,恢复速度较快,但是RDB丢失的数据更多

87 |

混合持久化有RDB的优点恢复速度快,也有AOF的优点丢失的数据少,同时也有其缺点,可读性更差,兼容性更差

88 |
89 | 90 | ## 13.Redis主从复制原理 91 |
92 | 答案 93 |

一开始,从节点向主节点建立连接,然后主节点生成RDB快照,将其发送给从节点,从节点清空自己的数据,然后载入RDB快照,在生成RDB快照的过程中,不会阻塞的主进程,期间的写操作命令记录在replication buffer内(如果replication buff 满了会删除缓存,重新和从节点建立连接)。在从节点加载RDB快照完成后,主节点将replication buffer中的数据发送给从节点

94 |

当从节点掉线重连后,主节点会采用增量复制的方式发送数据,首先会检查要发送的数据是否存在repl_backlog_buffer(在发送给从节点之前会先将命令写入这里)中,如果在则进行增量复制,否则进行全量复制

95 |
96 | 97 | ## 14.Redis哨兵模式原理 98 |
99 | 答案 100 |

哨兵一开始会监控主节点的状态,如果Ping不通主节点,则判定为主观下线,然后向其他哨兵发送命令,其他哨兵节点根据自身与主节点的网络状态,投出赞成或反对,如果赞成票数达到quorum值,则判定主节点为客观下线。然后通知其他哨兵,希望成为leader来进行主从切换,每个哨兵只有一次投票机会,如果得到半数以上的赞成票并且大于等于quorum值,则当选leader。

101 |

开始主从切换,在从节点中选出一个节点将其转化为主节点(选取规则:先过滤掉网络不好的,然后优先级,复制下标,节点ID排序),然后通知其他从节点更换复制目标,将新主节点的信息发送给客户端,继续监视旧主节点,当他上线后设置为新节点的从节点。

102 |
103 | 104 | ## 15.Redis集群原理 105 |
106 | 答案 107 |

Redis集群将所有数据自动分成16384个哈希槽,将数据分散在不同的节点上。节点之间基于Gossip协议进行通信,通过主从复制和故障转移保证高可用

108 |
109 | 110 | ## 16.Redis过期删除策略 111 |
112 | 答案 113 |

Redis的过期删除策略由惰性删除和定期删除组成

114 |

惰性删除是指当访问到某个key时,判断是否过期,如果过期了就将其删除,否则不做处理。

115 |

定期删除是指每隔一段随机抽取20个key,将过期的key删除,如果定期删除执行时间超过了25ms,那么直接结束,否则判断过期key是否超过25%,超过则继续抽取

116 |
117 | 118 | ## 17.Redis内存淘汰策略 119 |
120 | 答案 121 |

1.随机淘汰设置了过期时间的key

122 |

2.优先淘汰更早过期的key

123 |

3.淘汰设置了过期时间中的,最久未使用的key

124 |

4.淘汰设置了过期时间中的,最少使用的key

125 |

5,随机淘汰key

126 |

6.淘汰最久未使用的key

127 |

7.淘汰最少使用的key

128 |
129 | 130 | ## 18.Redis的LRU和LFU 131 |
132 | 答案 133 |

Redis的LRU算法和传统的LRU算法不同,Redis通过添加最后访问时间的字段,然后需要淘汰数据时,通过随机采样,然后淘汰最久没用使用的那个

134 |

Redis的LFU算法记录和该key上次访问的时间戳和访问频次,每次访问时,首先会根据当前与上次访问时间的距离对访问频次进行衰减,然后按照一定概率增加访问频次的值。当需要淘汰数据时,随机抽取一些key,然后删除掉访问频次最低的key

135 |
136 | 137 | ## 19.缓存雪崩、缓存击穿、缓存穿透 138 |
139 | 答案 140 |

缓存雪崩是指大量缓存在同一时间过期,此时大量的请求直接访问数据库,从而导致数据库宕机。避免的方法是随机生成过期时间或者设置为不过期

141 |

缓存击穿是指热点数据过期,此时有大量的请求访问该热点数据,从而导致大量请求直接访问数据库,导致数据库宕机。避免的方法是将热点数据设置为不过期,由后台异步更新缓存或者在热点数据快过期时,提前通知后台线程更新缓存以及重新设置过期时间。或者在加互斥锁,保证同一时间只有一个线程请求缓存,其他线程等待或返回空值

142 |

缓存穿透是指大量请求即不在缓存又不在数据库中的数据,从而使得数据库宕机。避免的办法是对于在数据库中没查到的数据回种空值或默认值。或者使用布隆过滤器快速判断数据是否存在,来减少对数据库的查询。

143 |
144 | 145 | ## 20.常见的缓存更新策略 146 |
147 | 答案 148 |

1.旁路缓存。在更新数据时,先修改数据库,再删除缓存。在查询数据时,先查询缓存,再查询数据库,再将数据写回缓存

149 |

2.写穿/读穿。在更新数据时,如果存在缓存,则修改缓存,由缓存组件将数据同步更新到数据库,否则直接修改数据库。在查询数据时,如果存在缓存则直接返回,否则由缓存组件从数据库查询数据,并写入缓存,然后返回

150 |

3.写回。在更新数据时,只更新缓存,同时将缓存数据设置为脏,然后返回。异步的将缓存中的数据更新到数据库

151 |
152 | 153 | ## 21.Redis 的大 key 如何处理 154 |
155 | 答案 156 |

1.分批次删除,对于一个大key,每次只删除key对应的部分数据

157 |

2.异步删除,使用unlink命令异步删除

158 |
159 | 160 | ## 22.如何用 Redis 实现分布式锁的? 161 |
162 | 答案 163 |

通过Redis的SetNX实现,如果不存在则插入成功,如果存在,则插入失败,很适合分布式锁的加锁和解锁

164 |
165 | 166 | ## 23.惰性删除和定期删除的优缺点 167 |
168 | 答案 169 |

惰性删除不会占用太多的系统资源对CPU友好,但是会导致过期key长期占用内存得不到释放,造成一定的空间浪费。

170 |

定期删除的优点是能够减少对系统资源的占用的同时还能够减少对内存空间的无效占用,但是效果不如定时删除好

171 |
172 | 173 | ## 24.Redis实现的分布式锁,如果获取到锁的线程挂了,一直占用该锁怎么办? 174 |
175 | 答案 176 |

给锁设置超时时间即可

177 |
178 | 179 | ## 25.Redis分布式锁容灾问题,如果一个Redis挂了会不会重复拿到这个锁 180 |
181 | 答案 182 |

存在这种可能,如果采用的是主从或者哨兵模式的话,在主节点申请到锁后,主节点挂了,加锁信息还没来得及同步到从节点,是可以重复加锁的

183 |
184 | 185 | ## 26.Redis如何解决集群情况下分布式锁的可靠性? 186 |
187 | 答案 188 |

使用Redlock,客户端向多个独立的Redis加锁,如果能够和半数以上的节点成功的完成操作,则认为加锁成功

189 |
190 | 191 | ## 27.主从模式下,如何处理过期键 192 |
193 | 答案 194 |

从节点不会让key过期,主节点发现key过期后,会发送删除命令给从节点

195 |
196 | 197 | ## 28.为什么Redis集群采用哈希槽而不是一致性哈希 198 |
199 | 答案 200 |

1.一致性哈希增删节点时,会导致部分数据无法命中,并且导致下一个节点的压力增大,造成缓存雪崩

201 |

2.哈希槽的数据分布比一致性哈希更加均匀

202 |

3.哈希槽增删节点更加便捷,只需要将原有的数据移动到其他节点即可

203 |
204 | 205 | ## 29.为什么Redis的哈希槽是16384个? 206 |
207 | 答案 208 |

因为如何有更多的槽位会导致心跳包更大,浪费带宽。主节点的配置信息的哈希槽是通过bitmap记录的,如果哈希槽越少,压缩率更高

209 |
210 | 211 | ## 30.Redis常见数据结构的实现 212 |
213 | 答案 214 |

1.String redis的字符串是通过int和sds实现的,sds不仅可以保存文本数据,还可以保存二进制数据。并且获取字符串长度的复杂度是O(1),因为sds存储了字符串的长度,并且sds是安全的,拼接字符串不会造成缓冲区溢出

215 |

2.List 因为压缩列表是连续存储的,发生修改时,导致联动更新,而双向链表的空间开销太大。所以将二者相结合 redis的list通过quicklist实现,quick本质上是个双向链表,里面存储的是压缩列表,结合了压缩列表和双向链表的优点,有效节省存储空间的同时有较高的效率。

216 |

3.hash redis的hash在元素个数少于512并且所有值小于64字节时,基于listpack实现,listpack沿用了ziplist的紧凑布局,通过不存在上一个元素的长度避免了连锁更新的问题,通过encoding记录了元素的数据类型和长度,通过element-tot-len记录encoding和data的长度,从而支持方向遍历,否则会采用哈希表,哈希表底层存储了两个字典,一个用于扩容,会在必要的时候进行扩容和缩容,rehash

217 |

4.set redis的set,在元素类型都是int并且元素个数不超过512的时候,会采用整数集合,整数集合的底层是一个有序数组。否则采用哈希表

218 |

5.zset zset在元素个数小于128并且每个元素的值都小于64字节时,采用listpack,否则采用跳表。跳表是一个有序数据结构,它通过在每个节点维护多个指向其他节点指针,从而达到快速访问节点的目的

219 |
220 | 221 | ## 31.Redis 采用单线程为什么还这么快? 222 |
223 | 答案 224 |

1.Redis的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了。

225 |

2.Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

226 |

3.Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求

227 |
228 | 229 | ## 32.Redis分布式锁,过期时间不够怎么办? 230 |
231 | 答案 232 |

可以使用看门狗机制,监控锁的过期时间,定期给锁续期

233 |
234 | 235 | ## 33.redis惊群效应 236 |
237 | 答案 238 |

在高并发场景下,某个缓存过期,然后大量请求打到数据库,导致数据库压力突增,然后另一段时间又有大量的缓存失效

239 |

通过加分布式锁,限流,本地缓存来解决

240 |
241 | 242 | ## 34.redis的哈希表的底层原理 243 |
244 | 答案 245 |

哈希表包括了两个哈希桶数组,一个用于存储普通数据,一个用于存储扩容时的数据。是数组+链表的结构,当发生哈希冲突时,会在链表后追加元素。当哈希冲突过于频繁,会进行扩容。

246 |

扩容条件为:

247 |

1.负载因子(实际的元素数量/哈希桶的数量)>1并且没在进行AOF日志重写或RDB快照生成(AOF日志重写和RDB快照生成都是由子进程完成的, ,如果进行扩容会增加不必要的内存写入操作)

248 |

2.负载因子>5

249 |

缩容条件为:负载因子<0.1

250 |

当插入操作时会判断是否满足扩容条件,如果满足则开始扩容。当删除操作时会判断是否满足缩容条件,如果满足则开始缩容。扩容和缩容都是一个渐进式的过程,每次进行操作时,都会移动一个哈希桶到另一个数组

251 |
252 | 253 | ## 35.AOF日志缓冲区刷盘策略 254 |
255 | 答案 256 |

1.Always 每次执行写操作后都将AOF日志刷盘

257 |

2.Everysec 每次执行写操作后都将AOF日志写入内核缓冲区,然后每秒刷盘

258 |

3.No 每次执行写操作后都将AOF日志写入内核缓冲区,具体刷盘时机由操作系统决定

259 |
260 | 261 | ## 36.布隆过滤器如何实现删除操作 262 |
263 | 答案 264 |

1.通过计数布隆过滤器实现,但是占用的空间会大很多

265 |

2.布谷鸟过滤器

266 |

布谷鸟过滤器的底层是哈希桶数组,每个元素记录的是key对应的指纹。

267 |

插入:首先计算出当前key对应的hash,如果已经存在,则将当前位置的key(这里的key实际存储的是key对应的指纹)剔除,然后计算出当前key的hash值与当前的下标进行异或,如果存在,则将当前位置的key进行剔除,直到达到最大剔除次数或没有冲突,则插入

268 |

查找:首先计算出当前key对应的hash1(对应首选桶的下标),和hash1和key对应的指纹的哈希值的异或值hash2(对应候选桶的下标),如果hash1或hash2存在对应的指纹,则直接返回true,否则返回false(如果发生桶溢出则会导致假阴性)

269 |

删除:首先计算出当前key对应的hash1(对应首选桶的下标)和和hash1和key对应的指纹的哈希值的异或值hash2(对应候选桶的下标),如果hash1或hash2存在对应的指纹,则删除掉一个(因此不能重复删除,必须保证删除的key是存在的key)

270 |

优点:

271 |

1.基于布谷鸟过滤器的布谷鸟过滤器,更加紧凑,因此可以更加节省空间

272 |

2.布隆过滤器在查询时需要多次哈希,而布谷鸟过滤器只需要一次哈希,因此效率更高

273 |

3.布谷鸟过滤器支持删除,而布隆过滤器不支持删除

274 |

缺点:

275 |

1.布隆过滤器采用的是备用桶的方案,首选桶和候选桶可以通过相互异或得出,因此桶的大小必须是2的指数倍

276 |

2.布谷鸟不能重复插入元素,因为重复插入会将原有元素剔除,然后导致首选桶和候选桶被占满,因此一个元素最多被插入2b

277 |

3.频繁的剔除元素可能导致哈希冲突加剧,从而导致性能下降

278 |

4.如果在元素没有被插入的时候删除,会导致误删除

279 |
280 | 281 | ## 37.redis跳表的底层实现 282 |
283 | 答案 284 |

跳表本质上是一个多层链表,平均复杂度为log(n)

285 |

插入操作:每个插入前会随机生成一个层数,如果超过当前跳表的层数则需要扩大跳表的层数,然后从最高层开始,找到第一个大于等于目标值的节点,插入在它的前面,直到所有层都添加完毕

286 |

查询操作:从最高层开始,当目标值大于当前节点的后一个节点的值时,继续往后走,当目标值小于当前节点的后一个节点的值,大于当前节点的值时,往下移动一层,直到目标值等于当前节点值,搜索结束

287 |

删除操作:从最高层开始,先进行搜索操作,找到目标值后,将当前节点及其下层节点删除

288 |
289 | 290 | ## 38.跳表的无锁实现 291 | 292 | 293 | ## 39.ZSet的原理和底层实现 294 |
295 | 答案 296 |

简单剖析下zset的原理,大抵上就是skiplist是由两个元素构成的,哈希表和跳表。哈希表主要是用来 297 | 保证元素的唯一性,去重,并且能够通过元素寻找对应的score值,而跳表主要是用来给元素value 298 | 进行排序并且通过score的范围来获取元素列表(就是找到最小符合的进行遍历得到的一连串)。

299 |

实现原理:跳表在创建节点的时候会生成一个0-1的随机数,如果随机数小于0.25那层数就增加1,一 300 | 直进行这个操作直到随机数大于0.25。因此这样会有个疑问。如果想要平均时间复杂度log(n),为什么这个因 301 | 子不是0.5呢?原因是因为要为了节约一半内存而牺牲一小部分性能来做权衡。而跳表节点中存在 302 | 有元素,元素的权重值,后向指针,level数组,level数组内有前向指针和跨度,后向指针是为了 303 | 能倒序遍历,跨度的话是为了计算排位,因为你如果只是一直遍历的话没有其他的元素告诉你到底 304 | 离开始遍历时的节点有多远。而查询时跳表会先从最高层开始遍历(我猜测时因为高层节点少,跨度 305 | 大,能更快遍历到),遍历时候会对权重和元素进行判断,如果当前节点的权重比较小,会访问该层 306 | 的下一个节点,如果权重相等的话会按元素的值来判断,如果当前节点的值比较小,就会访问下一 307 | 个节点,如果两个都不满足或者下一个节点是空,跳表就会把高度下降一层。

308 |
309 | 310 | ## 40.ZSet用跳表不用平衡树的原因 311 |
312 | 答案 313 |

从内存占用上看,跳表更灵活。平衡树每个节点包含两个指针指向左右子树,但是跳表的指针数 314 | 平均为1/(1-p),取决p大小。

315 |

做范围遍历更简单,平衡树找到指定范围的小值还要用中序遍历去寻找其他不大于大值的值。而 316 | 跳表找到小值对第一层链表进行遍历判断即可。

317 |

平衡树实现逻辑复杂,而跳表实现逻辑简单,操作快速。

318 |
319 | 320 | ## 41.Redis GEO底层实现 321 |
322 | 答案 323 |

要实现附近的人功能,本质上就是对于坐标(x,y)需要找到附近的点对。对于二维点对很难找到相邻点对,因此要将二维映射到一维。

324 |

我们通过 GEO HASH算法。对坐标的范围进行二分。假设l <= x <= r, 那么用二进制的0表示x位于[l,mid]之间,1表示x位于[mid,r]之间,然后将l,r的区间不断缩小,从而使得坐标更加精确,因此可以将坐标通过logn的复杂度得到若干个比特位,然后将x,y坐标的比特位交替放置。然后按其值排序,当需要查找附近的人时,计算出自己的比特值然后通过二分找到自己附近的值。

325 |

本质上 GEO HASH 就是将坐标映射到了多个区域,然后每次找到自己的区域后,查找附近的几个区域进行比较即可。

326 |
327 | 328 | ## 42.Redis HyperLogLog底层实现 329 |
330 | 答案 331 |

hyperloglog是算法原理是通过对key计算哈希值,然后转换为二进制64位,取出后14位作为桶的编号,然后剩余50位,找到第一次出现1的位置,范围是0~49,因此通过6个比特位就可以存下。每次写入时,更新桶对于的最大的1出现的位置。在查询时每次读取所有的桶的比特位,然后对所有的桶取调和级数并乘上一个系数即可

332 |

核心原理是基于伯努利实现,在实验次数足够多的条件下,可以近似估计结果。例如抛硬币,正面和反面概率是0.5,1表示正面,0表示反面,假设进行了N轮实验,最多maxv次才出现正面,则他最少进行了2^maxv次实现,maxv即1出现的位置。然后为了减少误差使用了调和级数和分桶,通过hash保证10分布均匀。

333 |
-------------------------------------------------------------------------------- /docs/handbook/场景题.md: -------------------------------------------------------------------------------- 1 | # 场景题 2 | 3 | ## 1.一个32位系统,dump结果有1G,但是用户申请512M却触发OOM了,有几种原因? 4 |
5 | 答案 6 |

1.内存被小的分配占用,导致没有足够大的连续空间分配给512M的请求

7 |

2.可能操作系统对单个进程可以使用的内存量有限制,即使系统内存足够,超出限制后也会出发OOM错误

8 |

3.可能有一部分内存是操作系统保留的内存,实际用户能使用的内存已经不足512M了

9 |

4.可能存在内存泄漏,即使系统报告有大量未使用的内存,但实际可用内存已经很少了

10 |
11 | 12 | ## 2.如何保证幂等性 13 |
14 | 答案 15 |

1.Token机制:客户端请求时,服务端发放一个Token,客户端提交请求时携带这个Token。服务器收到请求后,判断Token是否有效,有效则执行操作,并使Token失效,否则不执行任何操作。

16 |

2.悲观锁机制:在数据库中使用版本号或时间戳机制,尝试更新数据时检查版本号或时间戳,如果不匹配则不执行任何操作,否则执行操作并更新版本号或时间戳

17 |

3.使用唯一标识符:客户端为每个请求生成一个唯一标识符,服务端根据这个唯一标识符判断请求是否执行过,如果已经执行过不,则忽略,否则执行操作。

18 |
19 | 20 | ## 4.10亿个数,找出最大的10个 21 |
22 | 答案 23 |

堆/快速选择

24 |
25 | 26 | 27 | ## 5.10亿个数排序 28 |
29 | 答案 30 |

首先将数据进行分成n块,对每个块加载进内存进行排序,然后取出n个块中的第一个元素,放入堆中,然后弹出堆顶元素,然后根据弹出的元素去对应的块中取出一个元素放入,直到时堆被取空。

31 |

通过bitmap排序,将所有数字塞入bitmap,然后从小到大遍历一遍bitmap

32 |
33 | 34 | ## 6.如何把一个文件较快的发送到100w个服务器? 35 |
36 | 答案 37 |

首先将文件发送给1000个服务器,然后每一个服务器发送给另外1000个服务器

38 |
39 | 40 | ## 7.如何从大量的URL中找出相同的URL? 41 |
42 | 答案 43 |

背景:给定a,b两个文件,各存放50亿个URL,每个URL各占64B,内存限制是4G,请找出两个文件共同的URL

44 |

解法1:因为是URL,所以一般会有很多重复部分,我们可以使用Trie树即可

45 |

解法2:遍历文件a,b,对遍历到的URL求哈希值%1000,从而分成一共2000个文件,每个文件约300MB,不同文件的URL一定不同。然后遍历a的每一个小文件,写入hashset,然后遍历b对应的小文件,如果在hashset中存在,那么就是他们公共的URL

46 |
47 | 48 | ## 8.如何从大量数据中找到高频词? 49 |
50 | 答案 51 |

背景:有一个1GB大小的文件,文件每一行是一个词,每个词大小不超过16B,内存大小限制是1MB,要求返回频率前100的词

52 |

解法1:遍历该文件,对每个词求hash%1000,分成1000个小文件,使用Map统计出小文件中的每个词的出现次数,然后写入堆中,并只保留出现次数前100的词

53 |

解法2:使用外部归并排序对所有数据进行排序,然后按顺序遍历所有数据,并使用变量来统计数的出现次数,并插入堆中

54 |
55 | 56 | ## 9.如何在大量数据中找到不重复的整数 57 |
58 | 答案 59 |

背景:在2.5亿个整数中找到不重复的整数

60 |

解法1:首先遍历所有整数,将其哈希到若干个小文件中,然后遍历这些小文件,使用统计出现次数,然后将不重复的写入

61 |

解法2:使用位图法,遍历整数的时候,如果没出现过,对应的位就是00,如果出现过一次,对应的位就是01,如果出现过多次,对应的位就是10。总内存2^32*4=1GB。如果内存超过1GB就可以使用位图法

62 |
63 | 64 | ## 10.如何在大量数据中判断一个数是否存在 65 |
66 | 答案 67 |

解法1:使用位图法,遍历每一个整数,将其对应的位设置为1,如果要查询的位为1,那么就存在

68 |
69 | 70 | ## 11.点赞系统设计 71 |
72 | 答案 73 |

点赞系统分为接入层,应用层,异步任务,数据层。

74 |

接入层需要将流量进行划分,对于点赞系统使用同城多活来实现容灾,将写流量分到同一个机房,读流量均分到不同机房。当机房出现故障时将读写流量都切换到另一个机房

75 |

应用层需要将流量转发到异步任务中,在处理读请求时从数据层获取数据,并做限流和服务降级兜底策略

76 |

异步任务这里需要将数据进行聚合,然后更新数据和刷新缓存

77 |

数据层使用三级存储,Mysql + Redis + 本地缓存。Redis做缓存,本地缓存用于缓存热点数据,通过最小堆算法将热点数据加载到本地缓存。当缓存不可用时,通过对Mysql限流最大限度为用户提供服务,触发限流的请求使用兜底数据返回。在更新存储时一定要进行重试,直到写入成功为止,具有最终一致性。然后数据也应用多地备份,异步进行数据同步

78 |

在数据量较大时需要进行分库分表,对于水平分表而言,可通过按点赞者和被点赞者进行分库,保存两份数据,在写入时进行双写即可

79 |
80 | 81 | ## 12.分库分表策略 82 |
83 | 答案 84 |

对于水平分表时,通过选定分片键,可采用哈希或区间进行分段

85 |

通过对分片键进行哈希,问题是当需要进行扩容时需要进行rehash,但可以采用同步双写更新增量数据,然后使用脚本迁移旧库中的存量数据。但是每次扩容需要迁移全部的数据,迁移成本较大,可以使用一致性哈希算法,这样迁移的成本更小,但是会存在节点分配不均的问题,但是可以使用虚拟节点来解决分配不均的问题

86 |

通过对分片键的值域范围进行分段,问题是可能存在分配不均的问题,某些值域的访问比较频繁,某个值域的数据比较少。对于该问题可以进行哈希取模来解决

87 |

对于查询非分片键的记录时,可以通过建立映射表来查询,或者将数据冗余一份到ES中,通过ES进行查询。也可以通过基因法来实现,本质上是利用取模,要求表的数量必须是2的次幂,因此x%2^n等价于获取x二进制的后n位。因此我们对分片键求出%2^n的结果作为基因数,然后将基因数拼接到需要查询的键上,这样可以使得同一个分片键的所有非分片键都落在一个表上。查询的时候求后n位即可知道在哪个表上

88 |

分库主要是解决单数据库支持的连接数有上限并且磁盘空间有限,因此需要分库

89 |

分表主要是解决单表数据量过大时查询速度下降和热冷数据问题

90 |
91 | 92 | ## 13.短链系统设计 93 |
94 | 答案 95 |

方案1:通过哈希算法

96 |

将一个长链通过哈希算法生成短链,每次去数据库中查找是否存在相同的短链,如果存在则给当前的url添加上特殊字符,然后继续计算,直到不冲突为止。因为哈希冲突的概率特别低实际效率会很高。但如果请求量上来,我们可以通过布隆过滤器进行优化,对于不在布隆过滤器中的一定不存在于数据库中。然后我们还可以通过多级缓存来进行优化

97 |

方案2:通过自增ID实现

98 |

可以通过数据库自增ID来实现,当然这在并发量太大的情况下是扛不住的。但是我们可以实现多个发号器。

99 |

1.每个发号器负责一段ID。整体ID由多个发号器获取的ID组成

100 |

2.每个发号器负责不同的范围的ID,然后将请求负载均衡到每个发号器上

101 |

3.每个发号器有一个唯一前缀,通过唯一前缀和分配的ID来实现

102 |
103 | 104 | -------------------------------------------------------------------------------- /docs/handbook/微服务.md: -------------------------------------------------------------------------------- 1 | # 微服务 2 | 3 | ## 1.rpc和http的区别 4 |
5 | 答案 6 |

1.用途不同: http一般用于web浏览器和服务器之间的通信。rpc一般用于跨主机的方法调用

7 |

2.服务发现:http一般通过dns服务进行服务发现,而rpc通过专门的中间服务保存服务名和Ip地址(如Consul,etcd,zk)

8 |

3.底层链接形式:rpc中有一个链接池,发数据的时候从连接池中取一条出来,用完放回去下次再复用,有利于提高网络请求性能

9 |

4.传输的内容不同:http是使用的json传输数据,rpc用的protobuf传输数据

10 |
11 | 12 | ## 2.限流算法 13 |
14 | 答案 15 |

1.基于计数的限流算法:在固定时间窗口,每收到一个请求计数器+1,超过阈值后拒绝请求,到达下一个时间窗口后计数器清空

16 |

优点:实现简单,资源消耗少。 缺点:无法处理突发流量,无法解决突刺现象(一个时间窗口的末端和下一个时间窗口的始端,会导致请求数量超过阈值)

17 |

2.基于滑动窗口的限流算法:将时间分成多个固定大小的连续小窗口,随着时间的推进窗口向右滑动,使得窗口切换变得更加平滑

18 |

优点:平滑限流,适合突发流量。缺点:空间占用大,复杂性高

19 |

3.漏桶算法:桶有固定容量,桶上有固定的漏水口表示系统能处理请求的速度,当请求速率超过漏水速度,请求会被堆积起来

20 |

优点:流量稳定,可以防止系统负过载。缺点:无法处理突发流量缺乏弹性,资源利用率不高

21 |

4.令牌桶算法:桶有固定容量,定期向桶中放入令牌,请求到来时取令牌,没有令牌则拒绝请求

22 |

优点:允许突发流量,平滑限流 缺点:无法限制瞬时流量,内存占用大

23 |
24 | 25 | 26 | ## 3.熔断算法 27 |
28 | 答案 29 |

1.hystrix熔断算法:将熔断器的状态分为三种,关闭,开启,半开启。关闭状态是初始状态,当调用总数达到200,并且错误率超过50%(通过滑动窗口实现),则进入开启状态。开启状态会立即返回错误,当冷却时间到了之后会进入半开启状态。半开启状态会允许一定数量的请求取调用服务端,如果请求调用成功数量达到参数值,则进入关闭状态

30 |

2.google sre熔断算法:当请求总数>=K*请求成功数(K是熔断器的敏感参数),会打开熔断器,以max(0,请求总数-K*请求成功数)/(请求成功数+1)的概率被拒绝,从而实现自适应熔断

31 |

hystrix会导致如果服务出现波动,可能导致一段时间不可用。而 google sre的自适应性更强

32 |
33 | 34 | ## 4.负载均衡算法 35 |
36 | 答案 37 |

1.轮询

38 |

2.加权轮询

39 |

3.最少连接

40 |

4.一致性哈希

41 |

5.加权最少连接

42 |
43 | 44 | ## 5.单体架构和微服务架构 45 |
46 | 答案 47 |

1.单体架构就是将多个功能都写在一起,所有功能耦合在在一起,互相影响,不能单独开发和调试

48 |

2.微服务就是将单体架构的多个功能进行拆分成多个服务,每个服务都是一个独立运行的程序,服务之间相互独立,服务功能单一,一个服务可以调用其他服务的功能,实现复用

49 |
50 | 51 | ## 6.令牌桶底层设计 52 |
53 | 答案 54 |

每次请求的时候,根据上次添加令牌的时间与当前的时间差计算出需要添加多少的令牌,然后更新令牌总数,并且不能超过容量。如果令牌总数还充足,则请求成功,否则请求失败。并更新添加令牌的时间和令牌总数。

55 |
56 | 57 | ## 7.服务降级,熔断,限流 58 | 59 | ## 8.系统稳定性设计 60 |
61 | 答案 62 |

1.存储数据冗余,多副本。出问题自动切换

63 |

2.支持横向扩容,有突发流量自动扩容

64 |

3.对服务进行分级,对于非核心服务,在下游故障时通过拦截器自动熔断/人工 然后不再调用下游接口,走缓存 or 默认值 or配置的兜底数据。

65 |

4.配置限流策略,在大流量冲击下,保证服务不宕机

66 |
-------------------------------------------------------------------------------- /docs/handbook/操作系统.md: -------------------------------------------------------------------------------- 1 | # 操作系统 2 | 3 | ## 1.CPU如何保持缓存和内存的数据一致 4 |
5 | 答案 6 |

有两种方式,写直达和写回

7 |

写直达:首先判断该数据是否存在Cache中,如果存在,则写入Cache,再写入内存。否则直接写入内存

8 |

写回:首先判断该数据是否存在Cache中,如果存在,则直接写入Cache,将其标记为脏。如果不存在,定位到对应的Cache块,如果该块为脏,则先将其写入内存,然后再将要写入的数据从内存读出(为了获取到cache对应内存中的位置,使得下次查找时,可以直接找到内存中的数据),再写入到Cache,标记为脏。如果该块不为脏,则将要写入的数据从内存读出,然后将要写入的数据写入Cache,并标记为脏

9 |
10 | 11 | ## 2.多核CPU如何保证缓存一致性 12 |
13 | 答案 14 |

通过基于总线嗅探机制的MESI协议,维护了已修改,独占,共享,已失效四个状态。根据来自本地核心的请求,或者是来自其他CPU核心通过总线传输过来的请求,从而构成一个流动的状态机,对于处于已修改或独占状态的CacheLine,修改数据时,不需要发送广播给其他CPU核心

15 |

MESI协议保证了缓存一致性

16 |

1.它实现了基于总线嗅探机制实现了写传播

17 |

2.它基于状态机实现了事务的串行化(当一个核心修改了缓存行,其他核心都会将该缓存行设为无效,访问必须从内存中取)

18 |
19 | 20 | ## 3.什么是中断? 21 |
22 | 答案 23 |

中断是操作系统用来打断当前执行的进程,转而执行中断处理程序的一种机制

24 |

分为硬中断和软中断,硬中断是由硬件触发中断,用于快速处理中断。软中断,由内核触发中断,用来异步的完成硬中断没完成的工作

25 |
26 | 27 | ## 4.为什么要有虚拟内存 28 |
29 | 答案 30 |

1.虚拟内存使得进程的运行内存可以超过物理内存的大小,因为程序符合局部性原理,CPU访问内存会有很明显的重复访问的倾向性,对于那些没有经常访问的数据,我们可以将其换出物理内存

31 |

2.虚拟内存使得每个进程都有自己的页表,每个进程的虚拟内存空间都是独立的,解决了多进程地址空间冲突的问题

32 |

3.虚拟内存的页表中记录了一个页的读写权限,在内存访问方面,为操作系统提供了更好的安全性

33 |
34 | 35 | ## 5.内存分段机制 36 |
37 | 答案 38 |

39 | 内存分段机制将一个程序分成多个逻辑段,如代码段,栈段,堆段,数据段,每个段有自己的属性,分段机制下,虚拟地址由段选择因子和段内偏移组成。段选择因子包括段号,段的界限,段的特权等级。 40 |

41 |
42 | 43 | ## 6.内存分页机制 44 | 45 |
46 | 答案 47 |

48 | 将整个虚拟内存和物理内存都分成固定大小的页,页与页之间是紧密排列的,不会有外部碎片。分页机制下,虚拟地址由页号和页内偏移量组成,然后通过页表,将虚拟地址中的页号映射到物理页号。 49 |

50 |
51 | 52 | ## 7.内存分段和分页机制的缺点 53 |
54 | 答案 55 |

分段机制的缺点是容易产生外部碎片,内存交换效率低(因为容易产生外部碎片,所以经常需要将数据交换到磁盘,但是磁盘访问速度太慢,所以会导致内存交换效率很低)

56 |

分页机制的缺点是容易产生内部碎片(因为分页机制的最小单位是页,所以即使程序不足一页,也只能分配一页)

57 |
58 | 59 | ## 8.分页机制如何解决外部碎片和内存交换效率低的问题? 60 |
61 | 答案 62 |

63 | 出现外部碎片是因为有比较小的内存无法分配的出去,而分页机制是固定大小,因此不会有外部碎片问题,当内存空间不足时,会将少数的一个或几个页换出到磁盘上,不会花太多时间,因此内存交换效率比较高 64 |

65 |
66 | 67 | ## 9.段页式机制 68 |
69 | 答案 70 |

71 | 先将程序划分成多个有逻辑意义的段,再将每个段分成多个页。每个程序一个段表,每个段由有一张页表,在段页式机制下,虚拟地址由段号,段内页号,页内偏移组成。 72 |

73 |
74 | 75 | ## 10.malloc通过brk()和mmap()申请内存的区别 76 |
77 | 答案 78 |

通过brk()方式申请内存时,free释放内存时,并不会把内存归还给操作系统,而是在malloc的内存池中,待下次使用

79 |

通过mmap()方式申请内存时,free释放内存时,会把内存归还给操作系统,内存得到真正的释放

80 |
81 | 82 | ## 11.为什么不全部使用 mmap 来分配内存? 83 |
84 | 答案 85 |

86 | 因为使用mmap分配内存会导致每次都发生运行态的切换,还会导致缺页中断,会导致CPU消耗太大 87 |

88 |
89 | 90 | ## 12.为什么不全部使用 brk来分配内存? 91 | 92 |
93 | 答案 94 |

95 | brk分配内存很容易造成内存碎片,对于小块内存,堆内会产生越来越多的不可用的碎片,从而造成内存泄漏 96 |

97 |
98 | 99 | ## 13.malloc返回的地址结构 100 | 101 |
102 | 答案 103 |

104 | 包括内存的头信息和用户使用的内存块 105 |

106 |
107 | 108 | ## 14.内存回收机制 109 | 110 |
111 | 答案 112 |

113 | 当内存分配时,可分配内存不够就会触发后台内存回收,这个回收过程是异步的,然后检查是否有足够的空闲物理内存,如果还不够,则启用直接内存回收,这是同步的过程。如果还不够,则会触发OOM机制。内存回收的时候,对于文件页,如果是脏页,先写入磁盘,再回收,否则,直接回收,对于匿名页,先写入磁盘,再回收。在回收时,会从通过LRU算法,取出队尾的内存页,因为他是很少被访问的,将其回收 114 |

115 |
116 | 117 | ## 15.回收内存带来的影响 118 |
119 | 答案 120 |

1.对于后台内存回收,是异步回收的,因此不会阻塞进程

121 |

2.对于直接内存回收,是同步回收的,会阻塞进程,这会造成很长时间的延迟,以及系统的CPU利用率会升高,导致系统负载升高

122 |
123 | 124 | ## 16.在4g的机器上申请8g的内存会怎么样? 125 | 126 |
127 | 答案 128 |

129 | 首先要考虑是32位还是64位的操作系统,32位可申请的内存是3G,64位可申请的内存是128T。 130 | 如果超过对应的,将会申请失败。然后考虑是否访问内存,如果不访问,因为保存虚拟内存的数据结构需要内存,所以只要物理内存充足,就可以申请成功。如果物理内存不足,开启swap机制也可以申请成功。如果访问,超过物理内存后,不开启swap机制将会触发OOM。 131 |

132 |
133 | 134 | ## 17.swap机制 135 | 136 |
137 | 答案 138 |

139 | 当系统内存不足或大量内存闲置时,swap机制会将进程暂时不用的内存数据存储到磁盘中,并释放这些数据的内存。当进程再次访问这些内存时,再把它们从磁盘读到内存中来 140 |

141 |
142 | 143 | ## 18.linux如何避免预读失效和缓存污染 144 | 145 |
146 | 答案 147 |

linux是基于LRU算法,将其划分成活跃链表和非活跃链表,预读的页加入到非活跃链表的头部,当页被真正访问时,才将页插入活跃链表头部,如果预读的页没有被访问,也不会影响活跃链表中的热点数据

148 |

缓存污染是因为大量数据直接进入活跃链表,所以我们要提高进入活跃链表的门槛,因为大量的数据只会被读入一次,因为不会被加入到活跃链表,而造成热点数据被替换掉

149 |
150 | 151 | ## 19.进程虚拟空间的组成 152 | 153 |
154 | 答案 155 |

156 | 由栈,文件映射和匿名映射区,堆,BSS段,数据段,代码段组成。BSS段是未初始化的数据,文件映射和匿名映射区就是通过mmap申请的内存 157 |

158 |
159 | 160 | ## 20.用户空间和内核空间 161 | 162 |
163 | 答案 164 |

165 | 用户空间是应用程序的运行空间。内核空间是内核的运行空间, 166 | 进程在用户态时,只能访问用户空间。只有进入内核态,才能访问内核空间 167 |

168 |
169 | 170 | ## 21.进程上下文切换的过程 171 | 172 |
173 | 答案 174 |

175 | 首先会从用户态切换到内核态,然后保存进程上下文,然后加载另一个进程的进程上下文。 176 |

177 |
178 | 179 | ## 22.CPU上下文切换 180 | 181 |
182 | 答案 183 |

184 | 先将CPU上下文(CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到寄存器和程序计数器中,最后再跳转到程序计数器所指的新位置 185 |

186 |
187 | 188 | ## 23.进程上下文是什么? 189 | 190 |
191 | 答案 192 |

193 | 进程上下文包括进程的虚拟地址空间(堆,栈,代码段,数据段等),全局变量,进程控制块,寄存器,程序计数器等信息 194 |

195 |
196 | 197 | ## 24.什么是进程?什么是线程?什么是协程? 198 | 199 |
200 | 答案 201 |

202 | 进程是资源分配的最小单位,线程是cpu调度的最小单位。 203 | 进程是执行中的程序。线程是进程的一个执行流程。协程是一种用户态的,不被操作系统所管理的,完全由用户控制的,比线程更加轻量级的存在 204 |

205 |
206 | 207 | ## 25.进程和线程的区别? 208 | 209 |
210 | 答案 211 |

1.进程是资源分配的单位,线程是CPU调度的单位

212 |

2.进程拥有完整的资源,而线程只独享必不可少的资源,如寄存器和栈

213 |

3.进程上下文切换需要保存更多的上下文信息,而因为同一进程的线程共享了进程的信息,故需要保存的上下文更少

214 |

4.线程之间的访问需要进行协同和同步,否则会出现竞争条件和死锁

215 |
216 | 217 | ## 26.线程上下文切换 218 |
219 | 答案 220 |

如果两个线程属于同一个进程,则只需要切换线程的私有数据和寄存器等数据

221 |

如果两个线程不属于同一个进程,则和进程上下文切换一样。

222 |
223 | 224 | ## 27.线程和协程的区别 225 |
226 | 答案 227 |

1.线程的调度由操作系统控制,而协程的调度由用户自己控制

228 |

2.线程的切换需要由用户态切换到内核态,而协程的切换不需要

229 |

3.多核处理器的环境下,多线程可以是并行的,但是多协程是并发的

230 |

4.线程通常是抢占式的,而协程是协同式的。

231 |
232 | 233 | ## 28.进程调度算法 234 |
235 | 答案 236 |

进程调度算法包括先来先服务,最短作业优先,高响应比优先,时间片轮转,多级反馈队列,最高优先级

237 |

先来先服务对长作业有利,适合CPU密集型,不适合IO密集型的系统

238 |

最短作业优先,对长作业不利,可能导致长作业一直不能运行

239 |

高响应比优先权衡了短作业和长作业

240 |

最高优先级,可能会导致低优先级的作业永远无法运行

241 |

多级反馈队列,兼顾了长短作业,同时有较好的响应时间

242 |
243 | 244 | ## 29.页面置换算法 245 | 246 |
247 | 答案 248 |

最佳页面置换算法(太过理想没法实现,将未来最长时间不访问的页面置换)

249 |

最近最久未使用置换算法(LRU)

250 |

先进先出置换算法(FIFO)

251 |

时钟页面替换算法(维护一个环形链表,每个页面记录一个访问标志位,置换时,遇到访问标志位为1的就将其改成0,遇到0就将其置换,当页面被访问时,将标志位设为1

252 |

最不常用算法(LFU)

253 |
254 | 255 | ## 30.磁盘调度算法 256 |
257 | 答案 258 |

先来先服务

259 |

最短寻道时间优先

260 |

扫描算法(选定某一个方向,直到最后一个磁道,才调转方向,返回途中处理请求)

261 |

循环扫描算法(选定某一个方向,直到最后一个磁道,才调转方向,返回途中不处理请求)

262 |

LOOK算法(基于扫描算法的改进,磁头只会移动到最远的请求位置,然后立即反向移动,返回途中处理请求)

263 |

C-LOOK算法(基于循环扫描算法的改进,磁头只会移动到最远的请求位置,然后立即反向移动,返回途中不处理请求)

264 |
265 | 266 | ## 31.虚拟内存和物理内存的定义 267 |
268 | 答案 269 |

虚拟内存是一种内存管理技术,将程序使用的内存地址(虚拟地址)映射到物理内存的地址

270 |

物理内存就是实际使用的内存

271 |
272 | 273 | ## 32.select,poll,epoll 274 |
275 | 答案 276 |

select就是将已连接的socket列表拷贝到内核中,然后由内核检查是否有网络事件产生,通过遍历socket列表,然后将有网络事件产生的socket标记为可读或可写,然后再拷贝到用户态,用户态再遍历socket列表,找到可读或可写的socket,对其进行处理

277 |

poll就是将select使用的bitsmap替换成了链表,从而不会受到bitsmap长度的限制

278 |

epoll在内核中使用红黑树来关注进程所有待检测的socket,从而减少数据拷贝,使用事件驱动机制,内核里维护了一个链表来记录就绪事件,只将有事件发生的socket列表传递给应用程序,不需要扫描整个集合

279 |
280 | 281 | ## 33.用户态和内核态 282 |
283 | 答案 284 |

用户态是应用程序运行的状态,处于用户态时只能访问受限的内存

285 |

内核态是内核运行的状态,处于内核态时可以访问任何数据

286 |
287 | 288 | ## 34.操作系统进行内存管理的意义? 289 |
290 | 答案 291 |

1.优化内存使用效率

292 |

2.保证进程之间使用内存互不干扰,保证系统的安全性

293 |

3.更加高效的申请和释放内存

294 |
295 | 296 | ## 35.进程之间通信的方式,同一台主机哪个最快? 297 |
298 | 答案 299 |

1.匿名管道

300 |

本质上是内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联。因为管道是单向流动的,因此实现进程之间的通信必须使用两个管道,管道是用环形队列实现的,数据从写端流入读端流出,这就实现父子进程之间的通信

301 |

2.有名管道

302 |

匿名管道由于没有名字,只能用于父子进程之间的通信,因此出现有名管道

303 |

3.消息队列

304 |

消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构,但消息队列只能用于数据量较少的场景,因为数据量较大,使用消息队列就会造成频繁的系统调用,也就是需要消耗更多的时间以便内核介入

305 |

4.信号

306 |

信号是进程通信机制中唯一的异步通信机制,它可以在任何时候发送信号给某个进程,通过发送指定信号来通知进程某个异步事件的发送以迫使进程执行信号处理程序。信号处理完毕后,被中断的进程将恢复执行

307 |

5.信号量

308 |

信号量是一个特殊的变量,它的本质是计数器,记录了临界资源的数目,用于协调进程对共享资源的访问,让临界区同一时间只有一个进程访问它,常与共享内存配合使用

309 |

6.共享内存

310 |

两个不同进程的逻辑地址通过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存

311 |

7.socket

312 |

socket本质上是一个编程接口,是应用层与TCP/IP协议族通信的中间软件抽象层,它对TCP/IP进行了封装,把复杂的TCP/IP协议族隐藏在Socket接口后面。常用于跨主机通信

313 |

同一台主机,使用共享内存最快,因为直接进行读写,一个进程写入共享内存,另一个进程就可以直接读到

314 |
315 | 316 | ## 36.阻塞io模型,非阻塞io模型,io复用模型,信号驱动io模型,异步io模型,同步io,异步io 317 |
318 | 答案 319 |

1.阻塞io模型:就是当发生io系统调用时,进程被阻塞,转到内核空间处理,直到整个io处理完毕才返回

320 |

2.非阻塞io模型:就是当发生io系统调用时,如果数据还没准备好,则直接返回,不阻塞进程。如果数据准备好了,则将数据从内核拷贝到用户空间,然后返回。

321 |

3.io复用模型:进程将需要监听的socket传递给select,然后阻塞在select上,当有socket就绪时,就会返回。然后通知进程进行IO系统调用将数据从内核拷贝到用户空间,然后返回

322 |

4.信号驱动io模型:当进程发起一个io操作时,会向内核注册一个信号处理函数,然后进程返回,不阻塞。然后当数据准备好时,发送信号给进程,然后进程通过信号处理函数,发起IO系统调用将数据从内核拷贝到用户空间

323 |

5.异步io模型:当进程发起一个io操作时,直接返回,不会阻塞,然后当数据准备好,并拷贝到用户空间后,通知进程,可以获取数据了

324 |

6.同步io:是指发起io调用时,会阻塞进程,直到io操作完成

325 |

7.异步io:是指发起io调用时到io操作完成都是异步的,不会阻塞进程

326 |
327 | 328 | ## 37.一个进程有什么数据段? 329 |
330 | 答案 331 |

代码段,数据段,栈区,堆区

332 |
333 | 334 | ## 38.同一个线程有哪些段可以共享的? 335 |
336 | 答案 337 |

可以共享代码段,数据段

338 |
339 | 340 | ## 39.栈区和堆区有什么区别? 341 |
342 | 答案 343 |

1.栈区的内存分配和释放是自动进行的。堆区的内存分配和释放是由程序员通过代码显示进行的

344 |

2.栈区主要存储函数参数,局部变量。堆区存储程序运行过程中动态分配的内存

345 |

3.栈区的内存比堆区小的多

346 |

4.栈区内存分配和释放的速度非常快,但空间有限。堆区的内存分配和释放比较慢,且容易产生碎片,但可以提供较大的存储空间

347 |
348 | 349 | ## 40.使用共享内存要注意什么问题? 350 |
351 | 答案 352 |

1.多进程访问共享内存,必须使用适当的同步机制(如互斥锁,信号量等)来协调这些进程的访问,防止数据不一致和竞态条件

353 |

2.共享内存区域不会自动回收,使用完毕后必须显示回收,以避免内存泄漏

354 |

3.确保使用共享内存之前已经被初始化,避免不确定的行为

355 |
356 | 357 | ## 41.CPU如何读写磁盘 358 |
359 | 答案 360 |

1.读取:CPU向DMA发起IO请求,然后CPU去做其他工作,DMA负责向磁盘发起IO操作,但数据放入磁盘缓冲区后,通知DMA控制器,DMA控制器将数据冲磁盘缓冲区拷贝到内核缓冲区,然后向CPU发送数据读完信号,CPU再将内核缓冲区中的数据拷贝到用户缓冲区。

361 |

2.写入:

362 |
363 | 364 | ## 42.DMA的作用 365 |
366 | 答案 367 |

使得cpu不必将数据从磁盘缓冲区拷贝到内核缓冲区。这些都由DMA控制器完成

368 |
369 | 370 | ## 43.系统调用过程 371 |
372 | 答案 373 |

当用户程序执行到一个需要进行系统调用的节点的时候,该程序将对应的系统调用号,中断向量号等信息存入寄存器,然后调用ecall陷入内核态,切换保存上下文信息,接着触发硬件自动关闭中断(也可以软件实现关中断,但是用户就可以修改对应代码,不如硬件支持关中断安全,另一方面就是硬件直接支持的话速度更快,开销更少),此时有一个问题,用户态页表已经切换为了内核态页表,调用栈指针也切换为了内核态的,但是内核不知道下一条需要执行的内核页表指令是什么,这里的解决办法是内核虚拟内存和用户虚拟内存有一段相同的程序段tranpoline,切换到内核态后可以继续执行tranpoline,该程序段会调用usertrap(位于内核态负责调用用户需要的系统调用的一个中转程序),该程序会查看用户程序提前设置好的寄存器里的系统调用号等信息,去执行编写代码阶段提前注册好的中断处理程序,处理完成之后该程序负责开启中断(开启中断由软件层面实现)以继续接收中断,保存上下文信息,返回用户态,至此一次系统调用结束。

374 |
375 | 376 | ## 44.什么是IO多路复用? 377 |
378 | 答案 379 |

IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。

380 |
381 | 382 | ## 45.线程上下文 383 |
384 | 答案 385 |

栈,程序计数器,寄存器,线程优先级和调度信息等

386 |
387 | 388 | ## 46.线程调度的过程 389 |
390 | 答案 391 |

1.选择调度算法

392 |

2.确定优先级

393 |

3.创建和管理线程队列

394 |

4.上下文切换

395 |

5.执行线程

396 |
397 | 398 | ## 47.线程什么时候会阻塞 399 |
400 | 答案 401 |

1.线程休眠

402 |

2.等待获取锁

403 |

3.等待相关资源

404 |

4.读写数据

405 |

5.其他线程操作

406 |
407 | 408 | ## 48.死锁避免和死锁预防的方法 409 |
410 | 答案 411 |

死锁避免:

412 |

1.银行家算法

413 |

2.资源分配图算法

414 |

死锁预防:

415 |

1.确保至少有一种资源是可以共享的

416 |

2.进程一开始就申请所有需要的资源,或者在申请新资源时释放已占有的资源来实现。

417 |

3.通过让系统允许资源被剥夺并分配给其他进程

418 |

4.确保系统中的资源分配顺序是严格的,以便所有进程按顺序申请资源

419 |
420 | 421 | ## 49.多级页表的原理 422 |
423 | 答案 424 |

基于程序局部性原理,通过多级页表将页表分成多级,对于没有访问的页表,换出到磁盘,从而实现节省内存

425 |
426 | 427 | ## 50.公平锁和非公平锁 428 |
429 | 答案 430 |

公平锁:多个线程按照申请锁的顺序去获取锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁

431 |

优点:所有线程都能够得到锁,不会饿死在队列中 缺点:吞吐量会下降很多,除了第一个线程,其他线程都会阻塞,CPU唤醒阻塞线程的开销会很大

432 |

非公平锁:多个线程去获取锁时,会直接去尝试获取,获取不到,再进入等待队列,如果能获取到时,就直接获取到锁

433 |

优点:可以减少CPU唤醒线程的开销,提高吞吐量 缺点:可能导致队列中尾部线程饿死,一直拿不到锁

434 |
435 | 436 | ## 51.操作系统是如何执行程序的? 437 |
438 | 答案 439 |

首先操作系统会进行程序的装载,创建一个进程结构,包括栈,堆页表等,并进行初始化。此时代码还在磁盘中,在页表中只会记录代码在磁盘中的位置

440 |

然后进程会进入就绪状态,等待操作系统调度。

441 |

当进程被调度时,CPU会从程序入口取指令,但是这是一个虚拟地址,然后从页表中寻找,触发缺页中断,然后去磁盘中找,找到后会记录在页表中

442 |

当进程的时间片被用完的时候会被挂起,等待操作系统调度

443 |

当进程执行完成后,会将内存中的数据清理

444 |
445 | 446 | ## 52.为什么栈比堆快? 447 |
448 | 答案 449 |

1.栈是预分配内存的,因此在申请内存的时候几乎不需要时间。而堆是动态申请内存的。

450 |

2.栈的地址都是连续的,而堆不一定

451 |

3.cpu有专门的寄存器来操作栈,而堆是间接寻址的

452 |

4.栈是线程独享的,而堆是共享的,需要协同和同步

453 |

5.栈是在一级缓存中做的,而堆是在二级缓存中做的。

454 |
455 | 456 | ## 53.操作系统内存分配 457 |
458 | 答案 459 |

1.连续分配

460 |

固定分区分配:

461 |

由于没有根据需要的内存来分配分区大小,会产生很多的内部碎片

462 |

动态分区分配:

463 |

不会预先划分内存分区,动态的建立刚好合适的内存分区。即通过表记录空闲分区或空闲分区链

464 |

问题:会存在外部碎片,必须通过内存拼凑技术来解决,即将空闲的内存空间合并在一起,但是需要搬迁内存,开销较大

465 |

动态分区分配算法包括4种,首次适应算法,最佳适应算法,最小适应算法,邻近适应算法

466 |

首次适应算法即从低地址到高地址开始找,找到第一个符合条件的空闲分区

467 |

最佳适应算法即将空闲分区按容量从大到小排列,找到第一个符合条件的空闲分区,使得大内存进来能够有分区存放

468 |

问题:会产生越来越多小的,难以利用的内存块,即外部碎片

469 |

最大适应算法即将空闲分区按容量从小到大排列,找到第一个符合条件的空闲分区,避免剩余分区太小导致无法利用

470 |

问题:可能导致大内存分配不到

471 |

邻近适应算法即对首次适应算法的改进,将内存分配按地址递增排列构成一个循环链表,每次从上次查找完的位置开始,使得低地址和高地址都等概率的被使用,可能导致所有大分区都被分成小分区,从而无法分配大内存

472 |

2.非连续分配

473 |

分页存储管理:

474 |

将内存分成一个个页框,然后每个进程有一个页表,通过页表记录虚拟页号对应的物理页号,

475 |

分段存储管理:

476 |

段页存储管理:

477 |
478 | 479 | ## 54.malloc原理 480 |
481 | 答案 482 |

如果申请内存小于128K会采用brk,否则会采用mmap

483 |

brk本质上就是维护一个brk指针,表示已经使用的最后一个堆地址,当free的时候不会归还给操作系统而是放入malloc缓冲池

484 |

mmap本质上就是维护了一个空闲内存链表和一个红黑树,用于找到合适的内存,当free的时候会归还给操作系统

485 |

mmap每次都是新申请的一块内存,每次都会发生系统调用和缺页异常,因此性能很差0

486 |
487 | 488 | ## 55.cpu访问内存的全流程 489 |
490 | 答案 491 |

首先cpu拿到的是虚拟地址,首先会通过快表直接查询到物理地址,如果快表中没有,则从虚拟地址中获得虚拟页号和页内偏移。然后查询页表,得到物理页号,然后访问内存。

492 |

如果页表中没有,则触发缺页异常,会去物理内存中申请,首先会根据伙伴系统算法找到合适的物理内存,并将对应的信息写到页表和快表中

493 |
494 | 495 | ## 56.使用socket编程写一个tcp服务 496 |
497 | 答案 498 |

多线程模型:

499 |

对于一个tcp服务,首先调用socket接口创建一个socket,底层会生成一个文件描述符,用来表示该socket,然后调用bind绑定一个ip+端口,调用listen监听这个ip+端口的数据包,同时会在内核中创建半连接队列和全连接队列,accept函数会从全连接队列中取出已经完成三次握手的一个文件描述符,他是一个新创建socket。然后调用read读取数据,当读到eof的时候调用close关闭socket。同时为了实现支持多客户端,一般会使用多线程技术

500 |

io多路复用:

501 |

首先调用socket接口创建一个socket名为listenfd,底层会生成一个文件描述符,用来表示该socket,然后调用bind绑定一个ip+端口,调用listen监听这个ip+端口的数据包,同时会在内核中创建半连接队列和全连接队列,然后调用epoll_create接口获得一个epollfd,然后调用epoll_ctrl接口将listenfd注册到epollfd中,然后循环调用epoll_wait,当返回的时候会获得有读事件发生的fd列表,然后遍历这个列表,如果fd[i] == listenfd,说明是有新的连接建立了,则调用accept接口从全连接队列取出一个clientfd,然后将其注册到epollfd中去,如果!=listenfd,如果时间是读事件则调用recv读取数据,并调用write写数据

502 |

socket的send接口本质上是将数据拷贝到发送缓冲区(内存中的一块区域),然后调用操作系统的发送接口,将其发送给网卡,再传递到服务端网卡

503 |

socket的recv接口本质上是调用操作系统的接收接口,由操作系统通过网卡接收数据,将其拷贝到接收缓冲区,应用程序再从接收缓冲区读取数据

504 |

socket的close接口本质上是将socket的引用计数-1,当引用计数变成0时才会彻底关闭socket。即对于读方向将socket设置为不可读,对写方向,将发送缓冲区的数据发送给对端并进行四次挥手

505 |

socket的shutdown接口可以选择关闭读方向或写方向或都关闭,对于关闭读方向会将将接收缓冲区的数据丢弃,再有数据来会回复ACK并丢弃,调用read会读到EOF。对于关闭写方向会将发送缓冲区的数据发给对端并进行四次挥手

506 |
507 | 508 | ## 55.伙伴系统算法 509 |
510 | 答案 511 |

伙伴系统算法是为了解决连续内存分配的问题

512 |

将一个个页框分为大小为2^0,2^1,,,2^10个连续页框的内存块,分别放在不同的链表中,当分配一块物理内存时,假设需要x个页框,则找到最小的i满足2^i>=x并且对应的链表不为空,如果x>=2^{i-1},则将该内存块一页为而,直到不满足条件为止,将其中最小的>=x的内存块返回,其他的放回对应链表的尾部。当回还内存时,判断对应链表中是否有同属于一个内存块,如果有的并且相邻则将其合并

513 |

初始时只有x个大小为2^10的内存块,x为物理内存大小/(2^10*4KB)

514 |

由于伙伴系统算法只能处理2幂次的内存,否则会有大量浪费,因此有了slab对象,slab的内存从伙伴系统算法中获得,然后进行划分成非常多的小内存块进行管理

515 |
516 | 517 | ## 56.进程上下文切换完成流程 518 |
519 | 答案 520 |

上下文切换的时机:当进程的时间片用完会触发时钟中断或通过系统调用触发

521 |

首先会进入内核态,然后关闭自动中断,保存PCB进程控制块,进程地址空间(),页表,内核栈,寄存器等,这些信息都会保存到内核栈中,然后PCB会包含内核栈的指针,然后加载另一个进程的PCB,通过PCB找到内核栈,将其加载出来,并开启自动中断,进入用户态

522 |
523 | 524 | ## 57.文件系统 525 |
526 | 答案 527 |

一个文件由目录项(用于记录文件名,索引节点指针,目录层级关系)和索引节点(inode编号,文件大小,访问权限,数据在磁盘中的位置等等)

528 |

索引节点指向索引节点块,然后索引节点块又会指向下一级索引节点块,直到最后一级才指向数据块,这样查询效率高,易于文件增删,缺点是存储开销更大

529 |

对于磁盘空间的管理对磁盘空间进行分块,采用非连续分配算法,使用位图标识一个块是否被使用,而一个块能只能表示128MB的大小,因为需要分成N个块组,每个块组包含数据位图,多个数据块,inode位图,inode列表等等

530 |

软链接就是指通过新增inode节点,然后对应的数据块记录的是另一个文件的路径。硬链接就是通过目录项指向同一个inode节点

531 |
532 | 533 | ## 58.reactor模式 534 |
535 | 答案 536 |

reactor模式包括单reactor单线程,单reactor多线程,多reactor多线程

537 |

单reactor单线程就是io请求来的时候,分发器根据事件类型分类,如果是新连接建立则将其分发给acceptor处理,将其注册到epoll中,如果是读事件则分发给handler处理,进行read,业务处理,send

538 |

单reactor多线程就是在单线程的基础上使用多个线程处理读事件,并使用线程池进行复用

539 |

多reactor多线程就是main_reactor只负责处理新连接建立,accept取出连接后发给sub_reactor,sub_reactor将其注册到自己的epoll中,然后当事件发生时进行处理

540 |
541 | 542 | 543 | ## 59.操作系统堆和栈的底层原理 544 |
545 | 答案 546 |

堆内存的分配包括brk和mmap。brk本质上就是推brk指针,先进后出,因此会产生内存碎片,原理和栈一样,一般用于内存<128K的时候使用。mmap本质上是空闲块链表实现,每次free后会归还给操作系统

547 |

栈内存分配是通过维护esp指针(栈顶)和ebp指针(栈底),每次函数调用会先保存ebp指针,用于回溯,然后更新ebp到esp的位置,进行函数的调用

548 |
-------------------------------------------------------------------------------- /docs/handbook/算法与数据结构.md: -------------------------------------------------------------------------------- 1 | # 算法与数据结构 2 | 3 | ## 1.快速排序的复杂度?极端情况?如何优化? 4 |
5 | 答案 6 |

平均nlogn,最坏n^2,极端情况就是每次选到的都是最大值或最小值,采用三路排序

7 |
8 | 9 | 10 | ## 2.说几种排序算法?稳定性? 11 |
12 | 答案 13 |

14 |
15 | -------------------------------------------------------------------------------- /docs/handbook/计算机网络.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 计算机网络 4 | 5 | ## 1.TCP/IP,OSI 网络模型有哪几层 6 |
7 | 答案 8 |

TCP/IP包括4层,应用层,传输层,网络层,网络接口层。OSI网络模型包括7层物理层,数据链路层,网络层,传输层,会话层,表示层,应用层

9 |

应用层负责通过网络接口直接为用户提供服务

10 |

应用层包括HTTP,DNS,FTP,SMTP,DHCP

11 |

HTTP常用于web浏览器和服务端传输数据,基于TCP实现

12 |

DNS常用于域名解析IP地址,当数据大小超过512字节时采用TCP协议,否则采用UDP协议

13 |

FTP通常用于主机之间传输文件,基于TCP实现

14 |

SMTP通常用于邮件发送,SMTP没有权限验证并且是明文传输,而ESMTP支持权限验证和加密传输,基于TCP实现

15 |

DHCP协议通常用于动态分配ip地址,基于UDP实现

16 |

表示层负责数据处理(编码解码,加密解密,压缩解压缩)

17 |

会话层负责管理应用程序之间的会话

18 |

传输层负责进程之间的通信服务,即实现复用连接(虚电路)和分用(分发给不同的进程),差错控制,流量控制,连接建立和释放,可靠传输管理(以报文段为单位)

19 |

网络层负责把分组从源节点转发到目的节点,即实现路由选择,分组转发,拥塞控制,网际互联,差错控制,流量控制(以分组为单位),连接建立和释放(虚电路),可靠传输管理(接收方收到分组需要返回确认消息)

20 |

网络接口层是数据链路层和物理层的合并

21 |

数据链路层负责确保相邻节点之间的链路逻辑上无差错,即实现差错控制和流量控制(以帧为单位)

22 |

物理层负责在相邻节点传输透明比特流

23 |
24 | 25 | ## 2.网络分层的原因 26 |
27 | 答案 28 |

1.各层之间相互独立:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了

29 |

2.提高了整体灵活性:每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。

30 |

3.分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。

31 |
32 | 33 | ## 3.从输入 URL 到页面展示到底发生了什么? 34 |
35 | 答案 36 |

首先会先解析URL,然后根据域名通过DNS解析协议解析出IP地址,DNS解析会先发送给本地域名服务器,本地域名服务器会查看本地的缓存列表,如果没有就向根域名服务器发起DNS解析请求,根域名服务器不负责解析,根域名服务器会告诉本地域名服务器负责该域名的顶级域名服务器的IP地址,然后本地域名服务器向该顶级域名服务器发起DNS解析请求,顶级域名服务器会告诉本地域名服务器负责该域名的权威域名服务器的地址,然后本地域名服务器会向该权威域名服务器发起DNS解析请求,权威域名服务器会告诉本地域名服务器该域名的IP地址。拿到该IP地址后,封装成HTTP请求报文,交付给传输层,到了传输层后,要先三次握手建立连接,建立好连接后,会加上TCP首部然后交付给网络层,网络层根据分组转发和确定分组从源到目的经过的路径,然后添加上IP首部,交付给网络接口层,网络接口层要确定数据报要发给谁,需要通过ARP协议获取目标服务器的MAC地址,ARP协议会在以太网中广播,请求获取下一步要发给的MAC地址,获取到后,将对应的MAC地址缓存下来,然后给数据报添加帧头和帧尾,然后通过网卡,交换机,路由器最终到达目标服务器

37 |
38 | 39 | ## 4.HTTP是什么? 40 |
41 | 答案 42 |

超文本传输协议

43 |
44 | 45 | ## 5.HTTP常见状态码 46 |
47 | 答案 48 |

49 | 200请求成功一切正常,响应头包含body数据,204也是请求成功,但响应头没有body数据,206用于HTTP分块下载或断点续传,表示响应返回的body数据并不是资源的全部,而是其中的一部分 50 |

51 |

52 | 301表示永久重定向,表明请求的资源已经不存在了,302表示临时重定向,请求的资源还在,但暂时需要使用另外的url来访问,304表示资源未修改,用于告诉客户端可以继续使用缓存资源 53 |

54 |

55 | 400表示客户端请求的报文有误,401表示服务器鉴权失败,403表示权限不足,禁止访问,404表示请求的资源在服务器不存在或未找到 56 |

57 |

58 | 500表示服务器内部错误,501表示客户端请求的功能还不支持,502表示网关错误,503表示服务器正忙,无法响应客户端 59 |

60 |
61 | 62 | ## 6.GET 和 POST 有什么区别? 63 |
64 | 答案 65 |

1.用途不同:Get是用来从服务器上获得数据,而 Post是用来向服务器上传递数据。

66 |

2.Get是不安全的,因为Get会将请求参数放在URL中,而Post则不会

67 |

3.Get能够传输的数据量,因为Get会受到url长度的限制,而Post则不会

68 |

4.Get能被缓存而Post则不能被缓存

69 |

5.Post首部字段更多,所以Post会更慢

70 |
71 | 72 | ## 7.HTTP 和 HTTPS 有什么区别? 73 |
74 | 答案 75 |

1.HTTP是明文传输,HTTPS则在TCP和HTTP网络层之间加入了SSL/TLS安全协议,使得报文能够加密传输

76 |

2.默认端口不同,HTTP默认端口是80,HTTPS默认端口是443

77 |

3.HTTP连接建立更加简单,HTTPS还需要进行SSL/TLS握手,才能进行加密传输

78 |
79 | 80 | ## 8.HTTP1.0和HTTP1.1的区别 81 |
82 | 答案 83 |

1.HTTP1.0为短连接,HTTP1.1支持长连接

84 |

2.HTTP1.1增加了大量的状态响应码

85 |

3.HTTP1.1引入了更多的缓存控制策略,提供了更多可选择的缓存头

86 |

4.HTTP1.1支持管道网络传输,只要第一个请求发送出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间

87 |
88 | 89 | ## 9.HTTP1.1的缺陷 90 |
91 | 答案 92 |

1.发送冗长的首部。每次互相发送相同的首部造成的浪费较多

93 |

2.服务器是按请求的顺序发送响应的,如果服务器响应慢,会导致客户端一直请求不到数据,即可后面的请求已经处理好了,也无法发出响应,也就是队头阻塞

94 |

3.请求只能从客户端开始,服务器只能被动响应

95 |
96 | 97 | ## 10.HTTP2.0和HTTP1.1的区别 98 |
99 | 答案 100 |

1.HTTP2.0采用了头部压缩,对于头部中相同的部分使用一个索引号代替,使用HPACK算法维护了一张头信息表

101 |

2.HTTP2.0不再采用纯文本的形式,而是采用二进制的形式,增加了传输效率

102 |

3.HTTP2.0引入了stream的概念,一个TCP连接包含多个stream,一个stream包含一个或多个Message,一个Message包含一个或多个Frame,Fream是HTTP2.0的最小单位,不同stream可以乱序发送

103 |

4.HTTP2.0改善了请求-响应模式,客户端和服务器都可以建立stream,服务器可以给客户端推送数据

104 |
105 | 106 | ## 11.HTTP2.0的缺陷 107 |
108 | 答案 109 |

1.HTTP2.0虽然采用了stream模式,但是还是存在队头阻塞,因为stream是基于TCP的,后面的stream即使到达了,应用层也无法读取,需要按序读取stream

110 |
111 | 112 | ## 12.HTTP3.0和HTTP2.0的区别 113 |
114 | 答案 115 |

1.HTTP3.0实现了无队头阻塞,当某个流发生丢包时,只会阻塞这个流,其他流不会受影响

116 |

2.HTTP3.0采用了QUIC协议,能够更快的建立连接,握手过程只需要1个RTT

117 |

3.HTTP3.0实现了连接迁移,因为QUIC采用的并不是四元组来确定一条连接,而是通过连接ID来标记通信的两个端点,即使网络连接发生变化,上下文信息不变,可以无缝的复用原连接

118 |
119 | 120 | ## 13.什么是强制缓存和协商缓存 121 |
122 | 答案 123 |

强制缓存是指只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,通过cache-control和expires实现,cache-control表示过期的相对时间,expires表示过期的绝对时间

124 |

协商缓存是指与服务端协商之后,通过协商结果来判断是否使用本地缓存。

125 |

协商缓存有两种模式,last-modified模式,每次返回一个上一次修改的时间,然后请求的时候附带过去,服务器验证该时间是否是最新时间,如果不是则返回新的数据

126 |

last-modified的问题:1.文件重新保存,内容没变,时间戳会重置。2.文件保存较快,1s内完成,时间戳是以秒为单位,因此更新时间没变

127 |

etag模式:根据数据计算出一个哈希值,然后请求的时候附带过去,服务器验证该数据是否是最新的

128 |

etag的问题:1.服务器计算etag需要额外的时间,如果文件较大或改动频繁会影响服务器的性能。2.etag分为强验证和弱验证,弱验证根据数据的部分属性值生成,生成速度快,但可能没那么准确

129 |
130 | 131 | ## 14.TCP是什么? 132 |
133 | 答案 134 |

TCP是面向连接,可靠的,基于字节流的传输层通信协议

135 |

面向连接是指在进行数据传输之前必须建立连接

136 |

可靠的是指TCP能保证数据能够完整,准确的从发送方传输到接收方

137 |

基于字节流是指TCP传输的数据是一连串的无结构的字节流

138 |
139 | 140 | ## 15.有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少? 141 |
142 | 答案 143 |

客户端IP数 * 客户端端口号=2^32*2^16.这是理论最大限制,但是会受到服务器内存限制,文件描述符限制

144 |
145 | 146 | ## 16.UDP 和 TCP 有什么区别呢? 147 |
148 | 答案 149 |

1.连接:TCP是面向连接的,传输数据之前要建立连接。UDP是不需要连接的,即立刻传输数据

150 |

2.服务对象:TCP是一对一的两点服务,UDP支持一对一,一对多,多对多的交互通信

151 |

3.可靠性:TCP是可靠交付数据的,数据可以无差错,不丢失,不重复,按序到达。而UDP是尽最大努力交付的,不保证可靠交付数据

152 |

4.拥塞控制、流量控制:TCP有拥塞控制和流量控制机制,保证数据传输的安全性。UDP则没有,即可网络非常拥挤了也不会影响UDP的发送速率

153 |

5.首部开销:TCP首部较长,会有一定的开销UDP首部只有8个字节,开销较小

154 |

6.传输方式:TCP是流式传输,没有边界,但保证顺序和可靠。UDP是一个包一个包的传输,是有边界的,但可能丢包和失序

155 |
156 | 157 | ## 17.TCP和UDP的应用场景 158 |
159 | 答案 160 |

TCP通常用于HTTPS/HTTP,FTP,SSH等场景

161 |

UDP通常用于DNS,广播通信,视频,音频等多媒体通信

162 |
163 | 164 | ## 18.TCP 和 UDP 可以使用同一个端口吗? 165 |
166 | 答案 167 |

可以,在内核中TCP和UDP是两个完全独立的模块

168 |
169 | 170 | ## 19.TCP 三次握手过程 171 |
172 | 答案 173 |

首先客户端和服务端都处于close状态,服务端会主动监听某个端口,处于listen状态,客户端会初始化一个随机序列号,将该序列号置于TCP首部的序号字段中,同时将SYN标志位设置为1,表示SYN报文,然后发送给服务端,之后客户端进入syn-sent状态。

174 |

服务端收到SYN报文后,也初始化一个随机序列号,将该序列号置于TCP首部的序号字段中,将TCP首部的确认号字段的值设置为SYN报文TCP首部中序号的值+1,同时将SYN和ACK标志位设置为1,发送给客户端,之后服务端进入syn-rcvd

175 |

客户端收到服务端的报文后,还需要发送一个确认报文,该确认报文TCP首部确认号字段的值设置为服务端报文TCP首部的序列号+1,同时将ACK标志位设置为1,发送给服务端,之后客户端进入ESTABLISHED状态,服务端收到客户端的应答报文后,也进入ESTABLISHED状态

176 |
177 | 178 | ## 20.TCP为什么要三次握手? 179 |
180 | 答案 181 |

1.阻止重复历史连接的初始化

182 |

2.同步双方的初始序列号

183 |

3.能够减少资源开销(因为如果采用两次握手时,如果SYN报文阻塞,然后客户端重发SYN报文会导致建立多个无用的连接,浪费资源)

184 |
185 | 186 | ## 21.TCP为什么要随机生成初始序列号 187 |
188 | 答案 189 |

1.为了防止历史报文被下一个相同的四元组接收

190 |

2.防止黑客伪造相同序列号的TCP报文被对方接收

191 |
192 | 193 | ## 22.在IP层会分片,为什么TCP层还需要MSS呢? 194 |
195 | 答案 196 |

因为如果在IP层进行分片的话,由于IP层没有超时重传机制,所以会导致一个分片丢失,全部分片重传

197 |
198 | 199 | ## 23.什么是SYN攻击?如何避免SYN攻击 200 |
201 | 答案 202 |

通过短时间内伪造不同ip地址的syn报文,从而使得服务端的半连接队列被占满,使得客户端不能为正常用户提供服务

203 |

1.增大半连接队列

204 |

2.减少SYN-ACK重传次数

205 |

3.启用syncookies

206 |

4.调大netdev_max_backlog参数

207 |
208 | 209 | ## 24.TCP四报文挥手 210 |
211 | 答案 212 |

首先客户端打算关闭连接,会发送一个FIN报文,发送给服务端,之后客户端进入time_wait_1状态。

213 |

服务端收到该报文后向客户端发送ACK应答报文,之后服务端进入close_wait状态。

214 |

客户端收到服务端的ACK应答报文后,进入time_wait_2状态

215 |

等待服务端处理完数据后,向客户端发送FIN报文,之后进入last_ack状态。

216 |

客户端收到服务端的FIN报文后,发送一个ACK应答报文给服务端,然后进入time_wait状态。服务端接收到客户端的ACK应答报文后,进入close状态,自此服务端的连接已经关闭。等待2MSL后,客户端也自动进入close状态,自此客户端的连接已经关闭

217 |
218 | 219 | ## 25.为什么要四报文挥手,而不是三报文 220 |
221 | 答案 222 |

因为服务端在收到FIN报文时,可能还有需要发送和处理的数据,需要等待服务端处理和发送完数据后才会发送FIN报文给客户端表示同意关闭连接

223 |
224 | 225 | ## 26.为什么TIME_WAIT是2MSL 226 |
227 | 答案 228 |

MSL是最大报文生存时间,等待2MSL可以保证第四次挥手报文被服务端接收,如果没有接收,在2MSL之前也会收到服务端重传的FIN报文,然后重新等待2MSL

229 |
230 | 231 | ## 27.为什么需要 TIME_WAIT 状态? 232 |
233 | 答案 234 |

1.防止历史连接的数据,被后面相同四元组的连接错误的接收

235 |

2.保证被动关闭的一方,能被正确的关闭

236 |
237 | 238 | ## 28.TIME_WAIT过多有什么危害? 239 |
240 | 答案 241 |

1.占用系统资源

242 |

2.占用端口资源

243 |
244 | 245 | ## 29.大量TIME_WAIT的原因 246 |
247 | 答案 248 |

1.HTTP没有启用长连接

249 |

2.HTTP长连接超时(长连接尝试导致连接被关闭)

250 |

3.HTTP长连接的请求数量达到上限(因为达到上限后,会主动关闭连接)

251 |
252 | 253 | ## 30.大量CLOSE_WAIT的原因 254 |
255 | 答案 256 |

1.没有将服务端socket注册到epoll(因为没有注册到epoll中,所以也无法close)

257 |

2.新连接到来时,没有调用accept获取该连接的socket(因为没有将其取出,所以也无法close)

258 |

3.accept获取已连接的socket后,没有将其注册到epoll(因为没有注册到epoll,所以也无法close)

259 |

4.客户端关闭连接后,服务端没有调用close函数

260 |
261 | 262 | ## 31.如果已经建立了连接,但是客户端突然出现故障了怎么办? 263 |
264 | 答案 265 |

TCP通过保活机制会每隔一段时间发送探测报文,如果连续几个探测报文都没有得到相应,则会认为该TCP连接已经死亡。如果对端主机正常,将会重置保活时间,如果对端主机宕机,发送几次探测报文后,会报告该TCP连接已经死亡。如果对端主机宕机并重启,对端会产生一个RST报文,重置该连接

266 |
267 | 268 | ## 32.如果已经建立了连接,但是进程崩溃了怎么办? 269 |
270 | 答案 271 |

当进程崩溃后,操作系统内核会自己完成四次挥手

272 |
273 | 274 | ## 33.socket和TCP的关系 275 |
276 | 答案 277 |

在三次握手中,服务端和客户端首先初始化socket,然后服务端调用bind,listen,然后调用accept阻塞。然后客户端调用connect,此时会进行三次握手,客户端收到第二次握手后,connect会返回。服务端收到第三次握手后会将连接的socket放入accept队列,然后此时accept函数返回一个已连接的socket。然后调用read读数据阻塞,客户端调用write写数据,当读到EOF时,调用close

278 |

在四报文挥手时,首先客户端调用close,然后第一次挥手会将EOF写入接收缓冲区,然后当read读到EOF调用close进行第三次挥手

279 |
280 | 281 | ## 34.没有 listen,能建立 TCP 连接吗 282 |
283 | 答案 284 |

可以,只要两个客户端同时调用connect即可

285 |
286 | 287 | ## 35.没有 accept,能建立TCP连接吗 288 |
289 | 答案 290 |

可以,accept只是从accept队列中取出一个socket,并不参与三次握手

291 |
292 | 293 | ## 36.TCP的重传机制 294 |
295 | 答案 296 |

1.超时重传:发送数据时会设置一个定时器,当超过指定时间后,没有收到对方的ACK确认应答报文,就会重发该数据。

297 |

2.快速重传:当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

298 |

3.SACK:通过在TCP首部的选项字段添加SACK,将已收到的数据的信息发送给发送方,使得发送方可以只重传丢失的数据

299 |

4.D-SACK:通过使用SACK告诉发送方那些数据被重复接收了。可以让发送方知道是发送出去的数据包丢了,还是接收方回应的ACK丢了

300 |
301 | 302 | ## 37.TCP的滑动窗口 303 | 304 |
305 | 答案 306 |

滑动窗口分为发送窗口和接收窗口,发送窗口通过记录已发送但未收到ACK应答的第一个字节的位置,待发送但还接收方还可以处理的第一个字节的位置,未发送并且接收方不可以处理的第一个字节的位置

307 |

接收窗口通过记录未接收但还可以接收的第一个字节的位置,未接收并且不可以接收的第一个字节的位置

308 |

发送窗口的大小约等于接收窗口的大小,因为大小同步是有时延的

309 |
310 | 311 | ## 38.TCP流量控制的原理 312 |
313 | 答案 314 |

315 | TCP流量控制通过滑动窗口和调整TCP报文的大小来实现。 316 | 通过滑动窗口协调发送窗口和接收窗口的大小来使得发送方的发送速率不要太快,要让接收方来得及接收 317 | 因为TCP首部就有20个字节,所以需要避免发送太小的报文。所以通过控制TCP报文的大小,达到一定大小才能发送 318 |

319 |
320 | 321 | ## 39.TCP的拥塞控制 322 |
323 | 答案 324 |

通过维护一个拥塞窗口的大小来避免发送方填满整个网络

325 |

慢启动算法,刚建立TCP连接的时候,每收到一个ACK,拥塞窗口的大小就翻倍,当到达慢启动门限时,就会启用拥塞避免算法

326 |

拥塞避免算法: 当拥塞窗口超过慢启动门限时,每收到一个ACK,拥塞窗口就增加1/cwnd(拥塞窗口大小)

327 |

拥塞发生算法:当遇到超时重传的时候 ,拥塞窗口大小会设置为1,慢启动门限会设置为原来的一半,当遇到快速重传(即三个重复的ACK),拥塞窗口会设置为原来的一半,慢启动门限设置为原来拥塞窗口的一半(ssthresh = cwnd = 1/2 cwnd)

328 |

快速恢复算法: 当遇到快速重传时,会将拥塞窗口设置为原来的一半再加3,慢启动门限设置为原来拥塞窗口的一半,3表示已经确认接收到3个重复的ACK,如果再收到重复的ACK,拥塞窗口+1,如果收到新的数据则将拥塞窗口的大小设置为慢启动门限

329 |
330 | 331 | ## 40.为什么有了流量控制,还要拥塞控制? 332 |
333 | 答案 334 |

流量控制是为了避免发送方的数据填满接收方的缓存

335 |

拥塞控制是为了避免发送方的数据填满整个网络

336 |
337 | 338 | ## 41.TCP的全连接队列和半连接队列 339 |
340 | 答案 341 |

服务端收到客户端的SYN请求后,内核会将该连接存储到半连接队列中,并响应SYN+ACK,客户端会返回ACK,当服务器收到第三次握手的ACK时,内核会将该连接从半连接队列中移除,然后创建新的完全的连接,将其添加到全连接队列,等待调用accept时将其取出

342 |
343 | 344 | ## 42.当全连接队列和半连接队列满了会怎么样? 345 |
346 | 答案 347 |

当全连接队列满了后,会将后续的请求丢弃,或者回复RST报文

348 |

当半连接队列满了后,如果没有开启syncookies,将会丢弃,如果全连接队列满了并且需要重传SYN+ACK的包多余一个,也会将其丢弃,如果没有开启syncookies并且当前半连接队列的长度超过max_syn_backlog的3/4

349 |
350 | 351 | ## 43.半连接队列和全连接队列的最大长度? 352 |
353 | 答案 354 |

全连接队列的最大长度为min(somaxconn,max_syn_backlog)

355 |

半连接队列的理论最大长度为全连接队列最大长度和max_syn_backlog最小值的两倍

356 |

实际的最大长度还会受到一个条件的限制,还要与max_syn_backlog的3/4取min

357 |
358 | 359 | ## 44.什么情况下SYN包会被丢弃 360 |
361 | 答案 362 |

1.NAT网络下,开启tcp_tw_recycle参数(因为NAT下不同的主机,会被看作相同的IP地址,因为开启了recycle+timestamp他并不会对四元组做检查,而是对IP地址做检查,就会使得该SYN包被丢弃)

363 |

2.半连接队列满了并且没有开启syncookies,全连接队列满了

364 |
365 | 366 | ## 45.已经建立连接的TCP,收到一个SYN包会怎么办? 367 |
368 | 答案 369 |

1.如果这个SYN包的端口号和历史连接的端口号不同,则会被认为是建立一个新的连接,然后旧的连接,服务端会启用tcp保活机制,最后会将其释放掉

370 |

2.如果这个SYN包的端口号和历史连接的端口号相同,因为这个序列号是随机的,所以服务端会回复一个正确的序列号和ACK,客户端收到发现不是自己期望的ACK,会回一个RST报文,从而释放该连接

371 |
372 | 373 | ## 46.TCP如何处理乱序的数据? 374 |
375 | 答案 376 |

当收到乱序的数据时,会将其加入到乱序队列中,然后当延迟的数据到达后,会检查乱序队列中是否有能够接上的数据,如果有,判断是否带有FIN标志,如果有,则将其进行处理

377 |
378 | 379 | ## 47.处于TIME_WAIT状态时,收到SYN会发生什么 380 |
381 | 答案 382 |

首先会判断是否为合法的SYN,如果是,则会跳过TIME_WAIT状态,直接进入sync_recv状态,如果是不合法的SYN,则会回一个之前的ACK,对方收到后,收到的不是自己期望的ACK,则会发送RST报文

383 |
384 | 385 | ## 48.处于TIME_WAIT状态时,收到RST报文会直接关闭吗? 386 |
387 | 答案 388 |

取决于net.ipv4.tcp_rfc1337参数,如果设置为0,则提前结束TIME_WAIT状态,释放连接,如果设置为1,则会丢弃该RST报文

389 |
390 | 391 | ## 49.TCP连接中遇到主机崩溃和进程崩溃会怎么样? 392 |
393 | 答案 394 |

如果进程崩溃,最终内核会完成四次挥手

395 |

如果主机崩溃,如果没有数据交换,如果没有开启keepalive,那么服务端将会一直等待,如果开启了keepalive,再探测到对方已经崩溃后将会关闭该连接。如果有数据交换,如果主机崩溃后迅速重启,将会回复RST报文,关闭该连接。如果主机崩溃后没有重启,服务端重传超过一定次数后,最终也会关闭该连接

396 |
397 | 398 | ## 50.为什么tcp_tw_reuse是默认关闭的? 399 |
400 | 答案 401 |

开启tcp_tw_reuse就必须开启timestamp,然而这样会导致收到历史报文时,如果是RST报文,不会被丢弃

402 |

被动关闭连接的一方被不正常的关闭

403 |
404 | 405 | ## 51.TCP握手和TLS握手能同时进行吗? 406 |
407 | 答案 408 |

可以,但是必须保证客户端和服务端双方启用tcp fast open并且tls版本为1.3,并且之前已经建立连接

409 |
410 | 411 | ## 52.HTTP的keep-alive和TCP的keepalive是同一个东西嘛 412 |
413 | 答案 414 |

HTTP的keep-alive是应用层实现的,是HTTP的长连接

415 |

TCP的keepalive是传输层实现的,是TCP的保活机制

416 |
417 | 418 | ## 53.TCP有什么缺陷? 419 |
420 | 答案 421 |

1.TCP是在内核实现的,应用程序只能使用而不能修改,因为如果想要升级TCP协议,就只能升级内核,所以使得TCP的升级工作很困难

422 |

2.TCP连接建立的延迟,基于应用层的协议,都需要先进行三次握手才能传输数据,大多数使用HTTPS的,在建立TCP连接后,还需要进行TLS握手,增大了数据传输的延迟

423 |

3.TCP存在队头阻塞的问题,因为TCP是字节流协议,TCP层必须保证收到的字节数据是有序的,后面的字节数据即使收到了也无法从内核中读取数据

424 |

4.网络迁移需要重新建立连接:因为TCP是根据四元组来确定一条TCP连接的,所以一旦网络发生变化,IP地址就会发生变化,就需要重新建立连接

425 |
426 | 427 | ## 54.如何基于UDP实现可靠传输 428 |
429 | 答案 430 |

基于UDP实现的可靠传输已经有了-QUIC协议

431 |

1.通过Package Number 和Stream ID,Offset,可以支持乱序确认而不影响数据包的正确组装

432 |

2.Stream级别的流量控制,每个stream有独立的接收窗口,即使一个窗口无法移动,也不会影响其他的Stream

433 |

3.Connection级别的流量控制:他的接收窗口是所有stream之和

434 |

4.QUIC对TCP的拥塞控制进行了改进,因为QUIC是处于应用层的,所以不同应用程序可以设置不同的拥塞控制算法

435 |

5.QUIC支持更快的连接建立,通过连接ID来标识一个连接,即使网络发生变化,只要上下文信息还在,就可以复用原来的连接

436 |

6.QUIC还解决了TCP队头阻塞的问题。QUIC为每个stream都分配了一个滑动窗口,使得多个stream之间没有依赖关系,相互独立

437 |

7.QUIC建立连接的速度更快,QUIC内部包含了TLS,并且使用的是TLS1.3,QUIC自己的帧中就包含了TLS的记录,从而使得建立连接只需要1RTT,第二次连接只需要0RTT

438 |
439 | 440 | ## 55.QUIC是如何解决TCP队头阻塞问题的? 441 |
442 | 答案 443 |

QUIC对每个不同的stream都有一个滑动窗口,各个stream之间没有依赖关系,相互独立

444 |
445 | 446 | ## 56.QUIC是如何迁移连接的? 447 |
448 | 答案 449 |

450 | 使用连接ID来标记通信的两个端点,从而使得网络发生变化时,只需要仍保有上下文信息,就可以复用原来的连接 451 |

452 |
453 | 454 | ## 57.客户端的端口可以重复使用吗? 455 |
456 | 答案 457 |

只要不是相同的目的ip+目的端口就可以重复使用

458 |
459 | 460 | ## 58.如何解决客户端 TCP 连接 TIME_WAIT 过多,导致无法与同一个服务器建立连接的问题? 461 |
462 | 答案 463 |

开启net.ipv4.tcp_tw_reuse参数,对于相同的四元组,处于TIME_WAIT状态并且超过1秒,可以直接复用该连接

464 |
465 | 466 | ## 59.如果服务端没有listen,客户端建立连接会怎么样? 467 |
468 | 答案 469 |

因为服务端没有listen,所以无法找到对应的socket,所以会回复一个RST报文

470 |
471 | 472 | ## 60.用了TCP,数据一定不会丢失吗? 473 |
474 | 答案 475 |

1.半连接,全连接队列满了,导致连接建立失败

476 |

2.流量控制丢包,为了控制进入网卡的流量,在网络层有个队列,队列满了会导致丢包

477 |

3.网卡丢包:网线质量差

478 |

4.RingBuffer过小导致丢包:因为发送数据速度过快,数据还没有被内核读取就被覆盖,从而导致丢包

479 |
480 | 481 | ## 61.TCP四次挥手可以变成三次挥手吗? 482 |
483 | 答案 484 |

可以,但是得满足没有数据要发送并且开启了TCP延迟确认机制,这样第二次握手的ACK不会立马发送,而是等FIN一起发送

485 |
486 | 487 | ## 62.TLS/SSL原理 488 |
489 | 答案 490 |

以RSA密钥交换算法为例

491 |

1.客户端发送客户端tls版本号,密码套件列表,客户端随机数。

492 |

2.服务端收到后,发送确认使用的tls版本号,选择的密码套件,服务端随机数,服务端证书

493 |

3.客户端收到后验证服务端证书,获取到服务端公钥,然后生成一个随机数pre-master,根据pre-master,客户端随机数,服务端随机数计算出会话密钥,然后使用服务端公钥对pre-master进行加密,发送给服务端

494 |

4.服务端收到后,使用服务端私钥进行解密,获取到pre-master,然后计算出会话密钥

495 |
496 | 497 | ## 63.CA证书原理 498 |
499 | 答案 500 |

服务端向CA机构申请证书,服务端提供公钥,CA机构将服务端的公钥和信息打包成一个证书,然后使用签名算法计算一个签名,然后使用CA私钥将该签名加密。客户端收到该证书后,使用同样的签名算法计算签名,然后使用CA公钥对证书上的签名进行解密,判断和自己计算的签名是否一致。

501 |
502 | 503 | ## 64.TLS1.2和TLS1.3 504 |
505 | 答案 506 |

507 |
508 | 509 | ## 65.为什么tls握手需要三个随机数 510 |
511 | 答案 512 |

客户端随机数为了防止客户端的重放攻击

513 |

服务端随机数是为了防止服务端的重放攻击

514 |

pre-master是为了保证会话密钥是随机的

515 |
516 | 517 | 518 | ## 66.TCP如何保证顺序传输? 519 |
520 | 答案 521 |

1.序列号:通过序列号保证数据按照发送时的顺序进行处理

522 |

2.确认应答:通过发送下一个期望的数据序列号来确认之前的数据已经被接收

523 |

3.重传机制:如果发送端在一定时间内没收到ACK报文,则会重传之前的报文

524 |
525 | 526 | ## 67.Cookie和Session的区别及其应用 527 |
528 | 答案 529 |

cookie保存在客户端,session保存在服务端,保存的都是用户的信息

530 |

cookie不安全,容易发生CSRF攻击。session比较安全

531 |

cookie一般用于存储用户的登陆信息,个性化设置。而session一般用于存储用户的购物车,访问权限等

532 |
533 | 534 | ## 68.websocket原理 535 |
536 | 答案 537 |

websocket是基于http的,先通过http建立连接,然后在http请求中带上特殊的header头升级为websocket协议,然后进行websocket握手,客户端发送一个随机的base64编码的字符串,放在Sec-WebSocket-Key,服务端收到后用某个公开算法将该字符串变成另一个字符串,然后填在Sec-WebSocket-Accept中放回给客户端。客户端收到后也通过同样的公开算法将base64码变成另一个字符串,与服务端放回的进行比较,相同则验证通过

538 |
539 | 540 | ## 69.子网掩码的作用 541 |
542 | 答案 543 |

1.划分子网

544 |

2.确定网络地址

545 |

3.优化网络的流量,如果没有子网的话,一个广播会发送到同一网络的所有设备,通过子网,可以只在相应的子网内传输,减少了不必要的网络流量的干扰

546 |

4.通过将网络划分成子网,对于不同的子网使用不同的安全策略,从而提高网络的整体安全性

547 |

5.使用子网掩码可以更加灵活的分配ip地址,根据子网大小调整子网掩码,优化ip地址的使用

548 |
549 | 550 | ## 70.正向代理和反向代理 551 |
552 | 答案 553 |

正向代理是指客户端向代理服务器发送请求,由代理服务器代替客户端向目标服务器发送请求。为了隐藏客户端的IP地址,突破访问限制

554 |

反向代理是指客户端向代理服务器发送请求,由代理服务器决定向哪个服务器发送请求。为了隐藏服务端的IP地址,负载均衡,提高访问速度

555 |
556 | 557 | ## 71.http的method方法 558 |
559 | 答案 560 |

GET:请求资源

561 |

HEAD:类似于GET请求,但是返回的响应中没有具体的内容,用于获取报头

562 |

POST:向指定资源上传数据

563 |

DELETE:删除指定资源

564 |

OPTIONS:获取服务器支持的方法

565 |

PATCH:更新资源

566 |

PUT:将请求的内容放到指定位置

567 |
568 | 569 | ## 72.https单向认证和双向认证 570 |
571 | 答案 572 |

单向认证:只需要验证服务器的身份

573 |

双向认证:服务器和客户端的身份都要验证,一般用于企业内部,安全性较高的企业

574 |
575 | 576 | ## 73.HTTP长连接如何识别服务器响应是哪次请求的回复 577 |
578 | 答案 579 |

HTTP1.1及之前通过响应的顺序来表示,第一个收到的响应就是对应第一次请求的回复

580 |

HTTP2.0及之后引入了stream,通过stream id 可以表示是哪个请求

581 |
582 | 583 | ## 74.从应用层到网络层各层的header都有什么不同的功能 584 |
585 | 答案 586 |

应用层的header包括协议类型,操作类型,请求信息等

587 |

传输层的header包括序列号,控制标志,确认号,端口号

588 |

网络层的header包括源ip地址,目标ip地址,生存时间等

589 |
590 | 591 | ## 75.TCP粘包问题 592 |
593 | 答案 594 |

一次传输的数据可能包含多个包的数据,导致无法区分两个包的数据,所以需要解决

595 |

1.固定包的长度,不足就补特殊字符进行填充

596 |

2.在每个包的末尾补充结束字符,例如\r\n,用于区分不同包的数据

597 |

3.将消息分成头部和消息体,头部中保存整个消息的长度,每个包的消息体前都会包含其长度

598 |
599 | 600 | ## 76.ping的底层原理 601 |
602 | 答案 603 |

基于ICMP协议,发送一个ICMP的回送请求报文,对方收到后会回复一个ICMP的回送响应报文

604 |
605 | 606 | ## 77.traceroute的底层原理 607 |
608 | 答案 609 |

基于ICMP,通过设置TTL字段的长度,从1开始,然后获取沿途经过的路由器。因为当TTL为0后,对方会返回一个ICMP的超时报文,从而得到该路由器

610 |
611 | 612 | ## 78.IP层实现可靠传输 613 |
614 | 答案 615 |

1.序列号和确认机制

616 |

2.超时和重传

617 |

3.流量控制和拥塞控制

618 |

4.数据包重排和重组

619 |

5.错误检测

620 |
621 | 622 | ## 79.http和rpc 623 |
624 | 答案 625 |

1.功能层面:http是超文本传输协议,用于浏览器和服务端之间的通信,rpc是远程过程调用协议,用于服务和服务之间通信

626 |

2.实现层面:http是已经实现并成熟的应用层协议,定义好了通信报文的格式。而rpc是通信协议的一种规范,并没有具体实现,只要按rpc的规范实现的框架都可以称之为rpc协议,不同的框架可以自定义通信报文格式,序列化方式,通信协议的类型

627 |
-------------------------------------------------------------------------------- /docs/handbook/设计模式.md: -------------------------------------------------------------------------------- 1 | # 设计模式 2 | 3 | ## 1.创建型模式 4 | 5 | ### 1.1 工厂模式 6 | 7 | 工厂模式包含一个工厂接口,不同的工厂子类会实现工厂接口,从而生产不同的产品子类,这些产品子类都实现了产品接口 8 | 9 | 优点: 10 | 1.客户端只需要关系创建对应产品的工厂即可,无需关心创建细节 11 | 2.符合开闭原则,添加新的产品,只需要添加新的产品子类和工厂子类即可 12 | 13 | 缺点: 14 | 1.会导致出现非常多的子类,代码变的非常复杂 15 | 16 | ### 1.2 抽象工厂模式 17 | 18 | 抽象工厂模式包括一个工厂接口,不同的工厂子类会实现抽象工厂接口,抽象工厂接口提供了生成不同类型的产品的方法,不同的工厂子类实现抽象工厂接口,从而生产不同的产品子类。这些产品子类都实现了抽象产品接口 19 | 20 | 抽象工厂模式和工厂模式的区别就是抽象工厂对产品进行了一层抽象 21 | 22 | 优点: 23 | 1.客户端只需要关系创建对应产品的工厂即可,无需关心创建细节 24 | 2.符合开闭原则,添加新的产品,只需要添加新的产品子类和工厂子类即可 25 | 26 | 缺点: 27 | 1.在添加产品类型时,所有的工厂子类都需要修改 28 | 29 | ### 1.3 生成器模式 30 | 31 | 生成器模式就是一个主管类来负责控制产品的生成步骤,从而更好的复用特定的产品。然后有一个生成器接口,由生成器子类实现这些接口,主管类操作生成器来生成不同的产品 32 | 33 | 优点: 34 | 1.生成不同形式的产品时,可以复用相同的制造代码 35 | 2.符合单一职责原则,可以将复杂的构造代码从产品的业务逻辑中分离出来 36 | 37 | 缺点: 38 | 1.整体复杂程度会增加很多 39 | 40 | ### 1.4 原型模式 41 | 42 | 原型模式包含一个原型接口,有一个克隆自身的方法,然后由原型子类实现该接口。从而使得客户端可以通过接口直接克隆对象 43 | 44 | 优点: 45 | 1.可以更方便的生成复杂的对象 46 | 2.可以克隆对象无需与它们所属的具体类耦合 47 | 48 | 缺点: 49 | 1.克隆包含循环引用的复杂对象可能非常麻烦 50 | 51 | ### 1.5 单例模式 52 | 53 | 单例模式通过声明一个私有静态成员实例,然后对外提供一个获取实例的方法来保证实例只被生成一次 54 | 55 | 优点: 56 | 1.保证一个类只有一个实例 57 | 2.仅在首次请求的时候对实例进行初始化 58 | 3.获得了一个全局访问实例的节点 59 | 60 | 缺点: 61 | 1.违反了单一职责原则,单例模式同时解决了两个问题,保证只有一个实例,提供全局访问实例的节点 62 | 2.在多线程环境下需要特殊处理 63 | 3.不适用于变化的对象,在某些场景下需要对单例进行修改,会影响其他场景的数据 64 | 4.单例模式没有抽象层,因此很难进行扩展 65 | 66 | ## 2.结构性模式 67 | 68 | ### 2.1 适配器模式 69 | 70 | 适配器模式有一个客户端接口,由适配器类实现客户端接口,然后将客户端的数据与服务累需要的数据进行适配 71 | 72 | 优点: 73 | 1.符合单一职责原则,将数据转换的逻辑从业务逻辑中抽离出来 74 | 2.符合开闭原则,客户端只需要与客户端接口交互,可以不修改客户端代码,添加新类型的适配器 75 | 76 | 缺点: 77 | 1.代码整体复杂度增加,有时候改动服务类使其和其他代码兼容会更简单 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "design-pattern", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@qcyblm/vuepress-theme-vpx": "^1.0.7", 8 | "vuepress": "^1.9.7", 9 | "vuepress-plugin-baidu-tongji-analytics": "^1.0.2" 10 | }, 11 | "scripts": { 12 | "dev": "vuepress dev docs", 13 | "build": "vuepress build docs" 14 | }, 15 | "dependencies": { 16 | "vuepress-theme-reco": "^1.6.17" 17 | } 18 | } 19 | --------------------------------------------------------------------------------