Mysql技术架构和原理
Mysql体系结构
mysql体系架构如上图所示,组件分别为:
- 连接池组件,即跟客户端的链接管理,包括鉴权,连接数限制等
- 管理服务和工具组件,包括备份,复制等
- SQL接口组件
- 解析器,解析查询语句
- 优化器, 对解析之后的sql语句进行查询优化,比如选择索引
- cache组件,会缓存部分数据,解析之前会去缓存中查找数据,有的话,则无需解析、优化和执行sql
- 插件式存储引擎,值得注意的是,存储引擎是基于表的
- 物理文件
存储引擎
mysql目前默认是InnoDb存储引擎,之前默认是MyIsAm存储引擎,还有很多其他的引擎
可以通过show engines; 命令查看当前mysql支持的存储引擎
InnoDB存储引擎的特点是,支持事务,使用聚簇索引,数据放在逻辑表空间中,MVCC和行锁等
MyIsAM存储引擎特点是支持全文索引,但是不支持事务和表锁设计,
InnoDb存储引擎
InnoDb存储引擎的优点上面已经讲到了,下面将会介绍它的体系架构和各个模块功能
InnotDb体系架构
整体的架构如上所示
- 后台线程,MasterThread和IO Thread、PureThread等
- 内存池
内存池
缓冲池
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列表中的概率
- LRU List, 已读的页,会从Free list挪到Lru list
- Free List, 数据库启动时,会先加载到Free List
- Flush LIst,需要刷新到磁盘的脏页
从上图可以看出,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的存储结构如下,所有数据都被逻辑地存放在表空间中
- 段 ,组成了表空间
- 区,任何情况下,一个区的大小都为1MB,默认情况下一个区有64个连续的页,一页默认16KB,为了保证区中页的连续性,InnoDB存储引擎一次从磁盘中申请4到5个区
- 页是InnoDB磁盘管理的最小单位,可以通过innodb_page_size设置页的大小。常见页的类型为:数据页,undo页,系统页,事务数据页,插入缓冲位图页,插入缓冲空闲列表页,二进制大对象页
- InnoDb是面向行存放的,下面会详细介绍行结构
行记录格式
Compact行记录格式
mysql 5.0引入的,其结构如下
第一个部分表示变成部分的长度列表(按照列的逆序顺序),比如Varchar类型,NULL标志位表示这一行是否有NULL值的字段,记录头信息中有多个信息,
之后就是每一列的数据,其中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数据页结构如下图所示
下面分别介绍一下每个部分的功能
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+树索引的每个节点是一页
插入时的逻辑
不过有的时候,虽然按照上面的逻辑,需要将一个页进行拆分,保证平衡,但是由于这样会操作磁盘较多,可能会将记录移到另一个页中的空闲位置,类似于旋转操作
删除时的逻辑
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都是行级锁
- 意向锁, InnoDB为了支持不同粒度的锁操作,引入了意向锁(Intention Lock),将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁
有了意向锁之后,如果需要对记录加X锁,那么分别需要对 数据库、表、页加上意向锁IX,最后对记录上X锁
意向锁也分为IS和IX,即意向共享锁和意向排他锁
InnoDB的意向锁是表级别的锁,设计目的是为了在一个事务中揭示下一行将被请求的锁类型
右下图可知,意向锁之间是兼容的,IX对意向锁之外的所有锁都不兼容,X锁对所有锁都不兼容
一致性非锁定读
即通常所说的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)
有些场景下,如果一个事务中后面的操作不成功,但是想让前面的操作提交;比如抢票,从上海到杭州,再转一下,从杭州到武汉,那么期望至少能抢到上海到杭州的票。
具体如下图所示
- 链事务(Chained transactions)
链事务和保存点有相似,但是只能恢复到最近的一个保存点,不能恢复到很久以前的保存点
- 嵌套事务(Nested transactions)
顶层事务提交后,子事务才会真的提交
嵌套事务还可以继承锁、传递锁
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
purge
delete 和update 操作并不直接删除已有的数据。
因为并发情况下,一行记录在被delete的同时,其他事务可能还在引用它,因此会只是加上一个delete flag
update 操作会插入一条新的记录,同时原有记录加上一个delete flag
Innodb存储引擎会有机质进行purge操作,即真正地删除这些需要删除的记录
事务隔离级别
MySQL隔离级别是repeatable read
一般情况下,为了避免幻读,会认为使用SERIALAZIBLE。 但实际上,InndoDB存储引擎,由于使用了Next-Key算法,已经避免了幻读的产生,因此其已经能够避免了幻读的产生,也就是已经达到了Serializable隔离级别
参考资料
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技术内幕