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 中常见的数据类型有哪些?

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. 支付 / 审核流程:提交申请 -> 审核 -> 打款 / 驳回,流程固定但各业务方审核逻辑不同。