# Mysql ## Mysql的索引类型有那些 1. ### `从数据结构` - **`B+树索引`** - 适用于**范围查询** Between 和 **精确查询** in - 支持有序数字的查 排 聚合操作 是**Mysql的默认索引** - **`哈希索引`** - 基于哈希表的结构 - **不适用于范围查询 **但是适用于精确查询 & **查的非常快** - **`倒排索引`** ->全文索引 - **把“文档→单词”的形式变为“单词→文档”的形式** - **`R-树索引`** ->多维空间树 - 为空间(多维)查询准备的 - 计算地理位置 区域查询 2. **```从InnoDB B+树来看```** - **`聚簇索引`** - 也叫**主键索引** - 名字由来-> 索引的叶子节点存储完整的数据行数据 -翻译->索引的结构和数据的物理存储是**深度绑定**的 —-—> 索引的叶子节点本身就是数据行的物理存储位置。 - 拿到了主键也就拿到了 这个数据所有 - 索引的叶子节点存储的是数据行 可以直接访问所有数据 - 适合 范围查 &排序 - **`非~ `** -->>二级索引 - 仅仅和主键绑定 拿到了也只是拿到了主键和非~的 数值 想要找到其他的字段 你还要根据主键找(这一步也叫回表) 3. **`索引的性质`** - **`普通索引`** - **非主键**&**非唯一**索引 - ```mysql ALTER TABLE `table_name` ADD INDEX index_name ( `column` ) ``` - **`联合索引`** - 多个列合在一起的普通索引 - 为了提高多个查询条件的性能 - 要按照顺序 - ```mysql ALTER TABLE `table_name` ADD INDEX index_name ( `column1` ,`column2` ) ``` - **`主键索引`** - 每一行数据都有唯一的主键 - 每个表只能有一个 - 不能为NULL - ```MYSQL ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` ) ``` - **`唯一索引`** - 索引中的值是唯一的 防止数据重复 - 可以为NULL - ```mysql ALTER TABLE `table_name` ADD UNIQUE (`column`) ``` - **`全文索引`** 写过了 - ```mysql ALTER TABLE `table_name` ADD FULLTEXT index_name (`column`) ``` - **`空间索引`** 通常是R-树结构 - ```mysql CREATE SPATIAL INDEX idx_location ON places(location); ``` ​ ### 补充 #### 关于倒排索引 我们一般搜索文章 都是通过 文章名字 ->找里面的单词 这种结构,但是如果我们想要通过文章的内容(一篇文章里面有苹果这个词)我们如何找到对应的文章???? 很简单就是**倒排索引** 这个索引将原先的 (各位可以想象成Map的KV数值)原先的存储方式是K(文章)V(苹果) 但是现在不一样了 我们改变了KV的内容 将一个K(苹果)V(包含苹果的文章1,2,3....) 冷知识 (AI询问 正确与否代考就)未建索引时,搜索 “苹果” 需要逐篇扫描所有文章(全表扫描),时间复杂度是 **O (N)**(N 为文档总数),数据量大时会非常慢;而倒排索引通过 “关键词→文档列表” 的直接映射,查询时只需定位到关键词对应的倒排列表,时间复杂度接近 ***O (1)(定位关键词)+ O (M)(M 为匹配的文档数)***,M 通常远小于 N,因此速度会极大提升。 #### 为什么使用B树 B-树是一种**多路自平衡的搜索树** 它类似普通的平衡二叉树,不同的一点是B-树允许每个节点有更多的子节点。B-Tree 相对于 AVLTree 缩减了节点个数,使每次磁盘 I/O 取到内存的数据都发挥了作用,从而提高了查询效率。I/O渐进复杂度为O(h) 树非常矮,通常为 3~4 层,能存放千万到上亿的排序数据。树矮意味着访问效率高,从千万或上亿数据里查询一条数据,只用 3、4 次 I/O。 ## Mysql的 存储引擎有哪些?? 1. InnoDB - 支持事务/行级锁&外键 - 提供包并发性能 适用于高附载的OLTP(联机事务处理)应用 - 数据以聚集索引存储 提高检索效率 2. MyISAM (早期的存储引擎 已逐渐被 InnoDB ) - 不支持事物和外键 使用表级锁 - 适合读取多 更新少 ->>数据仓库 - 较高的读取性能 和 快的 表级锁定 3. MEMORY - 存在内存中 快 但是重启丢失 - 适用于临时数据|快速缓存 4. NDB - 高可用性和数据分布 适合大规模分布式应用 - 行级锁&自动分区 5. ARCHIVE - 适用于存储大量历史数据 支持高效的插入&压缩 - 不支持索引 适合日志数据的存储 ### 补充 #### 什么是聚集索引存储 为什么就提高了检索效率 就是聚簇索引 ## 为什么用Mysql作为B+树的索引 1. 高效查找 - B+树是一个AVL树 每个叶子节点到根节点的长度相同 插入、查找、删除的时间复杂度为**O(logN)** 2. 树的高度增长不快,使得磁盘的I/O次数减少 - B+树是一个多叉树,*** 非叶子节点仅仅保存主键或索引值和页面指针***,使得每一页都能容纳更多的记录 因此内存放的索引就多 更容易命中 3. 范围查询能力强 - B+树适合范围查询,因为叶子节点通过**链表链接** ### 扩展 1. B树的每个节点都存储的是完整的数据 而B+树则是存储的key&指针 完整的数据存储在叶子节点上,因此B+树可以存放更多的索引页 减少磁盘查询次数。 2. B+树叶子组成了链表 而B树智能化每一层查找 3. B+树查找的平均时间更加稳定 B树因为非叶子节点就保存了数据因此 返回的快慢取决于节点到根的距离 4. 为什么不用二叉树? - 可能会成为斜树 --> 一个链表 索引失效 5. 为什么不用红黑树? - 红黑树毕竟还是二叉树,一个结点点最多拥有两个直接子结点,当我们插入大量数据的时候,会导致的树的高度过高,I/O渐进复杂度为O(h)。所以还是没有有效的减少查找过程中磁盘I/O的存取次数。 ## 索引的最左前缀匹配原则是什么? 1. 索引的底层是一颗B+树,那么联合索引的底层也就是一颗B+树,只不过联合索引的B+树节点中存储的是键值。由于构建一棵B+树只能根据一个值来确定索引关系,所以数据库依赖联合索引最左的字段来构建。 2. 只有联合索引才能谈最左前缀匹配 3. 查询的顺序也是 按照从左到右的顺序查询 所以 - **原则**:联合索引必须从最左列开始匹配,且列之间连续。 - **范围条件的影响**:范围条件会阻断后续列的索引使用。 但是 如果 ```mysql ALTER TABLE staffs ADD INDEX index_staffs_nameAgePos(name,age,pos); explain select *from staffs where name='z3'and age=22 and pos='manager'; explain select *from staffs where pos='manager' and name='z3'and age=22; explain select *from staffs where age=22 and pos='manager' and name='z3'; ``` 这样**三个互换了顺序** ***也会进行联合索引*** 因为最主要是因为MySQL中有查询优化器***explain,***所以sql语句中字段的顺序不需要和联合索引定义的字段顺序相同,查询优化器会判断纠正这条SQL语句以什么样的顺序执行效率高,最后才能生成真正的执行计划 **部分索引顺序** 如果缺胳膊少腿(有最左的 但是中间的可能少了),也是按照正常的索引 即使跳过了中间的索引,也是可以使用到索引去查询 但是如果只查最后的索引(没有大哥了)直接整个表的查询了 **模糊索引** 类似模糊索引就会使用到like的语句 如果复合最左前缀的话,会使用到range或者是index的类型进行索引 **范围索引** 遇到范围索引就会停止 ## 三层B+树结构 B+树的默认数据页的大小为**16KB** - 每个节点页的大小为16KB - 假设每个数据结构的主键和数据大小为1KB - 每个内部节点 存储的是指向子节点的指针和索引键 - 第一层 根节点 指向1170个中间节点 - 第二层 假设每个指针是6字节 +8字节的索引键(bigint)=14字节 (一个节点的大小) 那么每个中间节点可以指向1170个叶子节点 **16KB/14Byte=1170**个 - 每个叶子节点可以存储16条数据结构 ## 什么是回表 上文已经提到 由于在非聚簇索引中 索引存储的值 是索引字段的值和对应的主键的值 而并没有数据 所以当拿到这个索引的时候需要返回表中 重新去取叶子节点的数据 这就是**回表** ``回表会带来随机IO`` 因为这个索引可能是无序的 所以查起来不好查 只能随机查询![image-20251024143700377](F:\记录\八股文\image-20251024143700377.png) ## 索引失效 - 联合索引却不符合**最左前缀** - 索引使用了计算 - 索引上函数 - like的随意使用 - or的随意使用 - 只有name有索引但是却使用了 select * from user where name="xxx" or id="xxx" - 随意字段的使用 - 也就是要查询的字段和匹配的字段不是一个类型 那么myslq就会做转换 **隐式字符编码转换** - 参数不同 - 表中两个不同的字段进行比较 - select * form user where id>name - 使用了ORDER BY - order by 后面如果不是**主键** 或者 不是**联合索引 **那么就不会走索引 神奇的地方就是 mysql查询都是按照索引查询的 但是问题就是这个索引是否生效 (有意思) ## 什么时候要建立索引 - 索引不是越多越好 毕竟建立索引需要空间 而且每次修改的时候还要额外去维护 - 有大量重复的地方 不建议上索引 - 长字段不要上索引 `but个人建议` 也是一个小tip 如果你做的是文章网站 那么好像 大概 可以给文章上倒排索引 - 当修改频率》》》查询 还是那句话**每次修改的时候还要额外去维护** - 通过建立联合索引 减少单个索引的建立 毕竟管理一个表 总比管理一批表合理(笑) ## 索引越多越好? 空间角度 你每次建立一个索引 就会建立一个索引表 一般来说一个索引表的大小就是16KB image-20251025125111136 时间角度 每次你写入一个数据 是不是需要在表中增加 这个插入要不要时间 ?? 你加完了 树的结构要改变 这个 为了维持B+树所作出的变形需不需要时间?? 而且 你mysql使用索引的时候是不是需要通过评估器来评估哪个索引更好 是不是需要时间 ## 如何使用explain语句进行分析 ![image-20251025163127819](F:\记录\八股文\image-20251025163127819.png) - `id` 就是查询的执行顺序的标识符 数字越大优先级越高 - *select_type* 查询的类型 有simple primary subquery - table 查询的数据表 - type 访问的类型 比如ALL index range 等等 一般来说 性能从好到垃圾是**const > eq_ref>ref>range>index>ALL** - possible_keys 可能用到的索引 *key* `实际用到的索引` - key_len 索引长度 - ref 索引的哪一列被使用 - *rows* 估计要扫描的行数 数值越小越好 - filtered 希纳是查询条件过滤掉行的百分比 - *Extra* 额外信息 Using + index --》 使用覆盖索引 +where 条件过滤 +temporary 临时表 + filesort 额外的排序步骤 image-20251025163843482 ## 如何进行mysql的优化 1)合理设计索引,利用联合索引进行覆盖索引的优化,避免回表的发生,减少一次查询和随机 I/O 2)避免 SELECT *,只查询必要的字段 3)避免在 SQL 中进行函数计算等操作,使得无法命中索引 4)避免使用 % LIKE,导致全表扫描 5)注意联合索引需满足最左匹配原则 6)不要对无索引字段进行排序操作 7)连表查询需要注意不同字段的字符集是否一致,否则也会导致全表扫描 除此之外,还可以利用缓存来优化,一些变化少或者访问频繁的数据设置到缓存中,减轻数据库的压力,提升查询的效率。 还可以通过业务来优化,例如少展示一些不必要的字段,减少多表查询的情况,将列表查询替换成分页分批查询等等。 补充的话就是三点: 命中索引 减少IO 减少回表 ## B+树查询的全过程 先从根节点找起 然后根据数据键与节点中的**索引值(二分)**然后找分支 叶子节点存储的数据行记录 但是一页有**16KB**的大小 存储的数据行不止一行 叶子节点中的**数据行** 利用页目录结构 利用二分 找到对应的组 定位后 利用链表找到对应的行 ### ## mysql中的count小知识 count(1)==count(0) 统计所有行数 **(包括null)** count(字段名)--》返回字段的行数 **不包括null** ### char&varchar char **定长** 当不满的时候会用空格填满 varchar **变长** 需要记录额外长度 1-2字节 支持的最大长度是65535 如果值为NULL 需要一个额外的1bit进行标记 双路排序 select字段多 放在StringBuffer里面 进行排序后 返回给客户端 单路排序 select字段少 放在StringBuffer里面 ## Mysql是如何实现事务的 - 锁 主要服务于**隔离性** - 利用`锁的机制` 使用数据并发修改的控制 来满足事物的`隔离性` - Redo Lock 主要服务于**持久性**,同时辅助原子性 - 重做日志 主要就是`记录数据库的所有操作` 当mysql崩溃的时候 就重放一遍redolock 就可以恢复 - Undo Lock回滚日志 主要服务于**原子性**,同时辅助隔离性(MVCC) - `保存数据的历史版本` 用于事物的回滚 使得事务执行失败以后能够回到之前的样子 实现原子性 - MVCC 多版本并发控制 主要服务于**隔离性** - 满足了非锁定读的需求 提高并发度 实现`读已提交`和`可重复读` 两中隔离级别 实现了事务的隔离性 `MySQL 事务的实现是**锁、Redo Log、Undo Log、MVCC 等机制协同作用的结果**:` - ### 什么是事务? - 事务就是用户定义的一系列执行SQL语句的操作, 这些`操作要么完全地执行,要么完全地都不执行`, 它是一个不可分割的工作执行单元。 **事物的四大特性** - 原子性(Atomicity) - 个事务中的所有操作要么全部提交成功,要么全部失败回滚 - 一致性(Consistency) - 数据库总是从一个一致性的状态转换到另一个一致性的状态 - 隔离性(Isolation) - 通常来说,一个事务所做的修改操作在提交事务之前,对于其他事务来说是不可见的。 - 持久性(Durability) - 一旦事务提交,则其所做的修改会永久保存到数据库。 ## 什么是MVCC 多版本并发控制 **是一种并发控制机制** 允许多个事务`同时读取`和`写入数据` 无需等待 ### 这么做的? 1. 数据库为每一个事物创建一个数据快照。每当数据被修改的时候,mysql不会立刻覆盖原有的数据,而不是生成新版本的记录。每个记录都保留了对应的版本号和时间戳 2. 多个版本串联起来形成了一条版本链 这样可以不同时刻启动的事务可以`无锁`的获得不同版本的数据。 3. 写操作可以继续 无非就是创建新的数据版本 ### 其实并不是存了很多个版本的数据 而是通过undolock的`保存数据的历史版本`来保留所有的版本 通过undolock来回溯到上面的版本 只是借助 undolog 记录每次写操作的反向操作 ## Mysql的日志类型 ### binlog 逻辑日志 二进制日志文件 - 用于记录mysql中的所有的DDL&DML - 在事务提交后产生的 所以可以用来回复数据库 - 记录的是操作 - 因此他可以跨平台使用 ### redolog 物理日志 - 用于恢复数据 保持数据的`一致性`和`持久性` - 而且只是 保持数据的`一致性`和`持久性` - 记录的是数据页的修改 ### undolog 回滚操作 - 作用 - 事务回滚 - MVCC支持 :通过版本链实现多版本的并发控制 image-20251027164300265 ## Mysql的事务级别 ### 读未提交 最低级的 可以读到其他事务为提交的数据 可能产生`脏读` ### 读已提交 可以读取其他事务已经提交的数据 但是可能会产生 `不可重读 ` ### 可重复读 确保数据在多个查询下的返回是一样的 但是可能会产生`幻读` mysql的默认的隔离级别 ### 串行化 并发执行的SQL事务的操作 其效果与这些SQL事务按某种顺序串行执行的效果相同 串行执行:sql事务在开启下一个事务之前要完成全部的操作 ## Mysql中有哪些锁 1. **行级锁** - 仅仅对特定的`行`加锁,允许其他事务**并发`访问`不同的行**,适用于高并发场景 2. **表级锁** - 对整个表枷锁 其他事物**不能**对表进行任何**读写操作** 适用于保证完整性的小型表 3. 意向锁 - 表锁 表示某个事物对某行数据加锁的意图,分为意向共享锁和意向排他锁,主要用户行级锁的与表级锁的结合 4. **共享锁** - 允许事物**并发的读取同一资源** **不能修改** 智能释放锁后 才能获得锁 5. **排他锁** - 只允许**一个事务**对资源进行**读写** 其他只有拿到锁才可以 6. 元数据锁 - 用于保护数据对象的元数据 防止DDL操作时其他事务对这些对象进行修改 7. **间隙锁** - 针对索引中的两个记录之间的间隙枷锁 防止其他事务在间隙中插入数据---》防止幻读 8. **临键锁** - 行级锁+间隙锁 锁定具体的行和前面的间隙 确保在一个范围内不会出现幻读 9. 插入意向锁 - 等待间隙的锁 用于指示事务在某个间隙中插入记录 允许其他事务进行共享锁 但在插入时会组织其他锁 10. 自增锁 - 插入自增列时枷锁 保证自增的唯一性 ## ## Mysql事务的二阶段提交的是什么 为了确保**redolock binlog**之间的一致性 使用的一种机制。 Mysql通过二阶段提交来保证在**crash recovery** 崩溃恢复 时 不会出现**数据丢失**和**数据不一致**的情况 ### 二阶段的两个阶段 - `准备阶段` 在事务提交时,Mysql的InnoDB引擎会先写入**redolog 将其状态标记为prepare**,表示事务已经准备提交但是还未真正完成 此时的redolog是预提交状态 还未标记为完成提交 - `提交阶段` redolog标记成prepare的时候 mysql server 会写入binlog 写入成功后 mysql会通知innodb 将redolog状态改为commit 完成整个事务的提交过程 ## Mysql如果发生死锁会怎么办 #### 自动检索与回滚 MySQL自带死锁检测机制 ,当检测到死锁时,数据库会自动回滚到其中一个事务,以解除死锁。通常会回滚到事务持有的资源最少的那时候。 ### 可以手动的kill发生死锁的语句 可以通过命令 来手动kill它 ```mysql SHOW ENGINE INNODB STATUS;//展示执行的事务和事务相关的锁 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;//获取事务id SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;//获取线程id KILL ;//手动中止事务 ``` ## Mysql是如何解决深度分页问题 什么是深度分页? 数据量很大的时候,按照分页访问后面的数据。 比如 ```mysql `SELECT * FROM mianshiya WHERE name = 'yupi' ORDER BY id LIMIT 99999990, 10;` ``` 这样的一条查询语句, 可以优化成: ```mysql sql SELECT * FROM mianshiya WHERE name = 'yupi' AND id >= ( SELECT id FROM mianshiya WHERE name = 'yupi' ORDER BY id LIMIT 99999990, 1 ) ORDER BY id LIMIT 10; ``` name 有索引的情况下,这样的查询直接扫描 name 的二级索引,二级索引的数据量少,且在子查询中能直接得到 id 不需要回表。将子查询得到的 id 再去主键索引查询,速度很快,数据量也小。 **如果直接扫描主键索引的话,数据量就比较大**,因为**主键索引包含全部的数据**。 当然上面的 SQL 改成 Join 也行,本质上是一样的。 比如 ```mysql select * from mianshiya inner join (select id from mianshiya where name = 'yupi' order by id limit 99999990,10) as mianshiya1 on mianshiya.id = mianshiya1.id ``` 其实本质上就是先查找在排序 而不是直接写成一句话 导致一边查找一边排序 方法二 记录id 每次分页返回当前最大的id 然后下次查询的时候 带上这个id 就可以利用id》maxid 过滤了 这种方式只适合 连续查询的时候 如果跳页就失效了 ## 什么是mysql的主从机制?是如何实现的? 主从机制 就是mysql的一种数据复制技术 将主数据库上面的数据同步到一个或者多个从服务器中 主要是用binlog(用于记录mysql中的所有的DDL&DML ) 把主数据库的binlog放到其他的数据库中 让其他的数据库重放就行了 ### 具体流程 1. 线程创立 --> 服务器开始主从机制后 会创建i/o sql线程 2. 连接建立 -->服务器io线程与主服务器建立连接,主服务器的binlog dump线程与之交互 3. 同步位置告知 --> 服务器的io线程 告诉主服务器的dump线程从何处开始接受binlog 4. 主服务器操作 --> 主服务器更新时 将更改的记录保存到binlog中 5. binlog传输 --> 主服务器的dump线程检测到binlog变化 从指定的位置读取 由从服务器io线程拉取 采用 6. 中继日志存储 --> 从服务器的io线程中 将接受的内存保存到relaylog中 7. 数据写入 --> 从服务器的sql线程读取relaylog内容 解析成具体操作后写入自身数据表 image-20251030165732479 ### 主从复制的类型 - 异步复制 主库不需要从库中相应 - 同步复制 主库同步等待所有的库确认收到数据 - 半同步复制 主库等待至少一个库收到数据 ## 主从同步延迟怎么处理 首先确定一点 这个延迟啊 无法避免只能尽力减少 - 二次查询:如果库查不到数据 那么就去主库查一遍 但是就把压力给到主库了 有点违法主从原则了 - 强制将写之后立马读的操作转移到主库上: 将代码写死了 比如一些写入之后立马查询的操作,绑定在一起,写死都走主库 - 关键业务读写都走主库 : - 使用缓存: 主库写入后同步到缓存中 这样查询时就可以先查询缓存,避免了延迟的问题 - 提升从库配置:这没什么可解释的 金钱的力量 # Redis ## Redis常见的数据结构 - String 最大长度是512MB - 缓存:存session - 计数器:统计访问量 - Hash 底层是哈希表 - 键值对,存储对象的属性。适合小规模数据 - 商品详情 - List 双向链表 - 消息队列 - 历史消息 - Set 无序且不重复 用哈希表实现 - 标签系统 - 唯一用户集合 - Sorted Set - 有序集合 但是每个集合都有一个分数(score)用于排序 底层是跳表实现 ## 高级的 - BItmap - 用位为单位存储的数据的高效的方式 - 用户在线 - HyperLongLong - 主要用于估算基数 适合估算网站的独立的用户数量 - GEO - 存储地理信息的位置 - Stream - 日志数据结构 支持高效的信息生产和消费模式 - 具有持久性和序列性化特性 - 消息队列 ## Redis为什么这么快 数据存储在内存 优秀的线程模型&IO模型 - redis采用了IO多路复用技术 - redis采用的单线程来执行命令 无需线程的切换 避免了 上下文切换所带来的开销 高效的数据结构 ## Redis为毛使用单线程?6.0以后又引入多线程 单线程的原因是因为 1. Redis的操作是基于内存的 其大多数操作的性能瓶颈 不是CPU导致的 2. 使用单线程模式 减少线程上下文切换所带来的性能开销 3. redis在单线程的情况下,使用IO多路分用模型 就可以提高redis的IO利用率 6.0使用多线程的原因是因为 随着数据规模的增多 请求量增多 Redis的瓶颈主要存在于网络IO,引入多线处理,可以提高网络IO处理速度 ## Redis的跳表的底层原理 多路链表 底层保留所有的元素 每一层链表都是下一层的子集 image-20251106152729662 ## Redis中的Hash 是一种键值对集合 可以将多个字段和值存储在一个键中 以便于管理一些数据 适合存一些小数据 支持快速的增删改查 非常适合存储对象的属性 ## Redis Zset的实现原理 是一种`跳表`+`哈希表` 跳表 -->存储数据结构的排序和快速查找 哈希表 -->成员与分数的映射,提供快速查找 当Zset的元素过于少的时候 将使用``zip list`` 来节省内存 - 元素个数<128 - 元素成员名和分值长度<64字节 ## 如何保证redis和Mysql的数据一致性 1. 先更新缓存 再跟新数据库 2. 先更新数据库 再更新缓存 3. 删缓存 更新数据库 等后续查询数据库把数据回种到缓存 以上都不建议 1. 先更新数据库 再删缓存 等后续查询数据库把数据回种到缓存 2. **缓存双删策略** 更新数据库之前先删一次缓存。更新换数据库以后 再延迟删缓存 3. 使用**binlgo异步更新**缓存,监听到数据库的binlog的变化,通过异步的方式更新Redis缓存 ## 击穿 穿透 雪崩 ### 击穿 热点数据失效 导致大量的请求直接访问数据库 **解决方案**: 1. 热点数据永不过时 -->比如双十一的时候 就可以把一些热点的关键词 给设置一个不可能过期的时间 2. 热点数据预热 --> 预加载 3. 动态缓存 --> 使用互斥锁,确保一个时间只能由一个请求去查询数据库查询并且更新缓存 **穿透**:查询一个不存在的数据 然后每次都查询 1. 分布式锁 2. 即使是一个空的key 也存入redis中 **雪崩**:多个缓存数据在同一时间内失效 导致大量的请求同时访问数据库 1. 采用随机过期时间策略 2. 双缓存机制 ## String的底层逻辑 SDS结构,并结合int,embstr,raw等不同的编码实现的 根据长度不同编码的也不同 以44字节为节点 分embstr和raw ## 分布式锁 1. set en nx命令+Lua脚本 - 加锁&解锁 2. 集群模式下 使用redlock ## 实现分布式锁的问题 1. 业务未到期 ,锁失效 2. 单点故障 3. 主从不重复的问题 4. 网络分区的问题 5. 时钟漂移的问题 6. 锁的可重复入的问题 7. 误解放锁的问题 ## Redis持久化机制 - RDB快照 通过某一时刻的数据快照来实现持久化,可以在特定时间间隔内保存数据的快照 适合灾难恢复和备份 能生成紧凑的二进制文件 但是可能在崩溃后丢失最后一次快照 - AOF日志 AOF通过将操作写道日志中 来实现持久化,支持将所有的写操作记下来以回复 文件更加准确 但是文件比较大 重写时会使用更多的资源 ## 主从复制的实现原理 复制流程 - 开始同步 - 从节点通过主节点发送PSYNC命令发起同步 - 全量复制 - 如果第一次链接或者之前的连接失效,从节点请求全量复制,主节点会将当前的数据快照 发送给从节点 - 增量复制 - 全量复制完成后,主从之间回保持一个长连接,然后主节点会通过这个连接 将后续的读写操作传递给从节点 主从架构 就是 主操作写入 从节点来读取 ## 数据过期后的删除策略是什么 - 定期删除 - 定期(1000ms默认)会随机检查一定数量的键,如果发现过期键 就删除 - 主要应用在后台持续清理过期数据 - 惰性删除 - 在每次访问的键的时候,redis检查键是否过期,过期就删掉。这种策略保证了在使用过程中 只删除不要的数据,不访问就不删除。 ## 如何解决热点key的问题 - 热点key的拆分 比如在key的前面加一些前缀 然后放到不同的实例上 - 多级缓存 (CDN 本地缓存) - 读写分离 通过主从复制 将请求分到其他的节点 - 降级和限流 在热点key过高的时候 应该采取限流的方法 减少对于redis的请求 必要时要使用降级的数据或者空值 ## 集群的实现原理是什么 采用hash slot 机制来分配数据 将整个空间分为16384个槽 每个redis负责一定范围的哈希槽 数据的key经过哈希计算后 分配到对应的节点 ## Big key问题 Redis 中的 "big Key" 是指一个内存空间占用比较大的键(Key),它的危害如下: - 内存分布不均。在集群模式下,不同 slot 分配到不同实例中,如果大 key 都映射到一个实例,则分布不均,查询效率也会受到影响。 - 由于 Redis 单线程执行命令,操作大 Key 时耗时较长,从而导致 Redis 出现其它命令**阻塞**的问题。 - 大 Key 对资源的占用巨大,在进行网络 I/O 传输时,会导致获取过程中产生的**网络流量较大**,进而产生网络传输时间延长甚至网络传输阻塞的现象,例如一个 key 2MB,请求 1000 次就会产生 2000 MB 流量。 - 客户端超时。因为操作大 Key **时耗时较长**,可能导致客户端等待超时。 ### 如何解决 开发方面: - 对存储的数据进行压缩 - 将大的key分成一些小的对象 - 使用适合的数据结构 业务层面: - 可以根据实际情况 减少存储的内容 - 优化业务逻辑 数据分布方面: - 采用redis集群方式 进行redis的部署 然后将大的key拆分到不同的服务器上面 # 设计模式 ## 单例模式哪几种实现方式 ?如何保证线程安全 - 饿汉式 --> 类加载的时候就加载 - ```java public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } } ``` - 懒汉式 --> 用到才创建 - ```java public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` - 双重检查锁定 --> 懒汉式+ 只在第一次检查实例为空的时候加锁 - ```java public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` - 内部静态类 --> 利用类加载机制 实现懒加载和线程安全 - ```java public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } ``` - 枚举单例 --> 通过枚举实现单例 简单防止反射和序列化攻击 ```java public enum Singleton { INSTANCE; public void bizMethod() { // 一些业务逻辑方法 } } //使用 Singleton singleton = Singleton.INSTANCE; singleton.bizMethod(); ``` ## 策略模式 一种行为设计模式,将每种算法封装起来,让他们可以相互替换,让算法独立于使用的客户端而变化 ### 特点 - 算法封装 - 动态替换 在运行时选择和替换算法 - 遵循开闭原则 ### 组成 - 策略接口:定义算法的通用接口 - 具体策略:实现算法的具体实现 - 上下文类:持有策略接口的使用,调用具体策略的方法 ## 模板方式 抽象类定义了一个算法骨架,具体的步骤由子类提供 - 抽象类 定义模板 - 具体类 实现抽象方法 - 模板方法 调用一系列步骤方法 ```java // 抽象类 abstract class DataProcessor { // 模板方法 public final void process() { readData(); processData(); writeData(); } protected abstract void readData(); // 读取数据 protected abstract void processData(); // 处理数据 protected void writeData() { // 写入数据 System.out.println("Writing data to output."); } } // 具体实现类A class CSVDataProcessor extends DataProcessor { @Override protected void readData() { System.out.println("Reading data from CSV file."); } @Override protected void processData() { System.out.println("Processing CSV data."); } } // 具体实现类B class JSONDataProcessor extends DataProcessor { @Override protected void readData() { System.out.println("Reading data from JSON file."); } @Override protected void processData() { System.out.println("Processing JSON data."); } } // 客户端 public class Main { public static void main(String[] args) { DataProcessor csvProcessor = new CSVDataProcessor(); csvProcessor.process(); DataProcessor jsonProcessor = new JSONDataProcessor(); jsonProcessor.process(); } } ``` ## 常见的设计模式 1. 单例模式 - 保证系统中对象只有一个实例 - 连接池 全局配置 2. 简单工厂 - 获取不同对象的时候可以使用 - 将对象的创建逻辑抽离复用 3. 策略 - 封装一组算法让他们之间能够相互替代 能有效避免if sles - 选择支付策略 4. 模板 - 提炼核心流程封装成一个模板方法 ## 工厂模式&抽象工厂模式 工厂模式关注的是 **创建单一类型的对象** 定义一个抽象方法 由子类实现具体对象的实例化 抽象工厂模式 关注的是 **创建一族相关对象 ** 提供一个接口来创建一组相关的或者相互依赖的对象 无需指定他们的具体类 # 计网 ## 常见的Http状态码 - 1XX 消息响应 - 2XX 成功 - 3XX 重定向 - 4XX 客户端错误 - 403权限 - 5XX 服务器错误 ## HTTP 请求包含哪些内容 请求头和请求体有哪些内容 组成部分 - 请求行 - GRT POST 之类的 - 请求路径 - HTTP版本 - 请求头 - 各种键值对 - 客户端环境 - 请求内容 - 认证信息 - 空行 - 分割体&头 - 请求体 - 要发送的数据 ## GET和POST的区别 GET - 请求数据 - 参数通过URL拼接 直接暴露于URL中不是很安全 - 有长度限制 2048字节 - 幂等性 重复请求不会改变服务器的状态 POST - 提交数据 - 参数在请求体中 - 非幂等 ## Http1.0 **无状态,无连接** :每次请求都要建立新的TCP **队头阻塞** :每个请求都按照顺序解决 **不支持持久连接**: 每次请求都要建立新的TCP 服务器消耗大 ## Http2.0 **二进制分帧:**将http报文分割成更小的二进制,提高了传输效率,支持**多路分用** **头部压缩**:减少HTTP头部的大小,降低网络的开销 **服务器推送**: 服务器主动向 客户端发送请求 减少客户端的请求次数 **多路分用**:在一个TCP连接上可以同时发送多个请求,解决了HTTP1.1的对头阻塞问题 ## Http3.0 基于QUIC协议:使用UDP协议,相当于TCP可靠性,QUIC通过自身实现可靠传输,减少了RTT 多路分用:在一个QUIC连接上可以同时传输多个请求和相应,并支持流优先级 更快的连接:减少了TLS和三握手的时间 更少的延迟 image-20251109195036812 HTTP&HTTPS **1.数据传输安全性** - **HTTP**:数据以明文传输,容易被窃听、篡改。 - **HTTPS**:通过 SSL/TLS 协议对数据进行加密传输,提供数据机密性和完整性保障。 2. **端口号** - **HTTP**:默认使用端口 80。 - **HTTPS**:默认使用端口 443。 3. **性能** - **HTTP**:无加密过程,连接建立速度稍快。 - **HTTPS**:基于 HTTP 上又加了 SSL或 TLS协议来实现的加密传输,加解密过程增加了计算开销,握手时间较长,但现代硬件和协议优化已使性能差距减小。 **4. SEO 影响** - **HTTP**:搜索引擎一般会降低未加密站点的排名。 - **HTTPS**:搜索引擎更倾向于优先展示 HTTPS 网站。 ## Tcp和Upd的区别 TCP - 可靠连接 - 面向连接 - 提供流量控制和拥塞控制 - 保持数据顺序 - 头部较大 - 性能较低 延迟大 - 数据传输模式 -->以字节流传输模式 - 适用于 文件传输 web 邮件 UDP - 不可靠 - 无连接 - 没有流量控制和拥塞控制 - 不保证数据顺序 - 头部节点较小(8字节) - 性能高 延迟小 - 数据报传输模式 - 实时通讯 语音 视频 游戏 ## 经典TCP三报文 客户端首先发送一个SYN(同步序列编号)消息给服务器,服务器收到后回复一个SYN-ACK(同步序列编号-确认)消息,最后客户端再发送一个ACK(确认)消息确认服务器已经收到SYN-ACK消息,从而完成三次握手,建立起一个可靠的TCP连接。 image-20251110202708473 ### 为什么三握手 - 避免历史错误连接的建立 减少双方错误的不必要的资源的消耗 - 帮助双方同步初始化 ## TCP是用来决解什么问题 可靠传输:TCP确保数据包在网络传输过程中不丢失,不重复 流量控制:TCP通过滑动窗口机制调节发送方的数据发送速率 ,防止因接收方的处理能力而被数据流所淹没 拥塞控制:TCP通过拥塞避免算法 (慢启动 拥塞避免 快速重传 快速回复) 来防止网络过载 确保网络资源的公平使用和稳定性 连接管理:TCP是面向连接的协议 赛用三次握手 和四次挥手 机制来管理会话,确保通信的可靠性和状态的同步 解决了数据不可靠的IP网络上的问题 ## 四挥手 四次挥手的作用就是 用于安全关闭一个已经建立的连接的过程 它确保双方都能完成数据传输并安全的释放连接资源 ### 为什么挥手需要四次? 主要是为了确保数据完整性。 TCP 是一个全双工协议,也就是说双方都要关闭,每一方都向对方发送 FIN 和回应 ACK。客户端发起连接断开,代表客户端没数据要发送的,但是服务端可能还有数据没有返回给客户端。就像我对你说我数据发完了,然后你回复好的你收到了。然后你对我说你数据发完了,然后我向你回复我收到了。这样才能保证数据不会丢失。 所以一个 FIN + ACK 代表一方结束数据的传输,因此需要两对 FIN +ACK,加起来就是四次通信。 ## 粘包和拆包 粘包:在Tcp传输中发送方的多个数据包在接收方被**合并成一个包接受**,导致多条数据粘在一起,接收方无法正确区分这些消息的边界 拆包:发送方的一个数据包在接受方被拆成了多个包接受,导致一个完整的消息被拆分成多个部分,接收方无法一次性接收到完整的数据 ### why 粘包:Tcp是面向字节流的协议 他不关心数据边界 拆包:可能由于网络传输中的MUT(最大传输单元)限制或发送缓冲区大小的限制,一个大包被拆分成了多个小包传输 ### 解决: 使用特定长度 添加消息分隔符 使用消息头 ## Tcp拥塞控制的步骤 ### ![image-20251111214414297](F:\记录\八股文\image-20251111214414297.png) ## TCP/IP四层模型 - 应用层 - 通过各种协议提供网络应哟个程序的功能 - 传输层 - 在两个主机之间提供端口到端口的通信服务 - 网络层 - 通过Ip协议提供数据包的路由和转发 - 网络接口层 - 在计算机和网络硬件之间传输数据 ## Cookie Session Token之间的区别 ### Cookie 存储在用户浏览器端的一个小型数据文件,用于跟踪和保存用户的状态信息 用途: 保持用户登陆状态,跟踪用户行为,存储用户偏好 **存在客户端** ### Session Session是服务器端保存用户状态的机制 每一个用户会话都会有已给唯一的SessionId 主要用于跟踪用户在服务器上的状态信息 -> 登陆状态和购物车内容 **存储在服务器端** ### Token **加密字符串,用于身份认证和授权**,可以包含用户信息和权限,用于验证用户身份和授权访问资源 认证后,后端服务会返回Token,**存储在客户端** 后续可无端访问服务端需要带上这个Token - Cookie-->客户端状态的简单存储和追踪 - Session--> 服务器端的复杂状态管理,需要存储大量会话数据 - Token--> 无状态的认证和授权,特别是在分布式和跨域环境下 ## 输入一个网站发生了什么 1. 浏览器解析URL 2. DNS解析成最终的ip 3. 浏览器选择TCP/UDP 4. IP 在TCP数据包的基础上 在封装源地址IP和目标地址IP等信息 5. MAC 通过MAC确保子网内设备两点之间的通信寻址 6. 网卡 网卡将二进制数据转化为电信号 7. 交换机 工作在MAC层,他会将数据包的MAC头找到另一个设备连接在交换机的哪个端口 8. 路由器 转发 三层网络设备 包含IP层 9. 层层验证 10. 服务器处理 11. 浏览器接受并且渲染 # 计算机组成原理 ## 线程和进程的区别 进程:资源分配的基本单位 每一个进程都有一个内存空间。 线程:是CPU的调度的基本单位 属于进程 一个进程可以包含多个线程 线程共享进程的测村空间和资源 ## 进程之间的通信方式 - 管道 单向通信方式 - 命名管道 与匿名管道类似 - 信息队列 允许进程向另一个进程发送消息 消息顺序存储 - 共享内存 - 信息量 - 信号 异步通信方式 - Socket 在网络上不同主机上的进程通信 - 文件 通过读写文件俩来进行通信 ### 调度算法 - 先来先服务 - 短作业有限 - 有限级调度 - 时间片轮转 - 最高相应比优先 计算影响比 - 多级反馈队列调度 结合多个调度策略