mysql体系架构如上图所示,组件分别为:
mysql目前默认是InnoDb存储引擎,之前默认是MyIsAm存储引擎,还有很多其他的引擎
可以通过show engines; 命令查看当前mysql支持的存储引擎
InnoDB存储引擎的特点是,支持事务,使用聚簇索引,数据放在逻辑表空间中,MVCC和行锁等
MyIsAM存储引擎特点是支持全文索引,但是不支持事务和表锁设计,
InnoDb存储引擎的优点上面已经讲到了,下面将会介绍它的体系架构和各个模块功能
整体的架构如上所示
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列表中的概率
从上图可以看出,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 信息也是每次先放到重做日志缓冲中中,然后以一定频率将其刷新到重做日志中,刷新的时机为:
存储一些元数据信息,比如锁、LRU
checkpoint主要分为两大类
数据库关闭之前,将所有的脏页进行刷新到磁盘
InnoDb的大多数操作都是在Master Thread中执行的,包括合并缓冲,刷新日志缓冲等。并且每隔操作的频率不同。
最新版本中,除了Master Thread,还有Page Clearner Thread,用来刷新脏页
插入数据时,由于有非聚簇索引的B+树需要维护,由于其索引可能是离散的,不是像自增id一样主键增大,放在最后就行,那么会影响性能,因此,如果索引页在缓冲区中,会先将索引插入到缓冲区,再异步和磁盘中进行合并操作
当然,索引不能是唯一的,不然不好判断新插入的数据是否满足唯一性
其实就是在从缓冲池刷新到磁盘的过程中,先在物理磁盘上共享表空间中备份一份,再刷新到磁盘。
如果刷新到磁盘过程中只刷新了一般,只写了页的一半就宕机了,恢复的时候,如果按照之前的没有double write, redo日志时,根本不知道页只刷新了一半,所以有了共享表空间后,就可以先拿到备份,也就是写入之前的,再结合redo日志,就能重做了
对于一些 = 的查询,innodb会通过一些规则和机制自动创建一些哈希索引,加快查询速度
参数文件即启动时候的配置文件,可以通过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.0引入的,其结构如下
第一个部分表示变成部分的长度列表(按照列的逆序顺序),比如Varchar类型,NULL标志位表示这一行是否有NULL值的字段,记录头信息中有多个信息,
之后就是每一列的数据,其中NULL值的列不会赋值
Mysql 5.0之前的格式,目前高版本也支持,是为了兼容数据
与Compact不同,第一个部分表示每个字段的偏移列表(按照列的逆序顺序),对于char类型列的NULL值也占用空间,varchar类型不占用空间
记录头信息中,包含n_fields,10个字节表示列的数量,即最大不超过1023
InnoDB 1.0.x之后,采用新的行记录格式,相对于之前的Redundant行记录格式,主要变化是采用了完全的行溢出的方式,下面讲一下何为行溢出数据
InndoDb一页只有64KB数据,即16384个字节,为了充分利用B+树的作用,每页数据至少会存放两个行记录,因此如果一个页中如果只能存放一条记录,那么会自动将行数据溢出,将数据放到另一个专门的溢出页中,将行记录中指针指向它
比如VARCHAR类型最大为65532个字节,如果一个表中一条记录,这个字段刚好这么大,显然,已经超过了这个页的最大值,那么VARCHAR这一列的数据,会放到uncompressed Blob page
对于Rebundant和Compact,数据页中VARCHAR列存储了786字节的数据,之后是指向行溢出页的指针
而对于Compressed 和Dynamic而言,没有这786个字节的数据,完全使用20个字节的指针指向行溢出页
InnoDB数据页结构如下图所示
下面分别介绍一下每个部分的功能
file header用于记录页的一些头信息,关键信息为:
page header记录数据页的状态信息,比如该页中记录的数量、索引id、当前页在索引中的位置等
该页的上下边界,虚拟的行记录
User Records就是实际的行记录数据
Free Space是空闲空间,一条记录被删除后,该空间也会加入到空闲空间,空闲空间也是链表结构
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等进行后续的查找准确的行记录
校验数据的完整性
InnoDB除了支持传统意义上的B+树索引,还支持全文索引和哈希索(InnoDB存储引擎会根据表的使用情况自动为表哈希索引,不能人为干预)
B+树是一种平衡查找树,之前将页数据结构时提到过,B+树索引的每个节点是一页
不过有的时候,虽然按照上面的逻辑,需要将一个页进行拆分,保证平衡,但是由于这样会操作磁盘较多,可能会将记录移到另一个页中的空闲位置,类似于旋转操作
聚集索引的叶子节点是数据页,而非聚集索引的叶子节点存放的不是数据页,而是存放了索引的键值信息和主键的值
当索引页由于插入记录,需要进行分裂,不一定会总是从中间记录开始分裂,比如那种自增插入的,如果从中间分裂了,那么左边这一页会一直没有数据插入,真实场景下,会选择合理的分裂点和分裂方向
使用show index from order_tab
命令可以查看所有索引信息,其中,Cardinality字段表示值不同的行数,这个数越大,对索引越有利。 数据库不会总是立即去更新这个值,不然开销会很大,条件为:
1、表中1/16的数据已经更新
2、计数器stat_modified_counter(表发生变化的次数) > 2000 000 000
另外,Innodb通过采用的方式,选择若干个叶子节点,统计每个页的不同记录的个数,再预估整个表的个数
遵循最左原则,其实上文学习过索引和索引页的结构后,就能理解
除了常见的不需要查询联合索引中没有的值可以使用覆盖索引外,向count(*)这种也可以走覆盖索引,以为不需要查行记录的数据
项目线上场景中,经常会遇到设置了索引,但是走了全表扫描的原因,有一个就是顺序读和随机读,有时候,虽然表面走非聚集索引,但是从非聚簇索引查询到主键值后,还要根据主键值从聚簇索引查数据,如果第一步的主键值不是顺序的,那么显然,读取磁盘时,会分别从磁盘的不同位置进行读取;因此有些实际场景下,不走非聚簇索引,直接扫描磁盘,由于数据是连续的,那么可以直接顺序读取磁盘数据,速度可能更快,尤其是要查询的数据占整个表的数据很大(一般情况下>20%)
哈希索引上文已经说过
全文索引其实就是倒排索引,跟es中的倒排索引基本原理是一致的
S和X都是行级锁
有了意向锁之后,如果需要对记录加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来控制自增长的模式
判断sql会加什么锁,一般需要判断查询的列是否是唯一的,比如查询的列是唯一索引,比如主键,where ${primary key} = ** 这种情况,但是如果是辅助索引,那么会加上间隙所或者next-key锁
就是多个事务中的,锁,锁住了对方需要的某个资源,都在等待。
在mysql中,如果发现死锁,会直接让其他事务抛出死锁异常放弃资源,让某一个事务正常执行
innod采用了wait-for graph的深度优先算法实现,判断是否死锁
在很多数据库中,对于锁的细节可能会做优化,比如锁住很多行时,可能会发生一些性能、内存问题,会将行锁变为页锁甚至表锁。
而InnoDB存储引擎,不存在锁升级的问题,因为它是根据页进行加锁的,采用的是位图的方式表示锁的是哪些行
由于锁很复杂,会单独写一篇博客,通过实践的方式,来展现什么场景,加什么锁,在什么地方加锁
就是最普通的事务
有些场景下,如果一个事务中后面的操作不成功,但是想让前面的操作提交;比如抢票,从上海到杭州,再转一下,从杭州到武汉,那么期望至少能抢到上海到杭州的票。
具体如下图所示
链事务和保存点有相似,但是只能恢复到最近的一个保存点,不能恢复到很久以前的保存点
顶层事务提交后,子事务才会真的提交
嵌套事务还可以继承锁、传递锁
InnoDB不支持嵌套事务
分布式事务(Distributed transactions)
mysql XA事务
实现事务,可以理解为实现事务的四大特性
对于隔离性,显而易见,想让事务之间不受影响,那么就是使用到了锁
对于原子性、一致性和持久性,通过数据库的redo log和undo log来实现,需要注意的是,redo 和undo并不是字面意思上的逆向过程,两者记录的内容都不一样
redo log用来实现事务的持久性,分为重做日志缓冲和重做日志文件
事务提交时,会将该事务的所有日志写入到日志缓冲中,才算commit成功,而日志缓冲fsync到日志文件中,则由一定的频率异步进行执行
重做日志中还记录了LSN,即单调递增的序列号,数据页中也记录了LSN,因此数据库启动时会根据LSN的差距,来进行恢复操作
重做日志记录的是对页进行的操作,即某一页,某个偏移量,数据的变更
重做日志是为了对页进行恢复,但是事务有时候还需要进行回滚操作,这时候就需要用到undo log
undo log位于数据库内部的undo segment中,即位于共享表空间中
undo log记录的是每一行的修改,并且当回滚时,并不是对数据进行物理地恢复到事务开始的样子,而是做逆向操作,比如update一条记录,那么回滚时,就update到原来的字段值。 这是因为并发情况下,可能别的事务已经修改了数据, 如果直接恢复到原来的值,可能会把别的事务已经修改的数据覆盖。
每次写新纪录
undo log的另一个作用就是mvcc
https://blog.csdn.net/SnailMann/article/details/94724197
mvcc是由undo log和read view来实现的。mvcc的作用是在不加锁的情况下实现多版本并发控制。
每个事务开启时都会生成自增的事务id,并且当有数据操作时,生成一个undo log日志,生成的undo log日志放在链表的首部,并且指向原来的首部。
每个事务还会维护活跃的事务id, 如果事务1和事务2操作同一条记录,事务2提交后,事务1中的活跃事务没有2,那么会认为事务2的事务id是默认的undo log的最新记录。隔离级别RC就是这样可以读取最新的提交。 而隔离级别RR会对于该条记录读取时生成一个快照,事务后续又再读的时候会直接从这个快照读取这条记录
delete 和update 操作并不直接删除已有的数据。
因为并发情况下,一行记录在被delete的同时,其他事务可能还在引用它,因此会只是加上一个delete flag
update 操作会插入一条新的记录,同时原有记录加上一个delete flag
Innodb存储引擎会有机质进行purge操作,即真正地删除这些需要删除的记录
MySQL隔离级别是repeatable read
一般情况下,为了避免幻读,会认为使用SERIALAZIBLE。 但实际上,InndoDB存储引擎,由于使用了Next-Key算法,已经避免了幻读的产生,因此其已经能够避免了幻读的产生,也就是已经达到了Serializable隔离级别
redo log和binlog写入时,如果有一个写入有问题,那么就会发生问题。问题如下:
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技术内幕