MySql技术内幕 - 事务

  数据库的事务会把数据库从一种状态转换到另一种状态,在数据库提交工作时,可以确保要么所有的修改都已经保存了,要么所有的修改都不保存。

  InnoDB存储引擎中的事务完全符合ACID的要求:

  1. 原子性 automicity
  2. 一致性 consistency
  3. 隔离性 isolation
  4. 持久性 durability

一、事务的实现

  事务隔离性通过锁机制来完成,原子性、一致性和持久性通过数据库的redo log 和 undo log来完成, redo log称为重做日志,用来保证事务的原子性和持久性,undo log用来保证

事务的一致性。虽然redo 和 undo 都是一种恢复操作,但是两种日志却不尽相同。redo log记录的是物理日志,记录的是页的物理修改操作,undo log是逻辑日志,是根据没行记录进行记录。

 

redo log : 

  重做日志用来实现事务的持久性,即事务ACID中的D。其由两部分组成,一部分是内存中的重做日志缓冲,一部分是重做日志文件。

InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交时,必须先将事务的所有日志写入到重做日志文件进行持久化,待事务的Commit操作完成才算完成。

  这里的日志是指重做日志,在InnoDB 存储引擎中,由两部分组成,即redo log 和 undo log 。 redo log是用来保证事务的持久性,undo log是用来帮助事务回滚及MVCC的功能,redo log基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取,而undo log是需要进行随机读取的。

  为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB存储引擎都需要调用一个fsync操作,由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓冲,为了确保重做日志写入磁盘,必须进行一次fsync操作,由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能

  InnoDB存储引擎允许用户手动设置非持久性,以此提高数据库的性能。即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行fsync操作。由于并非强制执行fsync操作,所以如果在期间如果出现宕机的情况发生,就很可能丢失一部分数据。

  参数inno_flush_log_at_trx_commit用来控制重做日志刷新到磁盘的策略。该参数默认值是1,表示事务提交时必须调用一次fsync操作,此外该参数还可设置为0和2

  1. 0 : 表示事务提交时不进行写入重做日志的操作,这个操作仅在master thread中完成,而master thread每一秒钟会进行一次fsync的操作
  2. 2 : 表示事务提交时将重做日志写入到重做日志文件,但是仅写入到操作系统的缓存中,不进行fsync操作,此情况如果期间发生数据库宕机而非操作系统宕机,那么数据是不会丢失的

binlog :

  在MySql中还有一种二进制日志:binlog ,其用来进行point-in-time的恢复及主从复制环境而建立,从表面上看binlog与redo log非常相似,都是记录了对于数据库的操作日志

但是不同的是binlog是MySql数据库层产生的,不管是用什么存储引擎都会产生。而redo log是InnoDB存储引擎产生的。

  binlog是一种逻辑日志,之前我们聊过,其可以选择三种类型。而redo log是一种物理日志,记录的是对于每个页的修改内容

  两种日志写入的时间点也不同,二进制日志只在事务提交完成之后进行一次写入,而redo log是在事务进行中不断地写入,这表现为日志并不是随事务提交的顺序而写入的

log block :

  在InnoDB存储引擎中,重做日志都是以512字节进行存储的,这意味着重做日志缓存,重做日志文件都是以块的方式进行保存的。称之为重做日志块,每块的大小为512字节。

若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储,此外由于重做日志块的大小和磁盘的扇页大小是一样的,因此其写入是可以保证原子性的

不需要doublewrite技术。

LSN :

  Long Sequence Number的缩写,其代表的是日志的序列号,在InnoDB存储引擎中,LSN占用8个字节,并且单调递增:

  1. 重做日志写入的总量
  2. checkpoint的位置
  3. 页的版本

LSN表示事务写入重做日志的字节总量,例如当前重做日志的LSN为1000,有一个事务写入了100字节的重做日志,那么LSN就变成了1100,其单位是字节。

LSN不仅记录在重做日志中,还存在于每个页中,在每个页的头部,有一个值FIL_PAGE_LSN,记录了该页的LSN。在页中,LSN表示该页最后刷新时LSN的大小,因为重做日志

记录的是每个页的日志,因此页中的LSN用来判断页是否需要进行恢复操作。

例如页P1的LSN为10000,而数据库启动时,InnoDB检测到写入重做日志中的LSN为13000,并且该事务已经提交,那么数据库就需要进行恢复操作,并将重做日志写到P1页。

undo log : 

  undo log主要是用来回滚数据的,undo是存放在数据库内部的一个特殊段中,这个段称为undo段,位于共享表空间中。因为undo log是逻辑日志,其实恢复的是一个逻辑结构

并不是将数据页恢复到之前的状态,只能保证收到影响的记录逻辑的恢复。

  除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo log来实现的,当用户读取一行记录的时候,如果该记录已经被锁住了,这时可以

通过undo log来读取之前的行的版本信息,以此实现非锁定读。

  还有一点是需要注意的是,undo的过程会产生redo log,这是因为undo log也需要持久性的保护。

  需要特别注意的是,事务在undo log segment分配页并写入undo log的这个过程中同样需要写入重做日志,当事务提交时,InnoDB存储引擎会做两件事:

  1. 将undo log放入列表中,供之后的purge操作
  2. 判断undo log所在页是否可以重用,若可以则分配给下个事务使用

  事务提交后不能马上删除undo log以及undo log所在的页,这是因为可能还有其他的事务需要通过undo log来得到行记录之前的版本信息,所以会将其放到一个链表中,是否可以删除由purge线程来决定。

  此外,InnoDB不会为每个事务分配一个单独的undo页,这样会比较浪费存储空间,因为在事务提交时,可能并不会马上释放页。所以当事务提交时,首先将undo log放入链表中

然后判断undo页的使用空间是否小于3/4,若是则表示该undo页可以被重用,之后新的undo log记录在当前undo log页的后面,由于存放undo log的列表是以记录进行组织的,而undo页

可能存放着不同的事务的undo log,因此purge操作需要涉及磁盘的离散读取,是一个比较缓慢的过程。

  在InnoDB存储引擎中,undo log分为两种:

  1. insert undo log
  2. update undo log

  insert undo log是指在insert操作中产生的undo log,因为insert操作的记录,只对事务本身可见,对其他事务不可见,故该undo log可以在事务提交后直接删除,不需要进行purge操作。

update undo log记录的是对delete 和 update操作产生的undo log,该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除,提交时放入undo log链表,等待purge线程进行最后的删除操作。

 

purge :

  purge操作是用来清理undo log的,对于delete操作,其实不是立即删除对应的记录,而是在聚集索引中的delete_flag设置为1,而真正的删除操作是在purge中进行的。

purge用于完成最终的delete和update操作,这样设计的目的是因为InnoDB存储引擎支持MVCC,所以记录不能在事务提交时立即删除,这时其他事务可能正在引用这行

故InnoDB存储引擎需要保存记录之前的版本,在purge线程中判断如果数据不再被其他事务所引用,才会真正的删除。

  我们知道在一个undo页上可以存储多个事务的undo log,但是其并不代表在全局的提交顺序,所以InnoDB存储引擎设计了一个history list的列表,用来记录事务的提交顺序,通过这个列表关联到undo页中的undo log。所以在purge进行清理的时候,会从链表的最后找到一个事务,然后判断undo  log是否还有被其他事务引用,如果没有继续判断当前undo页中其他log对应的事务的log是否有被引用,通过这种方式可以快速的查看一个undo页是否可以被重用,减少了很多随机读取,提高了purge的性能。

 

group commit : 

  每次提交事务的时候都会进行fsync操作,保证重做日志能够被写入到磁盘,但是如果每次只刷新一条重做日志,效率就会比较低,即使现在固态硬盘刷新的速度比较快了。

所以就出现了group commit机制,就是在每次执行fsync的时候,会刷新多条重做日志。这样就提高了事务提交的性能。

  但是如果我们开启了二进制日志,或者是主从架构,group commit机制就不能用了。这是因为在开启了二进制日志之后,为了保证存储引擎层中的事务和二进制日志的一致性

二者之间采用了二阶段事务:

  1. 当事务提交时InnoDB存储引擎进行prepare操作
  2. MySql数据库上层写入二进制日志
  3. InnoDB存储引擎将日志写入到重做日志文件中

保证二进制日志的顺序于存储引擎层的重做日志一致是为了在进行数据恢复的时候保证数据的一致性。

那么所以说在开启了二进制日志的情况下,我们就不能够使用group commit机制了,在遇到事务非只读事务比较多的情况下,数据库整体的性能就会下降。

这个问题在2010年的MySql数据库大会上已经提出了,最后由MariaDB的开发人员作出了最终的完美解决方案。从MySql 5.6版本开始采用了其解决方案,

并将其称为 Bianry Log Group Commit (BLGC)

在MySql数据库上层进行提交时首先按照顺序将其放入一个队列中,队列的第一个事务称为leader ,其他事务称为follower,leader控制着follower的行为,主要分为以下三步:

  1. Flush阶段,将每个事务的二进制日志写入到内存中
  2. Sync阶段,将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入
  3. Commit阶段,leader根据顺序调用存储引擎层的事务,InnoDB存储引擎本就支持group commit,所以会通过group commit写入重做日志

二、分布式事务

  InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现。

分布式事务是指允许多个独立的事务资源参与到一个全局的事务中,事务资源通常是关系型数据库系统,但是也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,要特别注意的是,在InnoDB存储引擎中,如果要想使用分布式事务,必须使用Serializable的隔离级别。

  XA事务由一个或多个资源管理器(Resource Manager)、一个事务管理器(Transaction Manager)和一个应用程序(Application)组成

  1. 资源管理器:提供访问事务资源的方法,通常一个数据库就是一个资源管理器
  2. 事务管理器:协调参与全局事务中的各个事务,需要和痊愈全局事务的所有资源管理器进行通信
  3. 应用程序:定义事务的边界,指定全局事务中的操作

分布式事务采用2PC的方式来实现,在分布式场景下可以与JTA配合使用。

 

其实MySql不仅仅支持与外部资源的分布式事务,同时在MySql的内部也存在分布式事务,在存储引擎与插件之间,或者在存储引擎与存储引擎之间,称之为内部XA。

最常见的内部XA事务存在于binlog与InnoDB存储引擎之间,由于复制的需要,因此目前大多数的数据库都开启了binlog功能。

在事务提交时,先写二进制日志,再写InnoDB存储引擎的重做日志,要求这两个日志的写入操作必须是原子的,否则会出现数据不一致的问题

在MySql中,InnoDB在提交事务时,先是做一个prepare的操作,将事务的xid写入,然后在执行二进制日志的写入,如果在其中如果出现数据库宕机,在重新启动的时候会去检查

事务是否提交成功,如果提交失败会重新提交一次。

 

posted @ 2020-03-26 22:15  SyrupzZ  阅读(180)  评论(0)    收藏  举报