一、三大范式(规范化)
1. 第一范式(1NF)
-
定义:要求数据库表的每一列都是原子的,即列中的值不能再分割成更小的部分。
-
示例:
-
❌ 错误设计:表中存储多个电话号码的字段,如
phone_numbers
(例如123-456, 789-012
)。 -
✅ 正确设计:每个电话号码都应该是单独的一行,如创建一个
phone_number
表,包含user_id
和phone_number
两列。
-
2. 第二范式(2NF)
-
定义:在满足第一范式的基础上,要求每个非主键字段都必须完全依赖于主键。即,不能存在部分依赖。
-
示例:
-
❌ 错误设计:假设一个表包含
order_id, product_id, product_name
,其中product_name
依赖于product_id
,而不是整个order_id
和product_id
的组合主键。 -
✅ 正确设计:将
product_name
移至另一个表中,设计为product
表,包含product_id
和product_name
,并通过外键引用。
-
3. 第三范式(3NF)
-
定义:在满足第二范式的基础上,要求所有非主键字段必须直接依赖于主键,不能存在传递依赖。
-
示例:
-
❌ 错误设计:一个表中存储
order_id, product_id, product_price
,其中product_price
依赖于product_id
,而product_id
又依赖于order_id
。 -
✅ 正确设计:将
product_price
移至product
表,通过外键关系来引用,避免传递依赖。
-
-
总结:
-
遵守三大范式的好处:
-
提高数据的一致性和准确性。
-
减少冗余数据和不一致的更新。
-
-
然而,在实际应用中,为了性能和可扩展性,可能会适度违反范式(如使用适当的冗余字段、使用反范式等)。
-
二、数据库设计常见坑(优化版)
在数据库设计中,主键和外键的设计至关重要,尤其是它们的不可变性,直接影响数据一致性和系统稳定性。以下是优化后的总结,重点强调逻辑外键也应尽量不可变,并给出更贴合实际业务的建议。
1. 主键设计不当
-
❌ 错误做法
• 使用业务字段(如手机号、身份证号、邮箱)作为主键。
• 主键设计为可变字段(如用户 ID 可修改)。 -
🔍 问题
• 主键是数据的唯一标识,如果主键可变,会导致关联数据失效(如订单、账单等依赖主键的表无法正确关联)。
• 业务字段可能变更(如用户换号、改邮箱),导致外键关联断裂,数据关系混乱。
• 索引重建成本高,主键变更可能导致索引失效,影响查询性能。 -
✅ 推荐做法
• 主键应选择不可变字段,推荐使用自增 ID 或 分布式唯一 ID(如雪花算法)。
• 业务字段(如手机号、邮箱)不应作为主键,应作为普通字段存储。
• 如果必须用业务字段(如用户 ID),确保其不可变性(如用户注册后不允许修改)。
2. 缺失或滥用外键
-
❌ 错误做法
• 滥用物理外键:所有表之间都加外键约束(影响写入性能、加重锁竞争,难以扩展)。
• 完全不用外键:依赖业务代码保证数据一致性,容易出错。 -
🔍 问题
• 物理外键:-
影响写入性能(每次更新需检查外键约束)。
-
加重锁竞争(高并发时可能死锁)。
-
难以扩展(分库分表时外键难以维护)。
• 无外键: -
数据一致性依赖业务代码,容易因程序 bug 导致脏数据。
-
-
✅ 推荐做法
• 核心数据或关系型平台(如订单、用户关系)可以使用物理外键,确保数据一致性。
• 非核心或高性能场景(如日志表、统计表)建议使用逻辑外键,由业务代码层保障数据一致性。
• 逻辑外键也应尽量不可变(如user_id
不应随意修改)。
3. 字段命名与结构不规范
-
❌ 错误做法
• 命名含糊(如status
、type
、flag
没有文档解释)。
• 一个字段承载多个意义(如extra_info
存储 JSON 数据,但未明确字段含义)。 -
🔍 问题
• 维护困难:后续开发人员难以理解字段含义。
• 查询效率低:JSON 字段无法高效索引,导致查询变慢。 -
✅ 推荐做法
• 命名具备上下文(如order_status
、vehicle_type_code
)。
• 结构化字段优于 JSON,除非是灵活的扩展字段(如用户自定义标签)。
• 如果必须用 JSON,应明确文档说明字段含义,并考虑使用 JSON Schema 校验数据格式。
4. 表设计不支持“历史数据归档”
-
❌ 错误做法
• 直接物理删除历史数据(如删除用户订单记录)。 -
✅ 推荐做法
• 软删:设计is_deleted
字段标记删除状态。
• 历史表:重要数据归档到历史表(如orders_archive
)。
• 操作日志:记录关键操作(如audit_log
表)。
5. 忽视查询效率和索引设计
-
❌ 错误做法
• 没有设计联合索引,仅靠单列索引。
• 频繁使用%LIKE%
查询,导致无法使用索引。 -
✅ 推荐做法
• 覆盖索引:确保最常用的查询路径能命中索引(避免回表)。
• 分析执行计划(EXPLAIN
)优化慢查询。
• 避免%LIKE%
查询,改用全文索引(如FULLTEXT
)。
6. 不考虑并发写和事务一致性
-
❌ 错误做法
• 高并发场景下直接统计字段总和(如用户余额),不加事务或乐观锁。 -
✅ 推荐做法
• 核心资产类字段(如余额、库存)加锁控制(乐观锁/悲观锁)。
• 事件日志 + 定时汇总:避免频繁实时计算(如电商订单统计)。
7. 数据冗余设计不合理
-
❌ 错误做法
• 重复存储太多字段(如订单表存客户手机号、姓名,而用户表已有)。 -
✅ 推荐做法
• 少量冗余字段可接受,但需有更新策略(如用户改名时同步更新订单表)。
• 或者通过关联查询获取数据,而非冗余存储。
8. 忽略数据迁移和版本演进
-
❌ 错误做法
• 一次性上线数据库结构,不考虑后期扩展。 -
✅ 推荐做法
• 保留扩展字段(如ext_info
JSON)。
• 表结构版本化,便于未来变更(如v1_orders
→v2_orders
)。
• 使用 Flyway/Liquibase 等工具管理数据库版本。
总结(重点强调逻辑外键不可变性)
坑点 | 错误做法 | 推荐做法 |
---|---|---|
主键设计 | 用业务字段(手机号)作主键 | 自增 ID / 雪花算法 |
外键设计 | 滥用物理外键 | 核心业务用物理外键,非核心用逻辑外键 |
字段命名 | 模糊命名(status) | 明确命名(is_active) |
历史数据 | 直接物理删除 | 软删 + 历史表 |
索引设计 | 无索引 / %LIKE% | 覆盖索引 + EXPLAIN 优化 |
并发控制 | 无锁更新 | 乐观锁 / 悲观锁 |
数据冗余 | 过度冗余 | 少量冗余 + 更新策略 |
扩展性 | 无版本控制 | JSON 扩展 + 表版本化 |
✅ 主键和外键(包括逻辑外键)应尽量不可变,避免因字段变更导致数据混乱。
✅ 物理外键适用于核心业务,逻辑外键适用于高性能场景,但都应保证关联字段稳定。
三、MySQL 索引机制
-
索引分类
-
按数据结构分类:
-
B+ 树索引:最常用的索引类型,InnoDB 和 MyISAM 均采用 B+ 树结构,支持范围查询和排序。
-
哈希索引:仅支持等值查询,不支持范围查询,适用于精确匹配的场景。MyISAM 提供此类型,InnoDB 默认不使用。
-
R 树索引:主要用于空间数据类型(如地理位置),常用于地理信息系统(GIS)中。
-
全文索引:用于支持全文搜索的特殊索引,主要用于文本字段的内容搜索。
-
-
按存储方式分类:
-
聚簇索引:InnoDB 中的主键索引属于聚簇索引,数据存储和索引结构融合在一起,叶子节点直接存储数据行。
-
非聚簇索引:数据存储与索引结构分离,叶子节点存储的是数据的地址(如行号、主键值等),查询时需要回表获取数据。
-
-
按列数分类:
-
单列索引:只有一个列的索引,通常用于对单个字段进行快速查找。
-
联合索引(组合索引):由多个列组成的索引,支持多个字段的联合查询。遵循最左前缀原则。
-
-
按约束分类:
-
唯一索引:值必须唯一,不允许重复,通常用于数据完整性约束(如邮箱、身份证号等)。
-
普通索引:常规的索引,没有唯一性要求,通常用于提高查询效率。
-
全文索引:专门用于全文检索,支持对文本字段的高效检索。
-
-
其他特殊索引类型:
-
空间索引(Spatial Index):专为地理数据设计,支持二维空间数据的快速查询。
-
位图索引(Bitmap Index):通常用于列取值数量较少(低基数)的情况,如性别(男、女)字段。
-
反向索引(Reverse Index):在一些特殊场景下(如处理 DNS 查询),采用反向索引。
-
-
-
InnoDB vs MyISAM 索引结构对比
特性 | InnoDB | MyISAM |
---|---|---|
主键索引 | 聚簇索引(叶子节点存数据) | 非聚簇(叶子节点存地址) |
二级索引 | 非聚簇,叶子节点存主键值 | 非聚簇,叶子节点存地址 |
是否支持事务 | ✅ 是 | ❌ 否 |
锁粒度 | 行级锁 | 表级锁 |
-
无主键时的特殊处理
-
InnoDB 会自动生成一个隐藏的
row_id
作为聚簇索引主键。
-
-
索引优缺点
-
✅ 优点:加快查询速度、减少 I/O、支持排序/分组优化。
-
❌ 缺点:占用空间、插入更新时维护开销大。
-
-
可能导致索引失效的场景
-
LIKE '%abc'
-
字段隐式转换(如
int = '123'
) -
使用函数包裹字段:
WHERE YEAR(time) = 2023
-
违反最左前缀原则(联合索引)
-
-
覆盖索引(索引覆盖查询)
-
查询字段全部被索引覆盖,无需回表,可极大提升查询效率。
-
-
索引下推(Index Condition Pushdown, ICP)
-
默认索引查找后需“回表”判断 WHERE 条件是否满足,ICP 将部分 WHERE 条件下推至存储引擎层的索引扫描阶段。
-
优势:减少回表次数,提高查询效率。
-
使用条件:MySQL 5.6+ 且查询条件能被索引匹配。
-
-
联合索引(组合索引)
-
遵循最左前缀原则。
-
索引
(a, b, c)
支持以下组合:a
,a,b
,a,b,c
,但不支持b,c
。 -
字段顺序不同代表不同索引结构。
-
-
聚簇 vs 非聚簇索引实现差异
-
聚簇索引(InnoDB 主键):B+ 树叶子节点存储整行数据。
-
非聚簇索引:
-
InnoDB 叶子节点存主键 → 再通过主键回表
-
MyISAM 叶子节点存数据地址
-
-
-
其他重要概念
-
唯一索引:值不可重复,用于唯一约束。
-
哈希索引:仅适合等值查询,不支持范围。
-
索引下推 ICP:WHERE 条件提前在索引阶段过滤。
-
索引排序:
ORDER BY
字段必须满足最左匹配才能用索引排序。 -
GROUP BY
/ORDER BY
:可配合联合索引使用,提高分组/排序效率。
-
四、MySQL 锁机制详解笔记
本笔记系统整理了 MySQL 中锁的分类、属性与底层机制,尤其聚焦于 InnoDB 引擎的行级锁实现细节,便于开发者理解并合理运用锁机制避免并发冲突和性能问题。
-
按锁粒度分类
锁类型 说明 全局锁 锁定整个数据库实例,如 FLUSH TABLES WITH READ LOCK
,常用于全库备份表级锁 锁定整张表,如 LOCK TABLES ... READ/WRITE
,MyISAM 中广泛使用,InnoDB 特殊情况使用(如 DDL)行级锁 锁定表中的单行数据,仅 InnoDB 支持,粒度最小、并发性最好 -
按使用模式分类
锁类型 场景说明 乐观锁(Optimistic) 适合多读少写,如版本号控制: UPDATE ... WHERE id=? AND version=?
悲观锁(Pessimistic) 假设冲突频繁,访问数据前加锁,如: SELECT ... FOR UPDATE
-
按锁属性分类(InnoDB 行锁类型)
锁类型 简写 说明 共享锁 S 锁 允许多个事务同时加锁,只能读不能写。如: SELECT ... LOCK IN SHARE MODE
排他锁 X 锁 仅允许一个事务持有,其他事务不能读也不能写。如: SELECT ... FOR UPDATE
意向共享锁 IS 表示事务准备对表中某些记录加 S 锁,由 InnoDB 自动加在表级,用于加速锁冲突检测 意向排他锁 IX 表示事务准备对表中某些记录加 X 锁,由 InnoDB 自动加在表级 锁兼容性说明
当前持有 \ 请求 S 锁 X 锁 IS 锁 IX 锁 S 锁 ✔ ✘ ✔ ✘ X 锁 ✘ ✘ ✘ ✘ IS 锁 ✔ ✘ ✔ ✔ IX 锁 ✘ ✘ ✔ ✔ -
按底层实现算法分类(InnoDB 特有)
锁类型 说明 记录锁 锁住索引上的某条记录,仅作用于主键或唯一索引上的行 间隙锁 锁住两个索引记录之间的“空隙”,防止幻读 临键锁 记录锁 + 间隙锁的组合。在默认 RR 隔离级别下,InnoDB 使用它防止幻读 -
典型使用场景与注意事项
-
共享锁(S 锁):多个事务可以并发读取,提高并发能力,但不能更新该记录。
-
排他锁(X 锁):适用于更新或删除场景,确保操作期间数据不被他人读取或修改。
-
IS/IX 锁:属于表级锁,不会直接阻塞行访问,但用于快速判断是否存在冲突,提高加锁效率。
-
记录锁:最常见的行锁形式,用于锁住主键或唯一索引的具体记录。
-
间隙锁:用于防止“幻读”问题,但也可能造成“锁定未命中记录”导致误阻塞。
-
临键锁:默认行为,结合间隙锁 + 记录锁,兼顾安全与一致性。
-
-
锁机制常见问题与优化建议
-
锁升级风险:避免使用不带索引的 WHERE 条件,否则可能锁全表。
-
死锁处理:InnoDB 自动检测并回滚“牺牲”事务,建议合理控制加锁顺序与范围。
-
事务隔离级别调整:在允许的业务条件下,可降低为 READ COMMITTED,减少间隙锁干扰。
-
分析工具:使用
SHOW ENGINE INNODB STATUS
查看死锁日志,定位冲突根因。
-
五、MySQL 事务机制详解笔记
-
事务的四大特性(ACID)
特性 说明 原子性 一组操作要么全部执行成功,要么全部失败。通过 undo log 回滚未完成操作。 一致性 事务执行前后,数据保持一致,如账户转账后两边总金额不变。 隔离性 多个事务并发执行时,互不干扰。通过锁或 MVCC 保证。 持久性 事务提交后对数据库的修改是永久的。通过 redo log 保证写入磁盘。 -
事务隔离级别(Isolation Levels)
隔离级别 脏读 不可重复读 幻读 InnoDB 默认 读未提交(RU) ✅ ✅ ✅ ❌ 读已提交(RC) ❌ ✅ ✅ ❌ 可重复读(RR) ❌ ❌ ✅ ✅ 串行化(SZ) ❌ ❌ ❌ ❌ -
读未提交(脏读):它的主要意思就是,A线程可以读到B线程没有提交的数据。比如A线程刚开始查询的数据为500,此时B线程更改了数据为800,但是并没有去提交线程,此时A再去查询数据得到为800。如果B线程提交后发生了错误需要回滚为500.此时A拿到的数据800就是一个脏数据。
-
读已提交(不可重复读):这个隔离级别解决了上个级别的问题,就是A线程读不到B线程没有提交的更改数据的值。也就是B线程如果没有提交的话,A线程读到的还是500,但是B线程提交了后读到的就是800,这样子就会导致,对于A线程来说,他这个事务时间里,两次读到数据的结果不一样。
-
可重复读(幻读):这个隔离级别就解决了上个级别的问题,他是通过快照的形式,在事务开始时,将此时的数据通过快照的形式保存下来,这个事务的时间段,无论其他线程做了什么操作,我查询数据都是到快照里查询,这样子就解决了不可重复读的问题,但是呢又会导致一个新的问题,就是说对于其他线程来说这个数据可能已经被更改为其他值,但是A线程还是之前原来的值。举个例子来说的话:就付钱的时候,你保存了你之前的钱包500块,在你还没有扣减钱包的时候,其他线程转了你300块,并提交了事务,此时你的钱包应该是800块,但是对于这个支付线程来说,你的钱包还是500块,然后扣减之后在更改数据,就会导致你钱少了,那300块不见了。(也就是幻读,自己线程看见的和其他线程不一样)。
两种解决方案:一是加乐观锁的方式,通过比对版本号。
二是悲观锁的方式,就是所有更新操作底层都会加写锁,它会去找最新版本的数据来读。
比如说update acoount set balance=money where id =1;
我给他改成update account set balance=balance-200 where id=1;
这样子的话,在数据更新的时候他就会去找最新版本的balance的值,而不是去快照读。就不会算错钱。
-
串行化:通过对查询语句增加读锁(共享锁),也就是sql语句后加lock in share mode语句。
-
-
事务隔离级别下的典型行为
-
读未提交(Read Uncommitted)
-
事务可以读取其他事务未提交的修改,导致脏读。
-
-
读已提交(Read Committed)
-
每次查询都读取最新已提交数据;避免脏读,但可能产生不可重复读。
-
-
可重复读(Repeatable Read)
-
事务期间快照一致,多次查询返回一致结果;可防止脏读、不可重复读,但可能产生幻读。
-
-
串行化(Serializable)
-
所有操作加锁,强一致性;并发性能差,几乎不用。
-
-
-
事务并发控制机制:乐观锁 / 悲观锁
类型 说明 乐观锁 基于版本号或时间戳控制,不加数据库锁,适合多读少写场景。 悲观锁 显式加锁,如 SELECT ... FOR UPDATE
,适合高并发写入场景。 -
MVCC(多版本并发控制)【详细实现原理】
-
作用:在 RC 和 RR 隔离级别下,InnoDB 使用 MVCC 提供非阻塞读(快照读);实现事务的隔离性且避免加锁,提高性能。
-
底层机制:
-
每行记录包含两个隐藏字段:
-
DB_TRX_ID
(事务 ID):最后修改该行的事务编号。 -
DB_ROLL_PTR
(回滚指针):指向一条 undo log 的地址,形成回滚链条(版本链)。
-
-
事务开始时生成一个快照版本号(Read View)。
-
-
Undo Log 链结构:
-
插入:生成“反向 DELETE” undo。
-
更新:旧数据写入 undo log,构建版本链。
-
删除:生成“反向 INSERT” undo。
-
查询时沿 undo 链查找对当前事务可见的版本。
-
-
不同隔离级别下的快照读取策略:
隔离级别 读取版本方式 RC 每次读都生成新的 Read View RR 整个事务只生成一个 Read View -
版本链示意:
当前行 ├─ trx_id = 100 ├─ roll_ptr → undo_log_100 (x=500) └─ trx_id = 80 └─ roll_ptr → undo_log_80 (x=400) └─ trx_id = 70
若当前事务版本为 75(RR),只能看到 x=400。
-
-
事务日志机制:Undo Log & Redo Log
日志类型 用途 作用 存储方式 Undo Log 回滚操作 实现原子性、MVCC 支持 每个事务独立管理 Redo Log 恢复数据 实现持久性 物理日志,分为 prepare/commit -
二阶段提交:undo 与 redo 日志配合提交,保证 ACID。
-
-
事务死锁与检测机制
-
❌ 死锁:多个事务持有锁并等待对方释放,形成循环。
-
✅ InnoDB 处理:
-
自动死锁检测;
-
回滚“影响最小”事务;
-
innodb_deadlock_detect = ON
。
-
-
排查工具:
SHOW ENGINE INNODB STATUS
查看最近死锁信息。 -
避免策略:
-
统一加锁顺序;
-
控制事务范围,尽快提交;
-
使用索引避免全表锁;
-
显式锁定策略(
FOR UPDATE
,LOCK IN SHARE MODE
)。
-
-
-
是否开启事务的判断
-
统计报表:需要一致性快照,使用
REPEATABLE READ
。 -
日常读写:性能优先,可用
READ COMMITTED
。 -
实时扣款:应用悲观锁(
FOR UPDATE
)或 CAS + 乐观锁。
-
六、MySQL 深度机制笔记(执行流程 + 存储结构 + 索引原理 + 日志系统 + 分布式扩展)
-
SQL 查询语句执行全过程(以 InnoDB 为例)
SELECT * FROM user WHERE id = 1;
执行流程:
-
Server 层解析与优化
-
SQL 词法/语法解析 → 生成语法树 → 查询计划优化(选择最优索引) → 调用执行器下发执行计划
-
明白了,你想要了解 SQL 查询执行顺序的补充。确实,SQL 查询的执行过程按照一定的顺序进行,虽然在书写时顺序可能是
SELECT
、FROM
、WHERE
、JOIN
等,但实际执行顺序是有所不同的。具体执行顺序如下:SQL 执行顺序(实际执行顺序 vs 书写顺序)
-
FROM 子句
-
作用:查询首先从
FROM
子句指定的表或数据源开始。这里是基础数据源的选择,首先加载所有相关表的数据(如果有多个表),准备参与后续操作。 -
优化:如果涉及到多个表的连接(
JOIN
),此时也会处理表连接的逻辑,确定数据的基础集。
-
-
JOIN 操作
-
作用:在
FROM
子句指定的数据源基础上,如果有连接(JOIN
),这时就开始执行连接操作。JOIN
操作会基于给定的条件(如ON
或USING
)将多个表的数据合并为一个结果集。 -
优化:
-
连接顺序:在多表连接时,优化器会根据表的大小、索引使用情况等来选择最优的连接顺序。
-
连接类型:选择适当的连接类型(如内连接
INNER JOIN
、左连接LEFT JOIN
等)可以影响查询的性能。 -
索引优化:对于连接操作中涉及的字段,合理使用索引可以大幅提升连接效率。
-
-
-
WHERE 子句
-
作用:在连接操作完成后,
WHERE
子句会进行数据筛选。此时,查询会根据WHERE
条件过滤出符合要求的记录,丢弃不符合条件的行。 -
优化:
-
索引选择:在
WHERE
子句中,如果涉及到大量数据的筛选,使用合适的索引可以减少全表扫描,提高查询性能。 -
筛选顺序:优化器在执行
WHERE
子句时,会尝试使用最优的筛选顺序,以减少需要扫描的数据量。
-
-
-
GROUP BY 子句
-
作用:如果查询包含
GROUP BY
,在WHERE
子句筛选后的数据会被分组。每一组的数据会执行聚合操作(如SUM()
、COUNT()
、AVG()
等)。 -
优化:
-
索引优化:如果分组字段有索引,MySQL 可以更高效地进行分组操作。
-
内存分配:在大数据量的分组时,合理配置内存缓冲区(如
sort_buffer_size
)有助于提高性能。
-
-
-
HAVING 子句
-
作用:
HAVING
子句用于对GROUP BY
后的结果进行过滤,和WHERE
子句不同,HAVING
是针对分组后的数据进行条件过滤。 -
优化:在聚合后的数据上使用
HAVING
条件时,可以减少无效的数据返回。合理使用HAVING
来优化性能。
-
-
SELECT 子句
-
作用:执行查询时,选择要返回的列数据。在此阶段,所有的查询操作(如聚合、连接等)都已完成,MySQL 会根据
SELECT
子句的要求返回结果。 -
优化:
-
字段选择:只选择需要的字段,避免返回不必要的数据列,减轻内存和 I/O 的压力。
-
使用索引覆盖:如果查询的字段完全可以由索引返回,使用索引覆盖查询可以减少回表操作,提升性能。
-
-
-
ORDER BY 子句
-
作用:
ORDER BY
用于对查询结果进行排序。在此阶段,数据已经被筛选并且准备好返回,但仍需要根据排序字段进行排序。 -
优化:
-
索引排序:如果排序字段有合适的索引,MySQL 可以直接利用索引进行排序,避免额外的内存排序。
-
限制排序的数据量:对于大型数据集,排序是昂贵的操作,可以结合
LIMIT
来减少排序的数据量,从而提高效率。
-
-
-
LIMIT 子句
-
作用:
LIMIT
用于限制查询返回的行数,通常与ORDER BY
一起使用,确保查询结果的分页或按需返回。 -
优化:
-
优化偏移量:当使用分页查询时,可以通过优化偏移量(如使用
JOIN
或索引优化分页查询)减少查询的复杂度。
-
-
-
-
执行器调用存储引擎(InnoDB)
-
Buffer Pool 缓存检查
-
命中:直接读取缓存
-
未命中:从磁盘加载数据页到 Buffer Pool
-
-
更新类操作流程
-
写 Undo Log(逻辑日志)→ 保证原子性与 MVCC
-
更新 Buffer Pool 中的数据
-
写 Redo Log(物理日志)→ 顺序写入日志缓冲区
-
Prepare 阶段:刷 Redo Log 到磁盘(持久化)
-
写 Binlog(Server 层),再写 Commit 标记到 Redo Log
-
提交成功(两阶段提交机制)
-
最终数据异步刷新到磁盘
-
-
崩溃恢复:若中途宕机,只要 Redo Log 存在提交标记,即可恢复。
-
-
日志系统全景图
日志类型 所属层级 作用 用于恢复 支持复制 Undo Log InnoDB 回滚、MVCC 支持 ✅ ❌ Redo Log InnoDB 持久化、崩溃恢复 ✅ ❌ Binlog Server 主从复制、数据恢复 ✅ ✅ Relay Log Slave 接收主库 binlog 执行 ✅ ✅ -
Redo Log 刷盘机制:
-
顺序写入 WAL 提升性能
-
innodb_flush_log_at_trx_commit
决定刷盘策略(0/1/2)
-
-
Binlog 刷盘策略:
-
sync_binlog=0
:不主动 fsync -
sync_binlog=1
:每次 commit fsync -
sync_binlog=N
:每 N 次 commit 才 fsync
-
-
-
InnoDB 存储结构
-
表空间(.ibd 文件)
-
段(Segment)→ 区(Extent,1MB,包含64页)→ 页(Page,16KB)→ 行(Row)
-
-
行格式
格式 说明 Redundant 5.0 之前版本,冗余结构,淘汰 Compact 默认格式,变长字段紧凑存储 Dynamic 大字段溢出页存储,节省页内空间 Compressed 页内数据压缩,减少磁盘空间 -
Null 值存储:Compact 格式使用 Null 位图表示哪些字段为 Null
-
VARCHAR 存储:变长字段长度列表记录实际长度(1~2 字节)
-
行溢出处理:
-
大字段(TEXT/BLOB)保留 20 字节指针,其余存溢出页
-
-
-
InnoDB 与 MyISAM 对比
特性 InnoDB MyISAM 事务支持 ✅ 支持(ACID) ❌ 不支持事务 锁粒度 行级锁 表级锁 主键类型 聚簇索引 非聚簇索引 外键支持 ✅ 支持 ❌ 不支持 崩溃恢复能力 强(支持 redo) 弱(需工具修复) 适用场景 高并发写、大数据一致性 读多写少场景 -
索引机制与结构
-
聚簇索引:数据存储即为主键索引(InnoDB)
-
二级索引:索引值指向主键,再通过主键回表
-
哈希索引:哈希值匹配快速查找,不支持范围查询
-
全文索引:支持文本自然语言处理的匹配
-
❌ 哈希索引局限:哈希冲突,性能下降;不支持范围查找/排序/下推
-
✅ B+ 树 vs B 树:
特性 B 树 B+ 树(InnoDB) 数据节点 所有节点存数据 仅叶子节点存数据 范围查询 差 ✅ 叶子节点链表支持范围查找 I/O 性能 中等 ✅ 更快范围/排序扫描 -
联合索引原理:
CREATE INDEX idx_name_age_pos ON user(name, age, position);
-
底层为多字段排序的 B+ 树,遵循最左前缀原则:可命中
(name)
,(name, age)
, 但不支持(age, position)
。
-
-
-
主键设计建议(推荐)
推荐 原因说明 自增整形主键 B+ 树插入效率高,维护有序链表开销低 ❌ UUID 无序、占空间大,页分裂频繁,性能差 ❌ 字符串主键 CPU 耗费大,空间占用多 -
MySQL COUNT 性能对比
SQL 含义 性能排序 COUNT(*)
全表记录数(含 NULL) ✅ 最快 COUNT(1)
类似于 COUNT(*)
✅ 近似 COUNT(column)
统计非 NULL 的指定列 ❌ 较慢 -
分布式数据库设计扩展
以下整合了雪花算法详解及时间回拨问题,结合分布式数据库实战笔记,格式化清晰,适合导出为 Markdown 或 PDF。
一、分库分表设计策略
-
目标:解决单库单表容量瓶颈、提升并发能力与可扩展性。
-
1️⃣ 水平分库 & 水平分表
类型 说明 水平分库 按主键范围或哈希将数据分散到多个数据库实例 水平分表 在同一库中将大表拆分为多张子表 -
2️⃣ 常用分片策略
策略 说明 示例 范围分片 按 ID/时间范围切分 id <1000→db1,≥1000→db2 哈希分片 对主键哈希取模 hash(uid)%N 枚举分片 显式配置分片规则 用户1–100→db1 混合分片 联合条件切分 时间+用户ID -
3️⃣ 数据路由实现方式
-
静态路由:硬编码表/库名,灵活性差
-
中间件路由:如 ShardingSphere、MyCat
-
ORM 拦截器:如 MyBatis 插件定制
-
二、全局唯一 ID 生成策略
-
目标:多库多表环境下生成全局唯一、趋势递增、高性能主键。
-
常见方案对比
策略 特点 优缺点 雪花算法(Snowflake) 64-bit 自增 ID,含时间戳+机房ID+序列号 ✅ 唯一性好、趋势递增;❌ 强依赖时钟 数据库自增表 中央库生成自增序列 ✅ 简单;❌ 单点瓶颈 Redis INCR 使用 Redis INCR
实现✅ 快速;❌ Redis 宕机影响可用 UUID 无状态唯一字符串 ✅ 无中心依赖;❌ 不可排序、占空间 美团 Leaf 号段模式/Snowflake 服务 ✅ 性能高、容错好;❌ 实现复杂 -
雪花算法结构图:
符号位 时间戳(41bit) 数据中心ID(5bit) 机器ID(5bit) 序列号(12bit) 1bit 基于时间递增 支持32个机房 每机房32台机器 每毫秒最多4096个ID
三、雪花算法详解与时间回拨问题
-
核心优势:
-
高性能:本地生成,避免集中写
-
唯一性:含时间戳、机房、序号
-
趋势递增:便于聚簇索引
-
-
时间回拨问题:
-
若系统时钟回拨,会导致序列号重复 ➔ 重复 ID。
-
-
应对策略:
方案 描述 优缺点 等待时钟追平(阻塞) 时间回拨时 sleep 直到追平 安全但阻塞业务 设置容忍窗口 小于 X ms 容忍回拨 简单实用,但仍有窗口 逻辑时钟(TiDB TSO) 每节点维护递增逻辑时间 最可靠,复杂度高 引入版本号 高位加一位版本号 增加复杂度 中心服务生成 ID Leaf/UidGenerator 模式 强中心依赖,易控 -
Java 示例:
if (currentTimestamp < lastTimestamp) { long offset = lastTimestamp - currentTimestamp; if (offset < MAX_DRIFT) { Thread.sleep(offset); } else { throw new RuntimeException("Clock moved backwards. Refusing to generate ID"); } }
四、跨库事务一致性处理
-
问题根源:多数据源无法用单机事务 ACID 保证一致性。
-
解决方案:
模式 描述 优缺点 XA 两阶段提交 MySQL 支持 XA 协议,先 prepare 后 commit ❌ 性能差、易阻塞 TCC 模式 应用层定义 Try/Confirm/Cancel 接口 ✅ 最终一致,适合补偿场景 可靠消息 + 最终一致性 MQ + 本地事务完成业务提交 ✅ 电商支付主流方案
五、分布式索引设计策略
-
问题:分片后全局索引缺失,导致跨库排序、聚合、模糊查询效率低。
-
解决方案:
-
ES 索引同步:将核心字段同步到 Elasticsearch,实现分页、排序、全文检索。
-
预聚合 + 缓存:常用统计结果存 Redis,减少跨库查询。
-
逻辑索引中台:汇聚多库数据至索引服务层,提供统一查询接口。
-
六、Binlog 同步与数据一致性(CDC)
-
适用场景:多写架构、消息驱动、数据同步。
-
常见工具对比:
工具 用途 特点 Canal 解析 MySQL binlog → MQ/ES 支持主从同步,广泛使用 Maxwell Java 编写,轻量级同步 适合中小场景 Debezium Kafka Connect 插件 适合 CDC + 流处理平台
-