seamew преди 1 година
родител
ревизия
9ebd0d5810
променени са 62 файла, в които са добавени 1431 реда и са изтрити 15 реда
  1. 1 1
      算法/排序算法/基本排序算法.md
  2. 6 0
      面经/2023年暑期实习/百度.md
  3. 5 0
      面经/工作/秋招.md
  4. 1 1
      面经/问答/JVM.md
  5. 251 0
      面经/问答/Mysql.md
  6. BIN
      面经/问答/assets/2306520394.png
  7. BIN
      面经/问答/assets/24-先来先服务.jpg
  8. BIN
      面经/问答/assets/25-最短作业优先算法.jpg
  9. BIN
      面经/问答/assets/26-响应比公式.jpg
  10. BIN
      面经/问答/assets/27-时间片轮询.jpg
  11. BIN
      面经/问答/assets/28-多级队列.jpg
  12. BIN
      面经/问答/assets/30c2c70721c12f9c140358fbdc5f2282.png
  13. BIN
      面经/问答/assets/3a6cb4e3f27241d3b09b4766bb0b1124.png
  14. BIN
      面经/问答/assets/9ebde5c11ad69447314c216acf188fc8.png
  15. BIN
      面经/问答/assets/a3e7d217ecb825e94bdc577a467eb29d.png
  16. BIN
      面经/问答/assets/b5681d905102439aa1a40412100da566.png
  17. BIN
      面经/问答/assets/c55029a32a6448d9aa35bb3de6efcecatplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp
  18. BIN
      面经/问答/assets/d528bae6fcec2357ba2eb8f324ad9fd5.png
  19. BIN
      面经/问答/assets/d565806a3b7c4735859befbcdad5a105.png
  20. BIN
      面经/问答/assets/dba2bc3af0938d2c087f85acc191fd3f.png
  21. BIN
      面经/问答/assets/dbb57b8d6071d011d05eeadd93269e13.png
  22. BIN
      面经/问答/assets/e94e504adc5a75a2d7f562dc44166511.png
  23. BIN
      面经/问答/assets/ec5c8e28d3ea308c6db2ac991a12ea80.png
  24. BIN
      面经/问答/assets/f109a45fe4904726b11fd36422f4abactplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp
  25. BIN
      面经/问答/assets/image-20210914195129983.png
  26. BIN
      面经/问答/assets/image-20230726214326341.png
  27. BIN
      面经/问答/assets/java8_concurrenthashmap.png
  28. BIN
      面经/问答/assets/redis单线程模型.drawio.png
  29. BIN
      面经/问答/assets/redologbuf.webp
  30. BIN
      面经/问答/assets/senfile-零拷贝.png
  31. BIN
      面经/问答/assets/v2-3ab6401c24be71551d1138762ed1a307_720w.webp
  32. BIN
      面经/问答/assets/v2-4c45bde8970caa0205d64b50095169c4_720w.webp
  33. BIN
      面经/问答/assets/v2-8a6a18065863900773f1491c82af4884_720w.webp
  34. BIN
      面经/问答/assets/v2-98b72fd8e29df55aecdafb8deff0312f_720w.webp
  35. BIN
      面经/问答/assets/v2-cce29042ec33e8a9724ef70da24b8aca_720w.webp
  36. BIN
      面经/问答/assets/wal.png
  37. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NjczMzY3,size_16,color_FFFFFF,t_70#pic_center-16899533239744.png
  38. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NjczMzY3,size_16,color_FFFFFF,t_70#pic_center.png
  39. BIN
      面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTAwMjIxNTg=,size_16,color_FFFFFF,t_70.png
  40. BIN
      面经/问答/assets/主从复制过程.drawio.png
  41. BIN
      面经/问答/assets/事务恢复.png
  42. BIN
      面经/问答/assets/后台线程.jpg
  43. BIN
      面经/问答/assets/回滚事务.png
  44. BIN
      面经/问答/assets/版本链.png
  45. BIN
      面经/问答/assets/磁盘调度-C-LOOK算法.png
  46. BIN
      面经/问答/assets/磁盘调度-C-SCAN算法.png
  47. BIN
      面经/问答/assets/磁盘调度-LOOK算法.png
  48. BIN
      面经/问答/assets/磁盘调度-先来先服务.png
  49. BIN
      面经/问答/assets/磁盘调度-扫描算法.png
  50. BIN
      面经/问答/assets/磁盘调度-最短寻道时间优先.png
  51. BIN
      面经/问答/assets/缓冲池.drawio.png
  52. 158 0
      面经/问答/kafka.md
  53. 161 1
      面经/问答/redis.md
  54. 87 1
      面经/问答/spring.md
  55. 86 0
      面经/问答/基础.md
  56. 21 4
      面经/问答/并发编程.md
  57. 269 4
      面经/问答/操作系统.md
  58. 43 0
      面经/问答/智力问题.md
  59. 105 2
      面经/问答/计网.md
  60. 155 0
      面经/问答/集合.md
  61. 20 1
      面经/项目/中煤项目.md
  62. 62 0
      面经/项目/项目.md

+ 1 - 1
算法/排序算法/基本排序算法.md

@@ -1,4 +1,4 @@
-## 归并排序
+# 归并排序
 
 ```cpp
 #include <bits/stdc++.h>

+ 6 - 0
面经/2023年暑期实习/百度.md

@@ -0,0 +1,6 @@
+1. object类有什么方法,finalilze会释放资源吗
+2. volitel关键字
+3. 线程同步方式
+4. springboot注解
+5. spring生命周期
+6. autowired单例

+ 5 - 0
面经/工作/秋招.md

@@ -16,4 +16,9 @@
 | 远景科技集团 | https://app.mokahr.com/campus-recruitment/envisiongroup/43123?type=school#/candidateHome/applications | 7月20号  | 初筛 |
 | 多益         | https://xz.duoyi.com/v40/#/positions                         | 7月20号  | 初筛 |
 | 地平线       | https://wecruit.hotjob.cn/SU62d915040dcad43c775ec12c/mc/position/campus?acotycoCode=gmscfw&recruitType=1 | 7月20号  | 初筛 |
+| 三禾信安     | https://sansec.zhiye.com/campus/detail?jobAdId=db7e79ba-6ae2-476f-b411-a1a3164a2da7 | 7月27号  | 初筛 |
+| 海柔创新     | https://hairobotics.zhiye.com/campus/detail?jobAdId=a67a3fce-7c13-43ad-b997-2dad7a33ea51 | 7月27号  | 初筛 |
+| 滴滴         | https://app.mokahr.com/campus_apply/didiglobal/96064?recommendCode=DSu3BRUr#/candidateHome/applications | 7月27号  | 初筛 |
+| 作业帮       | https://app.mokahr.com/campus-recruitment/zuoyebang/39595#/jobs | 7月27号  | 初筛 |
+| 快手         | https://campus.kuaishou.cn/#/campus/resume                   | 7月27号  | 初筛 |
 

+ 1 - 1
面经/问答/JVM.md

@@ -39,7 +39,7 @@ CMS主要目的为了避免老年代GC出现长时间的卡顿,主要步骤可
 
 CMS垃圾收集器的缺点
 
-* 产生大量空间碎片:因为老年代是标记-清算法,所以会产生大量的空间碎片
+* 产生大量空间碎片:因为老年代是标记-清算法,所以会产生大量的空间碎片
 * 对CPU资源敏感:应该它与用户线程并行, 所以会导致系统资源被占用
 * 内存需要预留:CMS垃圾收集器可以一边回收垃圾,一边处理用户线程,那需要在这个过程中保证有充足的内存空间供用户使用。如果CMS运行过程中预留的空间不够用,会报错
 

+ 251 - 0
面经/问答/Mysql.md

@@ -172,3 +172,254 @@ InnoDB和MyISAM是MySQL数据库中常见的两种存储引擎,它们有一些
 ```
 
 创建索引之后可以在索引中查找与关键词匹配的文档列表。可以使用布尔检索、向量空间模型或其他检索算法来计算文档与关键词的相关性,并返回匹配度高的文档结果。
+
+## binlog与主从复制
+
+binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
+
+MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
+
+这个过程一般是**异步**的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。
+
+![MySQL 主从复制过程](assets/主从复制过程.drawio.png)
+
+MySQL 集群的主从复制过程梳理成 3 个阶段:
+
+- **写入 Binlog**:主库写 binlog 日志,提交事务,并更新本地存储数据。
+- **同步 Binlog**:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
+- **回放 Binlog**:回放 binlog,并更新存储引擎中的数据。
+
+具体详细过程如下:
+
+- MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应。
+- 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志里,再返回给主库“复制成功”的响应。
+- 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志,然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。
+- 当一个事务在 MySQL 主库上被回滚时,相应的回滚操作会被写入 binlog,以确保主从复制和数据一致性。
+
+在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
+
+> 从库是不是越多越好?
+
+不是的。
+
+因为从库数量增加,从库连接上来的 I/O 线程也比较多,**主库也要创建同样多的 log dump 线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽**。
+
+所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。
+
+> MySQL 主从复制还有哪些模型?
+
+主要有三种:
+
+- **同步复制**:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
+- **异步复制**(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
+- **半同步复制**:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种**半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险**。
+
+## undo log
+
+ **undo log(回滚日志),它保证了事务的 ACID 特性中的原子性(Atomicity)**。
+
+![回滚事务](assets/回滚事务.png)
+
+每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
+
+- 在**插入**一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录**删掉**就好了;
+- 在**删除**一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录**插入**到表中就好了;
+- 在**更新**一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列**更新为旧值**就好了。
+
+一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
+
+- 通过 trx_id 可以知道该记录是被哪个事务修改的;
+- 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;
+
+版本链如下图:
+
+![版本链](assets/版本链.png)
+
+因此,undo log 两大作用:
+
+- **实现事务回滚,保障事务的原子性**。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
+- **实现 MVCC(多版本并发控制)关键因素之一**。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
+
+## Buffer Pool?
+
+Innodb 存储引擎缓冲池**(Buffer Pool)**
+
+![Buffer Poo](assets/缓冲池.drawio.png)
+
+有了 Buffer Poo 后:
+
+- 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
+- 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
+
+### Buffer Pool 缓存什么?
+
+InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样需要按「页」来划分。
+
+在 MySQL 启动的时候,**InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的`16KB`的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页**。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。
+
+所以,MySQL 刚启动的时候,你会观察到使用的虚拟内存空间很大,而使用到的物理内存空间却很小,这是因为只有这些虚拟内存被访问后,操作系统才会触发缺页中断,申请物理内存,接着将虚拟地址和物理地址建立映射关系。
+
+Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等。
+
+
+
+> **Undo 页是记录什么?**
+
+开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。
+
+> **查询一条记录,就只需要缓冲一条记录吗?**
+
+不是的。
+
+当我们查询一条记录时,InnoDB 是会把整个页的数据加载到 Buffer Pool 中,将页加载到 Buffer Pool 后,再通过页里的「页目录」去定位到某条具体的记录。
+
+
+
+## redo log
+
+为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,**这个时候更新就算完成了**。
+
+后续,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里,这就是 **WAL (Write-Ahead Logging)技术**。
+
+**WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上**。
+
+过程如下图:
+
+![img](assets/wal.png)
+
+> **什么是 redo log?**
+
+redo log 是物理日志,记录了某个数据页做了什么修改,比如**对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新**,每当执行一个事务就会产生这样的一条或者多条物理日志。
+
+在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。
+
+当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
+
+> **被修改 Undo 页面,需要记录对应 redo log 吗?**
+
+需要的。
+
+开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。
+
+不过,**在内存修改该 Undo 页面后,需要记录对应的 redo log**。
+
+> **redo log 和 undo log 区别在哪?**
+
+这两种日志是属于 InnoDB 存储引擎的日志,它们的区别在于:
+
+- redo log 记录了此次事务「**完成后**」的数据状态,记录的是更新**之后**的值;
+- undo log 记录了此次事务「**开始前**」的数据状态,记录的是更新**之前**的值;
+
+事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务,如下图:
+
+![事务恢复](assets/事务恢复.png)
+
+所以有了 redo log,再通过 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为 **crash-safe**(崩溃恢复)。可以看出来, **redo log 保证了事务四大特性中的持久性**。
+
+> **redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?**
+
+写入 redo log 的方式使用了追加操作, 所以磁盘操作是**顺序写**,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是**随机写**。
+
+磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。
+
+针对「顺序写」为什么比「随机写」更快这个问题,可以比喻为你有一个本子,按照顺序一页一页写肯定比写一个字都要找到对应页写快得多。
+
+可以说这是 WAL 技术的另外一个优点:**MySQL 的写操作从磁盘的「随机写」变成了「顺序写」**,提升语句的执行性能。这是因为 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上 。
+
+至此, 针对为什么需要 redo log 这个问题我们有两个答案:
+
+- **实现事务的持久性,让 MySQL 有 crash-safe 的能力**,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
+- **将写操作从「随机写」变成了「顺序写」**,提升 MySQL 写入磁盘的性能。
+
+> **产生的 redo log 是直接写入磁盘的吗?**
+
+不是的。
+
+实际上, 执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。
+
+所以,redo log 也有自己的缓存—— **redo log buffer**,每当产生一条 redo log 时,会先写入到 redo log buffer,后续在持久化到磁盘如下图:
+
+![事务恢复](assets/redologbuf.webp)
+
+redo log buffer 默认大小 16 MB,可以通过 `innodb_log_Buffer_size` 参数动态的调整大小,增大它的大小可以让 MySQL 处理「大事务」是不必写入磁盘,进而提升写 IO 性能。
+
+### redo log 什么时候刷盘?
+
+缓存在 redo log buffer 里的 redo log 还是在内存中,它什么时候刷新到磁盘?
+
+主要有下面几个时机:
+
+- MySQL 正常关闭时;
+- 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
+- InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
+- 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制,下面会说)。
+
+## mysql优化慢SQL
+
+### 开启慢查询日志
+
+首先开启慢查询日志,由参数`slow_query_log`决定是否开启,在MySQL命令行下输入下面的命令:
+
+```mysql
+set global slow_query_log=on;
+```
+
+或者可以使用druid连接池去监控
+
+其次设置一个慢查询阈值
+
+```mysql
+set global long_query_time=1;
+```
+
+只要你的SQL实际执行时间超过了这个阈值,就会被记录到慢查询日志里面。这个阈值默认是10s,线上业务一般建议把`long_query_time`设置为1s,如果某个业务的MySQL要求比较高的QPS,可设置慢查询为0.1s。
+
+最后通过
+
+```mysql
+show global variables like 'slow_query_log_file'
+```
+
+语句可以找到慢查询的日志
+
+### 使用**explain**分析sql执行计划
+
+* select_type
+
+![img](assets/v2-98b72fd8e29df55aecdafb8deff0312f_720w.webp)
+
+* type
+
+![img](assets/v2-4c45bde8970caa0205d64b50095169c4_720w.webp)
+
+* Extra:
+
+![img](assets/v2-3ab6401c24be71551d1138762ed1a307_720w.webp)
+
+### 关键字优化
+
+* 因为只有在内连接的时候,and就一并筛选,而Where是将两张表先合并一起,再做筛选
+* 优化value的插入方式,即insert (a, b) (1, ,2,3,4)
+* 避免索引失效
+* 避免使用not null not in 之类的关键字,均会导致索引失效
+
+## CHAR和VARCHAR的区别
+
+在MySQL中,`CHAR`和`VARCHAR`都是用于存储文本数据的数据类型,它们之间有以下区别:
+
+1. 存储方式:`CHAR`类型是固定长度的,当存储数据时,会使用固定的空间。例如,如果定义一个`CHAR(10)`类型的字段,无论实际存储的数据长度是多少,都会占用10个字符的空间。而`VARCHAR`类型是可变长度的,只会占用实际存储的数据长度加上额外的存储空间(1或2个字节用于记录数据长度),对于较短的数据,它占用的空间会更小。
+2. 存储效率:由于`CHAR`类型是固定长度的,所以在存储和检索时效率相对较高。而`VARCHAR`类型由于长度可变,可能会造成额外的存储空间和内存开销,所以在存储和检索大量数据时,效率可能稍低。
+3. 数据填充:对于`CHAR`类型,如果实际存储的数据长度小于定义的长度,会使用空格进行填充以达到指定长度。而`VARCHAR`类型不会进行填充。
+4. 索引和排序:在MySQL中,对于`CHAR`类型的列,可以更好地利用索引和排序的性能,因为它的长度固定,查询时可以更快地定位数据。而`VARCHAR`类型由于长度可变,可能会影响索引和排序的效率。
+
+## 索引失效场景
+
+* 最左前缀原则:要求建立索引的一个列都不能缺失,否则会出现索引失效
+
+* 索引列上的计算,函数、类型转换(列类型是字符串在条件中需要使用引号,否则不走索引)、均会导致索引失效:,索引的数据结构只能对原值做索引
+
+* 索引列中使用 is not null 会导致索引列失效:在where 语句种筛选 idx is null 时,由于索引字段不为空,所以该条件失效,无法查询;在where 语句种筛选 idx is not null 时,由于索引字段本身不为空,所以该条件也失效,会造成全表扫描;
+
+* 索引列中使用 like 查询的前以 % 开头会导致索引列失效。
+
+* 索引列用 or 连接时会导致索引失效:如果条件中有or,只要其中一个条件没有索引,其他字段有索引也不会使用。

BIN
面经/问答/assets/2306520394.png


BIN
面经/问答/assets/24-先来先服务.jpg


BIN
面经/问答/assets/25-最短作业优先算法.jpg


BIN
面经/问答/assets/26-响应比公式.jpg


BIN
面经/问答/assets/27-时间片轮询.jpg


BIN
面经/问答/assets/28-多级队列.jpg


BIN
面经/问答/assets/30c2c70721c12f9c140358fbdc5f2282.png


BIN
面经/问答/assets/3a6cb4e3f27241d3b09b4766bb0b1124.png


BIN
面经/问答/assets/9ebde5c11ad69447314c216acf188fc8.png


BIN
面经/问答/assets/a3e7d217ecb825e94bdc577a467eb29d.png


BIN
面经/问答/assets/b5681d905102439aa1a40412100da566.png


BIN
面经/问答/assets/c55029a32a6448d9aa35bb3de6efcecatplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp


BIN
面经/问答/assets/d528bae6fcec2357ba2eb8f324ad9fd5.png


BIN
面经/问答/assets/d565806a3b7c4735859befbcdad5a105.png


BIN
面经/问答/assets/dba2bc3af0938d2c087f85acc191fd3f.png


BIN
面经/问答/assets/dbb57b8d6071d011d05eeadd93269e13.png


BIN
面经/问答/assets/e94e504adc5a75a2d7f562dc44166511.png


BIN
面经/问答/assets/ec5c8e28d3ea308c6db2ac991a12ea80.png


BIN
面经/问答/assets/f109a45fe4904726b11fd36422f4abactplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp


BIN
面经/问答/assets/image-20210914195129983.png


BIN
面经/问答/assets/image-20230726214326341.png


BIN
面经/问答/assets/java8_concurrenthashmap.png


BIN
面经/问答/assets/redis单线程模型.drawio.png


BIN
面经/问答/assets/redologbuf.webp


BIN
面经/问答/assets/senfile-零拷贝.png


BIN
面经/问答/assets/v2-3ab6401c24be71551d1138762ed1a307_720w.webp


BIN
面经/问答/assets/v2-4c45bde8970caa0205d64b50095169c4_720w.webp


BIN
面经/问答/assets/v2-8a6a18065863900773f1491c82af4884_720w.webp


BIN
面经/问答/assets/v2-98b72fd8e29df55aecdafb8deff0312f_720w.webp


BIN
面经/问答/assets/v2-cce29042ec33e8a9724ef70da24b8aca_720w.webp


BIN
面经/问答/assets/wal.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NjczMzY3,size_16,color_FFFFFF,t_70#pic_center-16899533239744.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NjczMzY3,size_16,color_FFFFFF,t_70#pic_center.png


BIN
面经/问答/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTAwMjIxNTg=,size_16,color_FFFFFF,t_70.png


BIN
面经/问答/assets/主从复制过程.drawio.png


BIN
面经/问答/assets/事务恢复.png


BIN
面经/问答/assets/后台线程.jpg


BIN
面经/问答/assets/回滚事务.png


BIN
面经/问答/assets/版本链.png


BIN
面经/问答/assets/磁盘调度-C-LOOK算法.png


BIN
面经/问答/assets/磁盘调度-C-SCAN算法.png


BIN
面经/问答/assets/磁盘调度-LOOK算法.png


BIN
面经/问答/assets/磁盘调度-先来先服务.png


BIN
面经/问答/assets/磁盘调度-扫描算法.png


BIN
面经/问答/assets/磁盘调度-最短寻道时间优先.png


BIN
面经/问答/assets/缓冲池.drawio.png


+ 158 - 0
面经/问答/kafka.md

@@ -14,6 +14,12 @@ kafka的存储方案是**顺序追加写日志 + 稀疏哈希索引**
 
 /index文件中会生成三份文件XX.log,XX.index,XX.timeindex,日志是分块存储的,时间戳索引从XX.timeindex中找到对应的offset,再从XX.index中索引XX.log。
 
+
+
+> 这里通过mmap(内存映射)可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针
+
+![img](assets/v2-cce29042ec33e8a9724ef70da24b8aca_720w.webp)
+
 ### 页缓存--pagecache
 
 页缓存相对来说比较简单,页缓存在操作系统层面是保存数据的一个基本单位,Kafka 避免使用 JVM,直接使用操作系统的页缓存特性提高处理速度,进而避免了JVM GC 带来的性能损耗。
@@ -42,3 +48,155 @@ kafka就采用零拷贝技术来消费数据
 
 * 消费消息服务做幂等校验,比如 Redis 的 set、MySQL 的主键等天然的幂等功能。这种方法最有效。
 * 将 **`enable.auto.commit`** 参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。
+
+## kafka 的事务机制
+
+Kafka 事务与数据库的事务定义基本类似,主要是一个原子性:**多个操作要么全部成功,要么全部失败**。Kafka 中的事务可以使应用程序将**消费消息、生产消息、提交消费位移**当作原子操作来处理。
+
+- KAFKA的事务机制,在底层依赖于幂等生产者,幂等生产者是 kafka 事务的必要不充分条件;
+- 事实上,开启 kafka事务时,kafka 会自动开启幂等生产者。
+
+## kafka的幂等性
+
+当 kafka producer 向 broker 中的 topic发送数据时,可能会因为网络抖动等各种原因,造成 producer 收不到 broker 的 ack 确认信息。kafka幂等性就会保证在生产者内部逻辑问题引起的消息重复消费的时候,只有一个数据可以被正确的发送。
+
+> **需要注意的是如果使用try/catch捕获,用send手动发送,则会被视为不同的消息**
+
+**原理**
+
+- 在 producer 端,每个 producer 都被 broker 自动分配了一个 Producer Id (PID), producer 向 broker 发送的每条消息,在内部都附带着该 pid 和一个递增的 sequence number;
+- 在 broker 端,broker 为每个 topic 的每个 partition 都维护了一个当前写成功的消息的最大 PID-Sequence Number 元组;
+- 当 broker 收到一个比当前最大 PID-Sequence Number 元组小的 sequence number 消息时,就会丢弃该消息,以避免造成数据重复存储;
+- 当 broker 失败重新选举新的 leader 时, 以上去重机制仍然有效:因为 broker 的 topic 中存储的消息体中附带了 PID-sequence number 信息,且 leader 的所有消息都会被复制到 followers 中。当某个原来的 follower 被选举为新的 leader 时,它内部的消息中已经存储了PID-sequence number 信息,也就可以执行消息去重了。
+
+## kafka事务基本流程
+
+* **initTransactions**:方法用来初始化事务,这个方法能够执行的前提是配置了transactionalId,如果没有则会报出IllegalStateException:
+* **beginTransaction**:方法用来开启事务;
+* **sendOffsetsToTransaction**:方法为消费者提供在事务内的位移提交的操作;将偏移量提交到事务中,仅当整个交易(消费和生产)成功时,它才会提交。
+* **commitTransaction**:方法用来提交事务;
+* **abortTransaction**:方法用来中止事务,类似于事务回滚。
+
+```java
+producer.initTransactions();
+try {
+    producer.beginTransaction();
+    for (ProducerRecord<String, String> record : payload) {
+        producer.send(record);
+    }
+
+    Map<TopicPartition, OffsetAndMetadata> groupCommit = new HashMap<TopicPartition, OffsetAndMetadata>() {
+        {
+            put(new TopicPartition(TOPIC, 0), new OffsetAndMetadata(42L, null));
+        }
+    };
+    producer.sendOffsetsToTransaction(groupCommit, "groupId");
+    producer.commitTransaction();
+} catch (ProducerFencedException e) {
+    producer.close();
+} catch (KafkaException e) {
+    producer.abortTransaction();
+}
+```
+
+## 事务基本流程
+
+1. **存储对应关系,通过请求增加分区**
+   * Producer 在向新分区发送数据之前,首先向 TransactionalCoordinator 发送请求,使 TransactionalCoordinator 存储对应关系 (transactionalId, TopicPartition) 到主题 __transaction_state 中。
+2. **生产者发送消息**
+   * 基本与普通的发送消息相同,生产者调用 `producer.send()` 方法,发送数据到分区;
+   * 发送的请求中,包含 pid, epoch, sequence number 字段;
+3. **增加消费 offset 到事务**
+   * 生产者通过 `producer.senOffsetsToTransaction()` 接口,发送分区的 Offset 信息到事务协调者,协调者将分区信息增加到事务中;
+4. **事务提交位移**
+   * 在前面生产者调用事务提交 offset 接口后,会发送一个 TxnOffsetCommitRequest 请求到消费组协调者,消费组协调者会把 offset 存储到 Kafka 内部主题 __consumer_offsets 中。协调者会根据请求的 pid 与 epoch 验证生产者是否允许发起这个请求。
+   * 只有当事务提交之后,offset 才会对外可见。
+5. **提交或回滚事务**
+   * 用户调用 `producer.commitTransaction()` 或 `abortTransaction()` 方法,提交或回滚事务;(回滚就会跳过该事务)
+   * 生产者完成事务之后,客户端需要显式调用结束事务,或者回滚事务。前者使消息对消费者可见,后者使消息标记为 abort 状态,对消费者不可见。无论提交或者回滚,都会发送一个 EndTxnRequest 请求到事务协调者,同时写入 PREPARE_COMMIT 或者 PREPARE_ABORT 信息到事务记录日志中。
+
+> **需要注意的是:如果事务性生产者(Transactional Producer)发送的消息没有被提交,消费者是不会读取该消息之后的数据的。**
+
+## kafka 触发  Rebalance 
+
+Kafka 触发 Rebalance 主要有以下几种情况:
+
+1. 消费者组中新增或移除了消费者:当一个新的消费者加入消费者组或者一个消费者从消费者组中退出时,Kafka 会触发 Rebalance。这可以通过添加或删除消费者来实现动态扩缩容。
+2. 消费者组订阅的主题发生变化:当消费者组订阅的主题发生变化,即增加或删除了主题,Kafka 也会触发 Rebalance。这样可以确保新增或删除的主题的分区被正确地分配给消费者。
+3. 消费者健康状态发生变化:如果某个消费者长时间未发送心跳,或者被判定为失效,Kafka 会将该消费者标记为“退出”,并触发 Rebalance,重新分配该消费者的分区给其他正常的消费者。
+
+## kafka消息积压问题
+
+### 产生原因
+
+主要有三点产生原因:
+
+1. **实时/消费任务挂掉**
+2. **Kafka分区数设置的不合理(太少)和消费者"消费能力"不足**
+3. **Kafka消息的key不均匀,导致分区间数据不均衡**
+
+### 消息挤压会有什么严重的后果
+
+1. 消息被丢弃
+2. 磁盘空间被挤占
+3. 海量消息被挤压,系统反应较慢
+
+### 如何解决消息挤压问题
+
+1. 生产者减少消息发送速率
+2. 消费端做如下处理
+
+![img](assets/v2-8a6a18065863900773f1491c82af4884_720w.webp)
+
+3. 尽快复盘,找到挤压的原因
+
+## Kafka 消费组消费者分配策略
+
+### 1.Range**(范围)**
+
+Range是**对每个Topic而言**的(即一个Topic一个Topic分),首先对同一个Topic里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。然后用Partitions分区的个数除以消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。
+
+> EX:假如有10个分区,3个消费者线程,把分区按照序号排列0,1,2,3,4,5,6,7,8,9;消费者线程为C1-0,C2-0,C2-1
+
+如果有11个分区将会是:
+
+```shell
+C1-0:0,1,2,3
+C2-0:4,5,6,7
+C2-1:8,9,10
+```
+
+### 2.RoundRobin**(轮询)**默认的策略
+
+该策略的原理是将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序,然后通过轮询方式逐个将分区以此分配给每个消费者
+
+![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTAwMjIxNTg=,size_16,color_FFFFFF,t_70.png)
+
+举例,假设消费组中有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有3个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:
+
+> **消费者C0:t0p0、t0p2、t1p1**
+> **消费者C1:t0p1、t1p0、t1p2**
+
+### 3.**Sticky**
+
+Kafka从0.11.x版本开始引入这种分配策略,它主要有两个目的:
+
+① 分区的分配要尽可能的均匀;
+② 分区的分配尽可能的与上次分配的保持相同。
+
+## kafka实现延时队列的效果
+
+### 应⽤场景
+
+订单创建后,超过30分钟没有⽀付,则需要取消订单,这种场景可以通过延时队列来实现
+
+### 具体⽅案
+
+![image-20210914195129983](assets/image-20210914195129983.png)
+
+* kafka中创建创建相应的主题
+  * 消费者消费该主题的消息(轮询)
+  * 消费者消费消息时判断消息的创建时间和当前时间是否超过30分钟(前提是订单没⽀付)
+  * 如果是:去数据库中修改订单状态为已取消
+
+ * 如果否:记录当前消息的offset,并不再继续消费之后的消息。等待1分钟后,再次 向kafka拉取该offset及之后的消息,继续进⾏判断,以此反复。

+ 161 - 1
面经/问答/redis.md

@@ -18,7 +18,7 @@ Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将
 
 ### 惰性删除
 
-不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。**
+不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
 
 惰性删除策略的**优点**:
 
@@ -77,6 +77,14 @@ Redis 为了避免 AOF 文件越写越大,提供了 **AOF 重写机制**,当
 
 AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
 
+Redis 的**重写 AOF 过程是由后台子进程 \*bgrewriteaof\* 来完成的**,bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:(**写时复制(\*Copy On Write\*)**)
+
+- 执行客户端发来的命令;
+- 将执行后的写命令追加到 「AOF 缓冲区」;
+- 将执行后的写命令追加到 「AOF 重写缓冲区」;
+
+
+
 ### RDB
 
 RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,
@@ -203,3 +211,155 @@ AOF 文件的**前半部分是 RDB 格式的全量数据,后半部分是 AOF 
 ### Write Back(写回)策略;
 
 Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
+
+## 布隆过滤器
+
+### 什么是布隆过滤器?
+
+布隆过滤器它实际上是一个很长的二进制向量和一系列随机映射函数。以Redis中的布隆过滤器实现为例,Redis中的布隆过滤器底层是**一个大型位数组(二进制数组)+多个无偏hash函数。**
+**一个大型位数组(二进制数组)**:
+
+![位数组.png](assets/e94e504adc5a75a2d7f562dc44166511.png)
+
+**多个无偏hash函数:**
+无偏hash函数就是能把元素的hash值计算的比较均匀的hash函数,能使得计算后的元素下标比较均匀的映射到位数组中。
+
+如下就是一个简单的布隆过滤器示意图,其中k1、k2代表增加的元素,a、b、c即为无偏hash函数,最下层则为二进制数组。
+
+![布隆过滤器.png](assets/9ebde5c11ad69447314c216acf188fc8.png)
+
+### 增删查改
+
+* 增加元素
+
+往布隆过滤器增加元素,添加的key需要根据k个无偏hash函数计算得到多个hash值,然后对数组长度进行取模得到数组下标的位置,然后将对应数组下标的位置的值置为1
+
+- 通过k个无偏hash函数计算得到k个hash值
+- 依次取模数组长度,得到数组索引
+- 将计算得到的数组索引下标位置数据修改为1
+
+例如,key = Liziba,无偏hash函数的个数k=3,分别为hash1、hash2、hash3。三个hash函数计算后得到三个数组下标值,并将其值修改为1.
+如图所示:
+
+![增加元素.png](assets/a3e7d217ecb825e94bdc577a467eb29d.png)
+
+* 查询元素
+
+布隆过滤器最大的用处就在于判断某样东西一定不存在或者可能存在,而这个就是查询元素的结果。其查询元素的过程如下:
+
+- 通过k个无偏hash函数计算得到k个hash值
+- 依次取模数组长度,得到数组索引
+- 判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在
+
+关于误判,其实非常好理解,hash函数在怎么好,也无法完全避免hash冲突,也就是说可能会存在多个元素计算的hash值是相同的,那么它们取模数组长度后的到的数组索引也是相同的,这就是误判的原因。例如李子捌和李子柒的hash值取模后得到的数组索引都是1,但其实这里只有李子捌,如果此时判断李子柒在不在这里,误判就出现啦!因此布隆过滤器最大的缺点误判只要知道其判断元素是否存在的原理就很容易明白了!
+
+* 删除元素
+
+布隆过滤器对元素的删除不太支持,目前有一些变形的特定布隆过滤器支持元素的删除!关于为什么对删除不太支持,其实也非常好理解,hash冲突必然存在,删除肯定是很苦难的!
+
+* 修改元素
+
+无
+
+## Redis为什么快
+
+- 内存存储:Redis是一种基于内存的数据存储系统,所有数据都存储在内存中,这使得它可以快速地读取和写入数据,因为内存访问速度比磁盘快得多。
+- 精简的数据结构:Redis支持多种数据结构,如字符串、列表、哈希表、集合和有序集合等。这些数据结构经过精心设计,具有高效的操作复杂度,使得Redis在执行常见的数据操作时非常快速。
+- 单线程模型:Redis采用单线程模型来处理所有客户端请求。这意味着在任何给定时间点,Redis只会执行一个操作,避免了多线程之间的竞争条件和锁操作,从而减少了上下文切换的开销。单线程模型使得Redis非常高效,并且能够在现代计算机系统中充分利用CPU缓存。
+- 非阻塞I/O:Redis使用了非阻塞的I/O多路复用机制,通常使用epoll或kqueue等,这使得它能够同时处理多个客户端请求而不会导致线程阻塞。这样一来,在I/O等待的过程中,Redis可以继续处理其他请求,提高了系统的吞吐量。
+
+## Redis 线程模型
+
+### Redis 是单线程吗?
+
+**Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的**,这也是我们常说 Redis 是单线程的原因。
+
+但是,**Redis 程序并不是单线程的**,Redis 在启动的时候,是会**启动后台线程**(BIO)的:
+
+- **Redis 在 2.6 版本**,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
+- **Redis 在 4.0 版本之后**,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。
+
+![img](assets/后台线程.jpg)
+
+关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:
+
+- BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
+- BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,
+- BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;
+
+### Redis 单线程模式是怎样的?
+
+![img](assets/redis单线程模型.drawio.png)
+
+图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情:
+
+- 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket
+- 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
+- 然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。
+
+初始化完后,主线程就进入到一个**事件循环函数**,主要会做以下事情:
+
+- 首先,先调用**处理发送队列函数**,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
+- 接着,调用 epoll_wait 函数等待事件的到来:
+  - 如果是**连接事件**到来,则会调用**连接事件处理函数**,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
+  - 如果是**读事件**到来,则会调用**读事件处理函数**,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
+  - 如果是**写事件**到来,则会调用**写事件处理函数**,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
+
+###  Redis 采用单线程为什么还这么快?
+
+之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:
+
+- Redis 的大部分操作**都在内存中完成**,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
+- Redis 采用单线程模型可以**避免了多线程之间的竞争**,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
+- Redis 采用了 **I/O 多路复用机制**处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
+
+### Redis 6.0 之前为什么使用单线程?
+
+使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,**增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗**。
+
+### Redis 6.0 之后为什么引入了多线程?
+
+随着硬件性能提升,Redis 的性能瓶颈可能出现网络 IO 的读写,也就是:**单个线程处理网络读写的速度跟不上底层网络硬件的速度**。
+
+**Redis 多 IO 线程模型只用来处理网络读写请求,对于 Redis 的读写命令,依然是单线程处理**。
+
+![图片来源:后端研究所](assets/c55029a32a6448d9aa35bb3de6efcecatplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp)
+
+![Redis多线程与IO线程](assets/f109a45fe4904726b11fd36422f4abactplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp)
+
+**主要流程**:
+
+1. 主线程负责接收建立连接请求,获取 `socket` 放入全局等待读处理队列;
+2. 主线程通过轮询将可读 `socket` 分配给 IO 线程;
+3. 主线程阻塞等待 IO 线程读取 `socket` 完成;
+4. 主线程执行 IO 线程读取和解析出来的 Redis 请求命令;
+5. 主线程阻塞等待 IO 线程将指令执行结果回写回 `socket`完毕;
+6. 主线程清空全局队列,等待客户端后续的请求。
+
+思路:**将主线程 IO 读写任务拆分出来给一组独立的线程处理,使得多个 socket 读写可以并行化,但是 Redis 命令还是主线程串行执行。**
+
+## Redis 大 Key 对持久化有什么影响?
+
+当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
+
+AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):
+
+- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
+- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。
+
+大 key 除了会影响持久化之外,还会有以下的影响。
+
+- 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
+- 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
+- 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
+- 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。
+
+### 如何删除大KEY
+
+- 分批次删除
+
+每次分批删除,每次删除100个
+
+- 异步删除(Redis 4.0版本以上)
+
+从 Redis 4.0 版本开始,可以采用**异步删除**法,**用 unlink 命令代替 del 来删除**。这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。

+ 87 - 1
面经/问答/spring.md

@@ -54,4 +54,90 @@ Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
 2. 该注解通过`@Import`注解导入`AutoConfigurationImportSelector`,这个类实现了一个导入器接口`ImportSelector`。在该接口中存在一个方法`selectImports`,
 3. 该方法的返回值是一个数组,数组中存储的就是要被导入到`spring`容器中的类的全类名。在`AutoConfigurationImportSelector`类中重写了这个方法,
 4. 该方法内部就是读取了项目的`classpath`路径下`META-INF/spring.factories`文件中的所配置的类的全类名。
-5. 在这些配置类中所定义的`Bean`会根据条件注解所指定的条件来决定是否需要将其导入到`Spring`容器中。
+5. 在这些配置类中所定义的`Bean`会根据条件注解所指定的条件来决定是否需要将其导入到`Spring`容器中。
+
+## springboot三个注解
+
+### @SpringBootConfiguration
+
+* 作用: 声明定义Bean,嵌套了@Component组件
+* @SpringBootConfiguration源码是@Configuration:表示该类为主配置类,可用来装配bean
+* @Configuration的源码是@Component:说明Spring的配置类也是Spring的一个组件。
+
+![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NjczMzY3,size_16,color_FFFFFF,t_70#pic_center.png)
+
+* 它是JavaConfig形式的基于Spring IOC容器的配置类使用的一种注解。SpringBoot本质上就是一个Spring应用,通过这个注解来加载IOC容器的配置。所以在启动类里面标注了@Configuration,意味着它也是一个IOC容器的配置类
+
+### @ComponentScan
+
+- 作用:扫描主配置类包的所有包下的类,相当于xml配置文件中的context:component-scan。eg:pojo中的User类
+
+### @EnableAutoConfiguration(重点!!!)
+
+![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NjczMzY3,size_16,color_FFFFFF,t_70#pic_center-16899533239744.png)
+
+1. **@AutoConfigurationPackage**:自动配置包
+
+* 作用:给Spring容器中导入一个Registrar注册器组件
+
+`@AutoConfigurationPackage`和`@ComponentScan`一样,都是将`Spring Boot`启动类所在的包及其子包里面的组件扫描到IOC容器中,但是区别是`@AutoConfigurationPackage`扫描`@Enitity、@Mapper`等第三方依赖的注解,`@ComponentScan`只扫描`@Controller/@Service/@Component/@Repository`这些常见注解。所以这两个注解扫描的对象是不一样的。当然这只是直观上的区别,更深层次说,@AutoConfigurationPackage是自动配置的提醒,是Spring Boot中注解,而@ComponentScan是Spring的注解
+
+2. **@Import(AutoConfigurationImportSelector.class)**——核心注解
+
+* 该注解通过`@Import`注解导入`AutoConfigurationImportSelector`,这个类实现了一个导入器接口`ImportSelector`。在该接口中存在一个方法`selectImports`,
+
+## autowired变量都是单例的吗?
+
+默认都是单例对象,就算对象标注@Scope(value = “prototype”)也无法生效
+
+特殊情况是多例对象,当注入方和被注入方都被@Scope(value = “prototype”)标注才会使多例
+
+## bean生命周期
+
+- BeanDefinition 注册阶段 - `registerBeanDefinition`
+- BeanDefinition 合并阶段 - `getMergedBeanDefinition`
+- Bean 实例化前阶段 - `resolveBeforeInstantiation`
+- Bean 实例化阶段 - `createBeanInstance`
+- Bean 实例化后阶段 - `populateBean`
+- Bean 属性赋值阶段 - `populateBean`
+- Bean Aware 接口回调阶段 - `initializeBean`
+- Bean 初始化前阶段 - `initializeBean`
+- Bean 初始化阶段 - `initializeBean`
+- Bean 初始化后阶段 - `initializeBean`
+- Bean 初始化完成阶段 - `preInstantiateSingletons`
+- Bean 销毁前阶段 - `destroyBean`
+- Bean 销毁阶段 - `destroyBean`
+
+## AOP
+
+AOP(Aspect-Oriented Programming:面向切面编程)
+
+AOP 切面编程设计到的一些专业术语:
+
+| 术语              |                             含义                             |
+| :---------------- | :----------------------------------------------------------: |
+| 目标(Target)      |                         被通知的对象                         |
+| 代理(Proxy)       |             向目标对象应用通知之后创建的代理对象             |
+| 连接点(JoinPoint) |         目标对象的所属类中,定义的所有方法均为连接点         |
+| 切入点(Pointcut)  | 被切面拦截 / 增强的连接点(切入点一定是连接点,连接点不一定是切入点) |
+| 通知(Advice)      | 增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情  |
+| 切面(Aspect)      |                切入点(Pointcut)+通知(Advice)                 |
+| Weaving(织入)     |       将通知应用到目标对象,进而生成代理对象的过程动作       |
+
+## AOP流程
+
+1. 创建Advice
+
+一个advice就是一个方法,即目标对象必须要执行的完成的工作。
+
+因为spring aop只能代理目标方法,所有advice就会被解析成`MethodAdvice`。
+
+这里以`BeforeAdvice`举例。
+
+2. 创建Interceptor
+
+`MethodBeforeAdvice`,但只有这个类还不够,因为我们的方法还无法继续往下执行,需要将`MethodBeforeAdvice`转化为`methodInterceptor`。它封装了`MethodBeforeAdvice`,将其变成真正的**增强方法**,因为`methodInterceptor`封装了代理方法,提供了统一的逻辑。
+
+3. 创建Advisor
+
+一个advisor对应一个pointcut和一个advice。

+ 86 - 0
面经/问答/基础.md

@@ -36,3 +36,89 @@ Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种
 **本地内存**:每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
 
 ![JMM(Java 内存模型)](assets/jmm.png)
+
+## object类包含哪些方法
+
+Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
+
+
+
+```java
+/**
+ * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
+ */
+public final native Class<?> getClass()
+/**
+ * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
+ */
+public native int hashCode()
+/**
+ * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
+ */
+public boolean equals(Object obj)
+/**
+ * native 方法,用于创建并返回当前对象的一份拷贝。
+ */
+protected native Object clone() throws CloneNotSupportedException
+/**
+ * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
+ */
+public String toString()
+/**
+ * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
+ */
+public final native void notify()
+/**
+ * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
+ */
+public final native void notifyAll()
+/**
+ * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
+ */
+public final native void wait(long timeout) throws InterruptedException
+/**
+ * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
+ */
+public final void wait(long timeout, int nanos) throws InterruptedException
+/**
+ * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
+ */
+public final void wait() throws InterruptedException
+/**
+ * 实例被垃圾回收器回收的时候触发的操作
+ */
+protected void finalize() throws Throwable { }
+```
+
+## finalize会不会立即清理和释放资源
+
+1. 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
+2. 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中
+
+![img](assets/b5681d905102439aa1a40412100da566.png)
+
+3. Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
+4. 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
+5. FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法
+
+![img](assets/d565806a3b7c4735859befbcdad5a105.png)
+
+6. 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了  
+
+**finalize 缺点**
+
+* 无法保证资源释放:FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了
+
+* 无法判断是否发生错误:执行 finalize 方法时,会吞掉任意异常(Throwable)
+
+* 内存释放不及时:重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后,第二次 gc 时才能真正释放内存
+
+## 设计模式的原则
+
+1. 单一职责原则(Single Responsibility Principle,SRP):一个类应该只有一个引起它变化的原因。一个类应该只负责完成单一的职责或功能,这样可以提高代码的可读性和可维护性。
+2. 开放封闭原则(Open Closed Principle,OCP):软件实体(类、模块等)应该对扩展开放,对修改关闭。通过使用抽象和接口,可以使得系统容易进行扩展,而不需要修改已有的代码。
+3. 里氏替换原则(Liskov Substitution Principle,LSP):子类应该能够替换其父类并且保持程序的逻辑正确性。子类应该遵守父类定义的接口和规范。
+4. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该直接依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于具体实现细节,而是应该依赖于抽象。
+5. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口。一个类或模块应该只有与其相关的接口,而不应该依赖于不需要的接口。
+6. 迪米特法则(Law of Demeter,LoD):一个对象应该对其他对象有尽可能少的了解。减少对象之间的直接依赖关系可以提高代码的可维护性和灵活性。
+7. 组合/聚合复用原则(Composition/Aggregation Reuse Principle,CARP):优先使用组合或聚合关系,而不是继承关系,来实现代码复用。组合和聚合关系更加灵活,避免了类之间的紧耦合。

+ 21 - 4
面经/问答/并发编程.md

@@ -4,11 +4,11 @@
 
 `wait()`和`notify()`必须放在`synchronized`块中是因为这些方法依赖于对象的监视器锁(也称为互斥锁)。只有获得了对象的监视器锁(该锁即为调用`wait`方法的对象)的线程才能调用`wait()`和`notify()`方法。如果这些方法不在同步块中使用,就无法保证线程安全性。
 
-当一个线程调用`wait()`方法时,它会释放持有的锁并进入等待状态,等待其他线程调用`notify()`方法来唤醒它。如果`wait()`方法不在同步块中使用,那么该线程在释放锁之后可能已经被其他线程修改了。这样,即使其他线程调用了`notify()`方法唤醒了它,该线程也无法正确地处理唤醒事件。
+为了解决「lost wake up 问题」
 
-同样,`notify()`方法也必须在同步块中使用。如果一个线程在未获得锁的情况下调用`notify()`方法,那么它将无法通知任何等待的线程。因为它没有获取锁,所以它不能访问共享数据或执行必要的同步操作来确保正确的通知。
+**两个线程启动,消费者检查 `obj.count` 的值,发现 `obj.count <= 0` 条件成立,但这时由于 CPU 的调度,发生上下文切换,生产者开始工作,执行了 `count+1` 和 `obj.notify()`,也就是发出通知,准备唤醒一个阻塞的线程。然后 CPU 调度到消费者,此时消费者开始执行 `obj.wait()`,线程进入阻塞。但生产者已经早在消费者阻塞前执行了唤醒动作,也就导致消费者永远无法醒来了。**
 
-因此,使用`wait()`和`notify()`方法时,必须在同步块中使用它们,以确保线程之间的安全性并避免出现竞态条件。
+![1644918-20190619224558253-730645351.png](assets/2306520394.png)
 
 ## volatile 关键字
 
@@ -71,7 +71,7 @@ JVM的实现会在volatile读写前后均加上内存屏障,在一定程度上
 
 ## ReentrantLock锁
 
-`ReentrantLock` 实现了 `Lock` 接口,是一个可重入且独占式的锁,他的底层是基于CAS+AQS+LockSupport实现的
+`ReentrantLock` 实现了 `Lock` 接口,是一个可重入且独占式的锁(默认是非公平锁),他的底层是基于CAS+AQS+LockSupport实现的
 
 * CAS
 
@@ -260,3 +260,20 @@ ThreadLocalMap getMap(Thread t) {
 
 这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,使用完 `ThreadLocal`方法后最好手动调用`remove()`方法
 
+## 无锁化编程有哪些常见方法?
+
+1. 原子操作(Atomic Operations):使用原子操作可以保证对某个特定变量的操作是原子性的,即不会被其他线程干扰。比如使用原子加(Atomic Add)操作可以实现无锁的计数器。
+2. 无锁队列(Lock-Free Queue):通过使用原子操作和特定的数据结构,如CAS(Compare-and-Swap),可以实现无锁的队列,避免了使用锁带来的性能开销。无锁队列常用于实现高性能的生产者消费者模型。
+3. 无锁缓存(Lock-Free Caches):无锁缓存是一种无锁化设计的数据结构,用于高效地处理并发场景下的数据访问。通过使用原子操作和一些特殊的算法,可以实现对数据的并发读写而无需使用锁。类似于redis
+4. RCU(Read-Copy-Update):RCU 是一种读写锁的变体,适用于读操作远远多于写操作的场景。它通过使用副本机制,允许读操作和写操作同时进行,而不需要相互锁定。对于旧副本,可以采用延迟释放的方式来确保数据的一致性。CopyOnWrite
+5. 多个生产者和多个消费者,一样可以做到免锁访问,但要使用原子操作。这里的意思应该是不用原子操作级别的免锁,理由也很简单,生产者和消费者需要修改的位置是分开的(生产者加在尾部,消费者从头部消费)
+
+## 通过时间戳实现无锁编程
+
+通过时间戳实现无锁编程是一种常见的无锁技术之一,可以用于并发环境下对共享资源的访问控制。下面是一个基本的时间戳无锁编程的实现思路:
+
+1. 每个线程或进程维护一个自己的时间戳,代表该线程的操作序号。
+2. 每个共享资源也维护一个时间戳,表示该资源的操作序号。
+3. 线程在对共享资源进行修改时,首先读取该资源的时间戳,然后将自己的时间戳加1,并与资源的时间戳进行比较。
+4. 如果自己的时间戳大于等于资源的时间戳,则可以执行修改操作,并更新资源的时间戳为自己的时间戳。
+5. 如果自己的时间戳小于资源的时间戳,则说明其他线程已经修改了资源,此时需要根据具体的策略来处理冲突,如等待一段时间后重新尝试,或者放弃操作。

+ 269 - 4
面经/问答/操作系统.md

@@ -2,7 +2,7 @@
 
 **虚拟内存**是逻辑存在的内存,他的主要作用的简化内存管理。
 
-总的来说虚拟内存提供了下几个功能
+总的来说虚拟内存提供了下几个功能
 
 1. **隔离进程**:物理内存通过虚拟地址空间访问,虚拟地址空间与进程一一对应。每个进程都认为自己拥有了整个物理内存,进程之间彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
 2. **提升物理内存利用率**:有了虚拟地址空间后,操作系统只需要将进程当前正在使用的部分数据或指令加载入物理内存。
@@ -83,11 +83,21 @@ TLB即为页表缓存、转址旁路缓存、快表等。有了 TLB 后,那么
 
 ## 在 4GB 物理内存的机器上,申请 8G 内存会怎么样?
 
+32 位操作系统和 64 位操作系统的虚拟地址空间大小是不同的,在 Linux 操作系统中,虚拟地址空间的内部又被分为**内核空间和用户空间**两部分,如下所示:
+
+![img](assets/3a6cb4e3f27241d3b09b4766bb0b1124.png)
+
 - 在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
-- 在 64位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
+- 在 64 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存(没有被访问的前提下),即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区
   - 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
   - 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;
 
+## swap
+
+这种,将内存数据换出磁盘,又从磁盘中恢复数据到内存的过程,就是 Swap 机制负责的。
+
+Swap 就是把一块磁盘空间或者本地文件,当成内存来使用,它包含换出和换入两个过程
+
 ## 进程和线程
 
 * **进程(Process)** 是指计算机中正在运行的一个程序实例。操作系统的资源分配单位,进程是资源(包括内存、打开的文件等)分配的单位,
@@ -165,8 +175,8 @@ TLB即为页表缓存、转址旁路缓存、快表等。有了 TLB 后,那么
 - 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
 - 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
 - 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
-- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
-- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
+- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;软件中断
+- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;硬件中断
 
 ### 线程
 
@@ -272,6 +282,10 @@ DMA(Direct Memory Access)是一种硬件实现的技术,能够直接将数
 - 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
 - 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
 
+如果系统支持SG-DMA
+
+![img](assets/senfile-零拷贝.png)
+
 ## PageCache 有什么作用?
 
 **零拷贝使用了 PageCache 技术**,**PageCache其实本质上就是一种磁盘高速缓存**,通过 DMA 把磁盘里的数据搬运到内存里,这样就可以用读内存替换读磁盘。
@@ -311,3 +325,254 @@ DMA(Direct Memory Access)是一种硬件实现的技术,能够直接将数
 
 - 内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 PageCache 中,最后「**合并**」成一个更大的 I/O 请求再发给磁盘,这样做是为了减少磁盘的寻址操作;
 - 内核也会「**预读**」后续的 I/O 请求放在 PageCache 中,一样是为了减少对磁盘的操作;
+
+## 什么是一致性哈希?
+
+### 如何处理分配请求?
+
+直接使用Kafka的轮询分配策略,使用权重对其进行轮询。但是加权轮询算法是无法应对「分布式系统(数据分片的系统)」的,因为分布式系统中,每个节点存储的数据是不同的。
+
+当我们想提高系统的容量,就会将数据水平切分到不同的节点来存储,也就是将数据分布到了不同的节点。比如**一个分布式 KV(key-valu) 缓存系统,某个 key 应该到哪个或者哪些节点上获得,应该是确定的**,不是说任意访问一个节点都可以得到缓存结果的。
+
+### 使用哈希算法有什么问题?
+
+主要有两个问题?
+
+1. 数据分布不均匀,高频词的点,该节点数据可能过大
+2. 扩容时会发生再哈希,导致数据迁移问题
+
+### 一致性哈希
+
+![img](assets/30c2c70721c12f9c140358fbdc5f2282.png)
+
+- 首先,对 key 进行哈希计算,确定此 key 在环上的位置;
+- 然后,从这个位置沿着顺时针方向走,遇到的第一节点就是存储 key 的节点。
+
+> 缺点:
+>
+> **一致性哈希算法虽然减少了数据迁移量,但是存在节点分布不均匀的问题**。
+
+![img](assets/d528bae6fcec2357ba2eb8f324ad9fd5.png)
+
+### 如何通过虚拟节点提高均衡度?
+
+想要解决上述问题,就需要采用虚拟节点
+
+具体做法是,**不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。**
+
+比如对每个节点分别设置 3 个虚拟节点:
+
+- 对节点 A 加上编号来作为虚拟节点:A-01、A-02、A-03
+- 对节点 B 加上编号来作为虚拟节点:B-01、B-02、B-03
+- 对节点 C 加上编号来作为虚拟节点:C-01、C-02、C-03
+
+引入虚拟节点后,原本哈希环上只有 3 个节点的情况,就会变成有 9 个虚拟节点映射到哈希环上,哈希环上的节点数量多了 3 倍。
+
+![img](assets/dbb57b8d6071d011d05eeadd93269e13.png)
+
+## 内核优化
+
+* **文件描述符**,默认是1024,改到最大值65535
+
+## I/O多路复用
+
+### select/poll
+
+select 实现多路复用的方式是,将已连接的 Socket 都放到一个**文件描述符集合**,然后调用 select 函数将文件描述符集合**拷贝**到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过**遍历**文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合**拷贝**回用户态里,然后用户态还需要再通过**遍历**的方法找到可读或可写的 Socket,然后再对其处理。
+
+所以,对于 select 这种方式,需要进行 **2 次「遍历」文件描述符集合**,一次是在内核态里,一个次是在用户态里 ,而且还会发生 **2 次「拷贝」文件描述符集合**,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
+
+select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 `1024`,只能监听 0~1023 的文件描述符。
+
+poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
+
+但是 poll 和 select 并没有太大的本质区别,**都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合**,这种方式随着并发数上来,性能的损耗会呈指数级增长。
+
+### epoll
+
+epoll 通过两个方面,很好解决了 select/poll 的问题。
+
+*第一点*,epoll 在内核里使用**红黑树来跟踪进程所有待检测的文件描述字**,把需要监控的 socket 通过 `epoll_ctl()` 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 `O(logn)`。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
+
+*第二点*, epoll 使用**事件驱动**的机制,内核里**维护了一个链表来记录就绪事件**,当某个 socket 有事件发生时,通过**回调函数**内核会将其加入到这个就绪事件列表中,当用户调用 `epoll_wait()` 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
+
+epoll 支持两种事件触发模式,分别是**边缘触发(\*edge-triggered,ET\*)\**和\**水平触发(\*level-triggered,LT\*)**。
+
+这两个术语还挺抽象的,其实它们的区别还是很好理解的。
+
+- 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,**服务器端只会从 epoll_wait 中苏醒一次**,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
+- 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,**服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束**,目的是告诉我们有数据需要读取;
+
+> **select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。**
+
+## 进程调度算法
+
+###  先来先服务调度算法
+
+最简单的一个调度算法,就是非抢占式的**先来先服务(\*First Come First Severd, FCFS\*)算法**了。
+
+![FCFS 调度算法](assets/24-先来先服务.jpg)
+
+### 最短作业优先调度算法
+
+**最短作业优先(\*Shortest Job First, SJF\*)调度算法**同样也是顾名思义,它会**优先选择运行时间最短的进程来运行**,这有助于提高系统的吞吐量。
+
+![SJF 调度算法](assets/25-最短作业优先算法.jpg)
+
+###  高响应比优先调度算法
+
+前面的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和长作业。
+
+那么,**高响应比优先 (Highest Response Ratio Next, HRRN)调度算法**主要是权衡了短作业和长作业。
+
+**每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行**,「响应比优先级」的计算公式:
+
+![img](assets/26-响应比公式.jpg)
+
+从上面的公式,可以发现:
+
+- 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行;
+- 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;
+
+###  时间片轮转调度算法
+
+最古老、最简单、最公平且使用最广的算法就是**时间片轮转(\*Round Robin, RR\*)调度算法**。
+
+![RR 调度算法](assets/27-时间片轮询.jpg)
+
+**每个进程被分配一个时间段,称为时间片(\*Quantum\*),即允许该进程在该时间段中运行。**
+
+- 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程;
+- 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;
+
+另外,时间片的长度就是一个很关键的点:
+
+- 如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;
+- 如果设得太长又可能引起对短作业进程的响应时间变长。将
+
+通常时间片设为 `20ms~50ms` 通常是一个比较合理的折中值。
+
+### 最高优先级调度算法
+
+前面的「时间片轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。
+
+但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能**从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法**。
+
+进程的优先级可以分为,静态优先级或动态优先级:
+
+- 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
+- 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是**随着时间的推移增加等待进程的优先级**。
+
+该算法也有两种处理优先级高的方法,非抢占式和抢占式:
+
+- 非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
+- 抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。
+
+但是依然有缺点,可能会导致低优先级的进程永远不会运行。
+
+###  多级反馈队列调度算法
+
+**多级反馈队列(Multilevel Feedback Queue)调度算法**是「时间片轮转算法」和「最高优先级算法」的综合和发展。
+
+顾名思义:
+
+- 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
+- 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;
+
+![多级反馈队列](assets/28-多级队列.jpg)
+
+来看看,它是如何工作的:
+
+- 设置了多个队列,赋予每个队列不同的优先级,每个**队列优先级从高到低**,同时**优先级越高时间片越短**;
+- 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
+- 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
+
+可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的**兼顾了长短作业,同时有较好的响应时间。**
+
+## 磁盘调度算法
+
+### 先来先服务
+
+先来先服务(*First-Come,First-Served,FCFS*),顾名思义,先到来的请求,先被服务。
+
+那按照这个序列的话:
+
+98,183,37,122,14,124,65,67
+
+那么,磁盘的写入顺序是从左到右,如下图:
+
+![先来先服务](assets/磁盘调度-先来先服务.png)
+
+先来先服务算法总共移动了 `640` 个磁道的距离,这么一看这种算法,比较简单粗暴,但是如果大量进程竞争使用磁盘,请求访问的磁道可能会很分散,那先来先服务算法在性能上就会显得很差,因为寻道时间过长。
+
+### 最短寻道时间优先
+
+最短寻道时间优先(*Shortest Seek First,SSF*)算法的工作方式是,优先选择从当前磁头位置所需寻道时间最短的请求,还是以这个序列为例子:
+
+98,183,37,122,14,124,65,67
+
+那么,那么根据距离磁头( 53 位置)最近的请求的算法,具体的请求则会是下列从左到右的顺序:
+
+65,67,37,14,98,122,124,183
+
+![最短寻道时间优先](assets/磁盘调度-最短寻道时间优先.png)
+
+磁头移动的总距离是 `236` 磁道,相比先来先服务性能提高了不少。
+
+但这个算法可能存在某些请求的**饥饿**,因为本次例子我们是静态的序列,看不出问题,假设是一个动态的请求,如果后续来的请求都是小于 183 磁道的,那么 183 磁道可能永远不会被响应,于是就产生了饥饿现象,这里**产生饥饿的原因是磁头在一小块区域来回移动**。
+
+### 扫描算法
+
+最短寻道时间优先算法会产生饥饿的原因在于:磁头有可能再一个小区域内来回得移动。
+
+为了防止这个问题,可以规定:**磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向,这就是扫描(\*Scan\*)算法**。
+
+这种算法也叫做电梯算法,比如电梯保持按一个方向移动,直到在那个方向上没有请求为止,然后改变方向。
+
+还是以这个序列为例子,磁头的初始位置是 53:
+
+98,183,37,122,14,124,65,67
+
+那么,假设扫描调度算先朝磁道号减少的方向移动,具体请求则会是下列从左到右的顺序:
+
+37,14,`0`,65,67,98,122,124,183
+
+![扫描算法](assets/磁盘调度-扫描算法.png)
+
+磁头先响应左边的请求,直到到达最左端( 0 磁道)后,才开始反向移动,响应右边的请求。
+
+扫描调度算法性能较好,不会产生饥饿现象,但是存在这样的问题,中间部分的磁道会比较占便宜,中间部分相比其他部分响应的频率会比较多,也就是说每个磁道的响应频率存在差异。
+
+### 循环扫描算法
+
+扫描算法使得每个磁道响应的频率存在差异,那么要优化这个问题的话,可以总是按相同的方向进行扫描,使得每个磁道的响应频率基本一致。
+
+循环扫描(*Circular Scan, CSCAN* )规定:只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且**返回中途不处理任何请求**,该算法的特点,就是**磁道只响应一个方向上的请求**。
+
+还是以这个序列为例子,磁头的初始位置是 53:
+
+98,183,37,122,14,124,65,67
+
+那么,假设循环扫描调度算先朝磁道增加的方向移动,具体请求会是下列从左到右的顺序:
+
+65,67,98,122,124,183,`199`,`0`,14,37
+
+![循环扫描算法](assets/磁盘调度-C-SCAN算法.png)
+
+磁头先响应了右边的请求,直到碰到了最右端的磁道 199,就立即回到磁盘的开始处(磁道 0),但这个返回的途中是不响应任何请求的,直到到达最开始的磁道后,才继续顺序响应右边的请求。
+
+循环扫描算法相比于扫描算法,对于各个位置磁道响应频率相对比较平均。
+
+### LOOK 与 C-LOOK算法
+
+我们前面说到的扫描算法和循环扫描算法,都是磁头移动到磁盘「最始端或最末端」才开始调换方向。
+
+那这其实是可以优化的,优化的思路就是**磁头在移动到「最远的请求」位置,然后立即反向移动。**
+
+那针对 SCAN 算法的优化则叫 LOOK 算法,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,**反向移动的途中会响应请求**。
+
+![LOOK 算法](assets/磁盘调度-LOOK算法.png)
+
+而针 C-SCAN 算法的优化则叫 C-LOOK,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,**反向移动的途中不会响应请求**。
+
+![C-LOOK 算法](assets/磁盘调度-C-LOOK算法.png)

+ 43 - 0
面经/问答/智力问题.md

@@ -0,0 +1,43 @@
+## 香问题
+
+1. 两根可以烧一小时的香, 如何确定15分钟时间?
+
+第一根香点燃一端,
+第二根香点燃两端。
+当第二根香燃尽时,过去了半小时,因此第一根香总长度剩下半小时。
+此时若点燃第一根香的两端,直到燃尽就是15分钟了。
+
+2. 一根可以烧一小时的香, 如何确定15分钟时间
+
+点燃香的两端,同时任意点燃香中间的一个点(此时香会变成长度不等的两段,没关系),
+这样可以使得香以四倍速度燃烧,
+在短的一段燃尽的瞬间,再在另一段中间随便点燃一个点,使得香保持四倍燃烧速度即可。
+香全部燃尽时就是15分钟。
+
+3. 一根不均匀的绳子,全部烧完需要1个小时,问怎样烧能计时1个小时15分钟
+
+取出三条绳子。
+
+1、同时点燃“第一根的两头”和“第二根的一头”,第一根烧完时间过了“30分钟”;
+
+2、第一根烧完后马上点燃第二根的另一头,到第二根烧完时间又过了“15分钟”;
+
+3、第二根烧完后马上点燃第三根绳子的两头,当第三根烧完时间又用了“30分钟”。加起来总共=30+15+30=75分钟=一个小时十五分钟。
+
+## 大数问题
+
+1. 在100G文件中找出出现次数最多的100个IP
+
+针对top k类问题,通常比较好的方案是【分治+trie树/[hash](https://so.csdn.net/so/search?q=hash&spm=1001.2101.3001.7020)+小顶堆】,即先将数据集按照hash方法分解成多个小数据集,然后使用trie树或者hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出频率最高的前K个数,最后在所有top K中求出最终的top K。
+
+2. 如何在1亿个数中找出最大的100个数(top K问题)
+
+**第一种方法**:直接排序,放弃
+
+ **第二种方法为局部淘汰法**,该方法与排序方法类似,用一个容器保存前10000个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内这个10000个数就是最大10000个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这1亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。
+
+ **第三种方法是分治法**,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的10010000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^64=4MB,一共需要101次这样的比较。*
+
+ **第四种方法是Hash法**。如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。
+
+ **第五种方法采用最小堆**。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

+ 105 - 2
面经/问答/计网.md

@@ -127,8 +127,8 @@ TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号
 
 ### 第二次握手丢失了,会发生什么?
 
-- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 `tcp_syn_retries`内核参数决定;
-- 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 `tcp_synack_retries` 内核参数决定。
+- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 `tcp_syn_retries`内核参数决定;(由于没有收到ACK)
+- 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 `tcp_synack_retries` 内核参数决定。(由于没有收到ACK)
 
 ### 第三次握手丢失了,会发生什么?
 
@@ -385,3 +385,106 @@ TCP握手之后HTTPS需要四次握手建立连接。
 - HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
 - 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
 - HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
+
+## HTTP的发展历史
+
+1. HTTP/0.9:
+
+   - 于1991年发布,是最早的HTTP协议版本。
+   - 仅支持GET方法,没有Header、Body等概念。
+   - 请求响应都是纯粹的文本格式。
+
+2. HTTP/1.0:
+
+   - 于1996年发布,引入了许多现代HTTP的基本概念。
+   - 支持多种请求方法(GET、POST、HEAD等)和响应状态码。
+   - 引入了Header字段,使得请求和响应可以携带更多的信息。
+   - 支持多字符集、多部分发送、持久连接等特性。
+
+3. HTTP/1.1:
+
+   - 于1999年发布,是目前广泛使用的HTTP版本。
+   - 引入了持久连接(Keep-Alive)机制,复用TCP连接以减少建立连接的开销。
+   - 支持管道化(Pipeline)技术,允许客户端发送多个请求而无需等待响应。
+   - 引入了缓存管理、断点续传、虚拟主机等增强功能。
+   - 同时也有一些性能瓶颈和限制,如队头阻塞问题。
+
+   优化部分:
+
+   1. 通过缓存技术来避免发送 HTTP 请求
+   2. 减少 HTTP 请求的次数
+      * 将原本由客户端处理的重定向请求,交给代理服务器处理
+      * 将多个小资源合并成一个大资源再传输
+      * 按需访问资源
+
+   3. 通过压缩响应资源,降低传输资源的大小
+   4. 支持持久连接
+
+4. HTTP/2:
+
+   - 于2015年发布,基于Google的SPDY协议进行扩展。
+   - 引入了二进制分帧层(Binary Framing Layer),将消息划分为多个帧,实现并发处理和优先级控制。
+   - 支持头部压缩,减少数据传输量。
+   - 强制使用加密传输(TLS),提高安全性。
+   - 多路复用(Multiplexing)机制,允许在一个连接上同时发送多个请求和响应。
+
+5. HTTP/3:
+
+   - 正在进行中的开发,基于QUIC协议。
+   - QUIC是由Google开发的基于UDP的传输协议,旨在解决TCP的一些性能问题。
+   - HTTP/3将在传输层使用QUIC,提供更低的延迟和更好的性能。
+   - 增加了一些新特性,如0-RTT连接建立、流量控制、拥塞控制等。
+
+## 什么是RPC协议
+
+RPC(Remote Procedure Call)协议是一种用于实现分布式系统中不同计算机之间通信的协议。它允许一个计算机程序在网络上请求另一个计算机上的服务,就像调用本地函数一样,而不需要开发者显式处理网络通信细节。
+
+在 RPC 中,客户端程序发起请求,称为远程调用(remote call),而服务器程序提供服务并响应请求。客户端和服务器之间的通信过程对于开发者来说是透明的,就好像调用本地函数一样。
+
+一般来说,RPC 协议涉及以下几个要素:
+
+1. 客户端(Client):发起远程调用的程序。
+2. 服务器(Server):提供服务的程序。
+3. 通信协议(Protocol):客户端和服务器之间通信的规则和格式,定义了数据的传输方式和序列化协议。
+4. 接口定义(Interface Definition):客户端和服务器之间必须约定好接口和方法的定义,以便进行远程调用。
+5. 序列化(Serialization):将数据从对象的形式转换为字节流的过程,用于在网络上传输数据。
+
+> **虽然大部分 RPC 协议底层使用 TCP,但实际上它们不一定非得使用 TCP,改用 UDP 或者 HTTP,其实也可以做到类似的功能。**
+
+## RPC协议与HTTP协议的区别
+
+### 服务发现
+
+首先要向某个服务器发起请求,你得先建立连接,而建立连接的前提是,你得知道 **IP 地址和端口**。这个找到服务对应的 IP 端口的过程,其实就是**服务发现**。
+
+在 **HTTP** 中,你知道服务的域名,就可以通过 **DNS 服务**去解析得到它背后的 IP 地址,默认 80 端口。
+
+而 **RPC** 的话,就有些区别,一般会有专门的**中间服务**去保存服务名和IP信息,比如 **ZK**,甚至是 **Redis**。想要访问某个服务,就去这些中间服务去获得 IP 和端口信息。
+
+### 底层连接形式
+
+以主流的 **HTTP/1.1** 协议为例,其默认在建立底层 TCP 连接之后会一直保持这个连接(**Keep Alive**),之后的请求和响应都会复用这条连接。
+
+而 **RPC** 协议,也跟 HTTP 类似,也是通过建立 TCP 长链接进行数据交互,但不同的地方在于,RPC 协议一般还会再建个**连接池**,在请求量大的时候,建立多条连接放在池内,要发数据的时候就从池里取一条连接出来,**用完放回去,下次再复用**,可以说非常环保。
+
+![connection_pool](assets/ec5c8e28d3ea308c6db2ac991a12ea80.png)
+
+**由于连接池有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池**,比如 **Go** 就是这么干的。
+
+可以看出这一块两者也没太大区别,所以也不是关键
+
+### 传输的内容
+
+基于 TCP 传输的消息,说到底,无非都是**消息头 Header 和消息体 Body。**
+
+**Header** 是用于标记一些特殊信息,其中最重要的是**消息体长度**。
+
+**Body** 则是放我们真正需要传输的内容,而这些内容只能是二进制 01 串,毕竟计算机只认识这玩意。所以 TCP 传字符串和数字都问题不大,因为字符串可以转成编码再变成 01 串,而数字本身也能直接转为二进制。但结构体呢,我们得想个办法将它也转为二进制 01 串,这样的方案现在也有很多现成的,比如 **Json,Protobuf。**
+
+这个将结构体转为二进制数组的过程就叫**序列化**,反过来将二进制数组复原成结构体的过程叫**反序列化**。
+
+![序列化和反序列化](assets/dba2bc3af0938d2c087f85acc191fd3f.png)
+
+对于主流的 HTTP/1.1,虽然它现在叫**超文本**协议,支持音频视频,但 HTTP 设计初是用于做网页**文本**展示的,所以它传的内容以字符串为主。Header 和 Body 都是如此。在 Body 这块,它使用 **Json** 来**序列化**结构体数据。但是这里面的内容非常多的**冗余**,显得**非常啰嗦**。
+
+而 RPC,因为它定制化程度更高,可以采用体积更小的 Protobuf 或其他序列化协议去保存结构体数据,同时也不需要像 HTTP 那样考虑各种浏览器行为,比如 302 重定向跳转啥的。**因此性能也会更好一些,这也是在公司内部微服务中抛弃 HTTP,选择使用 RPC 的最主要原因。**

+ 155 - 0
面经/问答/集合.md

@@ -39,5 +39,160 @@ Java 中的 HashMap 底层原理使用数组和链表/红黑树实现,而不
 4. 如果一个节点是红色的,则它的子节点必须是黑色的。
 5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
 
+### 负载因子为什么是0.75
 
+而加载因子就是表示Hash表中元素的填满程度。
 
+> 加载因子 = 填入表中的元素个数 / 散列表的长度
+
+加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;
+
+加载因子越小,填满的元素越少,冲突发生的机会减小,但空间浪费了更多了,而且还会提高扩容rehash操作的次数。
+
+(泊松分布)选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择。
+
+## 解决冲突的方法
+
+### 1. 开放定址法
+
+```text
+Hi = (H(key) + di) MOD m,其中i=1,2,…,k(k<=m-1)
+```
+
+H(key)为哈希函数,m为哈希表表长,di为增量序列,i为已发生冲突的次数。其中,开放定址法根据步长不同可以分为3种:
+
+### 1.1 线性探查法(Linear Probing):di = 1,2,3,…,m-1
+
+简单地说,就是以当前冲突位置为起点,步长为1循环查找,直到找到一个空的位置,如果循环完了都占不到位置,就说明容器已经满了。举个栗子,就像你在饭点去街上吃饭,挨家去看是否有位置一样。
+
+### 1.2 平方探测法(Quadratic Probing):di = ±12, ±22,±32,…,±k2(k≤m/2)
+
+相对于线性探查法,这就相当于的步长为di = i2来循环查找,直到找到空的位置。以上面那个例子来看,现在你不是挨家去看有没有位置了,而是拿手机算去第i2家店,然后去问这家店有没有位置。
+
+### 1.3 伪随机探测法:di = 伪随机数序列
+
+这个就是取随机数来作为步长。还是用上面的例子,这次就是完全按心情去选一家店问有没有位置了。
+
+但开放定址法有这些缺点:
+
+- 这种方法建立起来的哈希表,当冲突多的时候数据容易堆集在一起,这时候对查找不友好;
+- 删除结点的时候不能简单将结点的空间置空,否则将截断在它填入散列表之后的同义词结点查找路径。因此如果要删除结点,只能在被删结点上添加删除标记,而不能真正删除结点;
+- 如果哈希表的空间已经满了,还需要建立一个溢出表,来存入多出来的元素。
+
+### 2. 再哈希法
+
+```text
+Hi = RHi(key), 其中i=1,2,…,k
+```
+
+RHi()函数是不同于H()的哈希函数,用于同义词发生地址冲突时,计算出另一个哈希函数地址,直到不发生冲突位置。这种方法不容易产生堆集,但是会增加计算时间。
+
+所以再哈希法的缺点是:增加了计算时间。
+
+### 3. 链地址法(拉链法)
+
+将冲突位置的元素构造成链表。在添加数据的时候,如果哈希地址与哈希表上的元素冲突,就放在这个位置的链表上。
+
+拉链法的优点:
+
+- 处理冲突的方式简单,且无堆集现象,非同义词绝不会发生冲突,因此平均查找长度较短;
+- 由于拉链法中各链表上的结点空间是动态申请的,所以它更适合造表前无法确定表长的情况;
+- 删除结点操作易于实现,只要简单地删除链表上的相应的结点即可。
+
+拉链法的缺点:需要额外的存储空间。
+
+从HashMap的底层结构中我们可以看到,HashMap采用是数组+链表/红黑树的组合来作为底层结构,也就是开放地址法+链地址法的方式来实现HashMap。
+
+### 4. 建立公共溢出区:将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中。
+
+## HashTable如何保证线程安全
+
+HashMap是非同步的,没有对读写等操作进行锁保护,是线程不安全的。
+Hashtable是同步的,所有的读写操作都进行了锁保护,是线程安全的。
+Hashtable的底层是**数组+链表**实现的
+
+简单一句话:加锁
+
+## ConcurrentHashMap1.7
+
+![image-20230726214326341](assets/image-20230726214326341.png)
+
+Java 7 中 `ConcurrentHashMap` 的存储结构如上图,`ConcurrnetHashMap` 由很多个 `Segment` 组合,而每一个 `Segment` 是一个类似于 `HashMap` 的结构,所以每一个 `HashMap` 的内部可以进行扩容。但是 `Segment` 的个数一旦**初始化就不能改变**,默认 `Segment` 的个数是 16 个,你也可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。
+
+### 初始化逻辑
+
+1. 必要参数校验。
+2. 校验并发级别 `concurrencyLevel` 大小,如果大于最大值,重置为最大值。无参构造**默认值是 16.**
+3. 寻找并发级别 `concurrencyLevel` 之上最近的 **2 的幂次方**值,作为初始化容量大小,**默认是 16**。
+4. 记录 `segmentShift` 偏移量,这个值为【容量 = 2 的 N 次方】中的 N,在后面 Put 时计算位置时会用到。**默认是 32 - sshift = 28**.
+5. 记录 `segmentMask`,默认是 ssize - 1 = 16 -1 = 15.
+6. **初始化 `segments[0]`**,**默认大小为 2**,**负载因子 0.75**,**扩容阀值是 2\*0.75=1.5**,插入第二个值时才会进行扩容。
+
+> 为什么在构造函数初始化s0?  
+>
+> 方便后期其他key落到不同的segment中,能够知道加载因子,和默认容量一些基本参数,就是相当于提供了一个模板
+
+### put方法逻辑
+
+1. 计算要 put 的 key 的位置,获取指定位置的 `Segment`。
+2. 如果指定位置的 `Segment` 为空,则初始化这个 `Segment`.
+
+    **初始化 Segment 流程:**
+
+    1. 检查计算得到的位置的 `Segment` 是否为 null.
+    2. 为 null 继续初始化,使用 `Segment[0]` 的容量和负载因子创建一个 `HashEntry` 数组。
+    3. 再次检查计算得到的指定位置的 `Segment` 是否为 null.
+    4. 使用创建的 `HashEntry` 数组初始化这个 Segment.
+    5. 自旋判断计算得到的指定位置的 `Segment` 是否为 null,使用 CAS 在这个位置赋值为 `Segment`.
+
+3. `Segment.put` 插入 key,value 值。
+
+### put方法低层逻辑
+
+1. `tryLock()` 获取锁,获取不到使用 **`scanAndLockForPut`** 方法继续获取。
+2. 计算 put 的数据要放入的 index 位置,然后获取这个位置上的 `HashEntry` 。
+3. 遍历 put 新元素,为什么要遍历?因为这里获取的 `HashEntry` 可能是一个空元素,也可能是链表已存在,所以要区别对待。
+4. 如果这个位置上的 **`HashEntry` 不存在**:
+
+    1. 如果当前容量大于扩容阀值,小于最大容量,**进行扩容**。
+    2. 直接头插法插入。
+
+5. 如果这个位置上的 **`HashEntry` 存在**:
+
+    1. 判断链表当前元素 key 和 hash 值是否和要 put 的 key 和 hash 值一致。一致则替换值
+    2. 不一致,获取链表下一个节点,直到发现相同进行值替换,或者链表表里完毕没有相同的。 
+       1. 如果当前容量大于扩容阀值,小于最大容量,**进行扩容**。
+       2. 直接链表头插法插入。
+
+6. 如果要插入的位置之前已经存在,替换后返回旧值,否则返回 null.
+
+### 扩容
+
+`ConcurrentHashMap` 的扩容只会扩容到原来的两倍。老数组里的数据移动到新的数组时,位置要么不变,要么变为 `index+ oldSize`,参数里的 node 会在扩容之后使用链表**头插法**插入到指定位置。
+
+## ConcurrentHashMap1.8
+
+![Java8 ConcurrentHashMap 存储结构(图片来自 javadoop)](assets/java8_concurrenthashmap.png)
+
+### 初始化逻辑
+
+* `ConcurrentHashMap` 的初始化是通过**自旋和 CAS** 操作完成的
+
+### put方法
+
+1. 根据 key 计算出 hashcode 。
+2. 判断是否需要进行初始化。
+3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
+4. 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。
+5. 如果都不满足,则利用 synchronized 锁写入数据。
+6. 如果数量大于 `TREEIFY_THRESHOLD` 则要执行树化方法,在 `treeifyBin` 中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树。
+
+> cas使用时候:没有发生冲突的时候
+> synchronized使用:index发生冲突的时候
+
+### get方法
+
+1. 根据 hash 值计算位置。
+2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
+3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
+4. 如果是链表,遍历查找之。

+ 20 - 1
面经/项目/中煤项目.md

@@ -175,4 +175,23 @@ During my time at university, I actively participated in the CMDB (Coal Mine Big
 
 During my internship at NCSE. I worked as a software development intern and actively contributed to the development of a traffic analysis project. Specifically, my responsibilities focused on the development of the parsing module.
 
-As part of my regular work and studies, I maintain a personal blog to document my experiences, enabling me to effectively summarize and share my knowledge. The link to my personal blog can be found on my resume.
+As part of my regular work and studies, I maintain a personal blog to document my experiences, enabling me to effectively summarize and share my knowledge. The link to my personal blog can be found on my resume.
+
+
+
+
+
+### 传输
+
+8C32G
+
+每秒5000,基本上每秒10M数据,具体矿上正常运行4天,一共是80G数据
+
+10条数据是1KB
+
+低峰每秒3000
+
+
+
+
+

+ 62 - 0
面经/项目/项目.md

@@ -0,0 +1,62 @@
+# 中煤项目
+
+## 传输模块
+
+要点:生产者端使用单topic单partition,保证顺序消费,重试次数设置为0,ACK设置为1。消费端设置线程池去多线程消费,使用redis记录偏移量,手动提交ACK
+
+使用list记录数据量
+
+项目演化过程:
+
+直接看监控,JVM监控,grafana
+
+* kafka挂了:主要原因是kafka线程连接数量过多,kafka报错,直接增大网络线程数和I/O线程数,优化监控模块连接复用
+* kafka挂了:CPU压力过大,集群压力过大,增大与ZK的默认响应事件zookeeper.connection.timeout.ms=60000
+* 消费乱序:消费者因为触发reblance导致消费乱序,原本的partition-2分区给了消费者-02,因为存储到一个list里面导致乱序
+* 消息堆积:消费者传输的消息体过大,导致传输延迟过大,直接减少传输的消息,改为多批次发送。问题得到缓解
+* 消费者挂:因为该消费者组压力过大,消费者内存达到2G,各个节点流量不一样,导致该节点压力过大。解决直接进行单topic单partition
+* 消息挤压:单topic单partition导致消费能力不足,使用线程池进行消费
+* 重复消费:因为消费者重启导致的,使用redis可以解决
+* CPU暴涨:使用线程池导致消费问题,具体解决方法。优化线程池参数减小=
+* 项目无法启动,报错:error,**文件描述符**,默认是1024,改到最大值65535,用户过多,调整一下
+* redis挂了怎么办:直接重试,三次就跳过
+* kafka消费者挂了怎么办:使用定时任务直接重启
+* websocket:因为nginx的原因,定时ping/pong
+* 内存过大:TOP命令,主要是kafka和hbase的配置问题
+
+BUG:
+
+* JMX监控问题:使用JMX导致无法连接,开放端口
+* JVM堆内存已满:**VisualVM**分析,因为hbase配置文件创建不当,改为单例模式
+* beanutils问题:由于名称原因,导致无法注入
+
+```java
+public static String decapitalize(String name) {
+    if (name == null || name.length() == 0) {
+        return name;
+    }
+    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
+        Character.isUpperCase(name.charAt(0))){
+        return name;
+    }
+    char chars[] = name.toCharArray();
+    chars[0] = Character.toLowerCase(chars[0]);
+    return new String(chars);
+}
+```
+
+* 线程池无法调用问题:自定义策略问题
+
+CPU标高:TOP命令
+
+# 实习项目
+
+要点:使用责任链模式进行判断,shortcut的功能,区分TCP和UDP,使用策略模式进行选择。选择合适的进行解析。使用bytebuffer进行解析。封装工具类,
+
+分库分表,16个表4*4,大概就是每个设备1秒10条消息。一共一天80万条数据。
+
+使用 哈希散列算法 分治路由
+
+* batch insert
+* 导致事务注解失效
+* SPI