Skip to content

存储引擎

InnoDB

  1. 逻辑存储结构:表空间、段、区、页、行
  2. 架构
    1. 内存结构
    2. 磁盘结构
  3. 事务原理
    1. 原子性 - undo log
    2. 持久性 - redo log
    3. 一致性 - undo log + redo log
    4. 隔离性 - 锁+MVCC
  4. MVCC:记录隐藏字段、undo log 版本链、readView

数据结构

  • row:
    • Trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列。
    • Roll_pointer:氧次对媒条引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

内存节构

Buffer Pool:

  • 缓冲池是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。
  • 缓冲池以Page页为单位,底层采用链表数据结构管理Page。根据状态,将Page分为三种类型:
    • free page:空闲page,未被使用。
    • clean page:被使用page,数据没有被修改过。
    • dirty page:脏页,被使用page,数据被修改过,也中数据与磁盘的数据产生了不一致。

Change Buffer:

  • 更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据Page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区Change Buffer中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。
  • 意义:与聚集索引不同,二级索引通常是非唯一的,并且以相对随机的顺序插入二级索引。同样,删除和更新可能会影响索引树中不相邻的二级索引页,如果每一次都操作磁盘,会造成大量的磁盘IO。有了ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。

Adaptive Hash Index:

  • 自适应hash索引,用于优化对Buffer Pool数据的查询。InnoDB存储引擎会监控对表上各索引页的查询,如果观察到hash索引可以提升速度,则建立hash索引,称之为自适应hash索引。
  • 自适应哈希索引,无需人工干预,是系统根据情况自动完成。
    • 参数: adaptive_hash_index

Log Buffer

  • 日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log . undo log),默认大小为16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘I/O。
  • 参数:
    • innodb_log_buffer_size:缓冲区大小
    • innodb_flush_log_at_trx_commit:日志刷新到磁盘时机
      • 1:日志在每次事务提交时写入并刷新到磁盘
      • 0:每秒将日志写入并刷新到磁盘一次。
      • 2:日志在每次事务提交后写入,并每秒刷新到磁盘一次。

磁盘结构

System Tablespace

  • 系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)
  • 参数
    • innodb_data_file_path

File-Per-Table Tablespaces

  • 每个表的文件表空间包含单个InnoDB表的数据和索引,并存储在文件系统上的单个数据文件中。
  • 参数
    • innodb_file_per_table

General Tablespaces

  • 通用表空间,需要通过CREATE TABLESPACE语法创建通用表空间,在创建表时,可以指定该表空间。

Undo Tablespaces

  • 撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间〈初始大小16M),用于存储undo log日志。

Temporary Tablespaces

  • InnoDB使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。

Doublewrite Buffer Files

  • 双写缓冲区,lnoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。

Redo Log

  • 重做日志小是用来实现事务的持久性。
  • 该日志文件由两部分组成:
    • 重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。
    • 当事务提交之后会把所有修改信息都会存到该日志中,用于在刷新脏页到磁盘时,发生错误时,进行数据恢复使用。

后台线程

  1. Master Thread

核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中,保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo页的回收。

  1. IO Thread

在InnoDB存储引擎中大量使用了AIO来处理IO请求,这样可以极大地提高数据库的性能,而IOThread主要负责这些IO请求的回调。

sql
show engine innodb status;
  1. Purge Thread

主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回收。

  1. Page Cleaner Thread

协助Master Thread 刷新脏页到磁盘的线程,它可议减轻Master Thread的工作压力,减少阻塞。

事务

事务特性

  1. 原子性 Atomicity:一些列操作要么全部成功,要么全部失败
  2. 隔离性 Isolation:事务的结果只有提交了其他事务才可见
  3. 一致性 Consistency:数据库总时从一个一致状态变到另一个一致状态(事务修改前后的数据总体保证一致 转账
  4. 持久性 Durability:事务提交或回滚后,对数据修改永久的

原子性 && 一致性 && 持久性

  • redo log
  • undo log
  1. redo log
    1. 重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
    2. 该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。
    3. 脏页的目的:事务中的各种操作可能是随机(随机磁盘IO)的,如果每次都直接写到磁盘会有性能问题,所有采用 Redolog buffer 将单独的数据页变化追加临时文件(顺序磁盘IO)(WAL:writer-Ahead Logging)

  1. undo Log 原子性
    1. 回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和MVCC(多版本并发控制)
    2. undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚
    3. Undo log销毁:undo log 在事务执行时产生,事务提交时,并不会立即删除 undo log,因为这些日志可能还用于MVCC
    4. Undo log存储:undo log 采用段的方式进行管理和记录,存放在前面介绍的rollback segment回滚段中内部包含1024个undo log segment

隔离性

  • MVCC
隔离级别

MySQL定义了四种隔离级别,包括一些具体规则,用于限定事务内外哪些改变是可见的,哪些改变是不可见的。低级别的隔离一般支持更高的并发处理,并且拥有更低的系统开销。

  1. 读未提交 Read Uncommited

在这个隔离级别,所有事务都可以“看到”未提交事务的执行结果。在这种级别上,可能会产生很多问题,除非用户真的知道自己在做什么,并有很好的理由选择这样做。本隔离级别很少用于实际应用,因为它的性能也不必其他性能好多少,而别的级别还有其他更多的优点。读取未提交数据,也被称为“脏读"

  1. 读以提交 Read Commited

大多数数据库系统的默认隔离级别(但是不是MySQL的默认隔离级别),满足了隔离的早先简单定义:一个事务开始时,只能′看见"已经提交事务所做的改变,一个事务从开始到提交前,所做的任何数据改变都是不可见的,除非已经提交。这种隔离级别也支持所谓的“不可重复读”。这意味着用户运行同一个语句两次,看到的结果是不同的。一个事务被提交以后才可以读到。

我有 A B 两个事务,A 里面有两个查询语句,B 里面有一个更新语句,可能在执行完 A 的第一条查询语句后,B 的更新事务提交了,这样就造成了 A 的两次查询不一致。

  1. 可重复读 Repeatable Read

MySQL 数据库默认的隔离级别。该级别解决了 READ UNCOMMITTED 隔离级别导致的问题。它保证同一事务的多个实例在并发读取事务时,会"看到同样的"数据行(快照读),解决了“不可重复读”。不过,这会导致另外一个棘手问题"幻读”。InnoDB和Falcon存储引擎通过**多版本并发控制机制(MVCC)**解决了幻读问题。

快照读:读取的是历史数据

  1. 可串行化 Serializable

该级别是最高级别的隔离级。它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简而言之,

SERIALIZABLE 是在每个读的数据行上加锁。在这个级别,可能导致大量的超时 Timeout 和锁竞争 Lock Contention 现象,实际应用中很少使用到这个级别,但如果用户的应用为了数据的稳定性,需要强制减少并发的话,也可以选择这种隔离级。

脏读

脏读是指一个事务读取了未提交事务执行过程中的数据。

当一个事务的操作正在多次修改数据,而在事务还未提交的时候,另外一个并发事务来读取了数据,就会导致读取到的数据并非是最终持久化之后的数据,这个数据就是脏读的数据。

不可重复读

不可重复读是指对于数据库中的某个数据,一个事务执行过程中多次查询返回不同查询结果,这就是在事务执行过程中,数据被其他事务提交修改了。

不可重复读同脏读的区别在于,脏读是一个事务读取了另一未完成的事务执行过程中的数据,而不可重复读是一个事务执行过程中,另一事务提交并修改了当前事务正在读取的数据。

幻读

幻读是事务非独立执行时发生的一种现象,例如事务 T1 批量对一个表中某一列列值为1的数据修改为2的变更,但是在这时,事务T2对这张表插入了一条列值为1的数据,并完成提交。此时,如果事务T1查看刚刚完成操作的数据,发现还有一条列值为1的数据没有进行修改,而这条数据其实是T2刚刚提交插入的,这就是幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点同脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

概念

当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select ... lock in share mode(共享锁),select .. for update、update、insert、delete(排他锁)都是一种当前读。

  • RR隔离级别下,在事务里面读取另一个事务更新的值可以用 lock in share mode

快照读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁是非阻塞读。

  • Read Committed:每次 select,都生成一个快照读
  • Repeatable Read:开启事务后第一个 select 语句才是快照读的地方
  • Serializable:快照读会退化为当前读

MVCC

  • 全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为 MySQL 实现 MVCC 提供了一个非阻塞读功能。MVCC 的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log 日志、readView
MVCC
  • 为事务中读取数据做处理
  • 每种隔离级别下读取要求不一样
  1. 三个隐式字段

DB_ROW_ID

  • 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段
  • 如果表有主键,选择主键,如果没有主键,选择唯一键,如果没有唯一键,选择自动生成一个6字节的rowid

命令 idb2sdi tab.idb 可以查看磁盘上的表结构

隐藏字段含义
DB_TRX_ID最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
DB_ROLL_PTR回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。
DB_ROW_ID隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。
如果表有主键,选择主键,如果没有主键,选择唯一键,如果没有唯一键,选择自动生成一个6字节的rowid。
  1. undo log
    1. 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
    2. 当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
    3. 而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除
    4. 不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

  1. Read View
    1. ReadView(读视图)是**快照读 **SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活跃的事务(未提交的) id。ReadView中包含了四个核心字段:
字段含义
m_ids当前活跃的事务ID集合
min_trx_id最小活跃事务ID
max_trx_id预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
creator_trx_idReadView创建者的事务ID
2. READ COMMITTED:在事务中**每一次**执行快照读时生成 ReadView
3. REPEATABLE READ:仅在事务中**第一次**执行快照读时生成 ReadView,后续复用该 ReadView
4. trx_id 是版本链中的事务id

MyIAM和InnoDB区别

  • InnoDB 聚簇索引,MyISAM 非聚簇索引
  • InnoDB 数据与索引一起保存 .ibd,MyISAM 表结构 .frm 索引 .myi 数据 .myd
  • InnoDB 支持事务、外键、行锁表锁,MyISAM 不支持事务、外键、只支持表锁
  • select count(*)
  • MyISAM 查询更优,InnoDB 更新更优
  • 都是 B+tree 索引
  • MyISAM 支持全文索引,InnoDB5.6 后支持

MyISAM

  • 不支持事务,每个查询都是原子的
  • 支持表级锁,每次操作都是对表加锁
  • 存储表的总行数
  • 一个 MyISAM 表有三个文件:表结构 .frm 索引 .myi 数据 .myd
  • 采用非聚簇索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性

Innodb

  • 支持 ACID 事务,支持四种隔离级别
  • 支持行级锁及外键约束,因此支持写并发
  • 不存储总行
  • 主键索引采用聚集索引(索引的数据域会存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要通过辅索引找到主键值,在访问辅索引;最好使用自增主键,防止插入数据时,为维持 B+树结构,文件的大调整。

使用场景

大多数时候使用 InnoDB 足矣,在读密集的的情况下,使用 MyISAM 也是合适的。不过,前提是的你的项目不介意 MyISAM 不支持事务、崩溃恢复等缺点。

  • MyISAM 适合读多写少:MyISAM 索引和数据分开存放,因此有读取更快的说法
  • InnoDB 适合插入更新频繁的:索引与数据一起存放,建立索引更复杂,使用行锁,更新频繁效率更高
  • 需要事务使用 InnoDB,高并发使用 InnoDB 支持行锁
  • InnoDB 更新快,MyISAM 查询快
    • MyISAM适合读多,更新少的场景。MyISAM使用非聚簇索引,数据和索引分开存的,而InnoDB数据和索引存一起的,数据量大时,一个内存页大小固定,读进内存的数据MyISAM就多一点(数据量小看不出差距,数据量大时差距就明显)。
    • 因为 MyISAM 只把索引指针读进内存,可以存更多,查询速度也就更快,而且 InnoDB 还需要维护其他数据,比如其他隐藏字段 row_id、tx_id 等

自增主键

InnoDB 引擎的自增值,其实是保存在内存里面,到了8.0以后才会持久化到磁盘。

也就是如果发生重启,表的自增值可以恢复为 MySQL 重启以前的值。(查看表结构,会看到自增主键=多少)。

  • 在 MySQL 5.7 及之前的版本,自增值保存在内存里,并没有持久化。每次重启后,第一次打开表的时候,都会去找自增值的最大值 max(id),然后将 max(id)+1 作为这个表当前的自增值。
  • 举例来说:如果一个表当前数据行里最大的 id 是10,AUTO_INCREMENT=11。这时候,我们删除 id=10 的行,AUTO_INCREMENT 还是11。但如果马上重启实例,重启后这个表的 AUTO_INCREMENT 就会变成10。也就是说,MySQL重启可能会修改一个表的 AUTO_INCREMENT 的值。
  • 在MySQL 8.0版本,将自增值的变更记录在了redo log中,重启的时候依靠redo log恢复重启之前的值。

自增修改机制:

  1. 如果插入数据时id字段指定为0、null 或未指定值,那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段。
  2. 如果插入数据时id字段指定了具体的值,就直接使用语句里指定的值。

自增新增机制:

  1. 如果准备插入的值>=当前自增值,新的自增值就是“准备插入的值+1”。
  2. 否则,自增值不变。

为什么自增主键不连续:

  1. 在MySQL 5.7及之前的版本,自增值保存在内存里,并没有持久化
  2. 事务回滚(自增值不能回退,因为并发插入数据时,回退自增ID可能造成主键冲突)
  3. 唯一键冲突(由于表的自增值已变,但是主键发生冲突没插进去,下一次插入主键=现在变了的子增值+1,所以不连续)

Innodb为什么推介用自增ID:

  1. 主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费。
  2. 新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗。
  3. 减少了页分裂和碎片的产生

UUID:大量的随机IO+页分裂导致移动大量的数据+数据会有碎片。

总结:自增ID有序,会按顺序往最后插入,而UUID无序,随机生成,随机插入,会造成频繁页分裂,内存碎片化,大量随机IO

聚簇索引和非聚簇索引

MySQL 的索引类型跟存储引擎是相关的,innodb存储引擎数据文件跟索引文件全部放在ibd文件中,而myisam的数据文件放在myd文件中,索引放在myi文件中,其实区分聚簇索引和非聚簇索引非常简单只要判断数据跟索引是否存储在一起就可以了。

innodb存储引擎在进行数据插入的时候,数据必须要跟索引放在一起,如果有主键就使用主键,没有主键就使用唯一键,没有唯一键就使用6字节的rowid,因此跟数据绑定在一起的就是聚簇索引,而为了避免数据冗余存储,其他的索引的叶子节点中存储的都是聚簇索引的key值,因此innodb中既有聚簇索引也有非聚簇索引,而myisam中只有非聚簇索到。