后端八股文
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(归档存储)
- 核心特性:只支持
INSERT和SELECT,不支持索引、高压缩率。 - 适用场景:日志归档、历史订单存储、只增不改的数据冷备份。
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) 为例:
- 先按
first_name排序 first_name相同的,再按last_name排序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. 节点容量计算
- 非叶子节点(索引页):
- 能存储的索引项数量 = 16×1024÷14≈1170 个。
- 即:一个根节点可以指向 1170 个中间节点。
- 叶子节点(数据页):
- 能存储的记录数 = 16×1024÷1≈16 条。
- 即:每个叶子节点实际存储 16 条数据。
3. 三层总容量计算
三层 B+ 树结构:1 (根节点)→1170 (中间节点)→1170×1170 (叶子节点)
总存储数 = 第二层节点数 × 第三层每条节点记录数
1170×1170×16≈2190 万条记录
最终结论与意义
- 存储能力:三层 B+ 树可以高效存储约 2000 万 条数据。
- 查询性能:意味着查询任意一条数据,最多只需 3 次磁盘 I/O(根节点 → 中间节点 → 叶子节点),I/O 开销极低,性能高效且稳定。
MySQL中的回表是什么?
回表是 InnoDB 引擎中,使用二级索引(非聚簇索引)查询时的额外操作:
二级索引的叶子节点只存储「索引列值 + 主键值」,不存储完整数据行。如果查询需要的字段不在二级索引中,就必须拿着主键值,再去聚簇索引(主键索引)中查询一次完整数据行,这个二次查询的过程就叫**回表。
简单来说:二级索引查不到完整数据,得回到主键索引再查一遍。
一、回表的完整流程(以 idx_age 二级索引为例)
- 第一步:二级索引定位主键
执行SELECT * FROM user WHERE age = 20时,先在idx_age二级索引树中,找到age=20对应的所有主键id(如id:101、id:505)。 - 第二步:聚簇索引回表查完整数据
拿着这些id,再去主键索引(聚簇索引)树中,逐个查询对应id的完整数据行,最终返回结果。
二、核心影响与优化
1. 性能影响
回表会多一次磁盘 I/O 操作,会降低查询效率,因此要尽量避免不必要的回表。
2. 优化方案:覆盖索引
如果查询的所有字段,都已经包含在二级索引中,就不需要回表,直接从二级索引中取数据即可,这就是覆盖索引。
- 示例:
SELECT id, age FROM user WHERE age = 20,id和age都在idx_age索引中,直接从索引取数,无需回表。
MySQL 中使用索引一定有效吗?如何排查索引效果?
索引不一定有效,用了索引也不一定快。
MySQL 最终是否使用索引、以及索引是否高效,由 优化器的成本计算 决定。优化器会权衡 索引成本 和 全表扫描成本,选择代价最低的执行计划。
一、索引为什么会 “失效”?(不生效的场景)
- 全表扫描更便宜:
- 表数据量极少(如几百行),走索引还要定位叶子节点,开销反而比直接扫全表高
- 优化器判定:走索引不如直接全表扫描
- 统计信息不准:
- 表数据分布变化导致 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) | 性能评价 |
|---|---|---|
| 真正用好索引 | type 为 ref / range / const |
高效低延迟,B+ 树快速定位 |
| 索引未完全利用 | type 为 index |
效率较差,需优化 |
| 索引完全失效 | type 为 ALL |
性能极差,IO 高,应避免 |
在 MySQL 中建索引时需要注意哪些事项?
索引不是越多越好,要在「查询收益」和「写入开销」之间做平衡,遵循「按需建、精准建、不冗余」的原则。
二、6 大核心注意事项
1. 索引不是越多越好
- 每个索引都会占用磁盘空间,且每次增删改操作都需要维护 B + 树结构,索引越多,写入性能越差。
- 只建真正需要的索引,避免冗余索引。
2. 区分度太低的字段不要建索引
- 区分度 = 不同值的数量 / 总数据量,区分度极低(如性别、状态)的字段,过滤效果差,索引几乎失效。
- 例外场景:如果某状态占比极低(如定时任务表 99% 成功、1% 失败),对失败状态建索引可过滤 99% 数据,此时建索引有意义。
3. 大字段不要建索引
TEXT、LONGTEXT、大VARCHAR等大字段,索引占用空间极大,加载到内存会挤占 Buffer Pool,影响其他热点数据性能。- 若必须用,可建前缀索引,只取字段前 N 个字符做索引,节省空间。
4. 写多读少的表要慎重建索引
- 若表的修改频率远大于查询频率,索引带来的写入开销会超过查询收益,得不偿失(如日志表、流水表)。
5. 高频查询 / 关联字段要建索引
WHERE条件中频繁出现的字段、多表JOIN的关联字段,优先建索引;多个字段常一起查询,优先建联合索引,一个索引覆盖多个查询场景。
6. 排序 / 分组 / 去重字段要建索引
ORDER BY、GROUP BY、DISTINCT后的字段建索引,可避免临时表和文件排序,大幅提升查询性能。
三、建索引决策流程图
按以下 5 个问题依次判断,决定是否建索引:
- Q1:是高频查询 / 排序 / 分组字段吗? → 否:不建议建
- Q2:区分度是否够高(>0.1 或唯一)? → 否:不建议建
- Q3:字段类型非常大(Text/Blob)? → 是:考虑前缀索引;否:继续
- Q4:已有联合索引能否覆盖? → 是:复用现有;否:继续
- Q5:写入频率远大于查询频率? → 是:不建议建;否:创建 / 复用索引
MySQL 中的索引数量是否越多越好?为什么?
MySQL 索引不是越多越好。
索引是一把双刃剑,它能加速查询,但会严重增加写入开销、优化器负担、空间占用,索引过多会直接导致系统性能下降。
索引过多的四大核心代价
1. 写入性能下降(写性能打折)
- 维护成本高:每次
INSERT、UPDATE、DELETE操作,都需要同步更新所有相关索引的 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未走索引)
二、核心判断标准
| 状态 | 判定标准 | 优化建议 |
|---|---|---|
| ✅ 高效查询 | type 为 ref/range/const、key 非空、rows 小、Extra 含 Using index |
无需优化 |
| ⚠️ 待优化 | type 为 index、Extra 含 Using filesort/Using temporary |
调整索引、SQL 语句 |
| ❌ 严重问题 | type 为 ALL、key 为 NULL |
紧急优化,避免全表扫描 |
MySQL 中如何进行 SQL 调优?
SQL 调优的核心目标是 减少磁盘 I/O 和 避免无效计算。
实际操作按 “三步走”:先定位慢 SQL → 再分析执行计划 → 最后针对性优化。
一、三大核心优化维度
1. 索引层面优化(最有效)
- 合理设计联合索引,利用覆盖索引:
查询只需要部分字段时,建立联合索引覆盖查询列,直接从索引取数,避免回表。
例:只需name和age,索引建为(name, age)。 - 严格遵守最左前缀原则:
联合索引(a, b, c),查询条件必须从最左列开始,跳过左侧列会导致索引失效。
例:WHERE b = 1无法命中(a,b,c)索引。 - 避免在索引列上做函数运算:
函数会导致索引失效,走全表扫描。
改法:WHERE YEAR(create_time) = 2024→WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'。
2. SQL 写法优化(代码层面)
- 禁止
SELECT *:只查必要字段,减少网络传输开销和内存占用。 - 避免前置模糊查询:
LIKE '%关键词'是全表扫描,必须优化。 - 连表查询字符集一致:
JOIN时字段字符集必须一致(如 utf8 和 utf8mb4),否则会发生隐式转换导致索引失效。
3. 架构层面优化(宏观降本)
- Redis 缓存热点数据:访问频率高、变化少的数据,直接走缓存,不查数据库。
- 大表分库分表:单表超过 2000 万行,查询性能会明显下降,需分表处理。
- 读写分离:利用主从架构,将读压力分摊到从库,提升读性能。
请详细描述 MySQL 的 B+ 树中查询数据的全过程
B + 树查询分两大阶段,完整链路:
根节点 → 中间节点 → 叶子节点 → 页目录二分定位 → 组内链表遍历
第一阶段:树层级垂直查找(定位叶子节点)
- 起点:从 B + 树的根节点开始
- 二分查找:将查询的键值与节点中存储的索引键做比较,用二分法确定目标值所在的区间
- 向下遍历:顺着对应区间的指针,走到下一层子节点
- 终止条件:重复上述过程,直到到达叶子节点(数据页)
- 特点:3 层 B + 树最多仅需 3 次磁盘 I/O,就能定位到叶子节点,效率极高
第二阶段:叶子节点内部查找(定位具体数据行)
叶子节点是一个 16KB 的 InnoDB 数据页,内部通过页目录加速查找:
- 页目录二分定位:页目录把记录分成若干组,每个槽指向组内最大记录。用二分法快速定位到目标记录所在的组
- 组内链表遍历:沿着组内的单向链表遍历,找到最终的目标数据行
MySQL 中 count( * )、count(1) 和 count(字段名) 有什么区别?
三者都是统计行数的聚合函数,核心差异在于对 NULL 值的处理逻辑:
| 写法 | 统计规则 | 示例(100 行,email 字段 20 行 NULL) |
|---|---|---|
count(*) |
统计所有行,包含字段为 NULL 的行 | 返回 100 |
count(1) |
统计所有行,包含字段为 NULL 的行 | 返回 100 |
count(字段名) |
只统计该字段不为 NULL的行,NULL 行不计入 | 返回 80 |
count(*)vscount(1)- MySQL 官方文档明确:两者性能完全一致,无任何差异
count(*)语义最清晰,是统计总行数的推荐写法
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:记录最后修改这条数据的事务 IDroll_pointer:指向这条记录上一个版本的undo log指针
每次执行UPDATE时,不会直接覆盖原数据:- 把旧值写入
undo log,生成历史版本 - 新值写入数据页,
roll_pointer指向旧版本 - 多个版本串联形成版本链,最新版本在链头,历史版本依次向后
2. 快照读的实现
普通 SELECT 走快照读(一致性非锁定读):
- 不加锁,根据事务启动时间,顺着版本链找到对自己可见的历史版本
- 直接返回可见版本数据,完全不阻塞写操作
- 写操作正常修改最新版本,读写各走各的,并发性能拉满
核心作用
- 实现读写不阻塞:读操作不加锁,写操作不阻塞读,大幅提升并发能力
- 保障事务隔离性:配合
ReadView实现不同隔离级别(如 RC、RR),解决不可重复读、幻读问题 - 支撑事务回滚:版本链基于
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. 本质区别
- 层级与通用性:
- binlog 是 Server 层日志,所有引擎通用;redo/undo 是 InnoDB 引擎独有,仅服务于 InnoDB 事务。
- 记录内容:
- binlog 是逻辑日志,记录数据变更的语义;
- redo log 是物理日志,记录数据页的物理修改;
- undo log 是逻辑日志,记录数据的反向操作。
- 写入方式:
- binlog 是追加写入,可无限扩容;
- redo log 是循环覆盖写入,空间固定,依赖 checkpoint 推进。
- 核心目标:
- binlog 服务于主从复制、数据恢复;
- redo log 服务于崩溃恢复、保证持久性;
- undo log 服务于事务回滚、保证原子性、支撑 MVCC。
MySQL 中的事务隔离级别有哪些?
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 特点 |
|---|---|---|---|---|
| 读未提交 | ✔ | ✔ | ✔ | 最低隔离级别,未提交的数据对其他事务可见,存在 脏读 风险。 |
| 读已提交 | ✖ | ✔ | ✔ | 只能读取已提交的数据,解决脏读问题,但可能出现 不可重复读 和 幻读 问题。 |
| 可重复读 | ✖ | ✖ | ✔ | MySQL 默认隔离级别,确保同一事务中多次读取结果一致,避免不可重复读,但可能出现 幻读。 |
| 串行化 | ✖ | ✖ | ✖ | 最高隔离级别,事务串行执行,避免所有并发问题,但会导致性能大幅下降。 |
- 脏读:事务读取到其他事务未提交的数据。
- 不可重复读:同一个事务中多次读取同一数据,前后结果不一致。
- 幻读:同一个事务中多次查询,返回的记录数不一致(如新增或删除了行)。
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 数据一致性设计的跨层事务协调机制,是主从复制和事务可靠性的核心基础。
核心流程
- 准备阶段:事务执行完,InnoDB 将 redo log 刷盘并设为
prepare状态,返回就绪信号。 - 提交阶段:Server 层将 binlog 刷盘,再通知 InnoDB 把 redo log 改为
commit,事务完成。
崩溃恢复逻辑
重启后通过 XID(事务唯一标识) 对账:
- redo log 为
prepare且 binlog 有对应记录 → 自动提交 - binlog 无对应记录 → 回滚事务
MySQL 中如果发生死锁应该如何解决?
一、什么是死锁
多个并发事务互相持有对方需要的锁,循环等待,导致所有事务无法执行,就是死锁。
二、死锁的解决方式
- MySQL 自动处理(InnoDB 默认开启)
- 主动死锁检测:检测到死锁时,自动回滚代价最小的事务(比如回滚行数最少的),释放锁,让其他事务继续执行。
- 超时回滚:通过
innodb_lock_wait_timeout设置锁等待超时,超时自动回滚事务,避免长时间阻塞。
- 手动干预
- 用
show engine innodb status/show processlist定位死锁事务,用kill 线程ID手动回滚阻塞事务。
- 用
三、死锁的避免方案
- 拆分大事务为小事务,快速释放锁,减少锁冲突。
- 所有事务按固定顺序申请锁,避免循环等待。
- 优化索引,让 SQL 走索引,避免全表扫描导致的表锁 / 大范围行锁。
- 适当降低隔离级别(如从 RR 降到 RC),关闭间隙锁,减少锁范围。
- 关闭不必要的长事务,及时提交 / 回滚。
MySQL 中如何解决深度分页的问题?
一、问题本质
LIMIT 9999990,10 会让 MySQL 扫描前 9999990 条记录再丢弃,IO 极高、性能极差。
二、三种优化方案(核心)
- 子查询 / Join 优化(最常用)
- 原理:先用二级索引快速定位起始 ID,再回表查数据
- 写法:先查起始 ID → 再用
id >=查数据 - 优点:利用索引,扫描量极小
- 游标分页(连续翻页)
- 原理:记录上一页最后一条 ID,下一页直接
id > last_id - 优点:性能最好,无扫描浪费
- 缺点:不能跳页(只能下一页 / 上一页)
- 搜索引擎(ES)
- 大数据量、复杂排序 / 搜索场景
- MySQL 不适合深度分页,交给 ES 处理
什么是 MySQL 的主从同步机制?它是如何实现的?
一、核心原理
MySQL 主从同步的核心是binlog(二进制日志)复制:主库将所有写操作记录到 binlog 中,从库拉取该日志并在本地重放,最终实现主从数据一致。
二、核心实现
- 主库 Dump 线程:监听主库 binlog 的变更,有新内容就主动推送 binlog 更新事件给从库。
- 从库 I/O 线程:向主库拉取 binlog 数据,将收到的日志写入从库本地的relay log(中继日志)。
- 从库 SQL 线程:读取 relay log,逐条解析并执行其中的 SQL 语句,完成数据同步。
三、完整流程
- 客户端向主库提交事务,主库执行事务并将操作写入 binlog;
- 主库 Dump 线程监听到 binlog 更新,推送事件给从库;
- 从库 I/O 线程拉取数据,写入本地 relay log;
- 从库 SQL 线程读取 relay log,重放 SQL,同步数据;
- 主库向客户端返回事务执行成功的响应。
如何处理 MySQL 的主从同步延迟?
一、核心前提
主从延迟无法完全消除,只能通过优化缩短延迟,或在业务层做规避。
二、业务层处理方案(面试高频)
- 关键业务强制走主库:写后读的强一致场景(如注册后立即登录),直接查主库,牺牲部分读写分离收益,对主库压力影响小。
- 延迟感知 + 窗口控制:写操作后记录时间戳,短时间内的读请求强制走主库,超过延迟窗口再走从库,可用 ThreadLocal、Redis 记录时间。
- 二次查询兜底:从库查不到数据时,再查一次主库,兜底数据一致性;缺点:恶意查询不存在的数据会变成对主库的攻击。
- 缓存前置:写入主库的同时写入缓存,读请求优先查缓存;缺点:引入了缓存一致性问题,属于用新问题换旧问题。
三、其他优化方向
- 硬件 / 配置优化:提升从库的硬件配置(CPU、磁盘、内存),解决从库性能不足导致的延迟。
- DBA 侧进阶优化:开启 MySQL 并行复制等,提升从库重放日志的效率(面试可补充的加分项)。
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 存储(极致省内存):
- 字段(field-value 对)数量 < 512
- 单个 field 和 value 的总长度 < 64 字节
任意一个条件不满足,就自动切换为HashTable存储,保证 O (1) 操作性能。
Redis Zset 的实现原理是什么?
一、核心底层结构
Redis Zset(有序集合)采用双结构组合实现,同时兼顾两种核心操作的性能:
- 跳表(Skip List):按
score排序,支持 O(log N) 时间复杂度的插入、删除、按score范围查询(如ZRANGEBYSCORE)。 - 哈希表(Hash Table):存储
member → score的映射,支持 O(1) 时间复杂度的单点查分(如ZSCORE)。
二、双结构设计的必要性(互补短板)
- 若只用跳表:按
member查score需遍历全表,复杂度退化为 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 字符串的三大致命问题:
- 获取长度需遍历,时间复杂度 O (n)
- 依赖
\0判断结束,无法存储二进制数据 - 手动扩容易导致缓冲区溢出
二、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 脚本:保证解锁时「先校验锁归属 + 再删除锁」的原子性,避免误删他人的锁
加锁流程
客户端执行命令:
SET lock_key uuid EX 30 NX(uuid是当前客户端唯一标识,用于解锁校验)Redis 检查
lock_key是否存在:- 不存在:设置成功,返回
OK,加锁成功 - 已存在:返回
nil,加锁失败
- 不存在:设置成功,返回
解锁流程(必须用 Lua 脚本保证原子性)
1 | if redis.call('GET', KEYS[1]) == ARGV[1] |
- 客户端执行上述 Lua 脚本
- 先
GET lock_key获取当前锁的 value - 判断 value 是否等于自己的
uuid:- 相等:执行
DEL lock_key,解锁成功 - 不相等:返回
0,说明锁不属于自己,不删除,避免误删
- 相等:执行
Redis 的 Red Lock 是什么?你了解吗?
一、RedLock 定义
RedLock 是 Redis 作者提出的分布式锁算法,核心是通过多个独立的 Redis 实例达成多数派共识,解决主从架构下的锁丢失 / 互斥失效问题。
二、核心背景:为什么需要 RedLock?
单机 / 主从架构的 Redis 分布式锁有致命漏洞,会导致锁失效:
- 单机风险:单节点挂掉,锁完全丢失
- 主从风险(最核心):客户端在主节点加锁成功后,主节点宕机,锁数据未同步到从节点 → 从节点晋升为主节点后,另一个客户端可再次加锁 → 两个客户端同时持有锁,互斥性被破坏,数据错乱
三、核心原理与流程##### 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命令,主节点执行全量复制。 - 流程:
- 主节点执行
BGSAVE生成 RDB 快照,同时将新写命令暂存到 Replication Buffer。 - 主节点将 RDB 文件发送给从节点,从节点清空旧数据并加载 RDB。
- 主节点发送 Replication Buffer 中的缓冲命令,从节点执行,完成数据全量同步。
- 主节点执行
2. 第二阶段:命令传播(全量同步后)
- 建立长连接:全量同步完成后,主从建立长连接。
- 核心逻辑:主节点每接收一条写命令,就异步发送给从节点执行,保持数据一致。
- 心跳机制:主从互相发送
Ping/Ack心跳包,确认对方存活。
3. 第三阶段:断线重连 & 增量同步(断线恢复)
- 背景:断线后重复全量同步浪费资源,Redis 2.8+ 引入增量同步。
- 核心机制:
- 主节点内部有环形缓冲区 repl_backlog_buffer(积压缓冲区),存储最近的写命令。
- 从节点重连后,发送
PSYNC runid offset告知主节点之前的读取位置。 - 主节点检查:
- Yes:缓冲区数据存在,只补发断线期间缺失的数据,即增量复制。
- No:缓冲区数据已被覆盖,重新全量复制。
Redis 数据过期后的删除策略是什么?
Redis 过期 Key 的删除采用 「惰性删除 + 定期删除」双策略配合 的方案,两者互补,兼顾性能与内存效率
1. 惰性删除(被动触发)
- 触发时机:每次对 Key 进行读写操作前
- 执行逻辑:先检查该 Key 是否已过期,过期则直接删除,返回空;未过期则正常执行操作
- 优缺点:
✅ 优点:不占用额外 CPU 做全量扫描,性能开销极小
❌ 缺点:冷数据(过期后无人访问)会一直占用内存,造成内存泄漏
2. 定期删除(主动清理)
- 触发时机:Redis 每 100ms 自动触发一次定时任务
- 执行逻辑:
- 随机抽取一批设置了过期时间的 Key 检查
- 删除其中已过期的 Key
- 限制单次清理的 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 的核心危害
- 内存分布不均:集群模式下,大 Key 集中在单个实例,导致节点负载失衡,查询效率下降
- 命令阻塞:Redis 单线程执行,操作大 Key 耗时极长,会阻塞其他所有命令,引发服务卡顿
- 网络阻塞:大 Key 网络 I/O 传输流量大,导致传输延迟高、网络拥塞
- 客户端超时:操作耗时过长,导致客户端等待超时,影响业务体验
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(上下文) | 持有策略接口的引用,把实际工作委托给策略对象,隔离调用方与具体策略 |
三、核心优势
- 开闭原则:新增算法只需新建策略类,无需修改原有代码(Context 代码零改动)
- 消除冗余判断:彻底替代
if-else分支,代码更易维护 - 算法解耦:不同算法独立封装,互不影响,可单独测试
- 灵活切换:运行时可动态切换策略,适配不同业务场景
四、经典代码示例
1 | // 1. 策略接口:统一支付规范 |
五、常见使用场景
- 支付系统:微信、支付宝、银联、Apple Pay 等多种支付方式
- 促销 / 定价系统:满减、打折、满赠、秒杀等不同价格计算规则
- 数据导出:Excel、CSV、PDF 等不同格式导出,接口统一
- 日志框架:Logback 的 Appender,支持控制台、文件、远程服务器等多种输出
- 排序 / 筛选算法:不同场景下的排序策略动态切换
什么是模板方法模式?一般用在什么场景?
模板方法模式是行为型设计模式。
核心思想:在抽象类中定义算法的执行骨架(流程),并固定步骤顺序;将其中可变的具体细节延迟到子类实现。
- 父类定骨架,子类填细节,子类不允许打乱执行顺序(模板方法通常用
final修饰)。
一、两大核心角色
| 角色 | 作用 | 核心组件 |
|---|---|---|
| AbstractClass(抽象类) | 定义算法骨架与执行顺序 | 1. 模板方法 (final):定死步骤流程,禁止子类修改2. 抽象步骤:强制子类实现的核心逻辑 3. 钩子方法:父类提供默认实现,子类按需覆盖 |
| ConcreteClass(具体子类) | 实现抽象步骤,填充业务逻辑 | 实现父类要求的抽象方法,覆盖钩子方法 |
二、核心优势
- 代码复用:子类共享父类的骨架逻辑,无需重复编写流程控制代码。
- 严格规范:父类锁定执行顺序,子类无法随意改变算法流程。
- 扩展方便:新增业务流程只需继承抽象类,实现指定方法,无需修改原有代码,符合开闭原则。
三、代码示例(数据处理场景)
1 | // 1. 抽象类:定义算法骨架 |
四、典型应用场景
模板方法模式适用于流程固定、细节多变的业务场景:
- 数据处理:如 CSV/JSON/XML 数据导入,读取 - 处理 - 写入流程固定,具体实现不同。
- 订单处理:校验库存 -> 扣减库存 -> 支付 -> 记录日志,各业务线流程一致但细节不同。
- Spring 框架应用:
Spring MVC的DispatcherServlet核心流程使用模板方法模式,定义了请求处理的完整骨架,具体处理器如Controller实现细节。 - 单元测试:父类定义测试流程(初始化 -> 执行测试 -> 销毁),子类实现具体的测试用例。
- 支付 / 审核流程:提交申请 -> 审核 -> 打款 / 驳回,流程固定但各业务方审核逻辑不同。



