Inno DB | InnoDB

Albert Wang / 2023-09-29 / 300 Words/has been Read   Times


InnoDB存储 #

InnoDB 存储引擎是 MySQL 中最常用的存储引擎之一,其存储结构是一个复杂的组合,充分利用了磁盘、内存和日志来提供高性能、事务支持和数据一致性。它的存储结构包括以下几个重要的组成部分:

表空间(Tablespaces): InnoDB 存储引擎的数据存储是基于表空间的,每个表都有自己的表空间,包括系统表空间和用户表空间。系统表空间包含了数据字典等系统信息,而用户表空间包含了用户创建的表和索引数据。表空间实际上由一个或多个数据文件组成,这些数据文件是磁盘上的物理文件,用于存储表的数据和索引。InnoDB 存储引擎的数据文件通常以 .ibd 扩展名结尾。

表空间由三个段的内容组成Leaf node segment,Non-Leaf node segment 和 Rollback segment

  • Leaf node segment:主要存的是表里的记录
  • Non-Leaf node segment:主要存主键索引和从索引
  • Rollback segment:存储事务回滚的数据

image-20230929233358990

我们主要介绍段的概念。正如上面列举的那样,段里面主要存储了数据,那它就需要支持我们去动态扩增,而扩增的单位就是 Extend,默认的大小是 1M。一个 Extend 里有 64 个 Page,所以 1 个 Page 的大小为 16K(一次从磁盘读进来的数据就是一页 16 K)。Page 里则是存储着真实的记录,但是由于记录大小本身不固定,所以 1 个 Page 里有多少条记录也是不确定的。

页是由下图所示的这几部分组成的,数据主要存储在 User Records 里, User Records 里存储的数据可能是按照主键由小到大排列,也可能是无序的插入,这个和具体的操作相关。如果发生了删除和重新插入这些操作,内部的存储就会更加无序。

我们可以通过 Page Directory 快速找到具体某一行的记录,Page Directory 有序排列了主键,可以通过二分法快速查找。

img

B+ 树索引结构 #

InnoDB 存储引擎使用 B+ 树索引结构来管理数据,包括聚集索引和辅助索引。聚集索引是表的主键索引,决定了数据在磁盘上的物理存储顺序,而辅助索引用于加速查询。

B+树中叶子节点是以 Page 为单位进行存储的。并且两个页之间的主键是有序的。既然是树结构存储,那就必然需要处理增删改查这些场景,尤其是对于插入数据时需要对页进行拆分的情况,如果插入操作导致数据页的分裂,InnoDB 还需要更新父节点的索引,以反映分裂操作的结果。 为了确保数据的持久性,InnoDB 会将插入操作写入重做日志(redo log)。这是为了在发生崩溃或故障时能够恢复插入操作。写入重做日志是一个必要的步骤,以保证事务的原子性和持久性。为了使分页的代价降到最低,一般来说主键都是自增的,不允许随意去定义。

img

执行删除操作时,会将数据标记为删除,并将删除操作记录到重做日志中。在一些情况下,删除操作可能导致数据页的合并。

我们刚刚谈的都是主键索引,对于辅助索引来说它仍然使用B+树,但是它的值是 key,也就是说我们需要先从辅助索引里找到 key,再通过主键索引去查具体的记录,外键一般都需要建立辅助索引。假设现在我们有一个需求,需要查找哪些网页中出现了我们需要的关键字,这种情况下我们就必须要建索引,不然就是一趟全量查找。但是如果建辅助索引,这个操作耗时仍然很恐怖,所以这种场景可以考虑使用 ES,ES 最著名的就是它的“倒排索引”,会把关键字作为键,主键作为值来建索引。

InnoDB 使用缓冲池来缓存表和索引的数据,以加速读取操作。缓冲池是内存中的一块区域,用于存储最近访问的数据页。这样,在查询时,如果数据页在缓冲池中,则可以快速获取,而不必从磁盘读取。

事务 #

事务是数据库里必不可少的一个概念,它也是并发控制的基本单位,基本在大大小小的考试面试中都会涉及到它的 ACID,这个没什么好说的。我们重点来了解一下事务隔离性中的隔离级别。所谓隔离性就是一个事务的执行不能被其他事务干扰。在介绍隔离级别之前我们先来了解一下脏读,不可重复读和幻读的概念。为了方便描述,我们假设现在有一个场景:大家购买从北京到上海的车票(假设都存在一张表里),发现就一趟车(表的记录数为1),车票余额的记录初始为100(这条记录里余额这个属性的值为100),假设买车票的时候需要做这几件事,第一件是读这条记录(READ 操作)把余额减1(WRITE 操作),然后需要等待用户付款,在付账成功之后再读一次这条记录确认修改成功(READ 操作),最后 commit 这个事务。整个过程需要两次读操作和一次写操作。

  • 脏读:读取了其他事务未提交的数据。对于上面提到的场景,可能有一个人开始订票的时候就开启了一个事务,先将记录减 1 变成 99(执行了 WRITE 操作),然后是调用第三方接口去付款,但是如果中途这个人不想继续订票了,那事务就会 Rollback,最终数据库的余额都是100。可是在事务开始到 Rollback 之间如果别的事务也查询(READ 操作)这个值就会查到 99,这个读到的数据就是“脏数据”。
  • 不可重复读:同样是上面的例子,假如我们解决了脏读的问题,现在有两个人开始订票,并且同时开启事务,他们读到的数据都是 100,在这个过程中 A 最终付款成功 commit 事务。但是B中间延迟了很久最终放弃了付款,可是在第二次读的过程中发现记录数还是变成了 99,B 在一个事务中不能重复读。B 并没有修改数据但是数据发生了变化会让 B 感觉不舒服,因为我们需要把每个事务隔离开,每个事务各自之间是不感知其他事务修改的。
  • 幻读:假如我们也解决了不可重复读的问题,这样我们是解决了读写一条记录的问题,但是我们没有解决在一个事务里会往表里面插入删除数据的场景。比如一开始大家发现就一趟车,记录数是 1,余额是 100,然后用户开启事务买票,在这个事务中间可能有查询了一次有几趟车,结果发现发现记录数变成了2,又出现了另一趟车,这就出现了幻读的问题。

上面我们都是假设这几种问题都被解决了,那么在实际中要怎么解决这三种问题呢?首先我们同样需要了解两种锁,共享锁和排他锁。共享锁是当一个事务在执行 READ 操作的时候会加的锁,加了之后这个事务和别的事务就都不能写但可以读。同样的,别的事务可以在共享锁的基础上再加共享锁,它们是兼容的。排他锁则是在执行 WRITE 操作的时候加的锁,加完之后这条记录就只能由该事务处理,别的事务就不能读写了,在加了排他锁之后就不允许其他事务再加共享锁了。这三个问题就是通过加这两种锁来实现的。

  • 脏读:解决脏读问题就是对记录加共享锁和排他锁,排他锁直到事务结束释放,共享锁读之前加,读完就释放;因为加了排他锁之后不允许其他事务加共享锁,所以其他事务会被阻塞住,直到拿了排他锁的事务 commit 或者 Rollback。
  • 不可重复读:对记录加共享锁和排他锁,和解决脏读问题的区别在于共享锁读在之前加,直到事务结束再释放;
  • 幻读:对整张表加共享锁。

四种隔离级别Read Uncommitted,Read Committed,Repeatable Read 和 Serializable 就是分别解决上面问题的一种或者几种,如下表所示:

image-20230930004010406

MySQL 一般用的隔离级别是 Repeatable Read, Oracle 默认是 Read Committed,并且 MySQL 采用了多版本并发控制( MVCC) 的实现机制来实现 Repeatable Read,按照上面加锁的实现,当一个事务加了排他锁之后别的事务就不能去读了,它会被阻塞住,但是 MVCC 会让这个事务读到它第一次读的那个值,同样保证前后读的值是一致的。

MVCC 是通过快照的方式来实现 Repeatable Read 的,当有事务写这条数据的时候会把新数据持久化到表里,把原来的数据写到 undo log。当一个事务需要读取数据时,它会查看 Undo Log 中的记录来获取数据的一个一致性快照,以确保读操作不受正在进行的写操作的影响。 MVCC 通过下面的这些 id 值来产生不同事务的快照:

  • creator_trx_id:生成 ReadView 的事务 id
  • trx_ids: 当前活跃的事务id,已经开始没有提交的事务 id
  • low_limit_id: 下一个将被分配的事务id(事务 id 是递增的)
  • up_limit_id: trx_ids 中最小的事务id

在查询的过程中先查看 creator_trx_id,如果和查询的事务 id 一致,说明是自己创建的,则数据可见;如果查询的事务 id (trx_ids)>= low_limit_id,说明 low_limit_id 实在查询事务之后开始的,则不可见;如果 trx_ids < up_limit_id,则可见;如果 up_limit_id <= trx_ids < low_limit_id,只要事务时活跃的,则不可见,否则可见。

image-20230930155518554

日志文件 #

在数据库中日志属于一等公民,需要保证数据的恢复机制,所以需要用到日志文件(Log Files) ,InnoDB 使用事务日志来记录已提交的事务,以确保数据的一致性和持久性。除了我们上面提到的 undo log 用于处理并发以外 MySQL 还有重做日志文件(redo log)和慢查询日志文件(slow query log)用来应对其他的业务场景。

  • 重做日志文件(Redo Log):InnoDB 存储引擎有两个重做日志文件,通常命名为 ib_logfile0ib_logfile1,它们用于记录事务的修改操作。它们的大小和数量由配置参数 innodb_log_file_size 和 innodb_log_files_in_group 决定。数据修改操作首先被写入 Redo Log,然后才被写入磁盘,这些日志文件允许数据库在崩溃后恢复到一致的状态。当数据库需要恢复到正常状态时,通过重演 Redo Log 中的操作,可以更快速地将数据库恢复到崩溃前的状态。

  • 慢查询日志文件(Slow Query Log):用于记录执行时间较长的 SQL 查询语句,以帮助优化查询性能。这个阈值由配置参数 long_query_time 控制,默认值为 10 秒。若要启用慢查询日志,需要编辑 MySQL 配置文件(通常是 my.cnf 或 my.ini)并设置以下参数

    slow_query_log = 1 # 启用慢查询日志,设置为 1 表示启用,0 表示禁用。
    slow_query_log_file = /path/to/slow-query.log
    long_query_time = 1  # 设置慢查询的时间阈值,以秒为单位
    

    定期分析慢查询日志是优化数据库性能的重要步骤。我们可以汇总和分析这些日志,找出性能瓶颈,并采取相应的措施来改进查询性能。

Last modified on 2023-09-29