聊聊我对InnoDB存储引擎的理解

主要文件:

  ibdata:主系统表空间,记录了InnoDB的核心信息,包括事务系统信息、元数据信息、change buffer的btree、防止数据损坏的double write buffer 等等,以16K为最小单位组织。

  idb:用户独立表空间,每个表都有单独的idb文件,存储了所有的索引数据,以16K为最小单位组织

  ib_logfile:redo日志文件,记录重做日志,以512位一个块组织

  undo独立表空间:存储回滚日志,以16K为最小单位组织

从上面的描述可以看出除了redo log日志外其他的文件都是以16K为最小组织单位,每16K为一个页(page),为什么要这么做?我理解是为了方便对磁盘数据的缓存。这涉及了InnoDB缓存机制,如果每次对数据库的更新操作都要直接读写文件肯定性能非常低下,所以MySQL实例启动后会在内存里开辟一个缓冲池用来缓存磁盘数据。缓冲池也是以页为单位进行磁盘数据的加载和刷新,由于一个page里的磁盘地址是连续的,所以顺序写一整个page 16K数据的效率会高于写随机16K字节。

InnoDB对所有磁盘数据读写请求都先请求缓存,如果缓存命中则直接修改缓存中的数据,并把修改的页标示为脏页,然后直接返回,内存读写效率非常高,所以用户线程的延迟就非常小。如果缓存没有命中则先将磁盘里对应的磁盘页加载到缓存里,再修改缓存,这种情况第一次可能会有些慢,但是相比于读写磁盘,效率还是高的,因为这里用户线程没有写磁盘的动作,但是后续对该页的读写缓存都会命中。

页缓存可以提高读写效率,但是要解决页刷新的问题。缓存中的脏页需要有一个策略刷新到磁盘上才行,一是因为缓存大小是有限制的,不可能给磁盘上所有数据都缓存到缓冲池里。二是因为缓存池中的数据不刷回磁盘随着机器重启内存数据丢失,对数据库的修改也全都丢失(这里先不考虑redo log)。InnoDB有完善的刷盘策略以保证数据的持久化和用户线程延迟之间的平衡。

缓存的特性决定了InnoDB的页存储模式,这是InnoDB最基本的文件组织方式,在这种组织方式下,表数据是如何存储的?

一张表可以创建一个聚集索引和多个非聚集索引,表数据存储在聚集索引中,聚集索引和非聚集索引的逻辑存储方式都是B+树。聚集索引和非聚集索引B+树的每一个节点,包含非叶子节点和叶子节点都是一个page,16K,随着数据的不断更新,B+树会不断有节点的合并和分裂,节点的合并和分裂也对应了磁盘上页的更新,是比较耗时的操作。每个索引在磁盘上会存储两个段,段是基于页之上索引特有的文件组织方式,一个段下可能有多个页。

聚集索引的叶子节点上存储的表数据,每个叶子节点上存储多行记录,所以通过B+树命中的并不是一个确定的行,而是一个范围。由于叶子节点是是固定大小16K,所以在这里不可能存储下过大的行记录。如果存储过大的列,比如Text、Blob,InnoDB会在这里存储一个指针,而把实际数据存储在额外的地方。

非聚集索引的叶子节点上存储的是聚集索引的键值,由于非聚集索引可能不是唯一的,所以非聚集索引叶子节点上存储可能不止一个主键值(这是我的理解,没有在相关资料上看到这句话)。当查询使用到非聚集索引,会首先通过非聚集索引找到匹配的聚集索引主键值,然后再到聚集索引里查询具体的数据。

还有一个问题,逻辑上的表是如何定位到磁盘上存储的页?是通过InnoDB的字典表来实现的,通过字典表可以查询某个表底层存储数据的段的位置,从而从逻辑页查询转到物理页查询。

表的存储方式是InnoDB的基石,事务也是InnoDB非常重要的功能,没有事务InnoDB也没有什么优势可言。事务有四大特性ACID,不同的数据库可能实现程度不同,InnoDB同时满足这四个要求。通过Undo log实现A:原子性、C:一致性、I:隔离型,通过Redo log实现了D:持久性。同时通过WAL保证日志能持久化。

Undo log记录的是逻辑日志,比如一条Insert语句,Undo log里会记录一条Delete语句,通过顺序的执行Undo log可以实现事务的回滚。对于Update或者Delete操作,提交以后的事务并不会立马删除Undo log,根据事务隔离级别要求,可能其他的事务要读取这个事务执行之前数据的版本,这个之前的版本就是通过查询Undo log来实现的,这也叫做非一致性快照读。

Redo log,Redo log记录的是物理日志,记录的是某个页某个位置增删改操作,Redo log总是比写表数据先刷盘,如果事务提交以后,缓冲池中的表数据脏页没有刷会磁盘,这时数据库重启,在重启时会判断Redo log里的记录是否存在没有应用到表数据页的记录,如果有,则会给那部分redo log应用到具体数据页上,保证事务在提交以后尽管系统挂掉数据也能正常恢复。

MySQL里还有个二进制日志,一般会拿这个二进制日志和Redo log做比较,它们都是记录数据修改动作的,区别如下:1. 二进制日志记录的是逻辑日志而Redo log记录的是物理日志 2. 二进制日志是MySQL服务层记录的日志,不是InnoDB特有的,所有的引擎都会记录。3. 二进制日志是用于做数据同步使用。

下面再关注一下在使用InnoDB时非常关注的一个问题:锁,多个事务并行操作时可能会由于锁的问题导致一个SQL等待或者执行失败,多个事务是否出现锁竞争和指定的事务隔离有关系,InnoDB支持四种事务隔离级别,从弱到强依次为: 读未提交、读已提交、可重复读、序列化读。读未提交和序列化读在Web应用中不会用到,主要关注读已提交和可重复读,InnoDB默认为可重复读。

读已提交意思是只要事务提交了,其他事务就能够读取到提交的修改,读已提交存在一个问题就是幻读,比如一个事务读取了一行记录,其他事务对这行记录修改并提交,那当前这个事务再查询这条记录发现变了,这就是所谓的幻读。

可重复读意思是一个事务开始以后读取的数据都是不变的,即使其他的事务有修改也看不到。

InnoDB通过Undo log实现了读已提交和可重复读,对于可重复读隔离级别,当一个事务开始并查询数据时,只查询那些事务号小于等于当前事务的数据,每行数据会有一个事务号,每个数据的修改都会加上当前全局事务号作为修改版本。对于读已提交,每个事务在查询时只查询数据的最新版本。

上面只是对于读的场景,通过Undo log可以实现无锁定读,也称非一致读。

如果两个事务都要对同一数据写,InnoDB是如何上锁的?

这个也称为一致性读,不管delete、update还是insert都会先执行一次一致性读,一致性读我理解就是对读取的数据上锁,除了更新操作外,还有 select for update、select for share也会执行一致性读,select for update、delete、update、insert会上X锁(排他锁),select for share会上S锁(共享锁)。

锁定的范围对于SQL选择的哪种索引有关系,如果选择的是聚集索引,则会锁定聚集索引中命中的记录,如果选择的是非聚集索引则会锁定非聚集索引命中的项和与非聚集索引匹配的聚集索引项。对于具体范围查询比如根据id、range等,都是锁定命中的具体行,对于范围查询,比如 id > 1等,这样会对所有ID > 1的数据都加锁,这种锁叫间隙锁。间隙锁不光在查询时会出现,在外键、子增长字段的场景下也会出现。具体问题可以参考MySQL技术内幕-InnoDB存储引擎6.3.4、6.3.5。

 

参考资料:

http://mysql.taobao.org/monthly/2016/02/01/

http://mysql.taobao.org/monthly/2019/10/01/

http://blog.sae.sina.com.cn/archives/2127

MySQL技术内幕-InnoDB存储引擎

高性能MySQL

 

 

 

 

附innodb表空间结构:

 

 

posted @ 2020-03-05 15:19  Birding  阅读(394)  评论(0编辑  收藏  举报