mysql技术架构和原理

Mysql技术架构和原理

Mysql体系结构

mysql体系架构如上图所示,组件分别为:

  • 连接池组件,即跟客户端的链接管理,包括鉴权,连接数限制等
  • 管理服务和工具组件,包括备份,复制等
  • SQL接口组件
  • 解析器,解析查询语句
  • 优化器, 对解析之后的sql语句进行查询优化,比如选择索引
  • cache组件,会缓存部分数据,解析之前会去缓存中查找数据,有的话,则无需解析、优化和执行sql
  • 插件式存储引擎,值得注意的是,存储引擎是基于表的
  • 物理文件

存储引擎

mysql目前默认是InnoDb存储引擎,之前默认是MyIsAm存储引擎,还有很多其他的引擎

可以通过show engines; 命令查看当前mysql支持的存储引擎

InnoDB存储引擎的特点是,支持事务,使用聚簇索引,数据放在逻辑表空间中,MVCC和行锁等

MyIsAM存储引擎特点是支持全文索引,但是不支持事务和表锁设计,

InnoDb存储引擎

InnoDb存储引擎的优点上面已经讲到了,下面将会介绍它的体系架构和各个模块功能

InnotDb体系架构

mysql技术架构和原理_第1张图片

整体的架构如上所示

  • 后台线程,MasterThread和IO Thread、PureThread等
  • 内存池

内存池

mysql技术架构和原理_第2张图片

缓冲池

InnoDb引擎下,各种数据最终的存储格式是磁盘,但是CPU速度和磁盘速度有鸿沟,因此,开辟一块内存区域,用作缓冲池,在对数据库操作时,先通过缓冲池来过度,这点其实跟操作系统cpu, 内存和磁盘的关系类似。

缓冲池中的数据通过Checkpoint的机制刷新回磁盘

另外,由上面的图可以看出,缓冲池不只是存放数据页,还有索引页

通过show engine innodb status;命令可以查看包括缓冲池在内的一些信息,有Buffer Pool等字段

每一页数据是16KB大小

缓冲池结构

缓冲池中数据结构是一种改良LRU列表(访问最频繁的在列表的头部,最先删除尾部的数据)

在原有的LRU列表中,加了一个midpint位置,使用了两个参数: innodb_old_blocks_pct和innodb_old_blocks_time

比如设置innodb_old_blocks_pct为37,那么当某页被读取时,并不会直接将该页插入到LRU的首部,而是插入到举例LRU尾部37%的位置,等过了innodb_old_blocks_time之后,再将37%位置的页移动到LRU首部,这样做的目的是为了避免不太常用的数据因为某些次查询就到了LRU首部,挤占了热点数据在LRU列表中的概率

mysql技术架构和原理_第3张图片

  • LRU List, 已读的页,会从Free list挪到Lru list
  • Free List, 数据库启动时,会先加载到Free List
  • Flush LIst,需要刷新到磁盘的脏页

mysql技术架构和原理_第4张图片

从上图可以看出,Free list和Lru list是有流通的,数据库启动时,一些数据会放在free list中,被读取后,会挪到LRU中,LRU中列表中尾部被淘汰的数据,会回到free list中。

flust list可以看做是lru的子集,当lru列表中有页被更新删除,即脏页时,flush列表会新增数据,不过是指针,指向LRU的某一页

https://cloud.tencent.com/developer/news/332123

重做日志缓冲

redo log 信息也是每次先放到重做日志缓冲中中,然后以一定频率将其刷新到重做日志中,刷新的时机为:

  • 1、Master Thread每一秒刷新一次
  • 2、每个事务提交时
  • 3、重做日志缓冲池剩余空间小于1/2时

额外内存池

存储一些元数据信息,比如锁、LRU

Checkpoint

checkpoint主要分为两大类

Sharp Checkpoint

数据库关闭之前,将所有的脏页进行刷新到磁盘

Fuzzy Checkpoint

  • Master Thread Checkpoint
  • FLUSH LRU Checkpoint
  • Aysnc/Sync Flush Checkpoint,有两个阈值,当日志文件大小超过async_water_mark时,刷新一次,>sync_water_mark时,刷新一次,这一块无需关注细节
  • Dirty Page too much Checkpoint

Master Thread

InnoDb的大多数操作都是在Master Thread中执行的,包括合并缓冲,刷新日志缓冲等。并且每隔操作的频率不同。

最新版本中,除了Master Thread,还有Page Clearner Thread,用来刷新脏页

InnoDb关键特性

插入缓冲

插入数据时,由于有非聚簇索引的B+树需要维护,由于其索引可能是离散的,不是像自增id一样主键增大,放在最后就行,那么会影响性能,因此,如果索引页在缓冲区中,会先将索引插入到缓冲区,再异步和磁盘中进行合并操作

当然,索引不能是唯一的,不然不好判断新插入的数据是否满足唯一性

double write

其实就是在从缓冲池刷新到磁盘的过程中,先在物理磁盘上共享表空间中备份一份,再刷新到磁盘。

如果刷新到磁盘过程中只刷新了一般,只写了页的一半就宕机了,恢复的时候,如果按照之前的没有double write, redo日志时,根本不知道页只刷新了一半,所以有了共享表空间后,就可以先拿到备份,也就是写入之前的,再结合redo日志,就能重做了

自适应哈希(AHI)

对于一些 = 的查询,innodb会通过一些规则和机制自动创建一些哈希索引,加快查询速度

AIO

  • 需要扫描多个数据页时,可以并发IO请求,然后等到所有IO操作完成
  • 对于连续的数据页,AIO会默认合并成一个IO请求

文件

参数文件

参数文件即启动时候的配置文件,可以通过mysql --help | grep my.cnf查询默认的配置文件,也可以没有,mysql会有默认的配置

一些参数可以通过Set命令来进行设置, 通过show variables like 'read_buffer_size'这样的命令来查看value

慢日志文件

mysql会将低于一个阈值的sql查询语句记录到慢日志文件中,sql如下:

mysql> show variales like 'long_query_time';

可以用mysqldumpslow命令查看sql文件

慢日志文件在这个文件夹

/usr/local/mysql/data/mysql

查询日志文件

顾名思义, 记录了所有对数据库请求的信息

二进制文件日志

即binlog日志,记录了对数据库的执行更改的所有操作,一般存放在/usr/local/mysql/data路径下,local根据主机名而变化。

可通过命令show variables like 'datadir'查看binlog地址
通过命令show master status查看binlog信息
binlog有三种格式,sql, row, mixed(在某些特殊场景下,比如表的存储引擎是NDB,使用了UUID()等不确定函数)

binlog_formt参数表示类型

row格式下肯定会占用更多空间,但是便于数据库的恢复和复制,

使用mysqlbinlog -vv binlog.000126 命令可以查看binlog语句,直接打开文件就是乱码,注意,这个命令,是linux命令,不是进入mysql控制台下的命令

表空间文件

Innoddb中表的数据都放在表空间中,有共享表空间,即ibdata1文件,配置了独立表空间后,就有了ibd文件,每个表,一个ibd文件

重做日志文件

/usr/local/mysql/data路径下ib_logfile前缀的文件是重做日志文件,默认会有另个,而且是循环引用的,因为一旦写入磁盘成功后,很多重做日志文件中的内容实际是没有意义的

另外,重做日志缓冲写入到磁盘的重做日志文件中时,按照512个字节进行写入的,刚好是一个扇区的大小,因此可以保证一定是可以写入成功的。

为了保证实物的ACID中的持久性,需要将innodb_lush_log_at_trx_commit设为为1,即有事务提交时一定要将重做日志刷新到磁盘,不然采用异步的方式,宕机时,未必能保证数据不丢失

InnoDB中的表都是根据主键顺序存放的,成为索引组织表,如果表没有主键,则判断是否有非空的唯一索引,有,则根据第一个非空的索引作为主键,否则,会创建6字节大小的指针作为主键

InnoDb的存储结构如下,所有数据都被逻辑地存放在表空间中

mysql技术架构和原理_第5张图片

  • 段 ,组成了表空间
  • 区,任何情况下,一个区的大小都为1MB,默认情况下一个区有64个连续的页,一页默认16KB,为了保证区中页的连续性,InnoDB存储引擎一次从磁盘中申请4到5个区
  • 页是InnoDB磁盘管理的最小单位,可以通过innodb_page_size设置页的大小。常见页的类型为:数据页,undo页,系统页,事务数据页,插入缓冲位图页,插入缓冲空闲列表页,二进制大对象页
  • InnoDb是面向行存放的,下面会详细介绍行结构

行记录格式

Compact行记录格式

mysql 5.0引入的,其结构如下

在这里插入图片描述

第一个部分表示变成部分的长度列表(按照列的逆序顺序),比如Varchar类型,NULL标志位表示这一行是否有NULL值的字段,记录头信息中有多个信息,

mysql技术架构和原理_第6张图片

之后就是每一列的数据,其中NULL值的列不会赋值

Redundant行记录格式

Mysql 5.0之前的格式,目前高版本也支持,是为了兼容数据

在这里插入图片描述

与Compact不同,第一个部分表示每个字段的偏移列表(按照列的逆序顺序),对于char类型列的NULL值也占用空间,varchar类型不占用空间

记录头信息中,包含n_fields,10个字节表示列的数量,即最大不超过1023

Compressed and Dynamic

InnoDB 1.0.x之后,采用新的行记录格式,相对于之前的Redundant行记录格式,主要变化是采用了完全的行溢出的方式,下面讲一下何为行溢出数据

行溢出数据

InndoDb一页只有64KB数据,即16384个字节,为了充分利用B+树的作用,每页数据至少会存放两个行记录,因此如果一个页中如果只能存放一条记录,那么会自动将行数据溢出,将数据放到另一个专门的溢出页中,将行记录中指针指向它

比如VARCHAR类型最大为65532个字节,如果一个表中一条记录,这个字段刚好这么大,显然,已经超过了这个页的最大值,那么VARCHAR这一列的数据,会放到uncompressed Blob page

对于Rebundant和Compact,数据页中VARCHAR列存储了786字节的数据,之后是指向行溢出页的指针

而对于Compressed 和Dynamic而言,没有这786个字节的数据,完全使用20个字节的指针指向行溢出页

InnoDb数据页结构

InnoDB数据页结构如下图所示

mysql技术架构和原理_第7张图片

下面分别介绍一下每个部分的功能

File Header

file header用于记录页的一些头信息,关键信息为:

  • 页在表空间中的偏移值,即第几页,因为页的大小是固定的
  • 上一页的指针、下一页的指针
  • 页的类型,分为B+树页节点,Undo Log页,索引节点,BLOB页等

Page Header

page header记录数据页的状态信息,比如该页中记录的数量、索引id、当前页在索引中的位置等

Infimun和Supremum

该页的上下边界,虚拟的行记录

User Records和Free Space

User Records就是实际的行记录数据

Free Space是空闲空间,一条记录被删除后,该空间也会加入到空闲空间,空闲空间也是链表结构

Page Directory

Page Directory存放的是一个稀疏索引,可以看做是一个Slots(槽),存储了部分记录的行记录指针,在Slot中按照索引键值存放

比如数据中有a, b ,c ,d ,e, f, g, h ,i

那么Page Directory中存放的可能就是infinum(必须), b, d , f,SUpremum(必须)

并且在行记录中的b, d, f中的record header中,n_owned值是该记录到上一个槽点记录中有多少数据,比如b, n_owned值就是2(a,b), d的n_owned值是3(b,c,d)

B+树索引本身并不能找到具体的记录,而是找到页,,数据库把页加载到内存后,先通过Page Directory,进行一个二叉查找,找到粗略的位置,再通过next_record等进行后续的查找准确的行记录

File Trailer

校验数据的完整性

索引与算法

InnoDB除了支持传统意义上的B+树索引,还支持全文索引和哈希索(InnoDB存储引擎会根据表的使用情况自动为表哈希索引,不能人为干预)

B+树

B+树是一种平衡查找树,之前将页数据结构时提到过,B+树索引的每个节点是一页

插入时的逻辑

mysql技术架构和原理_第8张图片

不过有的时候,虽然按照上面的逻辑,需要将一个页进行拆分,保证平衡,但是由于这样会操作磁盘较多,可能会将记录移到另一个页中的空闲位置,类似于旋转操作

删除时的逻辑

mysql技术架构和原理_第9张图片

B+树索引

聚集索引和非聚集索引
  • 聚集索引的叶子节点是数据页,而非聚集索引的叶子节点存放的不是数据页,而是存放了索引的键值信息和主键的值

  • 当索引页由于插入记录,需要进行分裂,不一定会总是从中间记录开始分裂,比如那种自增插入的,如果从中间分裂了,那么左边这一页会一直没有数据插入,真实场景下,会选择合理的分裂点和分裂方向

  • 使用show index from order_tab命令可以查看所有索引信息,其中,Cardinality字段表示值不同的行数,这个数越大,对索引越有利。 数据库不会总是立即去更新这个值,不然开销会很大,条件为:

    • 1、表中1/16的数据已经更新

    • 2、计数器stat_modified_counter(表发生变化的次数) > 2000 000 000

      另外,Innodb通过采用的方式,选择若干个叶子节点,统计每个页的不同记录的个数,再预估整个表的个数

联合索引

遵循最左原则,其实上文学习过索引和索引页的结构后,就能理解

覆盖索引

除了常见的不需要查询联合索引中没有的值可以使用覆盖索引外,向count(*)这种也可以走覆盖索引,以为不需要查行记录的数据

顺序读和随机读对索引查询的影响

项目线上场景中,经常会遇到设置了索引,但是走了全表扫描的原因,有一个就是顺序读和随机读,有时候,虽然表面走非聚集索引,但是从非聚簇索引查询到主键值后,还要根据主键值从聚簇索引查数据,如果第一步的主键值不是顺序的,那么显然,读取磁盘时,会分别从磁盘的不同位置进行读取;因此有些实际场景下,不走非聚簇索引,直接扫描磁盘,由于数据是连续的,那么可以直接顺序读取磁盘数据,速度可能更快,尤其是要查询的数据占整个表的数据很大(一般情况下>20%)

mysql5.6开始的索引优化
  • Multip-Range Read(MMR),辅助索引查询出键值对后,将键值对放到缓存中去,然后再根据主键id排序,再根据主键id的排序顺序来访问实际的数据文件
  • Index Condition Pushdown , 一般情况下,where条件中,对于不走索引的部分,会在从索引中拿出数据后,再根据where条件进行过滤,不过对于覆盖索引,根据最左原则,如果前边的索引使用了范围,或者后边使用了like等,那么后边的索引就不会用到索引,但是在Index Condition Pushdown模式下,在查找索引时,还是可以根据索引的叶子节点先进行过滤,再去查行记录数据信息

哈希索引和全文索引

  • 哈希索引上文已经说过

  • 全文索引其实就是倒排索引,跟es中的倒排索引基本原理是一致的

锁的类型

  • 共享锁(S), 允许事务读一行数据
  • 排他锁(X), 允许事务删除或更新一行数据

​ S和X都是行级锁

mysql技术架构和原理_第10张图片

  • 意向锁, InnoDB为了支持不同粒度的锁操作,引入了意向锁(Intention Lock),将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁

​ 有了意向锁之后,如果需要对记录加X锁,那么分别需要对 数据库、表、页加上意向锁IX,最后对记录上X锁

​ 意向锁也分为IS和IX,即意向共享锁和意向排他锁

InnoDB的意向锁是表级别的锁,设计目的是为了在一个事务中揭示下一行将被请求的锁类型

​ 右下图可知,意向锁之间是兼容的,IX对意向锁之外的所有锁都不兼容,X锁对所有锁都不兼容

mysql技术架构和原理_第11张图片

一致性非锁定读

即通常所说的MVCC

如果读取的行正在执行delete/update操作,这时候别的事务中的读取操作并不会等待delete/update操作完毕即X锁的释放,而是会读取行的一个快照数据

这个实现是通过undo端来实现的,undo端用来回滚数据,每行记录可能会有多个版本记录,所以这种方式称为MVCC,多版本并发控制

对于事务的隔离级别,read commited和read repeatable来说,读取的版本记录不太一样。

如果是read committed,那么事务中,每次读取的都是行的最新版本,但是read repeatable读取的都是事务刚进来时第一次读取时的版本

一致性锁定读

即希望其他事务不能读取
select *** for update , select *** lock in share mode分别是X锁和S锁

当然如果其他事务,直接使用一致性非锁定读,还是可以读到的

自增长和锁

自增长是指对于自增长的列,会有自增长的计数器,innodb提供了轻量级互斥量的自增长实现机制,提供innodb_autoinc_lock_mode来控制自增长的模式

锁的算法

  • Record lock, 行锁
  • Gap lock, 间隙锁,锁定一个范围,但不包含本身
  • Next-key lock: record lock+gaplock, 锁定包含自身的一个范围

判断sql会加什么锁,一般需要判断查询的列是否是唯一的,比如查询的列是唯一索引,比如主键,where ${primary key} = ** 这种情况,但是如果是辅助索引,那么会加上间隙所或者next-key锁

锁的问题

  • 脏读, 即read uncommited模式下,可能会看到并发时其他事务已经提交的数据,改为read commited模式即可解决
  • 不可重复读, 即读取过的数据,再读取一遍,发现变了,改为repeatable read即可解决,其实是采用了MVCC模式
  • Phantom Problem问题,即幻读
    即上面所说的,在辅助索引上加锁, 如果只是锁住这一行,那么如果另一个事务新增了一行等于这个辅助索引,那么显然是没有锁住的,因此next-key locking算法解决了这个问题

死锁

就是多个事务中的,锁,锁住了对方需要的某个资源,都在等待。

在mysql中,如果发现死锁,会直接让其他事务抛出死锁异常放弃资源,让某一个事务正常执行

innod采用了wait-for graph的深度优先算法实现,判断是否死锁

锁升级

在很多数据库中,对于锁的细节可能会做优化,比如锁住很多行时,可能会发生一些性能、内存问题,会将行锁变为页锁甚至表锁。

而InnoDB存储引擎,不存在锁升级的问题,因为它是根据页进行加锁的,采用的是位图的方式表示锁的是哪些行

由于锁很复杂,会单独写一篇博客,通过实践的方式,来展现什么场景,加什么锁,在什么地方加锁

事务

事务的四大特性

  • Automic ,原子性,事务中的事项,要么全部执行成功,要么全部失败
  • Consistency,一致性,事务执行之后需要满足数据库的各种约束,比如字段长度,唯一性约束;从某种层面来讲,C是ACID的目标
  • Isolation 隔离性,不同事务中的执行应该互不影响
  • Durability 持久性,磁盘损坏或自然灾害的原因之外,一旦提交,就是永久性的,数据库重启后,也能恢复

事务分类

  • 扁平事务(Flat Transactions)

​ 就是最普通的事务

  • 带有保存点的扁平事务(Flat transactins with savepoints)

​ 有些场景下,如果一个事务中后面的操作不成功,但是想让前面的操作提交;比如抢票,从上海到杭州,再转一下,从杭州到武汉,那么期望至少能抢到上海到杭州的票。

具体如下图所示

mysql技术架构和原理_第12张图片

  • 链事务(Chained transactions)

链事务和保存点有相似,但是只能恢复到最近的一个保存点,不能恢复到很久以前的保存点

  • 嵌套事务(Nested transactions)

mysql技术架构和原理_第13张图片

顶层事务提交后,子事务才会真的提交

嵌套事务还可以继承锁、传递锁

InnoDB不支持嵌套事务

  • 分布式事务(Distributed transactions)

      mysql XA事务
    

事务的实现

实现事务,可以理解为实现事务的四大特性

对于隔离性,显而易见,想让事务之间不受影响,那么就是使用到了锁

对于原子性、一致性和持久性,通过数据库的redo log和undo log来实现,需要注意的是,redo 和undo并不是字面意思上的逆向过程,两者记录的内容都不一样

redo log

redo log用来实现事务的持久性,分为重做日志缓冲和重做日志文件

事务提交时,会将该事务的所有日志写入到日志缓冲中,才算commit成功,而日志缓冲fsync到日志文件中,则由一定的频率异步进行执行

重做日志中还记录了LSN,即单调递增的序列号,数据页中也记录了LSN,因此数据库启动时会根据LSN的差距,来进行恢复操作

重做日志记录的是对页进行的操作,即某一页,某个偏移量,数据的变更

undo log

重做日志是为了对页进行恢复,但是事务有时候还需要进行回滚操作,这时候就需要用到undo log

undo log位于数据库内部的undo segment中,即位于共享表空间中

undo log记录的是每一行的修改,并且当回滚时,并不是对数据进行物理地恢复到事务开始的样子,而是做逆向操作,比如update一条记录,那么回滚时,就update到原来的字段值。 这是因为并发情况下,可能别的事务已经修改了数据, 如果直接恢复到原来的值,可能会把别的事务已经修改的数据覆盖。
每次写新纪录

undo log的另一个作用就是mvcc

https://blog.csdn.net/SnailMann/article/details/94724197

MVCC

mvcc是由undo log和read view来实现的。mvcc的作用是在不加锁的情况下实现多版本并发控制。
每个事务开启时都会生成自增的事务id,并且当有数据操作时,生成一个undo log日志,生成的undo log日志放在链表的首部,并且指向原来的首部。
每个事务还会维护活跃的事务id, 如果事务1和事务2操作同一条记录,事务2提交后,事务1中的活跃事务没有2,那么会认为事务2的事务id是默认的undo log的最新记录。隔离级别RC就是这样可以读取最新的提交。 而隔离级别RR会对于该条记录读取时生成一个快照,事务后续又再读的时候会直接从这个快照读取这条记录

purge

delete 和update 操作并不直接删除已有的数据。

因为并发情况下,一行记录在被delete的同时,其他事务可能还在引用它,因此会只是加上一个delete flag

update 操作会插入一条新的记录,同时原有记录加上一个delete flag

Innodb存储引擎会有机质进行purge操作,即真正地删除这些需要删除的记录

事务隔离级别

MySQL隔离级别是repeatable read

一般情况下,为了避免幻读,会认为使用SERIALAZIBLE。 但实际上,InndoDB存储引擎,由于使用了Next-Key算法,已经避免了幻读的产生,因此其已经能够避免了幻读的产生,也就是已经达到了Serializable隔离级别

三个重要日志的工作顺序

  • redo log,事物提交时,将事务日志写入到redo log buffer中。 redo log的目的是防止在断电时缓存在内存中的数据没有刷新到磁盘造成数据丢失,因此在事务提交之前就会进行写redo log操作,redo log也有缓冲区,但是可以配置每次强制将缓冲区刷到磁盘,并且要调用fsync方法,必然操作系统将文件先写入了操作系统的缓存中。 另外,redo log日志存储了数据的数据也的LSN,mysql重启时根据磁盘文件的LSN和redo log的lsn,判断是否需要redo以及从哪里开始redo。
    redo log保证的是数据库的持久性
    redo log存储的是记录的数据,而不是sql语句
  • undo log, undo log是为了事务的回滚操作。每次事务提交之前,就会生成一条undo log,并且带有相反了逻辑的sql语句。 undo log也会产生redo log。 当事务提交之前,undo log必然一定写到了磁盘中,并且表数据肯定也写到了磁盘中,这时发生回滚时才会能准确地找到之前的数据
  • binglog是为了数据同步和备份恢复,也是在事务提交之前

InnoDB二阶段提交

redo log和binlog写入时,如果有一个写入有问题,那么就会发生问题。问题如下:

  • 1、如果先写redo log再写binlog,在redo log写成功后,主库crash了,那么binlog没有数据,显然从库数据就缺失
  • 2、如果先写binlog再写redo log,在binlog写成功后,redo log没写成功,那么crash再重启后就会发生,从库数据比主库要多。
    为了解决这个问题,InnoDb使用了二阶段提交(XA)事务
  • Prepare阶段,先写入redo log和undo log,但是最后一步的commit记录并未写入, 此时binlog未被写入
  • Commit阶段,写入binlog文件,只要此时binlog文件写入成功了,那么其实可以判断这个事务是执行成功了,然后再写入commit记录到redo log日志中,如果redo log没有写入commit就失败了,其实也算成功。
    加入binlog还没写入就宕机了,那么就认为事务失败,同时redo log没有commit记录,那么mysql重启后也不会重新执行redolog中的操作。 如果binlog写入成功了,redolog没有commit记录,mysql重启后会根据redo log里的事务id去binlog中查询,如果binlog有,那么就进行redo log的重新执行

参考资料

https://relph1119.github.io/mysql-learning-notes/#/mysql/25-%E5%B7%A5%E4%BD%9C%E9%9D%A2%E8%AF%95%E8%80%81%E5%A4%A7%E9%9A%BE-%E9%94%81

mysql技术内幕

你可能感兴趣的:(分布式,mysql,数据库架构,dba,mysql)