当前位置: 首页 > news >正文

存储引擎 InnoDB

目录

InnoDB 架构基础

内存结构

磁盘结构

数据存储与索引机制

InnoDB 的行格式

B+Tree 索引结构

索引性能优化

事务处理与 ACID

多版本并发控制(MVCC)


InnoDB 架构基础

InnoDB 的架构包括内存结构磁盘结构两大部分。

内存结构

InnoDB 的内存结构是提升数据库性能的关键,通过合理的内存管理减少磁盘 I/O 操作,大幅提升数据访问速度。

InnoDB 的内存结构主要包括以下几个核心组件:

1. 缓冲池(Buffer Pool)
InnoDB 中最重要的内存组件,缓存了数据页、索引页、Undo 页、插入缓冲(Change Buffer)、自适应哈希索引页等
通过缓存减少磁盘 I/O 提升性能
页分为:
        脏页:内存中的页已修改但尚未写入磁盘
        干净页:内存中的页与磁盘数据一致
脏页会在 Checkpoint LRU 淘汰时由后台线程刷盘

管理机制:
LRU 算法:缓冲池使用改进的 LRU(最近最少使用)算法管理页的淘汰,将频繁访问的页保留在内存中
预读机制:根据访问模式自动预读可能需要的页到缓冲池,减少未来的 I/O 操作
Checkpoint 机制:定期将脏页批量写入磁盘,保证内存和磁盘数据的一致性,同时减少崩溃恢复时间

2. 日志缓冲(Log Buffer)

日志缓冲用于暂存事务生成的 Redo 日志,减少 Redo 日志的磁盘 I/O 次数
在事务提交(COMMIT)或定时触发时将数据写入磁盘上的 redo log 文件

3. 插入缓冲(Change Buffer)

当插入非唯一二级索引时,若索引页不在缓冲池中,InnoDB 不会立即去磁盘加载索引页,而是先将变更记录到 Change Buffer 中,待后续读取该索引页或系统空闲时,再将 Change Buffer 中的变更合并到实际索引页,将随机 I/O 转化为批量的顺序 I/O

4. 自适应哈希索引(Adaptive Hash Index)

AHI 是 InnoDB 根据查询模式自动构建的哈希索引,用于加速等值查询
InnoDB 监控索引的查询频率,当某些索引页被频繁访问时,自动为这些页上的索引键建立哈希索引,将 B+Tree 的随机访问转化为哈希表的 O (1) 访问

Redo 日志是可以理解为数据库的操作备份日志
当执行增删改操作时,数据库会先把修改了什么记录到 Redo 日志里,再去真正修改数据
如果数据库突然崩溃,重启后可以通过 Redo 日志重做之前的操作,保证数据不丢失
比如转账给朋友,Redo 日志会记给朋友账户加 1000 元,就算中途断电,重启后也能根据这条记录补完操作

非唯一二级索引
先简单理解索引:类似书的目录,能快速找到数据位置。
主键索引:最核心的索引,每个值唯一,能直接定位到数据。比如身份证号
二级索引:除了主键外的其他索引,用来辅助查询。比如手机号、姓名
非唯一:指二级索引的值可以重复,比如姓名索引,可能有多个人叫张三
比如查年龄 = 25 的所有用户,如果给年龄建了非唯一二级索引,数据库就不用全表扫描,直接通过索引找到所有符合条件的记录位置,再去查具体数据。

随机 I/O 指读写磁盘时,数据的位置是零散的、不连续的,需要频繁移动磁盘读写头找位置。就像在书架上找书,先找第 5 排的书,再找第 2 排,再找第 8 排,来回跑,效率低。
数据库里如果频繁查询分散的数据,比如随机查不同 ID 的记录,就会产生大量随机 I/O,速度较慢。

顺序 I/O 指读写磁盘时,数据的位置是连续的,磁盘读写头不用来回移动,顺着读 / 写就行。就像找书时,书都按顺序放在第 3 排,从第 1 本读到第 10 本,不用来回跑,效率高。
比如数据库批量写入数据、或读取连续的日志文件,都是顺序 I/O,速度比随机 I/O 快很多。

查询模式指数据库在实际运行中,常用什么样的方式查数据
比如:
有的系统经常查用户 ID=XXX(等值查询)
有的系统经常查年龄> 20 且性别 = 女(范围查询)
有的查询很频繁(热点查询),有的偶尔查一次
数据库会通过统计这些查询模式,优化自己的性能,比如自动建索引

哈希索引是一种通过哈希算法快速定位数据的索引
原理:把索引值(比如用户 ID)通过哈希算法转换成一个数字(哈希值),直接用这个数字定位数据位置,就像用钥匙直接开锁,一步到位
优点:等值查询速度极快,比普通索引快很多
缺点:不支持范围查询,因为哈希值是无序的

B+Tree 是数据库里最常用的索引结构,长得像一棵倒挂的树,专门优化磁盘读写的索引
结构:最上层是根节点,中间是枝节点,最下层是叶子节点(真正存数据位置的地方)。叶子节点之间用链表连起来,方便范围查询
优点:
层数少(通常 3-4 层),查数据时磁盘 I/O 次数少(比如查根节点→枝节点→叶子节点,3 次 I/O 就能找到)
叶子节点有序且连续,支持范围查询

磁盘结构

InnoDB 的磁盘结构负责数据的持久化存储,确保事务的 ACID 特性,尤其是持久性和一致性

InnoDB 的磁盘结构由以下部分组成:

1. 表空间(Tablespaces)

表空间是 InnoDB 存储数据的基本单位,MySQL 8.0 中提供了多种表空间类型以满足不同需求:

表空间类型特点默认文件名主要存储内容
系统表空间存储核心元数据ibdata1数据字典、undo 日志(老版本)、双写缓冲等
独立表空间每张表单独存储表名.ibd对应表的数据和索引
Undo 表空间专门存储 Undo 日志undo_001, undo_002事务回滚和 MVCC 所需的 Undo 日志
临时表空间存储临时数据ibtmp1 等临时表、排序和哈希操作的中间结果

老版本(MySQL 5.7 及之前)Undo 日志默认存储在系统表空间(ibdata1) 中

2. 重做日志(Redo Log)

Redo Log 保障事务持久性,记录了数据页的物理修改,用于数据库崩溃后的恢复。

特点:
物理日志:记录的是数据页的物理变化,如页 X 的偏移量 Y 处修改为 Z,与具体业务逻辑无关
循环写入:Redo Log 文件以固定大小循环写入,当 Redo Log 文件写满后从开头重新开始。
WAL 机制:采用 Write-Ahead Logging 机制,事务提交时先写 Redo Log,再修改数据页,确保即使数据页未刷盘,也能通过 Redo Log 恢复。

Redo Log 的记录逻辑:Redo Log 记录的是数据页的物理修改(比如 页 X 偏移量 Y 改为 Z),但它依赖于数据页本身是完整的。如果数据页已经损坏(半页写),Redo Log 不知道原来的完整页面是什么样,无法基于损坏的页面恢复正确的修改。

3. 撤销日志(Undo Log)

Undo Log 支持事务回滚和 MVCC(多版本并发控制),记录了事务的逻辑反向操作
作用:
事务回滚:当事务执行 ROLLBACK 时,InnoDB 通过 Undo Log 撤销已执行的操作,恢复数据到事务开始前的状态。
MVCC 支持:为读取操作提供一致性视图,当读取数据时,若事务修改,可通过 Undo Log 访问历史版本。
生命周期:Undo Log 在事务提交后不会立即删除,会保留一段时间供 MVCC 读取,之后由后台线程清理。

4. 双写缓冲(Doublewrite Buffer)

双写缓冲用于防止半页写问题,提高了数据写入的可靠性
半页写问题:当数据库写入数据页时,若发生断电等意外,可能导致数据页只写入部分,造成数据页损坏,且无法通过 Redo Log 恢复(Redo Log 基于完整页修改)
解决机制:
写入脏页时,先将完整的页数据写入双写缓冲的磁盘区域(位于系统表空间)
成功写入双写缓冲后,再将页数据写入实际的数据文件位置。
若写入过程中发生故障,恢复时可从双写缓冲读取完整页数据进行修复。

MVCC 是数据库实现高并发读写的核心技术,简单理解就是同一份数据保留多个历史版本,让读写操作不冲突。

举个例子:当修改一条数据时,比如把价格从 100 改成 200,数据库不会直接覆盖旧数据,而是生成一个新的版本存起来,旧版本暂时保留。此时其他用户读取这条数据时,仍然能看到修改前的旧版本(100),新版本(200)提交后新启动的事务可见。
核心作用:解决读写冲突,写操作不阻塞读操作,读操作也不阻塞写操作,同时保证事务隔离性(比如可重复读隔离级别)
依赖的技术:主要通过 Undo 日志存储旧版本数据,通过隐藏列,如 DB_TRX_ID 事务 ID、DB_ROLL_PTR 回滚指针关联不同版本。

一致性视图是 MVCC 中读操作时看到的数据版本快照,可以理解为事务启动时拍下的一张数据快照

核心逻辑:事务启动时,数据库会生成一个视图,记录当前活跃的所有事务 ID(还没提交的事务)。之后这个事务读取数据时,只会看到在视图生成前已提交的事务修改,或者自己修改的数据,看不到视图生成后其他事务的修改
举个例子:
事务 A 启动,生成一致性视图(此时活跃事务只有 A 自己)
事务 B 启动并修改了数据(未提交),事务 A 读取时看不到 B 的修改
事务 B 提交后,事务 A 再次读取,仍然看不到 B 的修改(因为视图生成时 B 还没提交,属于活跃事务)
作用:保证事务在可重复读隔离级别下,多次读取的数据一致,不受其他事务干扰

读已提交(RC):利用 MVCC 实现时,每次执行读操作(如 SELECT)都会生成一个新的一致性视图。因此,事务能看到当前时间点已提交的所有修改,符合读已提交的要求(只能看到已提交的变更)
可重复读(RR):利用 MVCC 实现时,事务启动时生成一个固定的一致性视图,后续所有读操作都基于这个视图。因此,事务内多次读取结果一致,符合可重复读的要求(不受其他事务中途提交的影响)

数据存储与索引机制

InnoDB 的行格式

行格式(Row Format)定义了数据行在磁盘上的存储结构,InnoDB 提供了多种行格式适应不同场景

1. 紧凑行格式(Compact)
MySQL 5.6 之前的默认行格式,存储空间利用率高。
列偏移量以紧凑方式存储,减少冗余。
每行开头存储变长字段长度列表(针对 VARCHAR、TEXT 等变长类型),按列顺序逆序排列。
NULL 值列表,用二进制位图标记哪些列值为 NULL,进一步节省空间。
数据部分按列顺序存储,无冗余偏移量信息。
适用于中小规模数据,对存储空间敏感的表。

Compact 行格式的紧凑在哪里?
NULL 用位图标记:一个二进制位就能标记一列是否为 NULL,比存NULL字符串省空间。
数据直接按顺序存:没有冗余的偏移量,比如这列数据从第 X 字节开始,直接紧凑排列。

Compact 行格式的长度列表采用逆序存放,是为了方便从总行长推算各字段起始位置,从而避免存储冗余的偏移量表,间接实现了存储更紧凑。

2. 冗余行格式(Redundant)
MySQL 5.0 之前的默认行格式
每行开头存储所有列的偏移量,空间利用率低,但便于解析。
现在主要用于兼容性,不推荐使用。

3. 动态行格式(Dynamic)
MySQL 5.7 及以上版本的默认行格式,是 Compact 格式的增强版。
针对大字段(VARCHAR > 255、TEXT、BLOB)采用溢出存储策略
        当行数据总大小超过页大小(16KB)的一半时,大字段数据会被迁移到溢出页(Overflow Page)。原数据行中仅保留 20 字节指针(指向溢出页地址和长度),大幅减少主数据页的空间占用。解决了大字段导致的页碎片化问题,提升缓存效率。

规则:
字段 ≤ 40 字节 → 永远存原行,不溢出
字段 > 40 字节 → 原行先留 20 字节指针,是否实际溢出看行总大小是否 > 8KB

如果一行里有多个字段都大于 40B,InnoDB 会优先把最长的字段溢出,直到行总大小 ≤8KB 为止

4. 压缩行格式(Compressed)
在 Dynamic 格式基础上增加页级压缩机制。
数据页写入磁盘前会通过 zlib 算法压缩,默认压缩比约 50%。读取时先解压到内存缓冲池,后续访问直接使用内存中的未压缩版本。
适用于存储大量数据、磁盘 IO 成本高的场景。优点是节省空间,缺点是压缩/解压增加 CPU 消耗。

只有Redundant行格式是非紧凑的,过时的,其他的都是较新的行格式,紧凑的

B+Tree 索引结构

InnoDB 使用 B+Tree 作为索引结构,支持高效的范围扫描和顺序访问。

聚簇索引(Clustered Index)

每张 InnoDB 表必须有且只有一个聚簇索引。
默认情况下,主键就是聚簇索引:
索引的非叶子节点:存储主键值
索引的叶子节点:存储整行数据
如果表没有主键:
会选择第一个非空唯一索引作为聚簇索引
如果没有唯一索引,则会生成一个隐藏的 row_id
特点:表数据按主键顺序存储,范围查询效率高

聚簇索引:数据和索引绑在一起的索引
可以把聚簇索引理解成带数据的目录,就像一本书的目录不仅有章节标题,还直接把章节内容印在目录页上。
例子:
假设有一张学生表,主键是学号。聚簇索引就像按学号排序的目录,目录的每个叶子节点不仅有学号,还直接包含该学生的姓名、年龄、成绩等所有信息。用学号查数据时,找到索引位置就能直接拿到完整数据,速度很快。

二级索引(Secondary Index / Non-Clustered Index)

除聚簇索引外的索引都是二级索引。
叶子节点存储:
索引列值
对应行的主键值(而不是物理地址)
查找过程:先找到主键,再回表到聚簇索引定位整行数据。

如果查询所需的所有字段都包含在二级索引中,这个二级索引就成为了覆盖索引,查询时可以直接使用该索引获取数据,避免回表,从而提高性能

二级索引(也叫辅助索引)是只指路的目录,就像一本书除了主目录,还有一个按关键词排序的辅助目录,目录里只有这个关键词在主目录的第 X 页,需要再通过主目录找内容。
特点:
一张表可以有多个二级索引,比如按姓名、年龄分别建索引
索引不存完整数据,只存指针:叶子节点存储的是主键值(聚簇索引的键),而不是整行数据。
查询需要回表:通过二级索引找到主键后,必须再去聚簇索引中查完整数据,除非是覆盖索引,即索引包含了查询所需的所有字段
例子:
还是学生表,按姓名建了二级索引。这个索引的叶子节点存储的是姓名 + 学号,比如 “张三 → 1001”。用姓名查 “张三的成绩” 时,步骤是:

先在二级索引中找到 “张三” 对应的学号 1001
再到聚簇索引中用学号 1001 找到完整数据,获取成绩

索引性能优化

优化索引时应注意:

1. 联合索引设计:最左前缀原则
联合索引(多列索引)的生效顺序严格遵循最左前缀匹配,即索引仅对查询条件中从最左列开始的连续匹配生效。
反例:对 (a, b, c) 建立索引,WHERE b = 1 AND c = 2 无法使用索引。
正例:WHERE a = 1、WHERE a = 1 AND b = 2、WHERE a = 1 AND b = 2 AND c = 3 均可使用索引。
设计建议:将区分度高的列(如身份证号)放在左侧,频繁过滤的列优先。

2. 避免过度索引:过多索引会拖慢写操作,因为每次插入/更新都需要维护多个索引。
3. 覆盖索引:通过包含所需字段,减少回表次数。
4. 索引维护:碎片多时可以 OPTIMIZE TABLE 或重建索引。

事务处理与 ACID

InnoDB 提供了完整的事务支持,满足 ACID 四大特性:

原子性(Atomicity):事务要么全部执行,要么全部回滚。
一致性(Consistency):事务完成后,数据库从一个一致状态转到另一个一致状态。
隔离性(Isolation):并发事务之间逻辑上相互独立。
持久性(Durability):事务提交后的更改持久保存,即使系统崩溃也能恢复。


 

多版本并发控制(MVCC)

问题背景:
在并发场景下,如果没有 MVCC,所有读取都要加锁,读和写会严重互相阻塞
解决思路:
MVCC 通过保存行的多个版本,让读操作读取历史版本,而写操作只操作最新版本,从而避免大多数读写冲突。所以叫 Multi-Version Concurrency Control(多版本并发控制)

MVCC 的底层实现
1. 行的隐藏列
在 InnoDB 每一行记录后面,都有几个 隐藏列:
trx_id:最后一次修改该行的事务 ID
roll_ptr:指向 undo log(撤销日志)的指针,可以沿着它找到该行的历史版本
可能还有一个 row_id,当表没有主键时自动生成,和 MVCC 无关

2. Undo Log
每次修改(UPDATE / DELETE)都会把旧值写入 Undo Log
Undo Log 存的是逻辑反操作(旧版本),并且形成一个链表(版本链)
如果需要历史版本,就顺着 roll_ptr 找旧值

3. Read View(读视图)
当事务开始时,会生成一个 Read View,里面记录了:
当前活跃事务 ID 的集合
本事务的最小活跃 ID、最大分配 ID
事务在查询时,会根据 Read View 判断某个版本是否对我可见
如果版本太新(由未提交事务产生),对我不可见
如果版本较旧,或者由已提交事务产生,对我可见

快照读 vs 当前读

快照读(Snapshot Read)
普通 SELECT 就是快照读
读到的是 Read View 生成时可见的版本(旧数据),不会加锁

当前读(Current Read)
SELECT … FOR UPDATE、UPDATE、DELETE 等,必须读到最新版本
会加锁(行锁/间隙锁/next-key 锁),防止并发修改

读已提交(RC):每次执行读操作(如 SELECT)都会生成一个新的一致性视图。事务只能读到其他事务已经提交的数据。
可重复读(RR):事务启动时生成一个固定的一致性视图,后续所有读操作都基于这个视图。事务里多次读取同一行结果要一致,还要避免幻读。

快照读(Snapshot Read)
普通 SELECT,不加锁,通过 Read View + MVCC 决定能看到哪些版本
在 RR 下:整个事务共用一个 Read View ,避免不可重复读、幻读
在 RC 下:每次查询新建一个 Read View ,可能出现不可重复读、幻读

当前读(Current Read)
需要最新数据,必须加锁:SELECT … FOR UPDATE、UPDATE、DELETE、INSERT
在 RR 下:使用 Next-Key Lock 住范围,避免幻读
在 RC 下:只加记录锁,不加间隙锁 ,可能出现幻读

锁机制

表级锁(Table Lock)
锁整个表,粒度大,并发性低。
典型场景:ALTER TABLE、LOCK TABLES。
InnoDB 一般不会主动使用表锁,除非特殊语句。

行级锁(Row Lock)
通过索引项实现,不是直接锁物理行
并发性高,是 InnoDB 默认的核心锁。
包含以下几种:
1. 记录锁(Record Lock)
锁定索引上的一个具体记录
例子:SELECT * FROM t WHERE id=5 FOR UPDATE; → 只锁住 id=5

2. 间隙锁(Gap Lock)
锁定索引值之间的“空隙”,防止其他事务在这个范围插入新记录
例子:SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE;
会锁定 10 和 20 之间的空隙,防止别人插入 id=15

3. Next-Key Lock
= 记录锁 + 间隙锁
作用:既锁定已有记录,又锁定前后的间隙
是 InnoDB 在 RR 隔离级别下的默认锁方式,用来避免幻读

4. 插入意向锁(Insert Intention Lock)
一种特殊的间隙锁
当事务打算插入一条新记录时,先在目标间隙上加插入意向锁
允许多个事务同时持有(不冲突),只有真正插入时才判断是否和 Next-Key 锁冲突

锁与隔离级别的关系
RC(Read Committed)
快照读:不加锁,每次读新视图
当前读:主要用记录锁,一般不加间隙锁
可能出现幻读(新插入的数据被读到)

RR(Repeatable Read,默认)
快照读:整个事务用同一个 Read View
当前读:使用 Next-Key Lock(记录锁 + 间隙锁)
可以避免幻读

幻读指的是:
一个事务按照相同条件两次读取,第二次读到了第一次没有的行

和不可重复读的区别:

不可重复读:同一行被别的事务修改,读出来的值前后不一致

幻读:同一范围内出现了新增的行

http://www.dtcms.com/a/336548.html

相关文章:

  • 【Python】Python 面向对象编程详解​
  • k8s-单主机Master集群部署+单个pod部署lnmp论坛服务(小白的“升级打怪”成长之路)
  • 集成电路学习:什么是SIFT尺度不变特征变换
  • oom 文件怎么导到visualvm分析家
  • 双指针和codetop2(最短路问题BFS)
  • 闭区间是否存在一个开区间包含之
  • ESP32S3在圆形240x240 1.8寸GC9A01 SPI显示屏显示双眼睛表情
  • 寻找数组的中心索引
  • ai测试(六)
  • [Java恶补day50] 174. 地下城游戏
  • 数据结构03(Java)--(递归行为和递归行为时间复杂度估算,master公式)
  • 数学建模 13 SVM 支持向量机
  • 原子操作及基于原子操作的shared_ptr实现
  • PYTHON让繁琐的工作自动化-PYTHON基础
  • 【撸靶笔记】第五关:GET - Double Injection - Single Quotes - String
  • 基于STM32单片机智能RFID刷卡汽车位锁桩设计
  • Qt同步处理业务并禁用按钮
  • linux系统------kubenetes单机部署
  • LeetCode 分类刷题:2962. 统计最大元素出现至少 K 次的子数组
  • 5G虚拟仿真平台
  • [激光原理与应用-292]:理论 - 波动光学 - 驻波的本质是两列反向传播的相干波通过干涉形成的能量局域化分布
  • 安全多方计算(MPC)简述
  • Compose笔记(四十六)--Popup
  • Houdini 粒子学习笔记
  • 服装外贸管理软件 全流程优化解决方案
  • 学习记录(二十)-Overleaf如何插入参考文献
  • Chrome 插件开发实战:从入门到上架的全流程指南
  • 最长回文子串问题:Go语言实现及复杂度分析
  • 63.不同路径
  • Django前后端交互实现用户登录功能