MySQL

MySQL的存储引擎有哪些?它们之间有什么区别?

MySQL 存储引擎是插件式的,不同引擎决定了数据的存储方式、索引类型、事务支持等特性。

  • 面试重点InnoDB vs MyISAM 的区别是必考点;95% 以上线上业务用 InnoDB

5 种常见存储引擎(核心特性对比)

1. InnoDB(默认引擎,OLTP 首选)
  • 核心特性:支持事务、行锁、MVCC、外键、聚簇索引,适合高并发、数据一致性要求高的 OLTP 场景(如订单、用户系统)。
  • 核心优势:数据安全、并发高、符合 ACID。
  • 存储上限:64TB。
2. MyISAM(老版本默认,基本淘汰)
  • 核心特性:不支持事务、表锁、无 MVCC、不支持外键。
  • 适用场景:读多写少、对数据一致性要求极低的场景(如早期的报表系统)。
  • 现状:MySQL 8.0 已被移除,属于被淘汰的引擎。
  • 存储上限:256TB。
3. MEMORY(内存存储)
  • 核心特性:数据全存内存,读写速度极快;重启 MySQL 数据丢失
  • 适用场景:临时表、会话缓存、高频访问的热数据缓存。
  • 限制:受内存限制,存储无上限但受物理内存制约。
4. Archive(归档存储)
  • 核心特性:只支持 INSERTSELECT,不支持索引、高压缩率。
  • 适用场景:日志归档、历史订单存储、只增不改的数据冷备份。
5. NDB(MySQL Cluster 专用)
  • 核心特性:支持分布式、高可用、数据自动分片、行锁。
  • 适用场景:电信级别的大规模分布式集群,对高可用要求极高的系统。
  • 存储上限:384EB。
核心区别对比表
核心特性 InnoDB (生产首选) MyISAM (已淘汰) MEMORY (内存) Archive (归档)
事务支持 ✅ 支持 (ACID) ❌ 不支持 ❌ 不支持 ❌ 不支持
锁粒度 🔓 行锁 (细粒度并发) 🔒 表锁 (低并发) 表锁 行锁
MVCC 多版本并发 ✅ 支持 ❌ 不支持 ❌ 不支持 ❌ 不支持
外键 ✅ 支持 ❌ 不支持 ❌ 不支持 ❌ 不支持
索引类型 聚簇索引 + B + 树 仅 B + 树 哈希索引 无索引
存储上限 64TB 256TB 受内存限制 无限制
适用场景 高并发在线业务 读多写少 (已淘汰) 临时缓存 / 会话表 日志 / 历史归档

MySQL InnoDB 引擎中的聚簇索引和非聚簇索引有什么区别?

1. 聚簇索引(Clustered Index)
  • 本质:索引的叶子节点直接存储完整的数据行,索引结构与数据物理存储绑定。
  • 规则:一张表只能有一个聚簇索引,InnoDB 默认以主键作为聚簇索引;无主键则选唯一非空索引,再无则隐式生成行 ID。
  • 核心特点:数据的物理存储顺序 = 索引顺序,决定了表的物理存储结构。
2. 非聚簇索引(Non-clustered Index,二级索引)
  • 本质:索引的叶子节点只存储索引列的值 + 主键值,不存储完整数据。
  • 规则:一张表可以有多个非聚簇索引(如普通索引、唯一索引、联合索引)。
  • 核心特点:索引结构独立于数据存储,不改变数据的物理顺序。

核心区别对比表

对比维度 聚簇索引 非聚簇索引
叶子节点存储 完整的整行数据 索引列值 + 主键值
数量限制 一张表仅 1 个 一张表可多个
数据存储关系 索引即数据,决定数据物理存储顺序 索引与数据分离,不改变物理顺序
查询效率 主键查询一步到位,无需回表,速度极快 需通过主键回表查询完整数据(覆盖索引除外),效率更低
默认索引 主键自动成为聚簇索引 手动创建的普通 / 唯一 / 联合索引

关键差异与回表逻辑

1. 核心差异

聚簇索引叶子节点存完整数据,非聚簇索引只存主键值,这是两者最本质的区别。

2. 回表操作
  • 用非聚簇索引查询时,先在索引树中找到对应主键,再拿着主键去聚簇索引中查询完整数据行,这个过程叫回表,会多一次 IO,降低查询效率。
  • 覆盖索引:如果查询的列都包含在非聚簇索引中,就不需要回表,直接从索引中取数据,大幅提升性能。

MySQL 的索引类型有哪些?

MySQL 索引可从 3 个维度完整分类,面试按这个结构回答逻辑最清晰:

  • 数据结构维度 → 存储方式维度 → 索引性质维度

各维度详细分类

1. 数据结构维度(底层实现)
索引类型 核心特点 适用场景
B+ 树索引(默认) 多层平衡树,叶子节点用链表串联,支持等值查询 + 范围查询 + 排序,是 InnoDB/MyISAM 默认索引 绝大多数业务场景(主键、普通索引等)
哈希索引 哈希函数直接定位,等值查询 O (1),不支持范围查询、排序 Memory 引擎默认,InnoDB 自适应哈希索引自动管理
全文索引 文本分词倒排索引,类似搜索引擎原理 TEXT 类型字段的关键字检索(如文章内容搜索)
空间索引(R 树) 基于 R 树实现,专门处理地理坐标等多维数据 地理位置、区域查询、距离计算(GIS 场景)
2. 存储方式维度(InnoDB 专属)
索引类型 核心特点 数量限制
聚簇索引 叶子节点存储完整数据行,数据按索引顺序物理存储,索引即数据 一张表仅 1 个,默认是主键索引
非聚簇索引(二级索引) 叶子节点仅存储索引列值 + 主键值,需通过主键回表查询完整数据 一张表可多个(普通索引、唯一索引等)

核心补充:InnoDB 表必须有聚簇索引,无主键则用唯一非空索引,再无则隐式生成行 ID。

3. 索引性质维度(业务约束)
索引类型 核心特点
主键索引 唯一且非空,一张表仅 1 个,InnoDB 中就是聚簇索引
唯一索引 保证列值不重复,允许 NULL(可多个 NULL),一张表可多个
普通索引 无唯一约束,仅用于加速查询,最常用
联合索引 多列组合索引,遵循最左前缀原则,列顺序直接影响索引效率
全文索引 专门用于文本检索,对应数据结构维度的全文索引
空间索引 地理空间数据专用,对应数据结构维度的空间索引

Mysql索引的最左前缀匹配原则是什么?

最左前缀匹配原则,是**联合索引(多列索引)的核心使用规则:

使用联合索引时,查询条件必须从索引的最左列开始,按顺序依次匹配,不能跳过左侧列,否则右侧列的索引会失效,无法利用索引加速查询。

一、底层原理

联合索引在 B+ 树中,是按「从左到右」的顺序排序存储的:
以联合索引 (first_name, last_name, age) 为例:

  1. 先按 first_name 排序
  2. first_name 相同的,再按 last_name 排序
  3. last_name 相同的,最后按 age 排序
    MySQL 查询时,会优先用最左列作为匹配依据,依次向后匹配。如果跳过最左列,右侧列在 B+ 树中是无序的,无法利用索引快速定位,导致索引失效。

二、生效 / 失效示例(以 (a,b,c) 联合索引为例)

查询条件 是否命中索引 说明
WHERE a = ? ✅ 完全命中 匹配最左列,索引生效
WHERE a = ? AND b = ? ✅ 完全命中 按顺序匹配前两列,索引生效
WHERE a = ? AND b = ? AND c = ? ✅ 完全命中 完整匹配三列,索引生效
WHERE b = ? AND c = ? ❌ 失效 跳过最左列 a,索引完全失效
WHERE a = ? AND c = ? ⚠️ 部分命中 a 列命中,c 列因跳过 b 失效

为什么 MySQL 选择使用 B+ 树作为索引结构?

MySQL 索引的核心目标是最大限度减少磁盘 I/O 次数:数据库数据存储在磁盘,随机读写比内存慢 10 万倍,因此索引结构必须尽可能降低磁盘访问次数,B+ 树完美适配这一需求。

B+ 树的三大核心优势

1. 树高极矮,磁盘 I/O 次数最少

B+ 树是多叉平衡树,单个节点可存储成百上千个索引键,3 层 B+ 树就能存储两千多万条数据,查询任意数据最多仅需 3 次磁盘 I/O
对比红黑树(二叉树):存储相同数据量需要二十多层,I/O 次数爆炸,完全不适合磁盘存储场景。

2. 非叶子节点仅存索引,缓存命中率更高

B+ 树的非叶子节点只存储索引 key 和指针,不存储完整数据,因此 16KB 的数据页能塞进更多索引项,内存中可缓存更多索引,大幅提升缓存命中率,进一步减少磁盘访问。

3. 叶子节点双向链表串联,范围查询效率极高

B+ 树的叶子节点用双向链表有序串联,范围查询时,定位到起点后直接顺着链表向后扫描即可,无需回根节点重复查找,顺序 I/O 比随机 I/O 快得多,完美适配数据库的范围查询、排序等高频场景。

MySQL 三层 B+ 树能存多少数据?

三层 B+ 树大约能存储 2000 万条记录(精确计算约 2190 万),这是面试必背的结论。
它完美体现了 B+ 树 “矮胖” 的特性,通过多叉树结构将磁盘 I/O 控制在最低(通常仅需 3 次 I/O 即可完成查询)。

核心计算逻辑

1. 核心预设条件
  • 页大小:InnoDB 默认页大小为 16KB
  • 索引项大小
    • 主键(bigint):8 字节
    • 页指针:6 字节
    • 合计:8+6=14 字节。
  • 每条记录大小:假设为 1KB(实际业务中记录通常更小,存储容量会更大)。
2. 节点容量计算
  1. 非叶子节点(索引页)
    • 能存储的索引项数量 = 16×1024÷14≈1170 个。
    • 即:一个根节点可以指向 1170 个中间节点。
  2. 叶子节点(数据页)
    • 能存储的记录数 = 16×1024÷1≈16 条。
    • 即:每个叶子节点实际存储 16 条数据。
3. 三层总容量计算

三层 B+ 树结构:1 (根节点)→1170 (中间节点)→1170×1170 (叶子节点)
总存储数 = 第二层节点数 × 第三层每条节点记录数
1170×1170×16≈2190 万条记录

最终结论与意义

  1. 存储能力:三层 B+ 树可以高效存储约 2000 万 条数据。
  2. 查询性能:意味着查询任意一条数据,最多只需 3 次磁盘 I/O(根节点 → 中间节点 → 叶子节点),I/O 开销极低,性能高效且稳定。

MySQL中的回表是什么?

回表是 InnoDB 引擎中,使用二级索引(非聚簇索引)查询时的额外操作:
二级索引的叶子节点只存储「索引列值 + 主键值」,不存储完整数据行。如果查询需要的字段不在二级索引中,就必须拿着主键值,再去
聚簇索引(主键索引)中查询一次完整数据行,这个二次查询的过程就叫**回表。

简单来说:二级索引查不到完整数据,得回到主键索引再查一遍

一、回表的完整流程(以 idx_age 二级索引为例)

  1. 第一步:二级索引定位主键
    执行 SELECT * FROM user WHERE age = 20 时,先在 idx_age 二级索引树中,找到 age=20 对应的所有主键 id(如 id:101id:505)。
  2. 第二步:聚簇索引回表查完整数据
    拿着这些 id,再去主键索引(聚簇索引)树中,逐个查询对应 id 的完整数据行,最终返回结果。

二、核心影响与优化

1. 性能影响

回表会多一次磁盘 I/O 操作,会降低查询效率,因此要尽量避免不必要的回表。

2. 优化方案:覆盖索引

如果查询的所有字段,都已经包含在二级索引中,就不需要回表,直接从二级索引中取数据即可,这就是覆盖索引

  • 示例:SELECT id, age FROM user WHERE age = 20idage 都在 idx_age 索引中,直接从索引取数,无需回表。

MySQL 中使用索引一定有效吗?如何排查索引效果?

索引不一定有效,用了索引也不一定快。
MySQL 最终是否使用索引、以及索引是否高效,由 优化器的成本计算 决定。优化器会权衡 索引成本全表扫描成本,选择代价最低的执行计划。

一、索引为什么会 “失效”?(不生效的场景)

  1. 全表扫描更便宜
    • 表数据量极少(如几百行),走索引还要定位叶子节点,开销反而比直接扫全表高
    • 优化器判定:走索引不如直接全表扫描
  2. 统计信息不准
    • 表数据分布变化导致 MySQL 统计信息失真,优化器误判索引效果,选择了全表扫描

二、如何排查索引效果?(用 EXPLAIN 命令)

EXPLAIN 分析 SQL 执行计划,重点看 3 个核心字段

1. type(访问类型,核心指标)

表示 MySQL 如何查找索引,效率排序:system > const > eq_ref > ref > range > index > ALL

  • ref / range / const:真正利用了索引的快速查找能力,索引有效
  • index:全索引扫描(遍历所有索引叶子),性能差但比全表扫描好(优化优化)。
  • ALL:全表扫描(最坏情况),索引完全失效,需尽量避免。
2. key(实际使用的索引)

显示查询实际使用的索引名称。

  • NULL:没有使用索引。
  • 索引名:命中了该索引。
3. rows(预估扫描行数)

MySQL 预估执行查询时需要扫描的行数。

  • 数字越小越好:数字越大说明索引筛选效果差,查询代价越高。

三、索引使用判断标准

索引状态 判定标准 (EXPLAIN) 性能评价
真正用好索引 typeref / range / const 高效低延迟,B+ 树快速定位
索引未完全利用 typeindex 效率较差,需优化
索引完全失效 typeALL 性能极差,IO 高,应避免

在 MySQL 中建索引时需要注意哪些事项?

索引不是越多越好,要在「查询收益」和「写入开销」之间做平衡,遵循「按需建、精准建、不冗余」的原则。

二、6 大核心注意事项

1. 索引不是越多越好
  • 每个索引都会占用磁盘空间,且每次增删改操作都需要维护 B + 树结构,索引越多,写入性能越差。
  • 只建真正需要的索引,避免冗余索引。
2. 区分度太低的字段不要建索引
  • 区分度 = 不同值的数量 / 总数据量,区分度极低(如性别、状态)的字段,过滤效果差,索引几乎失效。
  • 例外场景:如果某状态占比极低(如定时任务表 99% 成功、1% 失败),对失败状态建索引可过滤 99% 数据,此时建索引有意义。
3. 大字段不要建索引
  • TEXTLONGTEXT、大VARCHAR等大字段,索引占用空间极大,加载到内存会挤占 Buffer Pool,影响其他热点数据性能。
  • 若必须用,可建前缀索引,只取字段前 N 个字符做索引,节省空间。
4. 写多读少的表要慎重建索引
  • 若表的修改频率远大于查询频率,索引带来的写入开销会超过查询收益,得不偿失(如日志表、流水表)。
5. 高频查询 / 关联字段要建索引
  • WHERE条件中频繁出现的字段、多表JOIN的关联字段,优先建索引;多个字段常一起查询,优先建联合索引,一个索引覆盖多个查询场景。
6. 排序 / 分组 / 去重字段要建索引
  • ORDER BYGROUP BYDISTINCT后的字段建索引,可避免临时表和文件排序,大幅提升查询性能。

三、建索引决策流程图

按以下 5 个问题依次判断,决定是否建索引:

  1. Q1:是高频查询 / 排序 / 分组字段吗? → 否:不建议建
  2. Q2:区分度是否够高(>0.1 或唯一)? → 否:不建议建
  3. Q3:字段类型非常大(Text/Blob)? → 是:考虑前缀索引;否:继续
  4. Q4:已有联合索引能否覆盖? → 是:复用现有;否:继续
  5. Q5:写入频率远大于查询频率? → 是:不建议建;否:创建 / 复用索引

MySQL 中的索引数量是否越多越好?为什么?

MySQL 索引不是越多越好。
索引是一把双刃剑,它能加速查询,但会严重增加写入开销、优化器负担、空间占用,索引过多会直接导致系统性能下降。

索引过多的四大核心代价

1. 写入性能下降(写性能打折)
  • 维护成本高:每次 INSERTUPDATEDELETE 操作,都需要同步更新所有相关索引的 B+ 树。
  • 树操作频繁:索引触发 B+ 树的页分裂、页合并,写入并发高时,锁竞争加剧,写入性能直接被打骨折。
  • 示例:一张高并发订单表如果建了 10 个索引,新增一条订单数据,需要同时修改 10 棵 B+ 树,写入性能直接打骨折。
2. 优化器负担加重(选错索引)
  • 解析耗时:索引越多,优化器分析可选索引的时间越长,查询计划生成耗时增加。
  • 统计信息不准:索引过多导致优化器统计信息难以精准分析,甚至可能选错索引,导致全表扫描。
3. 空间膨胀(磁盘 / 内存浪费)
  • 磁盘占用:每个二级索引都是独立的 B+ 树,存储主键和索引列,千万行数据的表,索引可能占用 GB 级空间,磁盘空间浪费严重。
  • 内存浪费:索引加载到内存(Buffer Pool)会占用大量内存,导致缓存的热数据变少,整体查询性能下降。
4. DDL 操作变慢(业务风险)
  • 表结构变更困难:给表添加字段、删除字段等 DDL 操作,需要重建所有索引。
  • 小表变久表:索引越多,DDL 耗时越长,小表变久表,甚至导致业务不可用,影响业务稳定性。

如何使用 MySQL 的 EXPLAIN 语句进行查询分析?

EXPLAIN 是 MySQL 分析 SQL 执行计划的核心工具,在 SELECT 语句前加上 EXPLAIN,就能查看优化器的执行逻辑,判断索引是否生效、查询是否存在性能瓶颈。

一、四大核心关注字段

1. type(访问类型,核心性能指标)

决定查询效率的核心字段,性能从高到低排序:
const > eq_ref > ref > range > index > ALL

  • const:主键 / 唯一索引等值查询,最多匹配 1 行,性能最快
  • eq_ref:多表关联时用主键 / 唯一索引关联,每次只取 1 行
  • ref:非唯一索引等值查询,可能返回多行
  • range:索引范围扫描(BETWEEN>< 等条件)
  • index:扫描整棵索引树,比全表扫描好但性能仍差
  • ALL:全表扫描,大表出现此值需紧急优化
2. key(实际使用的索引)
  • possible_keys:优化器候选索引
  • key:优化器最终选择的索引
  • 若为 NULL:说明完全没走索引,需排查索引失效问题
3. rows(预估扫描行数)
  • 表示 MySQL 预估执行查询需要扫描的行数
  • 值越小越好:几百行和几十万行的差距,对应毫秒级和秒级的性能差异
4. Extra(额外信息,细节排查)
  • Using index:走了覆盖索引,直接从索引取数,无需回表,性能最优
  • Using filesort:排序未用索引,需额外文件排序,需优化
  • Using temporary:使用了临时表,需优化(常见于 GROUP BY 未走索引)

二、核心判断标准

状态 判定标准 优化建议
✅ 高效查询 typeref/range/constkey 非空、rows 小、ExtraUsing index 无需优化
⚠️ 待优化 typeindexExtraUsing filesort/Using temporary 调整索引、SQL 语句
❌ 严重问题 typeALLkeyNULL 紧急优化,避免全表扫描

MySQL 中如何进行 SQL 调优?

SQL 调优的核心目标是 减少磁盘 I/O避免无效计算
实际操作按 “三步走”:先定位慢 SQL再分析执行计划最后针对性优化

一、三大核心优化维度

1. 索引层面优化(最有效)
  • 合理设计联合索引,利用覆盖索引
    查询只需要部分字段时,建立联合索引覆盖查询列,直接从索引取数,避免回表
    例:只需 nameage,索引建为 (name, age)
  • 严格遵守最左前缀原则
    联合索引 (a, b, c),查询条件必须从最左列开始,跳过左侧列会导致索引失效。
    例:WHERE b = 1 无法命中 (a,b,c) 索引。
  • 避免在索引列上做函数运算
    函数会导致索引失效,走全表扫描。
    改法:WHERE YEAR(create_time) = 2024WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'
2. SQL 写法优化(代码层面)
  • 禁止 SELECT *:只查必要字段,减少网络传输开销和内存占用。
  • 避免前置模糊查询LIKE '%关键词' 是全表扫描,必须优化。
  • 连表查询字符集一致JOIN 时字段字符集必须一致(如 utf8 和 utf8mb4),否则会发生隐式转换导致索引失效。
3. 架构层面优化(宏观降本)
  • Redis 缓存热点数据:访问频率高、变化少的数据,直接走缓存,不查数据库。
  • 大表分库分表:单表超过 2000 万行,查询性能会明显下降,需分表处理。
  • 读写分离:利用主从架构,将读压力分摊到从库,提升读性能。

请详细描述 MySQL 的 B+ 树中查询数据的全过程

B + 树查询分两大阶段,完整链路:

根节点 → 中间节点 → 叶子节点 → 页目录二分定位 → 组内链表遍历

第一阶段:树层级垂直查找(定位叶子节点)

  1. 起点:从 B + 树的根节点开始
  2. 二分查找:将查询的键值与节点中存储的索引键做比较,用二分法确定目标值所在的区间
  3. 向下遍历:顺着对应区间的指针,走到下一层子节点
  4. 终止条件:重复上述过程,直到到达叶子节点(数据页)
  • 特点:3 层 B + 树最多仅需 3 次磁盘 I/O,就能定位到叶子节点,效率极高
    第二阶段:叶子节点内部查找(定位具体数据行)
    叶子节点是一个 16KB 的 InnoDB 数据页,内部通过页目录加速查找:
  1. 页目录二分定位:页目录把记录分成若干组,每个槽指向组内最大记录。用二分法快速定位到目标记录所在的组
  2. 组内链表遍历:沿着组内的单向链表遍历,找到最终的目标数据行

MySQL 中 count( * )、count(1) 和 count(字段名) 有什么区别?

三者都是统计行数的聚合函数,核心差异在于对 NULL 值的处理逻辑

写法 统计规则 示例(100 行,email 字段 20 行 NULL)
count(*) 统计所有行,包含字段为 NULL 的行 返回 100
count(1) 统计所有行,包含字段为 NULL 的行 返回 100
count(字段名) 只统计该字段不为 NULL的行,NULL 行不计入 返回 80
  1. count(*) vs count(1)

    • MySQL 官方文档明确:两者性能完全一致,无任何差异
    • count(*)语义最清晰,是统计总行数的推荐写法
  2. count(字段名)

    • 无索引时:需要全表扫描 + 额外判断 NULL,性能远慢于前两者
    • 有索引(如主键、普通索引)时:性能差距大幅缩小,接近前两者

MySQL 中 varchar 和 char 有什么区别?

对比维度 char(n) varchar(n)
存储方式 固定占用 n 个字符空间,不足用空格填充 按实际存储长度占用空间,额外用 1~2 字节记录实际长度
额外开销 1~2 字节存储长度信息
存储效率 短字符串会浪费存储空间 按需分配,更节省空间
读取效率 定长读取,无需计算长度,理论略快 需要解析长度信息,理论略慢(实际业务中差距可忽略)
适用场景 长度固定的短字符串,如 MD5 摘要、国家 / 手机号代码、固定长度编码 长度不固定的字符串,如用户名、地址、商品描述等绝大多数场景

MySQL 是如何实现事务的?

MySQL InnoDB 引擎通过 四大核心组件 实现事务的 ACID 四大特性,是事务可靠性的底层保障。

一、ACID 特性与对应实现组件

ACID 特性 核心含义 对应实现组件
原子性 (Atomicity) 事务要么全做,要么全不做,无中间状态 Undo Log(回滚日志)
一致性 (Consistency) 事务执行前后,数据从一个正确状态到另一个正确状态 原子性 + 隔离性 + 持久性 共同保障
隔离性 (Isolation) 多个事务并发执行时,互不干扰 锁机制 + MVCC(多版本并发控制)
持久性 (Durability) 事务提交后,数据永久生效,宕机不丢失 Redo Log(重做日志)+ WAL 机制

二、四大核心组件详解

1. Undo Log:保障原子性
  • 作用:记录数据修改前的版本,用于事务回滚。
  • 原理:每次修改数据前,先把原值写入 Undo Log;事务回滚时,按日志反向操作,把数据恢复到修改前的状态,保证 “要么全做,要么全不做”。
  • 额外价值:为 MVCC 提供历史版本数据,支持快照读。
2. Redo Log + WAL:保障持久性
  • 作用:记录数据修改后的版本,用于宕机后数据恢复,实现 “提交即永久生效”。
  • WAL(Write-Ahead Logging)机制:事务提交时,先把修改写入 Redo Log 日志文件,再异步刷盘到数据页;即使宕机,重启后重放 Redo Log 即可恢复数据,避免每次修改都实时刷盘的性能损耗。
  • 核心优势:顺序写日志性能远高于随机写数据页,兼顾性能与数据安全。
3. 锁机制:保障隔离性(写写冲突)
  • 作用:解决事务间的写写冲突,保证串行化访问,避免数据混乱。
  • 核心锁类型
    • 行锁:锁定单行数据,仅阻塞写操作,支持高并发。
    • 间隙锁:锁定索引间隙,防止幻读,保证隔离性。
  • 原理:两个事务同时修改同一行数据,必须等前一个事务释放锁,后一个才能执行,避免并发修改导致数据不一致。
4. MVCC(多版本并发控制):保障隔离性(读写并发)
  • 作用:实现读写不阻塞,大幅提升并发性能,是 InnoDB 高并发的核心。
  • 原理:通过 Undo Log 维护数据的多个历史版本,读操作不加锁,直接读取自己事务可见的版本;写操作正常加锁,实现 “读不阻塞写,写不阻塞读”,并发度拉满。
  • 核心依赖:Undo Log 的版本链 + ReadView 可见性判断。

三、一致性的协同保障

一致性不是单一组件实现的,而是 原子性、隔离性、持久性共同作用的结果

  • 原子性保证事务不中途中断
  • 隔离性保证并发事务互不干扰
  • 持久性保证数据永久生效
    三者结合,确保数据始终处于正确状态。

MySQL 中的 MVCC 是什么?

MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 实现高并发读写的核心机制,核心思想是让读写操作互不阻塞

  • 写操作不阻塞读操作,读操作也不阻塞写操作,大幅提升并发性能。
  • 本质是通过维护数据的多个历史版本,让读操作读取历史快照,写操作修改最新版本,实现读写分离。

核心实现原理

1. 版本链的生成

InnoDB 为每条记录添加两个隐藏字段

  • trx_id:记录最后修改这条数据的事务 ID
  • roll_pointer:指向这条记录上一个版本的 undo log 指针
    每次执行 UPDATE 时,不会直接覆盖原数据:
  • 把旧值写入 undo log,生成历史版本
  • 新值写入数据页,roll_pointer 指向旧版本
  • 多个版本串联形成版本链,最新版本在链头,历史版本依次向后
2. 快照读的实现

普通 SELECT快照读(一致性非锁定读)

  • 不加锁,根据事务启动时间,顺着版本链找到对自己可见的历史版本
  • 直接返回可见版本数据,完全不阻塞写操作
  • 写操作正常修改最新版本,读写各走各的,并发性能拉满

核心作用

  1. 实现读写不阻塞:读操作不加锁,写操作不阻塞读,大幅提升并发能力
  2. 保障事务隔离性:配合 ReadView 实现不同隔离级别(如 RC、RR),解决不可重复读、幻读问题
  3. 支撑事务回滚:版本链基于 undo log 生成,为事务回滚提供数据支持

MySQL 中的日志类型有哪些?binlog、redo log 和 undo log 的作用和区别是什么?

MySQL 有三大核心日志,各司其职:

  • binlog(归档日志):Server 层日志,负责主从复制、数据恢复
  • redo log(重做日志):InnoDB 引擎独有,负责崩溃恢复、保证数据不丢
  • undo log(回滚日志):InnoDB 引擎独有,负责事务回滚、支撑 MVCC

三大日志详细对比

1. 核心属性对比表
对比维度 binlog redo log undo log
所属层级 MySQL Server 层(跨引擎通用) InnoDB 引擎层(仅 InnoDB 有) InnoDB 引擎层(仅 InnoDB 有)
记录内容 逻辑操作(原始 SQL / 行变更前后值) 物理变更(数据页的偏移量修改值) 逻辑操作(数据修改前的旧值)
写入方式 追加写入(可无限追加,空间可扩容) 循环写入(固定空间,写满需 checkpoint 推进) 事务内写入,事务提交后异步清理
核心作用 1. 主从同步(从库重放 binlog 保持一致)

2. 数据恢复(配合全量备份回放到指定时间点)
崩溃安全(crash-safe):MySQL 宕机重启后,重放 redo log 恢复未刷盘的脏页,保证数据不丢 1. 事务回滚:按日志反向操作恢复数据,保证原子性

2. 支撑 MVCC:提供历史版本链,实现快照读
与事务的关系 事务提交时写入(2PC 协调) 事务执行中持续写入(WAL 机制) 事务执行中写入,回滚时使用
2. 本质区别
  1. 层级与通用性
    • binlog 是 Server 层日志,所有引擎通用;redo/undo 是 InnoDB 引擎独有,仅服务于 InnoDB 事务。
  2. 记录内容
    • binlog 是逻辑日志,记录数据变更的语义;
    • redo log 是物理日志,记录数据页的物理修改;
    • undo log 是逻辑日志,记录数据的反向操作。
  3. 写入方式
    • binlog 是追加写入,可无限扩容;
    • redo log 是循环覆盖写入,空间固定,依赖 checkpoint 推进。
  4. 核心目标
    • binlog 服务于主从复制、数据恢复
    • redo log 服务于崩溃恢复、保证持久性
    • undo log 服务于事务回滚、保证原子性、支撑 MVCC

MySQL 中的事务隔离级别有哪些?

隔离级别 脏读 不可重复读 幻读 特点
读未提交 最低隔离级别,未提交的数据对其他事务可见,存在 脏读 风险。
读已提交 只能读取已提交的数据,解决脏读问题,但可能出现 不可重复读幻读 问题。
可重复读 MySQL 默认隔离级别,确保同一事务中多次读取结果一致,避免不可重复读,但可能出现 幻读
串行化 最高隔离级别,事务串行执行,避免所有并发问题,但会导致性能大幅下降。
  1. 脏读:事务读取到其他事务未提交的数据。
  2. 不可重复读:同一个事务中多次读取同一数据,前后结果不一致。
  3. 幻读:同一个事务中多次查询,返回的记录数不一致(如新增或删除了行)。

MySQL 默认的事务隔离级别是什么?为什么选择这个级别?

MySQL InnoDB 引擎默认的事务隔离级别是可重复读(Repeatable Read,简称 RR)

为什么选择 RR 作为默认值?(历史原因与兼容性)

1. 历史遗留:binlog 格式限制

早期 MySQL 的 binlog 仅支持 Statement 格式,记录的是原始 SQL 语句。

  • 若隔离级别为 读已提交(RC):并发事务提交顺序可能与执行顺序不一致。
  • 问题:从库重放 binlog 时,会因为顺序不一致导致主从数据不一致
  • 解决方案:可重复读(RR)级别 + 间隙锁兜底,确保 binlog 记录顺序与执行顺序一致,保证主从数据一致。
2. 现状与兼容性
  • 如今 binlog 已支持 Row 格式(记录行数据变更,而非 SQL),使用 RC 级别也不会有主从不一致问题。
  • 但 MySQL 为了向前兼容,保留了 RR 作为默认值。
3. 大厂现状与趋势
  • 现状:互联网公司普遍使用 RC(读已提交) 级别。
  • 理由
    • RC 级别加锁更少,锁冲突更少,并发性能更高。
    • 配合 Row 格式 binlog,已能保证主从数据一致性。
  • 结论:默认值仍为 RR(历史原因),但现代架构优化更趋向于选择 RC。

数据库的脏读、不可重复读和幻读分别是什么?

脏读、不可重复读、幻读,都是并发事务执行时可能出现的数据一致性问题,严重程度依次递减。

三者详细定义与区别

1. 脏读(Dirty Read)
  • 定义:一个事务读取到了另一个事务还未提交的数据。
  • 风险:如果对方事务后续回滚,你读到的数据就变成了不存在的 “脏数据”,数据一致性直接被破坏。
  • 示例:事务 A 修改数据但未提交,事务 B 读取了这个修改后的值,随后 A 回滚,B 读取的数据就失效了。
2. 不可重复读(Non-Repeatable Read)
  • 定义:在同一个事务内,两次读取同一行数据,结果却不一样。
  • 原因:两次读取之间,有其他事务修改并提交了这行数据。
  • 核心特点:关注同一行数据的内容被修改
  • 示例:事务 A 两次查询用户余额,中间事务 B 修改并提交了余额,导致 A 两次查询结果不一致。
3. 幻读(Phantom Read)
  • 定义:在同一个事务内,两次执行相同的范围查询,返回的行数却不一样。
  • 原因:两次查询之间,有其他事务插入或删除了符合查询条件的数据行。
  • 核心特点:关注数据的行数发生了变化
  • 示例:事务 A 两次查询订单表中金额 > 100 的订单,中间事务 B 插入了一条金额 > 100 的订单并提交,导致 A 两次查询的结果行数不一致。

MySQL 中有哪些锁类型?

锁的粒度划分:行锁(给一整行加锁)、表锁(给一整张表加锁)、页锁(给一个数据页或索引页加锁)
锁的区间划分:间隙锁(锁的是两个索引值之间的间隙)、临键锁(锁定行及其前面的间隙)
锁的级别划分:

  • 共享锁(读锁):数据被某个事务获取,其他事务只能读不能写 - 排他锁(写锁):数据被某个事务获取,其他事务不能读不能写
  • 意向锁:表级锁,用于声明事务未来要在表的行上加锁,分为意向共享锁(IS,计划加S锁)和意向排他锁(IX,计划加X锁),核心作用是提升表锁的加锁效率,避免逐行检查

MySQL 事务的二阶段提交是什么?

二阶段提交(2PC)是 MySQL 为保证InnoDB 的 redo log 和 Server 层的 binlog 数据一致性设计的跨层事务协调机制,是主从复制和事务可靠性的核心基础。

核心流程

  1. 准备阶段:事务执行完,InnoDB 将 redo log 刷盘并设为prepare状态,返回就绪信号。
  2. 提交阶段:Server 层将 binlog 刷盘,再通知 InnoDB 把 redo log 改为commit,事务完成。

崩溃恢复逻辑

重启后通过 XID(事务唯一标识) 对账:

  • redo log 为prepare且 binlog 有对应记录 → 自动提交
  • binlog 无对应记录 → 回滚事务

MySQL 中如果发生死锁应该如何解决?

一、什么是死锁

多个并发事务互相持有对方需要的锁,循环等待,导致所有事务无法执行,就是死锁。

二、死锁的解决方式

  1. MySQL 自动处理(InnoDB 默认开启)
    • 主动死锁检测:检测到死锁时,自动回滚代价最小的事务(比如回滚行数最少的),释放锁,让其他事务继续执行。
    • 超时回滚:通过innodb_lock_wait_timeout设置锁等待超时,超时自动回滚事务,避免长时间阻塞。
  2. 手动干预
    • show engine innodb status/show processlist定位死锁事务,用kill 线程ID手动回滚阻塞事务。

三、死锁的避免方案

  • 拆分大事务为小事务,快速释放锁,减少锁冲突。
  • 所有事务按固定顺序申请锁,避免循环等待。
  • 优化索引,让 SQL 走索引,避免全表扫描导致的表锁 / 大范围行锁。
  • 适当降低隔离级别(如从 RR 降到 RC),关闭间隙锁,减少锁范围。
  • 关闭不必要的长事务,及时提交 / 回滚。

MySQL 中如何解决深度分页的问题?

一、问题本质

LIMIT 9999990,10 会让 MySQL 扫描前 9999990 条记录再丢弃,IO 极高、性能极差

二、三种优化方案(核心)

  1. 子查询 / Join 优化(最常用)
  • 原理:先用二级索引快速定位起始 ID,再回表查数据
  • 写法:先查起始 ID → 再用 id >= 查数据
  • 优点:利用索引,扫描量极小
  1. 游标分页(连续翻页)
  • 原理:记录上一页最后一条 ID,下一页直接 id > last_id
  • 优点:性能最好,无扫描浪费
  • 缺点:不能跳页(只能下一页 / 上一页)
  1. 搜索引擎(ES)
  • 大数据量、复杂排序 / 搜索场景
  • MySQL 不适合深度分页,交给 ES 处理

什么是 MySQL 的主从同步机制?它是如何实现的?

一、核心原理

MySQL 主从同步的核心是binlog(二进制日志)复制:主库将所有写操作记录到 binlog 中,从库拉取该日志并在本地重放,最终实现主从数据一致。

二、核心实现

  1. 主库 Dump 线程:监听主库 binlog 的变更,有新内容就主动推送 binlog 更新事件给从库。
  2. 从库 I/O 线程:向主库拉取 binlog 数据,将收到的日志写入从库本地的relay log(中继日志)
  3. 从库 SQL 线程:读取 relay log,逐条解析并执行其中的 SQL 语句,完成数据同步。

三、完整流程

  1. 客户端向主库提交事务,主库执行事务并将操作写入 binlog;
  2. 主库 Dump 线程监听到 binlog 更新,推送事件给从库;
  3. 从库 I/O 线程拉取数据,写入本地 relay log;
  4. 从库 SQL 线程读取 relay log,重放 SQL,同步数据;
  5. 主库向客户端返回事务执行成功的响应。

如何处理 MySQL 的主从同步延迟?

一、核心前提

主从延迟无法完全消除,只能通过优化缩短延迟,或在业务层做规避。

二、业务层处理方案(面试高频)

  1. 关键业务强制走主库:写后读的强一致场景(如注册后立即登录),直接查主库,牺牲部分读写分离收益,对主库压力影响小。
  2. 延迟感知 + 窗口控制:写操作后记录时间戳,短时间内的读请求强制走主库,超过延迟窗口再走从库,可用 ThreadLocal、Redis 记录时间。
  3. 二次查询兜底:从库查不到数据时,再查一次主库,兜底数据一致性;缺点:恶意查询不存在的数据会变成对主库的攻击。
  4. 缓存前置:写入主库的同时写入缓存,读请求优先查缓存;缺点:引入了缓存一致性问题,属于用新问题换旧问题。

三、其他优化方向

  • 硬件 / 配置优化:提升从库的硬件配置(CPU、磁盘、内存),解决从库性能不足导致的延迟。
  • DBA 侧进阶优化:开启 MySQL 并行复制等,提升从库重放日志的效率(面试可补充的加分项)。

Redis

Redis 中常见的数据类型有哪些?

1. String(字符串)

  • 特性:最基础类型,支持文本、数字、二进制,单值最大 512MB,支持原子自增(INCR)。
  • 典型场景:缓存会话 / 页面数据、计数器(阅读量、点赞数)、分布式锁。

2. Hash(哈希)

  • 特性:键值对集合,适合存储对象属性,可单独修改字段,无需整体覆盖。
  • 典型场景:商品详情、用户信息(ID 为 key,字段存属性)。

3. List(列表)

  • 特性:有序双向链表,支持两端操作(LPUSH/RPOP 等)。
  • 典型场景:消息队列、简单生产者 - 消费者模型、最新消息列表。

4. Set(集合)

  • 特性:无序、元素不重复,支持集合运算(交集 / 并集 / 差集),去重效率高。
  • 典型场景:标签系统、独立访客统计、共同关注 / 好友推荐。

5. ZSet(有序集合)

  • 特性:在 Set 基础上给每个元素绑定 score 分数用于排序,底层用跳表实现。
  • 典型场景:排行榜(游戏积分、热搜榜)、Top N 数据查询。

Redis 为什么这么快?

1. 基于内存(核心根本)

Redis 是纯内存数据库,数据全部存放在内存中,内存的读写速度远快于磁盘,从根源上保证了极致性能。

2. 单线程模型(避免开销)

核心命令执行采用单线程,彻底避免了多线程的线程切换、上下文切换、锁竞争带来的额外开销,执行效率拉满。

3. I/O 多路复用(高并发支撑)

在单线程基础上,用 I/O 多路复用技术(epoll/select/kqueue),让单个线程可以同时处理成千上万的客户端连接,实现高并发,不浪费单线程性能。

4. 高效的数据结构(O (1) 操作)

内置 String、Hash、List、Set、ZSet 等多种经过高度优化的数据结构,绝大多数核心命令的时间复杂度都是 O(1),读写操作极快。

5. Redis 6.0+ 多线程优化(网络性能升级)

6.0 版本引入多线程,仅用于网络请求的读写解析,核心命令执行仍为单线程,既利用了多核 CPU 优势,又不破坏单线程的原子性,进一步提升了网络 I/O 性能。

为什么 Redis 设计为单线程?6.0 版本为何引入多线程?

一、核心问题 1:Redis 为什么设计为单线程?

1. 根本原因

Redis 的性能瓶颈不在 CPU,而在内存和网络 I/O,单线程足以支撑极高的 QPS(单机 10 万 +),完全满足绝大多数业务需求。

2. 单线程的核心优势
  • 无锁竞争:彻底避免多线程下的锁、死锁问题,代码更简单、bug 更少
  • 无上下文切换开销:省去线程切换的 CPU 消耗,执行效率拉满
  • 配合 I/O 多路复用:单线程就能同时处理海量客户端连接,高并发能力拉满
  • 保证原子性:所有命令天然原子执行,无需额外加锁

二、核心问题 2:Redis 6.0 为什么引入多线程?

1. 根本原因

业务量爆发后,网络 I/O 成为新瓶颈:单线程同步处理「数据从内核拷贝到用户空间」的操作,高并发下处理不过来,限制了吞吐量。

2. 多线程的设计逻辑
  • 仅用于网络 I/O:多线程只负责「网络请求的读写、解析」,核心命令执行仍为单线程
  • 两全其美:既利用多核 CPU 分摊网络压力、提升吞吐量,又不破坏单线程的原子性和无锁优势,无需担心线程安全问题

Redis 中跳表的实现原理是什么?

1. 核心原理

跳表本质是多层有序链表

  • 最底层(Level 0)存储全部数据,上层链表是下层的子集,相当于分层索引
  • 把普通链表 O (n) 的查找时间,优化到 O(log n),性能接近平衡树,实现更简单

2. 核心操作

  • 查找:从最高层开始向右遍历,遇到比目标大的节点就下一层,重复直到找到 / 确定不存在
  • 插入:先查找到插入位置,再随机决定新节点的索引层数(Redis 用 25% 概率向上加一层),最后在各层插入节点,整体呈金字塔结构
  • 删除:找到目标节点,在所有层中修改前驱指针指向后继,完成多层同步删除

3. Redis 用跳表的优势(面试加分)

  • 实现比红黑树简单,代码易维护
  • 插入 / 删除效率高,无需像红黑树那样频繁调整结构
  • 支持范围查询,天然适合 ZSet 按 score 排序的场景

Redis 的 hash 是什么?

1. 基本定义

Redis Hash 是一个键映射多个 field-value 字段对的数据结构,可看作存储对象的结构。

2. 底层存储结构(版本区分)

  • Redis 6.0 之前ziplist + HashTable 双结构
  • Redis 6.0 及之后ListPack + HashTable 双结构

3. 结构切换规则(触发条件)

当同时满足以下两个条件时,使用 ziplist/ListPack 存储(极致省内存):

  1. 字段(field-value 对)数量 < 512
  2. 单个 field 和 value 的总长度 < 64 字节
    任意一个条件不满足,就自动切换为 HashTable 存储,保证 O (1) 操作性能。

Redis Zset 的实现原理是什么?

一、核心底层结构

Redis Zset(有序集合)采用双结构组合实现,同时兼顾两种核心操作的性能:

  1. 跳表(Skip List):按 score 排序,支持 O(log N) 时间复杂度的插入、删除、按 score 范围查询(如 ZRANGEBYSCORE)。
  2. 哈希表(Hash Table):存储 member → score 的映射,支持 O(1) 时间复杂度的单点查分(如 ZSCORE)。

二、双结构设计的必要性(互补短板)

  • 若只用跳表:按 memberscore 需遍历全表,复杂度退化为 O(N),性能极差。
  • 若只用哈希表:无法实现按 score 排序和范围查询,完全不满足有序集合的核心能力。
  • 双结构配合:插入 / 删除时同步更新两个结构,整体复杂度仍为 O(log N),同时覆盖两种高频操作。

三、小元素优化:listpack 压缩存储

当元素数量较少时,Zset 会用更省内存的 listpack 替代跳表 + 哈希表,触发条件(可配置):

  • 元素个数 ≤ 128(配置项:zset-max-listpack-entries
  • 单个元素长度 ≤ 64 字节(配置项:zset-max-listpack-value
  • 任意一个条件不满足,自动升级为跳表 + 哈希表结构。

Redis 中如何保证缓存与数据库的数据一致性?

不推荐的 3 种方案(并发下极易不一致)

方案 问题
1. 先更新缓存,再更新数据库 并发下,后执行的旧请求会覆盖新数据,导致缓存脏数据
2. 先更新数据库,再更新缓存 并发下同样会出现旧数据覆盖新数据,缓存不一致
3. 先删除缓存,再更新数据库 并发读时,会把旧数据回种到缓存,造成长期脏数据

实际可用的 3 种方案(按场景选型)

1. 先更新数据库,再删除缓存(Cache-Aside 旁路缓存)
  • 适用场景:对实时一致性要求高的业务
  • 原理:先写数据库,再删缓存,极端情况仅短暂不一致,概率极低
  • 优点:实现简单,一致性高
  • 缺点:极端并发下仍有极小概率出现脏数据
2. 缓存双删(延迟双删)
  • 适用场景:解决「先删缓存再更新数据库」的并发问题
  • 原理:先删缓存 → 更新数据库 → 延迟一段时间(如 500ms)再删一次缓存
  • 优点:大幅降低并发读导致的旧数据回种概率
  • 缺点:延迟时间难精准,仍有极小概率不一致
3. Binlog 异步更新(最终一致性方案)
  • 适用场景:对最终一致性要求高、可接受短暂延迟的业务
  • 原理:用 Canal 等工具监听 MySQL Binlog,通过消息队列异步更新 / 删除缓存
  • 优点:不侵入业务代码,一致性最好,性能高
  • 缺点:有短暂延迟,无法做到实时一致

Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?

一、核心定义与区别

三者本质都是缓存失效导致请求直接打数据库,核心差异在失效原因和影响范围:

问题 核心原因 影响范围 典型场景
缓存穿透 查询的数据缓存和数据库都不存在 单个非法请求 攻击者用不存在的 ID 疯狂请求,每次都穿透到 DB
缓存击穿 单个热点 Key过期瞬间,大量并发请求涌入 单个热点 Key 秒杀商品缓存到期,数万请求同时打 DB
缓存雪崩 大批量 Key 同时过期,或 Redis 整体宕机 全量 / 大面积 Key 缓存批量到期、Redis 集群故障,所有请求直打 DB

二、对应解决方案

1. 缓存穿透
  • 布隆过滤器:提前把所有合法 Key 存入布隆过滤器,非法请求直接拦截,不访问缓存和 DB
  • 缓存空值:对不存在的请求,在缓存中存一个空值(设置短过期时间),避免重复打 DB
2. 缓存击穿
  • 互斥锁(分布式锁):热点 Key 过期时,只让一个线程去查 DB 更新缓存,其他线程等待
  • 热点 Key 永不过期:对超热点数据不设置过期时间,后台异步更新缓存
  • 提前预热:在热点 Key 过期前,主动刷新缓存,避免过期瞬间的并发冲击
3. 缓存雪崩
  • 打散过期时间:给 Key 的过期时间加随机值,避免批量同时过期
  • 高可用架构:搭建 Redis 集群 / 哨兵,避免单点故障导致缓存全面失效
  • 多级缓存:本地缓存(Caffeine)+ Redis 分布式缓存,兜底保护
  • 限流降级:对 DB 做限流,防止被海量请求打垮

Redis String 类型的底层实现是什么?(SDS)

一、核心底层结构

Redis String 底层用 SDS(Simple Dynamic String,简单动态字符串) 实现,不直接用 C 语言原生 char 数组,解决了 C 字符串的三大致命问题:

  1. 获取长度需遍历,时间复杂度 O (n)
  2. 依赖\0判断结束,无法存储二进制数据
  3. 手动扩容易导致缓冲区溢出

二、SDS 核心设计

SDS 在字符数组前加了头部结构,包含 3 个核心字段:

  • len:记录当前字符串实际长度,O (1) 获取长度
  • alloc:记录已分配的总空间,alloc - len 可直接算出剩余空间
  • buf:存储实际数据,末尾仍保留\0,兼容部分 C 字符串函数
核心优势
  • 杜绝缓冲区溢出:扩容前先检查alloc - len,空间不足再重新分配
  • 二进制安全:不依赖\0判断结束,可存储图片、音频等二进制数据
  • 高效内存管理:预分配冗余空间,减少内存重分配次数

三、String 三种编码优化(Redis 自动切换)

编码类型 适用场景 特点
int 编码 能解析为整数的字符串 直接用 int 存储,不分配 SDS,极致省内存
embstr 编码 长度 ≤ 44 字节的短字符串 redisObject 和 SDS 分配在一块连续内存,一次 malloc 完成,访问更快
raw 编码 长度 > 44 字节的长字符串 redisObject 和 SDS 分开存储,方便独立扩容

Redis 中如何实现分布式锁?

Redis 实现分布式锁的核心是 SET key value EX seconds NX 命令 + Lua 脚本,同时满足互斥性、防死锁、原子性三大核心要求:

  • NX(Only if Not eXists):保证只有一个客户端能加锁,实现互斥
  • EX(Expire):给锁设置过期时间,防止程序崩溃导致死锁
  • Lua 脚本:保证解锁时「先校验锁归属 + 再删除锁」的原子性,避免误删他人的锁
    加锁流程
  1. 客户端执行命令:SET lock_key uuid EX 30 NXuuid 是当前客户端唯一标识,用于解锁校验)

  2. Redis 检查 lock_key 是否存在:

    • 不存在:设置成功,返回 OK,加锁成功
    • 已存在:返回 nil,加锁失败

解锁流程(必须用 Lua 脚本保证原子性)

1
2
3
4
5
6
if redis.call('GET', KEYS[1]) == ARGV[1]
then
return redis.call('DEL', KEYS[1])
else
return 0
end
  1. 客户端执行上述 Lua 脚本
  2. GET lock_key 获取当前锁的 value
  3. 判断 value 是否等于自己的 uuid
    • 相等:执行 DEL lock_key,解锁成功
    • 不相等:返回 0,说明锁不属于自己,不删除,避免误删

Redis 的 Red Lock 是什么?你了解吗?

一、RedLock 定义

RedLock 是 Redis 作者提出的分布式锁算法,核心是通过多个独立的 Redis 实例达成多数派共识,解决主从架构下的锁丢失 / 互斥失效问题。

二、核心背景:为什么需要 RedLock?

单机 / 主从架构的 Redis 分布式锁有致命漏洞,会导致锁失效:

  1. 单机风险:单节点挂掉,锁完全丢失
  2. 主从风险(最核心):客户端在主节点加锁成功后,主节点宕机,锁数据未同步到从节点 → 从节点晋升为主节点后,另一个客户端可再次加锁 → 两个客户端同时持有锁,互斥性被破坏,数据错乱

三、核心原理与流程##### 1. 部署要求

需要部署 N 个(通常建议≥5 个)完全独立的 Redis 节点(主节点 / 独立实例,无主从关系)。

2. 加锁逻辑

客户端尝试向多数节点(N/2 + 1) 发送 SET lock_key uuid NX EX 命令:

  • 若在多数节点上成功加锁,则视为加锁成功
  • 若多数节点失败,则加锁失败
3. 容错机制
  • 即使少部分节点宕机 / 故障,只要多数节点存活,锁依然可用
  • 单个节点故障不影响整体锁的安全性,解决了主从数据不同步导致的锁失效问题
4. 关键细节
  • 容错设计:需考虑时钟漂移和网络延迟,使用随机等待时间 + 合理超时设置
  • 锁归属校验:解锁仍需用 Lua 脚本校验 uuid,防止误删

Redis 实现分布式锁时可能遇到的问题有哪些?

Redis 分布式锁落地时的核心坑点,按影响优先级总结如下:

1. 锁过期但业务未执行完
  • 问题:锁超时时间设置过短,业务逻辑执行时间超过锁的 TTL,锁自动释放,其他线程获取锁,导致数据错乱。
  • 本质:锁的有效期与业务执行时长不匹配。
2. 误删别人的锁
  • 问题:线程 A 锁超时释放,线程 B 获取新锁;A 执行完后删除锁,误将 B 的锁删除,导致锁互斥失效。
  • 本质:解锁时未校验锁的归属,直接删除。
  • 解决:用唯一标识(如 UUID)标记锁,解锁用 Lua 脚本校验归属后再删除。
3. 主从同步延迟(锁丢失)
  • 问题:主节点写入锁后宕机,锁数据未同步到从节点;从节点晋升为主节点,锁丢失,其他客户端可重复加锁,互斥性被破坏。
  • 本质:主从异步复制导致数据不一致。
  • 解决:用 RedLock(红锁)多实例多数派共识,或 Redisson 的主从锁优化。
4. 单点故障
  • 问题:单机 Redis 部署,节点宕机后锁服务完全瘫痪,无法提供分布式锁能力。
  • 本质:单节点存在单点风险。
  • 解决:搭建 Redis 集群 / 哨兵,或用 RedLock 多实例部署。
5. 时钟漂移
  • 问题:多节点系统时间不一致,导致锁的 TTL 判断不准,出现锁提前失效、延迟释放等问题。
  • 本质:分布式系统时钟无法完全同步。
  • 解决:尽量保证服务器时间同步,避免极端时钟偏差。
6. 锁不可重入
  • 问题:同一线程无法重复获取同一把锁,递归调用场景下会直接死锁。
  • 本质:原生 SET NX 不支持重入。
  • 解决:用 Redisson 等成熟框架实现可重入锁,记录锁持有者与重入次数。

Redis 的持久化机制有哪些?

1. RDB(快照持久化)
  • 核心原理:在某个时间点,将整个内存数据全量 dump 成二进制文件
  • 优点:恢复速度极快、文件体积小,非常适合做备份和灾难恢复
  • 缺点:两次快照之间的数据会丢失,数据丢失风险高(如 5 分钟一次快照,最多丢 5 分钟数据)
2. AOF(日志持久化)
  • 核心原理:将每一条写命令追加到日志文件末尾(Log Append Only)
  • 优点:数据安全性极高,最多丢失 1 秒数据(取决于 fsync 策略)
  • 缺点:文件体积大、恢复时需要重放所有命令,数据量大时恢复速度极慢
3. 混合持久化(Redis 4.0+ 引入)
  • 核心原理:结合 RDB+AOF 的优势,AOF 重写时先写一份 RDB 全量快照,后续增量命令用 AOF 格式追加
  • 恢复流程:先快速加载 RDB 基准数据,再回放 AOF 增量日志
  • 综合优势:完美平衡了 RDB 的快速恢复和 AOF 的高数据安全性,是生产环境推荐方案
特性 RDB AOF 混合持久化
数据安全性 低(丢快照间隔内数据) 高(最多丢 1 秒) 高(同 AOF)
恢复速度 极快 极慢(数据量大时) 快(同 RDB)
文件体积 中等
适用场景 备份、灾备 高数据安全要求 生产环境主流方案

Redis 主从复制的实现原理是什么?

Redis 主从复制是指从节点(Slave)同步主节点(Master)的数据,实现读写分离、数据备份与高可用的机制。

1. 第一阶段:建立连接 & 全量同步(初次复制)
  • 触发:从节点首次连接,发送 PSYNC 命令,主节点执行全量复制
  • 流程
    1. 主节点执行 BGSAVE 生成 RDB 快照,同时将新写命令暂存到 Replication Buffer
    2. 主节点将 RDB 文件发送给从节点,从节点清空旧数据并加载 RDB。
    3. 主节点发送 Replication Buffer 中的缓冲命令,从节点执行,完成数据全量同步。
2. 第二阶段:命令传播(全量同步后)
  • 建立长连接:全量同步完成后,主从建立长连接。
  • 核心逻辑:主节点每接收一条写命令,就异步发送给从节点执行,保持数据一致。
  • 心跳机制:主从互相发送 Ping/Ack 心跳包,确认对方存活。
3. 第三阶段:断线重连 & 增量同步(断线恢复)
  • 背景:断线后重复全量同步浪费资源,Redis 2.8+ 引入增量同步
  • 核心机制
    1. 主节点内部有环形缓冲区 repl_backlog_buffer(积压缓冲区),存储最近的写命令。
    2. 从节点重连后,发送 PSYNC runid offset 告知主节点之前的读取位置。
    3. 主节点检查:
      • Yes:缓冲区数据存在,只补发断线期间缺失的数据,即增量复制
      • No:缓冲区数据已被覆盖,重新全量复制

Redis 数据过期后的删除策略是什么?

Redis 过期 Key 的删除采用 「惰性删除 + 定期删除」双策略配合 的方案,两者互补,兼顾性能与内存效率

1. 惰性删除(被动触发)
  • 触发时机:每次对 Key 进行读写操作前
  • 执行逻辑:先检查该 Key 是否已过期,过期则直接删除,返回空;未过期则正常执行操作
  • 优缺点
    ✅ 优点:不占用额外 CPU 做全量扫描,性能开销极小
    ❌ 缺点:冷数据(过期后无人访问)会一直占用内存,造成内存泄漏
2. 定期删除(主动清理)
  • 触发时机:Redis 每 100ms 自动触发一次定时任务
  • 执行逻辑
    1. 随机抽取一批设置了过期时间的 Key 检查
    2. 删除其中已过期的 Key
    3. 限制单次清理的 CPU 时间,避免把 CPU 吃满,循环采样直到时间上限
  • 优缺点
    ✅ 优点:主动清理冷数据,避免内存浪费
    ❌ 缺点:无法 100% 清理所有过期 Key,只能通过采样控制清理效率
    两种策略互补:
  • 惰性删除负责「访问时兜底」,保证过期 Key 不会被业务读到
  • 定期删除负责「主动清理冷数据」,避免内存被长期占用
  • 两者必须配合使用,才能在性能和内存之间取得平衡

如何解决 Redis 中的热点 key 问题?

热点 Key 是指被高频访问的 Key(如秒杀商品、热搜数据),单秒请求量可达几十万。

  • 痛点:Redis 单线程处理命令,热点 Key 会把单个节点 CPU 吃满,导致流量倾斜、其他请求等待,甚至引发集群雪崩。
  • 核心解决思路分散压力

四大解决方案(按推荐优先级 / 效果排序)

1. 热点 Key 拆分(数据分片)
  • 原理:将一个热点 Key 复制成多份,打散到不同 Redis 节点上。
  • 操作:将 product:12345 拆分为 product:12345_0 ~ product:12345_9 共 10 个分片。
  • 路由:请求时根据用户 ID 或哈希取模路由到不同分片。
  • 效果:将单点压力分散至多个节点,彻底突破单节点 QPS 限制。
2. 多级缓存(本地缓存 + Redis)
  • 原理:在 Redis 前加一层 JVM 本地缓存(如 Caffeine/Guava),阻断网络请求。
  • 流程:请求优先走本地内存,未命中再去请求 Redis。
  • 效果:一级缓存可拦截 90% 以上 流量,实现零网络 I/O,大幅降低 Redis 压力。
3. 读写分离(扩从节点)
  • 原理:利用主从架构,将读请求分摊到多个从节点。
  • 操作:配置一主多从,扩充从节点数量(如 5-10 个)。
  • 路由:写请求走主库,读请求分摊到各个从库。
  • 效果:单节点压力直接除以数量级,单热点 Key 的读压力可被多从节点分担。
4. 限流降级(兜底保护)
  • 原理:在网关层或客户端对热点 Key 做流量控制。
  • 策略:超过阈值的请求直接返回降级数据(如 “太火爆了”)或友好提示。
  • 效果:牺牲部分用户体验,防止海量请求击穿 Redis 及下游数据库,避免服务整体雪崩。

Redis 集群的实现原理是什么?

Redis 集群(Cluster)是为了解决单机内存、并发瓶颈的分布式方案,核心是通过分片存储实现水平扩展,用三个核心关键词概括:分片、Gossip 协议、去中心化

三大核心原理

1. 数据分片:哈希槽(Hash Slot)
  • 核心机制:集群将整个数据空间划分为 16384 个哈希槽(slot),每个主节点负责维护一部分槽位。
  • 数据路由:写入 Key 时,先对 Key 做 CRC16(Key) % 16384 计算,得到对应的槽位,再将数据存储到负责该槽位的节点。
  • 优势:扩容 / 缩容极其方便,新增节点只需从其他节点迁移部分槽位,无需重算全量数据位置。
2. 节点通信:Gossip 协议(去中心化)
  • 核心机制:集群节点是去中心化的,无中心节点,通过 Gossip 协议互相通信。
  • 通信逻辑:每个节点定期随机选择邻居节点,交换彼此的状态信息(如负责的槽位、存活状态),短时间内让所有节点达成一致,同步集群拓扑。
  • 优势:避免单点故障,节点动态上下线时,集群可自动感知并同步状态。
3. 客户端路由:MOVED 重定向
  • 核心逻辑:客户端可连接集群任意节点:
    • 若请求的 Key 属于当前节点,直接返回数据;
    • 若不属于,节点返回 MOVED 错误(包含目标节点的 IP:Port),客户端根据重定向信息直连正确节点获取数据。
  • 补充:集群不做代理转发,由客户端自行维护路由,提升性能。

Redis 中的 Big Key 问题是什么?如何解决?

Big Key 指内存占用极大的 Redis Key,不只是字符串长度大,也包括集合类型(List/Hash/Set/ZSet)元素数量极多的情况。

Big Key 的核心危害

  1. 内存分布不均:集群模式下,大 Key 集中在单个实例,导致节点负载失衡,查询效率下降
  2. 命令阻塞:Redis 单线程执行,操作大 Key 耗时极长,会阻塞其他所有命令,引发服务卡顿
  3. 网络阻塞:大 Key 网络 I/O 传输流量大,导致传输延迟高、网络拥塞
  4. 客户端超时:操作耗时过长,导致客户端等待超时,影响业务体验

Big Key 的解决方案(三维度)

1. 开发层面(直接优化)
  • 拆分大对象:将大集合拆分为多个小 Key,比如 100 万 field 的 Hash 按 ID 取模拆成 100 个小 Hash
  • 数据压缩:用 Snappy、LZ4 等算法压缩数据后再存储,减少内存占用
  • 优化数据结构:比如用 String 存 JSON 的场景,换成 Hash 结构,大幅节省内存
2. 业务层面(从源头避免)
  • 只存必要数据:低频数据(如历史订单、详细地址)不缓存,按需查数据库
  • 限制缓存范围:比如评论列表只缓存前 100 条热门数据,从根源避免大 Key 产生
3. 架构层面(分布式兜底)
  • Redis Cluster 集群部署:将拆分后的小 Key 分散到不同节点,分散压力,提升响应速度

设计模式

单例模式有哪几种实现?如何保证线程安全?

单例模式是保证一个类在整个程序生命周期中只有一个实例,并提供全局唯一访问点的设计模式,核心是私有构造 + 全局唯一实例

Java 主流 5 种实现方式

实现方式 核心原理 线程安全 懒加载 优缺点
饿汉式 类加载时直接创建实例 ✅ 是(JVM 类加载保证) ❌ 否 优点:实现简单、天然线程安全;缺点:类加载即初始化,未使用会浪费资源,不支持懒加载
懒汉式(加锁版) 首次访问时创建,加synchronized保证安全 ✅ 是(加锁保证) ✅ 是 优点:按需创建、节约资源;缺点:每次获取都加锁,性能差
双重检查锁定(DCL) 两次 null 检查 + volatile禁止指令重排 ✅ 是(volatile + 锁) ✅ 是 优点:懒加载、高性能(仅首次加锁);缺点:实现稍复杂,仍有反射攻击风险
静态内部类 利用类加载机制,内部类延迟加载实例 ✅ 是(JVM 类加载保证) ✅ 是 优点:懒加载、线程安全、实现简洁,生产推荐;缺点:仍有反射攻击风险
枚举单例(Java 特有) 利用枚举天然单例特性 ✅ 是(JVM 枚举特性保证) ❌ 否 优点:天然线程安全、防反射 / 序列化攻击、实现极简,最佳实践;缺点:不支持懒加载

什么是策略模式?一般用在什么场景?

一、核心定义

策略模式是行为型设计模式,核心思想是:把一系列算法封装成独立的策略类,让它们可以互相替换;调用方只依赖抽象策略接口,完全不关心具体算法的实现细节。

它的核心价值是消除代码中大量的 if-else / switch-case,让代码更整洁、扩展性更强。

二、三大核心角色

角色 作用
Strategy(策略接口) 定义所有算法的统一抽象方法,是调用方依赖的唯一接口
ConcreteStrategy(具体策略) 实现策略接口,封装不同的算法逻辑(如微信支付、支付宝支付)
Context(上下文) 持有策略接口的引用,把实际工作委托给策略对象,隔离调用方与具体策略

三、核心优势

  1. 开闭原则:新增算法只需新建策略类,无需修改原有代码(Context 代码零改动)
  2. 消除冗余判断:彻底替代 if-else 分支,代码更易维护
  3. 算法解耦:不同算法独立封装,互不影响,可单独测试
  4. 灵活切换:运行时可动态切换策略,适配不同业务场景

四、经典代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 1. 策略接口:统一支付规范
interface PayStrategy {
void pay(int amount);
}

// 2. 具体策略:微信支付
class WeChatPay implements PayStrategy {
@Override
public void pay(int amount) {
System.out.println("微信支付 " + amount + " 元");
}
}

// 具体策略:支付宝
class AliPay implements PayStrategy {
@Override
public void pay(int amount) {
System.out.println("支付宝支付 " + amount + " 元");
}
}

// 3. 上下文:隔离调用方与具体策略
class PayContext {
private PayStrategy strategy;

// 动态设置策略
public void setStrategy(PayStrategy strategy) {
this.strategy = strategy;
}

// 委托策略执行支付
public void executePayment(int amount) {
strategy.pay(amount);
}
}

// 使用
public class Main {
public static void main(String[] args) {
PayContext context = new PayContext();
// 切换微信支付
context.setStrategy(new WeChatPay());
context.executePayment(100);

// 切换支付宝
context.setStrategy(new AliPay());
context.executePayment(200);
}
}

五、常见使用场景

  1. 支付系统:微信、支付宝、银联、Apple Pay 等多种支付方式
  2. 促销 / 定价系统:满减、打折、满赠、秒杀等不同价格计算规则
  3. 数据导出:Excel、CSV、PDF 等不同格式导出,接口统一
  4. 日志框架:Logback 的 Appender,支持控制台、文件、远程服务器等多种输出
  5. 排序 / 筛选算法:不同场景下的排序策略动态切换

什么是模板方法模式?一般用在什么场景?

模板方法模式是行为型设计模式
核心思想:在抽象类中定义算法的执行骨架(流程),并固定步骤顺序;将其中可变的具体细节延迟到子类实现。

  • 父类定骨架,子类填细节,子类不允许打乱执行顺序(模板方法通常用 final 修饰)。

一、两大核心角色

角色 作用 核心组件
AbstractClass(抽象类) 定义算法骨架与执行顺序 1. 模板方法 (final):定死步骤流程,禁止子类修改

2. 抽象步骤:强制子类实现的核心逻辑

3. 钩子方法:父类提供默认实现,子类按需覆盖
ConcreteClass(具体子类) 实现抽象步骤,填充业务逻辑 实现父类要求的抽象方法,覆盖钩子方法

二、核心优势

  1. 代码复用:子类共享父类的骨架逻辑,无需重复编写流程控制代码。
  2. 严格规范:父类锁定执行顺序,子类无法随意改变算法流程。
  3. 扩展方便:新增业务流程只需继承抽象类,实现指定方法,无需修改原有代码,符合开闭原则。

三、代码示例(数据处理场景)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 1. 抽象类:定义算法骨架
abstract class DataProcessor {
// 模板方法:final 修饰,定死执行顺序,子类不可修改
public final void process() {
readData(); // 步骤1:读取
processData(); // 步骤2:处理
writeData(); // 步骤3:输出
}

// 抽象步骤:强制子类实现
protected abstract void readData();
protected abstract void processData();

// 钩子方法:提供默认实现,子类可选覆盖
protected void writeData() {
System.out.println("Writing data to output.");
}
}

// 2. 具体子类:CSV 数据处理器
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.");
}
}

// 3. 具体子类:JSON 数据处理器
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(); // 执行顺序:读CSV -> 处理CSV -> 写默认输出

DataProcessor jsonProcessor = new JSONDataProcessor();
jsonProcessor.process(); // 执行顺序:读JSON -> 处理JSON -> 写默认输出
}
}

四、典型应用场景

模板方法模式适用于流程固定、细节多变的业务场景:

  1. 数据处理:如 CSV/JSON/XML 数据导入,读取 - 处理 - 写入流程固定,具体实现不同。
  2. 订单处理:校验库存 -> 扣减库存 -> 支付 -> 记录日志,各业务线流程一致但细节不同。
  3. Spring 框架应用Spring MVCDispatcherServlet 核心流程使用模板方法模式,定义了请求处理的完整骨架,具体处理器如 Controller 实现细节。
  4. 单元测试:父类定义测试流程(初始化 -> 执行测试 -> 销毁),子类实现具体的测试用例。
  5. 支付 / 审核流程:提交申请 -> 审核 -> 打款 / 驳回,流程固定但各业务方审核逻辑不同。

谈谈你了解的最常见的几种设计模式,说说他们的应用场景

一、核心高频设计模式

1. 单例模式
  • 核心定义:保证一个类在全局中只有一个实例,并提供全局访问点。
  • 应用场景
    • 数据库连接池(HikariCP、Druid):全局共用一个连接池实例,避免重复创建连接。
    • 配置中心客户端:配置信息全局唯一,避免重复加载配置。
    • 日志对象、线程池等全局资源管理。
2. 策略模式
  • 核心定义:将不同的算法 / 业务逻辑封装成独立的策略类,可互相替换,调用方依赖抽象接口。
  • 应用场景
    • 多渠道支付(支付宝、微信、银联):每个支付渠道是一个独立策略。
    • 促销规则(满减、打折、秒杀):不同促销逻辑封装为不同策略。
    • 数据导出(Excel、CSV、PDF):不同格式的导出逻辑作为策略。
3. 模板方法模式
  • 核心定义:在抽象类中定义算法骨架,固定执行顺序,可变步骤延迟到子类实现。
  • 应用场景
    • 支付流程:参数校验 → 核心支付 → 后置处理,固定流程不变,各渠道仅实现核心支付逻辑。
    • 数据处理流程:读取 → 处理 → 写入,流程固定,不同数据源实现不同步骤。
    • 任务调度流程:初始化 → 执行任务 → 清理资源,子类实现具体任务逻辑。
4. 简单工厂模式
  • 核心定义:由一个工厂类根据传入的标识,创建并返回对应的实例对象。
  • 应用场景
    • 支付渠道选择:根据 channel 参数,返回对应的支付策略实现类。
    • 日志处理器创建:根据日志级别 / 类型,创建不同的日志处理实例。
    • Spring 中通过 Map 注入实现工厂模式,根据标识直接获取实现类。

二、高频组合场景:策略 + 模板方法 + 简单工厂(支付系统示例)

1. 模式分工
模式 作用 支付场景中的体现
模板方法 固定通用流程,复用骨架逻辑 抽象类 AbstractPayService 定义 pay 方法,固定「参数校验 → 核心支付 → 后置处理」流程
策略模式 封装不同渠道的核心逻辑 AliPayServiceWechatPayService 等子类实现 doPay 方法,封装各渠道的支付逻辑
简单工厂 统一创建与获取实例 PayServiceFactory 根据 channel 参数,返回对应的支付策略实现类
2. 核心代码示例(支付场景)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 策略接口:定义支付统一规范
public interface PayService {
void pay(PayDto payDto);
}

// 模板方法基类:固定支付流程骨架
public abstract class AbstractPayService implements PayService {
@Override
public void pay(PayDto payDto) {
paramValidate(payDto); // 通用参数校验
doPay(payDto); // 子类实现的核心支付逻辑
afterProcess(); // 通用后置处理
}

private void paramValidate(PayDto payDto) {
// 通用参数校验逻辑
}

private void afterProcess() {
// 通用后置处理(如日志记录、状态更新)
}

// 抽象方法:由不同支付渠道实现核心逻辑
public abstract void doPay(PayDto payDto);
}

// 具体策略:支付宝支付
public class AliPayService extends AbstractPayService {
@Override
public void doPay(PayDto payDto) {
// 调用支付宝 SDK 核心支付逻辑
}
}

// 具体策略:微信支付
public class WechatPayService extends AbstractPayService {
@Override
public void doPay(PayDto payDto) {
// 调用微信支付 SDK 核心支付逻辑
}
}

// 简单工厂:统一获取支付渠道实例
@Component
public class PayServiceFactory {
@Resource
private Map<String, PayService> payServiceMap;

public PayService getPayService(String channel) {
return payServiceMap.get(channel);
}
}

你认为好的代码应该是什么样的?

好的代码核心是易读、易维护、可扩展、高可靠,就像一本清晰易懂的书,能让别人快速理解、安全修改

1. 清晰易懂(可读性)
  • 命名有意义:变量名能回答 “这是什么”(如 userCount 而非 cnt),方法名能回答 “这干什么”(如 validateEmail 而非 check)。
  • 布尔变量:用 is/has/can 开头(如 isValidhasPermission),直观反映返回值含义。
  • 避免缩写:除非行业通用(如 msg),否则不用 usr 这类模糊缩写。
  • 长度与作用域匹配:循环变量 i 没问题,但类成员变量需要有意义的名字。
2. 高内聚低耦合(可维护性)
  • 高内聚:每个模块 / 类有单一职责,功能集中,不混杂无关逻辑。
  • 低耦合:模块间依赖松散,修改一个模块时,对其他模块影响最小。
3. 可测试性

每个功能模块应设计为独立、可验证的单元,方便编写单元测试,确保代码正确性,快速发现潜在问题。

4. 易于扩展和修改(扩展性)

遵循开闭原则,能在不破坏现有功能的前提下,方便地扩展新需求,避免过度设计和僵化实现。

5. 遵循设计原则
  • SOLID 原则:单一职责、开闭、里氏替换、接口隔离、依赖倒置。
  • 辅助原则:DRY(不重复自己)、KISS(保持简单)、YAGNI(你并不需要它),避免不必要的复杂性。
6. 性能优良

在保证功能正确的前提下,选择合适的算法、数据结构和缓存策略,避免不必要的资源浪费。

7. 良好的错误处理

能处理各种异常情况,提供清晰的错误信息,避免系统崩溃,错误处理精确且易于调试。

8. 规范和一致性

遵循团队编码规范,缩进、命名、注释风格统一,方便团队协作和代码维护。

工厂模式和抽象工厂模式有什么区别?

一、核心区别

两者的核心区别在于创建对象的维度不同:

  • 工厂模式(工厂方法):关注单一类型对象的创建,一个工厂只生产一种产品。
  • 抽象工厂模式:关注一组 / 一族相关对象的创建,一个工厂负责生产一套配套的产品。

二、详细对比

1. 核心定义与比喻
模式 核心关注点 比喻
工厂模式(工厂方法) 创建单一类型的对象 一条手机生产线,只生产特定型号的手机;换型号就换一条新生产线。
抽象工厂模式 创建一组相关 / 互相依赖的对象 一个品牌工厂,生产手机 + 耳机 + 电脑等整套同品牌生态产品,不同品牌的产品不能混搭。
2. 代码实现差异
  • 工厂模式(工厂方法):抽象工厂只定义一个创建单一产品的方法,具体工厂实现该方法,生产对应型号的手机。
1
2
3
4
// 工厂模式:一个工厂只创建一种产品
public abstract class PhoneFactory {
public abstract Phone createPhone();
}
  • 抽象工厂模式:抽象工厂定义多个方法,创建一组相关产品(手机、耳机、电脑),具体工厂实现所有方法,生产同一品牌的整套产品。
1
2
3
4
5
6
// 抽象工厂模式:一个工厂创建一族相关产品
public interface DeviceFactory {
Phone createPhone();
Earphone createEarphone();
Laptop createLaptop();
}
3. 适用场景
  • 工厂模式:适用于产品类型单一、新增产品型号的场景,比如新增一种手机型号,只需新增一个具体工厂类。
  • 抽象工厂模式:适用于产品之间有强关联性、需要保证产品族一致性的场景,比如手机 + 耳机 + 电脑的品牌生态,需要保证所有产品属于同一品牌。

三、关键总结

  • 工厂模式解决的是单一产品的创建问题,新增产品需要新增工厂;
  • 抽象工厂模式解决的是产品族的创建问题,新增品牌需要新增工厂,新增产品类型则需要修改抽象工厂接口。

计算机网络

常见的 HTTP 状态码有哪些?

1xx 信息响应(请求已接收,继续处理)

  • 100 Continue:服务器已收到请求的初步部分,客户端可继续发送请求体。
  • 101 Switching Protocols:服务器同意切换协议(如从 HTTP 升级到 WebSocket)。

2xx 成功(请求已成功接收并处理)

  • 200 OK:请求成功,最常用。
  • 201 Created:资源创建成功(POST 请求创建新用户 / 订单时常用)。
  • 204 No Content:请求成功,但无响应体(DELETE 删除资源后常用)。

3xx 重定向(需进一步操作才能完成请求)

  • 301 Moved Permanently:资源永久重定向,浏览器会更新书签,搜索引擎更新索引。
  • 302 Found:资源临时重定向,下次仍访问原地址。
  • 304 Not Modified:资源未修改,客户端可直接使用本地缓存(配合缓存机制使用)。

4xx 客户端错误(请求有问题,服务器无法处理)

  • 400 Bad Request:请求格式错误(参数错误、JSON 格式错误等)。
  • 401 Unauthorized:未认证(未登录或 Token 过期,需先认证)。
  • 403 Forbidden:已认证但无权限访问(如普通用户访问管理员接口(403))。
  • 404 Not Found:资源不存在(URL 写错或资源已被删除)。

5xx 服务器错误(服务器处理请求时出错)

  • 500 Internal Server Error:服务器内部错误(代码抛异常、空指针、数据库连接失败等)。
  • 502 Bad Gateway:网关 / 代理从上游拿到无效响应(上游服务挂了或响应格式错误)。
  • 503 Service Unavailable:服务暂时不可用(服务器维护、流量过大扛不住)。
  • 504 Gateway Timeout:网关等待上游响应超时(上游服务处理太慢)。

HTTP 请求包含哪些内容,请求头和请求体有哪些类型?

一、HTTP 请求整体结构

HTTP 请求由 4 个核心部分 组成:

  1. 请求行
  2. 请求头
  3. 空行(分隔符)
  4. 请求体

二、各部分详解

1. 请求行

包含三个核心元素:

  • 请求方法(如 GET/POST

  • 请求路径(如 /api/users

  • 协议版本(如 HTTP/1.1

    示例:GET /api/users HTTP/1.1

2. 请求头

键值对形式,用于向服务器传递客户端信息,分为三类:

类型 说明 常见字段示例
通用头部 请求和响应都能使用 Cache-Control(缓存控制)、Connection(连接保持)
请求头部 仅用于请求,描述客户端信息 Host(目标主机)、User-Agent(客户端标识)、Accept(可接受的响应格式)、Authorization(认证信息)
实体头部 描述请求体的元信息 Content-Type(请求体格式)、Content-Length(请求体大小)
3. 空行

一个换行符,作用是分隔请求头和请求体,服务器通过它判断请求头是否结束。

4. 请求体
  • 仅在 POST/PUT 等方法中存在,GET 请求一般无请求体。
  • 用于向服务器提交数据,格式由 Content-Type 决定。

三、请求体常见格式

格式 说明 适用场景
application/x-www-form-urlencoded 传统表单提交格式,数据编码为 key1=value1&key2=value2 普通表单提交
multipart/form-data 多部分表单,用分隔符分割数据,支持不同类型 文件上传
application/json JSON 格式,结构化数据传输 RESTful API(最常用)
text/plain 纯文本格式 较少使用
application/xml XML 格式 老系统、SOAP 接口

HTTP 中 GET 和 POST 的区别是什么?

  • GET:用于获取资源,按规范不应改变服务器状态,是幂等的。
  • POST:用于提交数据,通常会产生副作用(如创建 / 更新资源),非幂等。

核心差异对比

对比维度 GET POST
参数传递方式 参数拼接在 URL 上,长度受浏览器 / 服务器限制(通常 2KB 左右) 参数放在 请求体(Body) 中,理论上无大小限制,适合传输大数据
安全性 参数直接暴露在 URL 中,会被浏览器历史、服务器日志、代理缓存记录,不适合传输密码等敏感信息 参数藏在请求体中,相对隐蔽,但本质仍是明文,真正安全需依赖 HTTPS
幂等性 规范上是幂等的:同一个请求发 10 遍,结果都一样,不改变服务器状态 规范上非幂等:同一个请求发 10 遍,可能创建 10 条数据,会改变服务器状态
缓存机制 可被浏览器、CDN 缓存,适合图片、静态页面等不变资源 默认不缓存,每次都会打到服务器
应用场景 资源查询、获取操作 数据提交、创建 / 更新操作

HTTP 1.0 和 2.0 有什么区别?

HTTP 版本演进的核心目标,是解决连接成本高、队头阻塞、头部冗余三大痛点,性能逐步升级。

一、版本演进核心逻辑

版本 核心痛点 关键升级
HTTP/1.0 短连接,每次请求都要建 / 断 TCP,效率极低 基础文本协议,仅支持串行请求
HTTP/1.1 队头阻塞(串行请求,前一个卡住后一个都得等)、头部冗余 长连接(Keep-Alive)、虚拟主机、分块传输
HTTP/2.0 1.1 无法解决的队头阻塞、带宽浪费 多路复用、二进制分帧、头部压缩、服务端推送

二、各版本核心差异详解

1. HTTP/1.0 与 1.1 的区别
  • 连接方式
    • 1.0:短连接,每次请求都要三次握手 + 四次挥手,效率极低。
    • 1.1:默认开启 长连接(Keep-Alive),同一个 TCP 连接可复用,减少频繁建连开销。
  • 虚拟主机支持
    • 1.0:无 Host 头,一台服务器只能部署一个网站。
    • 1.1:引入 Host 头,支持一台服务器部署多个虚拟主机,是云主机的基础。
  • 传输优化
    • 1.0:仅支持 Content-Length,大文件传输体验差。
    • 1.1:支持 Chunked 分块传输,可流式响应。
2. HTTP/1.1 与 2.0 的区别(核心升级)
对比项 HTTP/1.1 HTTP/2.0
连接模型 单车道串行,队头阻塞严重 多车道多路复用,一个 TCP 承载无数并行流
协议格式 纯文本,解析慢、易出错 二进制分帧,机器解析更快、更健壮
头部传输 明文全量重复传输,冗余严重 HPACK 头部压缩,减少 70%+ 流量
请求处理 必须按顺序排队,响应也需按序返回 真正并行,请求响应可乱序传输,互不干扰
服务端推送 不支持 支持 Server Push,主动推送资源(如 CSS/JS)

HTTP 2.0 和 3.0 有什么区别?

两者的核心差异在于底层传输层协议的变革:HTTP/2.0 基于 TCP,HTTP/3.0 基于 UDP(QUIC 协议),由此带来了性能、连接、传输方式的全方位升级。

1. 传输层协议不同
  • HTTP/2.0:基于 TCP,虽然应用层实现了多路复用,但底层仍受 TCP 约束。
  • HTTP/3.0:基于 UDP + QUIC 协议,在 UDP 之上实现了可靠传输机制,彻底摆脱 TCP 的历史包袱。
2. 彻底解决队头阻塞
  • HTTP/2.0 问题:应用层多路复用无法解决 TCP 层的队头阻塞。一旦 TCP 丢包,会暂停所有流的数据传输,等待重传,导致后续请求全部阻塞。
  • HTTP/3.0 解法:QUIC 协议的流是真正独立的。某一个流丢包,只会阻塞该流,其他流不受影响,弱网环境下性能提升巨大。
  • 通俗比喻:HTTP/2.0 是把 100 个人塞进一辆公交车,前面堵车全车都动不了;HTTP/3.0 是给每个人发了一辆车,谁丢包只影响自己,互不干扰。
3. 建连速度大幅提升(0-RTT)
  • HTTP/2.0:需要 TCP 三次握手 + TLS 握手,最快也要 2-3 个 RTT 才能发送数据。
  • HTTP/3.0:QUIC 将传输层握手与 TLS 1.3 加密握手合并。老用户(之前连过)可直接 0-RTT 发送数据,实现 “打招呼的同时就把事办了”。
4. 支持连接迁移
  • HTTP/2.0:基于 TCP 四元组(IP+Port)识别连接,从 Wi-Fi 切到 4G 导致 IP 变化时,连接会断开,必须重连。
  • HTTP/3.0:基于 Connection ID 识别连接,无论 IP 如何变化,只要 ID 不变,连接就不会断,视频通话、下载等场景体验更稳定。
5. 头部压缩算法升级
  • HTTP/2.0:使用 HPACK,依赖 TCP 的有序传输,包乱序会导致动态字典错乱。
  • HTTP/3.0:升级为 QPACK,允许头部帧乱序到达,完美适配 UDP 的无序传输特性。

HTTP 和 HTTPS 有什么区别?

HTTP 和 HTTPS 的核心差异,是 HTTPS 在 HTTP 基础上加入了 SSL/TLS 加密层,解决了 HTTP 明文传输的安全问题。

一、核心区别详解

1. 安全性(最本质的区别)
  • HTTP:明文传输,存在三大安全风险:
    • 被窃听:账号密码等敏感信息直接暴露在传输链路上,易被第三方获取。
    • 被篡改:数据在传输过程中可能被运营商或黑客修改(如植入广告)。
    • 被冒充:无法验证服务器身份,用户可能访问钓鱼网站。
  • HTTPS:通过 SSL/TLS 协议实现三重保障:
    • 加密:解决窃听问题,数据传输为密文。
    • 校验:解决篡改问题,数据完整性校验防止被修改。
    • 数字证书:解决冒充问题,验证服务器身份的合法性。
2. 性能差异
  • HTTP:仅需 TCP 三次握手即可传输数据,速度快,无额外开销。
  • HTTPS:需要在 TCP 握手后额外进行 TLS 握手(传统 TLS 为 4 次交互),且加解密会消耗 CPU 资源,性能略低。
  • 加分项:HTTP/2 的多路复用和 TLS 1.3 的优化(握手仅需 1-RTT 甚至 0-RTT),已让两者性能差距几乎可忽略。
3. 默认端口不同
  • HTTP 默认端口:80
  • HTTPS 默认端口:443
4. 搜索引擎与用户体验
  • HTTP:搜索引擎收录优先级低,浏览器会直接在地址栏标红提示 “不安全”,严重影响用户信任。
  • HTTPS:谷歌、百度等优先收录 HTTPS 网站,浏览器显示安全锁标识,用户体验更好。

TCP 和 UDP 有什么区别?

TCP 和 UDP 是传输层的两大核心协议,核心差异在于 可靠性与实时性的取舍:TCP 追求可靠传输,UDP 主打低延迟。

核心区别对比

对比维度 TCP UDP
连接方式 面向连接,需要三次握手建立连接 无连接,知道 IP 和端口即可直接发送
可靠性 可靠传输,保证数据送达、有序、无重复,支持重传 不可靠传输,不保证送达,可能丢包、乱序
流量 / 拥塞控制 有滑动窗口、拥塞控制,自动调节传输速率 无控制机制,发送速率完全由应用层决定
顺序保证 保证数据按发送顺序到达 不保证数据顺序,可能乱序到达
头部开销 较大,20 字节起步 较小,固定 8 字节
传输模式 字节流,无边界,需处理粘包 / 拆包 数据报,有边界,每个数据包独立
性能延迟 延迟较大,性能开销高 延迟小,性能开销低
适用场景 对准确性要求高的场景:文件传输、HTTP/HTTPS、邮件 对实时性要求高、可容忍少量丢包的场景:视频通话、直播、在线游戏

说说 TCP 的三次握手?

三次握手是 TCP 建立可靠连接的核心流程,通过三次报文交互,确保客户端和服务器的收发能力正常,并同步初始化序列号,为后续可靠传输奠定基础。

三次握手完整流程

1. 第一次握手(客户端 → 服务器)
  • 客户端发送 SYN 报文(SYN=1),携带随机初始化序列号 seq=x
  • 客户端状态:SYN_SENT(同步已发送)。
  • 作用:客户端向服务器发送连接请求,告知自己的初始序列号。
2. 第二次握手(服务器 → 客户端)
  • 服务器收到 SYN 后,回复 SYN+ACK 报文(SYN=1ACK=1)。
  • 确认号 ack = x+1(确认客户端的请求),同时携带服务器自己的初始化序列号 seq=y
  • 服务器状态:SYN_RCVD(同步收到)。
  • 作用:服务器确认收到客户端请求,并向客户端发送自己的初始序列号。
3. 第三次握手(客户端 → 服务器)
  • 客户端收到 SYN+ACK 后,发送 ACK 报文(ACK=1)。
  • 确认号 ack = y+1(确认服务器的请求)。
  • 客户端状态变为 ESTABLISHED(已建立连接)。
  • 服务器收到该 ACK 后,状态也变为 ESTABLISHED,连接正式建立。
  • 补充:第三次握手的报文可以携带数据。

TCP 是用来解决什么问题?

TCP 的核心目标,是在不可靠的 IP 网络之上,实现可靠、高效的端到端数据传输。IP 层只负责 “发出去”,不管丢包、乱序、重复;而 TCP 在 IP 层之上,通过四大核心机制解决了这些问题。

TCP 解决的四大核心问题

1. 可靠传输(解决 “丢包、乱序、重复” 问题)
  • 核心机制:序列号(Seq) + 确认号(ACK) + 超时重传
  • 原理:每个字节数据都有编号,接收方收到后回复 ACK;发送方超时未收到 ACK 则重传,确保数据不丢、不重复、按顺序到达。
2. 流量控制(解决 “发送太快,接收方处理不过来” 问题)
  • 核心机制:滑动窗口 + 接收窗口通告
  • 原理:接收方通过 “窗口大小” 告诉发送方自己还能接收多少数据,发送方据此控制发送速率,防止把接收方的缓冲区撑爆。
3. 拥塞控制(解决 “发送太快,把网络打爆” 问题)
  • 核心机制:慢启动、拥塞避免、快速重传、快速恢复
  • 原理:发送方感知网络拥塞状态,动态调整发送速率,避免因网络拥堵导致大量丢包,防止网络 “打爆”。

4. 连接管理(解决 “连接的建立与断开” 问题)

  • 核心机制:三次握手建立连接、四次挥手断开连接
  • 原理:通过状态同步和参数协商,确保双方通信 “有始有终”,资源正确释放,避免半开连接或无效连接占用资源。

说说 TCP 的四次挥手?

四次挥手是 TCP 全双工连接优雅断开的过程,核心是双方互相确认 “我没有数据要发送了”,并释放连接资源。

四次挥手完整流程(以客户端主动断开为例)

1. 第一次挥手(客户端 → 服务器)
  • 客户端发送 FIN 包,停止发送数据,进入 FIN_WAIT_1 状态。
  • 服务器收到 FIN 后,不再接收客户端数据,但仍可继续向客户端发送数据。
  • 通俗理解:客户端说 “我没数据发了,想挂电话”。
2. 第二次挥手(服务器 → 客户端)
  • 服务器收到 FIN 后,回复 ACK 包,确认收到断开请求,进入 CLOSE_WAIT 状态。
  • 客户端收到 ACK 后,进入 FIN_WAIT_2 状态,等待服务器的断开请求。
  • 通俗理解:服务器说 “知道了,但你先等会儿,我还有话没说完”。
3. 第三次挥手(服务器 → 客户端)
  • 服务器数据发送完毕后,发送 FIN 包给客户端,进入 LAST_ACK 状态。
  • 通俗理解:服务器说 “我也没数据发了,挂吧”。
4. 第四次挥手(客户端 → 服务器)
  • 客户端收到 FIN 后,回复 ACK 包,进入 TIME_WAIT 状态,等待 2MSL(最长报文寿命) 后才进入 CLOSED 状态。
  • 服务器收到该 ACK 后,立即进入 CLOSED 状态,连接正式断开。
  • 通俗理解:客户端说 “好的,再见”。

TCP 的粘包和拆包能说说吗?

粘包和拆包是 TCP 面向字节流特性带来的典型问题,核心原因是 TCP 不保留消息边界,导致接收方无法直接区分每条消息的起止位置。

一、核心概念

  • 粘包:发送方发送的多个独立消息,被接收方一次性收到,数据 “粘” 在一起,分不清哪到哪是一条消息。
    例:发送方发了 "ABC""EF",接收方收到 "ABCEF"

  • 拆包:发送方发送的一个完整消息,被接收方拆分成多个部分分次收到。

    例:发送方发了 "ABCDEF",接收方分两次收到 "ABC""DEF"

二、产生原因

  1. TCP 面向字节流:TCP 是流式协议,没有消息边界的概念,数据像水流一样传输,不区分独立的 “包”。
  2. 发送缓冲区限制:一个大包可能因缓冲区大小限制,被拆分成多个包发送。
  3. Nagle 算法:会把多个小包攒在一起发送,节省带宽但会导致粘包。
  4. MTU 限制:超过网络最大传输单元(MTU)的包,会被 IP 层分片,导致拆包。

三、解决方案

方案 原理 适用场景
定长消息 规定每条消息固定长度,不足则补空字符 消息长度固定、场景简单的情况
分隔符 用特殊字符(如换行符 \n)分隔消息 文本协议、消息内容不会包含分隔符的场景
长度字段 + 内容 消息头部携带长度字段,接收方先读长度,再读取对应字节数的内容 通用、最常用的方案,如 HTTP、自定义协议

说说 TCP 拥塞控制的步骤?

TCP 拥塞控制的核心目标,是防止发送方把网络 “打爆”,通过动态调整发送速率,避免因网络拥塞导致大量丢包。它分为四个核心阶段:

四大阶段详解

1. 慢启动(Slow Start)
  • 场景:连接刚建立时,发送方不知道网络的承受能力,需要 “试探”。
  • 机制:从 1 个 MSS(最大分段大小)开始发送,每收到一个 ACK,拥塞窗口 cwnd 增加 1 个 MSS。
  • 效果:每个 RTT(往返时间)内 cwnd 翻倍,呈指数级增长,直到碰到慢启动阈值 ssthresh 或发生丢包。
2. 拥塞避免(Congestion Avoidance)
  • 场景:当 cwnd 增长到 ssthresh 后,网络已接近饱和,需要 “小心翼翼” 探测上限。
  • 机制:每个 RTT 内 cwnd 只增加 1 个 MSS,呈线性增长,避免过快发送导致网络拥塞。
3. 快速重传(Fast Retransmit)
  • 场景:发送方收到 3 个重复的 ACK,说明某个包大概率丢失了。
  • 机制:不用等待超时计时器,直接重传丢失的那个包,减少等待时间。
4. 快速恢复(Fast Recovery)
  • 场景:快速重传之后,为了避免网络再次拥塞,需要调整拥塞窗口。
  • 机制
    1. ssthresh 设为当前 cwnd 的一半
    2. cwnd 更新为新的 ssthresh
    3. 之后进入拥塞避免阶段,线性增长 cwnd,快速恢复到丢包前的传输速率。

TCP/IP 四层模型是什么?

TCP/IP 四层模型是互联网实际使用的网络分层架构,从下到上依次为:网络接口层、网络层、传输层、应用层。核心作用是把网络通信拆分成独立层级,每层只负责自己的功能,通过标准接口交互,实现解耦与易维护。

一、四层结构与核心协议

1. 网络接口层(最底层)
  • 职责:负责在物理网络上收发数据帧,封装 IP 包,通过 MAC 地址在局域网寻址。
  • 核心协议:以太网、Wi-Fi、PPP
  • 核心比喻:数据传输的 “物理公路”,负责把数据帧在局域网内传递。
2. 网络层
  • 职责:核心是 IP 协议,负责跨网络的路由与转发,决定数据包走哪条路到达目的地。
  • 核心协议:IP(IPv4/IPv6)、ICMP、ARP
  • 核心比喻:数据传输的 “交通导航系统”,负责把数据包从一个网络转发到另一个网络。
3. 传输层
  • 职责:提供端到端的进程通信服务,负责数据的可靠 / 快速传输。
  • 核心协议:TCP(可靠、有连接、有确认)、UDP(快速、不可靠)
  • 核心比喻:数据传输的 “快递员”,确保数据从发送端进程准确到达接收端进程。
4. 应用层(最上层)
  • 职责:面向具体应用的协议,定义应用如何使用网络服务。
  • 核心协议:HTTP/HTTPS(网页)、FTP(文件)、SMTP(邮件)、DNS(域名解析)
  • 核心比喻:数据传输的 “业务服务系统”,为用户提供具体的网络应用功能。

二、分层的核心好处

1. 解耦(最大优势)

每层只负责自己的事,通过标准接口与上下层交互。例如:更换网卡,只要仍遵循以太网协议,上层的 IP、TCP、HTTP 都无需修改。

2. 独立演进

各层可以独立升级,不影响其他层。例如:

  • IPv4 升级到 IPv6,传输层和应用层不受影响;
  • TCP 优化拥塞控制算法,应用层代码不用改。
3. 易于排查

出问题时可快速定位层级:

  • ping 不通 → 网络层问题;
  • 连接建不起来 → 传输层问题;
  • 请求返回 500 → 应用层问题。

Cookie、Session、Token 之间有什么区别?

三者都是用户身份识别和状态管理的方案,核心区别在于存储位置、状态维护方式和适用场景

一、核心定义与通俗比喻

方案 核心定义 通俗比喻
Cookie 存储在浏览器本地的一小段数据,每次请求会自动带上 自己带着写满信息的纸条去办事
Session 用户状态保存在服务器,客户端只存一个 sessionId 办事大厅发的号码牌,资料存在柜台
Token 自包含凭证,服务端不存数据,验签即可确认身份(如 JWT) 身份证,证件本身就能证明身份

二、核心区别对比

对比维度 Cookie Session Token(如 JWT)
存储位置 客户端(浏览器本地) 服务端(内存 / 数据库) 客户端(HTTP Header)
状态维护 无状态,数据全存在客户端 有状态,依赖服务端存储会话数据 无状态,凭证自包含,服务端无需存状态
适用场景 简单客户端状态存储(如记住用户偏好、追踪行为) 传统 Web 应用,服务端需保存较多会话数据 分布式系统、移动端、跨域场景,扩展性好
扩展性 差,跨域 / 移动端支持有限 差,服务端集群需共享 Session 好,天然支持分布式、跨域、前后端分离
安全性 易被篡改、窃取,依赖服务端校验 安全性高,用户数据存在服务端 依赖签名校验,防篡改,需妥善保管凭证

从网络角度来看,用户从输入网址到网页显示,期间发生了什么?

这是一个从应用层到物理层,再跨越网络到达服务器,最后原路返回并渲染的完整过程。核心是数据封装、网络传输、解封装处理、渲染四个环节。

全流程分为三个阶段

1. 第一阶段:准备与打包(浏览器端)
  1. 解析 URL:拆解网址,提取出协议(HTTP/HTTPS)、域名、路径。

  2. DNS 解析:通过浏览器缓存 -> 系统缓存 -> 路由器缓存 -> DNS 服务器,最终拿到目标服务器的 IP 地址

  3. 建立连接

    • 基于 IP 地址,通过 TCP 三次握手 与服务器建立可靠连接。
    • 若为 HTTPS,还需进行 TLS 握手,协商加密密钥,确保安全传输。
  4. 构造请求:浏览器组装成完整的 HTTP 请求报文

  5. 协议栈封装:数据从应用层向下,层层添加头部,最终变成二进制数据包。

    • 传输层:加 TCP 头(源端口、序列号等)。
    • 网络层:加 IP 头(源 IP、目的 IP)。
    • 数据链路层:加 MAC 头(源 MAC、目的 MAC),并通过 ARP 协议 查到下一跳(通常是网关)的 MAC 地址。
2. 第二阶段:网络传输(路上的事)
  1. 物理发送:网卡将二进制数据转为电 / 光信号,通过网线 / Wi-Fi 发送。

  2. 交换机转发:数据到达局域网交换机,根据 MAC 地址表 转发到路由器。

  3. 路由器转发:数据出局域网,到达路由器。

    • 路由器查看 IP 路由表 决定下一跳。
    • 关键操作:剥掉旧的 MAC 头,换上新的 MAC 头(下一跳设备 MAC),IP 头的 TTL(生存时间)减 1,然后将数据包转发,跨越互联网。
  4. 到达服务器:数据包经过路由,最终到达目标服务器。

3. 第三阶段:处理与渲染(服务器端)
  1. 服务器解封装

    • 服务器网卡接收数据,操作系统按层拆包:去掉 MAC 头 -> 去掉 IP 头 -> 去掉 TCP 头。
    • 最终把纯净的 HTTP 请求报文交给 Nginx 等 Web 服务进程。
  2. 处理与响应

    • 服务器处理请求(查询数据库、执行业务逻辑),生成 HTTP 响应报文
    • 响应报文同样经过封装(TCP/IP/MAC),原路返回给浏览器。
  3. 浏览器渲染

    • 浏览器收到 HTML、CSS、JS 等资源。
    • 解析 DOM 树、CSSOM 树,生成渲染树,进行布局与绘制,最终将网页展示给用户。

线程和进程有什么区别?

核心结论:进程是操作系统分配资源的最小单位,线程是 CPU 调度的最小单位,线程属于进程,是进程内的独立执行流。

一、核心定义与通俗比喻

  • 进程:正在运行的程序实例,拥有独立的内存空间(代码段、数据段、堆等),进程间相互隔离。

    比喻:工厂里的独立车间,有自己的厂房、设备和资源。

  • 线程:进程内的独立执行单元,一个进程可包含多个线程,共享进程的内存与资源(堆、全局变量、文件句柄等),仅栈和寄存器私有。

    比喻:车间里的工人,共享车间设备,但有自己的工位和进度。

二、核心区别对比

对比维度 进程(Process) 线程(Thread)
内存空间 进程间完全隔离,拥有独立地址空间,数据不互通 同进程内线程共享堆、代码段、全局变量,仅栈、寄存器私有
资源开销 开销大:创建 / 销毁成本高,进程切换需保存完整上下文,缓存易失效 开销小:创建 / 销毁成本低,线程切换仅需保存少量寄存器,速度快
通信方式 复杂,需通过 IPC(管道、消息队列、共享内存) 简单,直接读写共享变量即可,但需注意线程同步
稳定性 稳定性高:一个进程崩溃不影响其他进程 稳定性低:一个线程崩溃会导致整个进程崩溃,所有线程挂掉
调度单位 资源分配单位 CPU 调度的最小单位

进程之间的通信方式有哪些?

进程间通信(IPC)是为了解决进程间资源隔离、无法直接访问对方内存的问题,常见方式有以下 6 种:

一、核心通信方式详解

1. 管道(Pipe)
  • 原理:最简单的 IPC 方式,数据单向流动,一端写入,另一端读出。
  • 分类
    • 匿名管道:仅用于有亲缘关系的进程(如父子进程),cat file | grep xxx 就是典型应用。
    • 命名管道:在文件系统有名字,无关进程也能使用。
  • 特点:单向通信,实现简单,适合简单数据流。
2. 消息队列(Message Queue)
  • 原理:允许进程发送 / 接收带类型的消息,消息在队列中排队,接收方可按类型选择性读取。
  • 特点:比管道灵活,但每条消息有大小限制(Linux 默认 8KB),适合结构化消息传递。
3. 共享内存(Shared Memory)
  • 原理:多个进程直接读写同一块物理内存,无需数据拷贝,是最快的 IPC 方式
  • 注意:无内置同步机制,必须配合信号量 / 互斥锁使用,否则会出现竞态条件。
  • 适用场景:大数据量、高频交互场景,如数据库缓冲池、高性能缓存系统。
4. 信号量(Semaphore)
  • 原理:本身不传递数据,用于进程同步 / 互斥,控制多个进程对共享资源的访问顺序。
  • 作用:解决竞态条件,实现资源互斥访问或限流。
5. 信号(Signal)
  • 原理:异步通知机制,用来告诉进程发生了某个事件(如 Ctrl+C 发送 SIGINTkill -9 发送 SIGKILL)。
  • 特点:只能传递信号编号,无法携带复杂数据,适合事件通知、进程控制。
6. 套接字(Socket)
  • 原理:最灵活的通信方式,既能本机进程间通信,也能跨网络通信。

  • 分类

    • Unix 域套接字:本机进程间通信,效率比 TCP 高。
    • 网络套接字:基于 TCP/UDP 协议,是分布式系统通信的基础。

二、IPC 方式对比表

通信方式 数据流向 适用场景 效率
管道 单向 父子进程、Shell 管道 中等
消息队列 双向 结构化消息传递、解耦 中等
共享内存 双向 大数据量、高频交互 最高(需同步)
信号 单向 事件通知、进程控制 低(仅传信号号)
信号量 无数据传输 进程同步 / 互斥 -
套接字 双向 跨网络 / 本地灵活通信 本地高、网络中等

进程的调度算法你知道吗?

进程调度的核心是决定 CPU 该让哪个进程跑、跑多久,不同算法各有适用场景。

一、核心调度算法详解

1. 先来先服务(FCFS)
  • 规则:谁先到谁先跑完,按请求顺序执行。
  • 特点:实现简单,但容易出现 “护航效应”—— 前面一个大任务会让后面所有小任务长时间等待,平均等待时间长。
2. 短作业优先(SJF)
  • 规则:执行时间短的任务优先执行,能把平均等待时间压到最低。
  • 问题:需要提前知道每个任务的执行时间,实际系统中很难获取。
  • 抢占式变种:SRTF(最短剩余时间优先),剩余执行时间最短的进程优先抢占 CPU。
3. 优先级调度
  • 规则:给进程设置优先级分数,优先级高的进程优先执行。
  • 特点:系统进程优先级通常高于用户进程,但可能导致低优先级任务饥饿,永远得不到执行机会。
4. 时间片轮转(RR)
  • 规则:每个进程执行固定时间片(如 10ms),时间片到了就切换到下一个进程。
  • 特点:公平轮转,能保证每个程序都有响应,Linux 桌面系统普遍采用该思路,适合交互式场景。
5. 多级反馈队列(MLFQ)
  • 规则:融合多种算法的综合调度方案:
    1. 新进程先进入高优先级队列,时间片短;
    2. 一个时间片没跑完的进程,自动下降到下一级队列,时间片变长;
    3. 短任务快速在高层队列完成,长任务慢慢沉到底层队列用大时间片执行。
  • 优势:兼顾短任务响应速度和长任务执行效率,是现代操作系统的主流调度思路。
6. 最高响应比优先(HRRN)
  • 规则:通过计算响应比决定调度顺序,响应比公式:(等待时间 + 执行时间) / 执行时间
  • 优势:平衡长短任务的等待时间,避免短任务过多导致长任务饥饿,适合批处理环境。