MySQL事务

MySQL事务

何为事务?事务即一组操作,这组操作要么都执行,要么都不执行。事务是数据库区别文件系统的重要特性之一。数据库中引入事务的主要目的在于:事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保所有修改都已经保存,或者所有修改都不被保存

1 事务概述

1.1 ACID特性

关系型数据库(例如:MySQLSQL ServerOracle 等)事务都有 ACID 特性:

  1. 原子性Atomicity) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  3. 隔离性Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

1.2 事务分类

  • 扁平事务:所有操作处于同一层次,其所有操作都是原子的,要么都执行,要么都回滚。它是最简单,但是实际生产环境中使用最频繁的事务。
  • 带有保存点的扁平事务:在扁平事务中指定一些保存点,允许每次回滚到某个保存点。
  • 链式事务:可以看作保存点事务的一种变体,相当于把原来带若干个保存点的扁平事务,从保存点拆分成若干个小事务,然后把这些小事务链到一起。其本质是:将提交事务和下一个事务的开始合并成一个原子操作,这样系统崩溃时,可以回滚到最近执行完的小事务时的状态。
    • 目的:解决带保存点扁平事务,保存点不能持久化的问题;
    • 和保存点扁平事务的区别:链式事务只能回滚到最近一次保存点(相当于小事务),而带保存点的扁平事务可以回滚到任意保存点。
  • 嵌套事务:在一个根事务中调用子事务,同样子事务中可以调用子子事务,以此类推。需要注意的是,InnoDB不支持该类型事务。
  • 分布式事务:事务中的操作需要访问不同网络节点。比如将“ATM机上从招行储蓄卡向工行储蓄卡转账”看做一个事务,那么该事务设计到ATM、招行数据库、工行数据库三个不同节点。

2 事务隔离级别

2.1 并发事务带来的问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题:

  1. 脏读使用未提交数据。当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

  2. 丢失修改:同时写。指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。

  3. 不可重复读: 读的过程中数据被修改。指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

  4. 幻读: 幻读与不可重复读类似, 指当事务不是独立执行时发生的一种现象。例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。

    注:注意幻读和不可重复读的区分,幻读指 前后多次读取,数据总量不一致 ,不可重复读指 前后多次读取,数据内容不一致

2.2 隔离级别

SQL标准定义了4中隔离级别:

  • READ-UNCOMMITTED : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • READ-COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • REPEATABLE-READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生
  • SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读

InnoDB默认支持的隔离级别是REPEATABLE-READ,但是与标准SQL不同的是,InNoDB存储引擎在REPEATABLE-READ事务隔离级别下,使用Next-Key Lock锁算法,因此避免幻读的产生。所以说,InNoDB在默认的REPEATABLE-READ下的事务隔离级别已经达到了SQL标准的 SERIALIZABLE隔离级别。

3 事务实现原理

以MySQL的InnoDB为例:

  • 使用 redo log(重做日志) 保证事务的持久性
  • 使用 undo log(回滚日志) 来保证事务的原子性
  • 通过 锁机制MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 REPEATABLE-READ )。

保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。

3.1 redo log

3.1.1 redo概述

重做日志用来实现事务的持久性,由两部分组成:一是内存中的重做日志缓存,它是易失的;二是重做日志文件,它是持久的。当 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。

MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool 中。后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log file 里。

如上图所示,每提交一个事务,buffer都应该执行一次fsync操作,将缓存中的数据写入file中。但是InnoDB允许用户手工设置非持久性的情况发生,以提高数据库的性能。即当事务提交时,日志不写入重做日志文件,而是等待一个时间周期后再执行fsync操作。下面介绍下redo的刷盘策略。

3.1.2 刷盘策略

InnoDB存储引擎为刷盘策略提供了参数 innodb_flush_log_at_trx_commit,它支持3种策略:

  • 0 :设置为 0 的时候,表示每次事务提交时不进行刷盘操作,这个操作仅在master thread中完成,而在master thread中每1秒进行一次重做日志文件的fsync操作。
  • 1 :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)。
  • 2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入文件系统的缓存中,不进行fsync。这种情况下数据库宕机不会丢失数据,操作系统宕机会丢失文件系统cache中的数据。

注: 当 redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动刷盘。

3.1.3 对比binlog

binlog和redo log看起来都是记录了对于数据库操作的日志,但是两者存在很多区别:

  1. binglog是MySQL数据库上层产生的,redo log是InnoDB存储引擎中产生的;
  2. binlog记录的时SQL语句(逻辑日志),redo log记录的是页更改信息(物理日志);
  3. binlog只在事务提交完成后进行一次写入,而redo log是随着事务的进行而不断写入。

3.1.4 log block

在InnoDB存储引擎中,重做日志(包括缓存和文件)都是以512字节大小的块方式进行存储的,称之为重做日志块。若一个页中产生的重做日志数量大于512字节,就需要分割成多个重做日志块进行存储。每个存储块包含3个部分:日志块头(12字节)、日志块尾(8字节)和日志数据(492字节)。

3.1.5 log group

硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的。 文件组只是1个逻辑上的概念,并没有1个实际物理文件来表示log group信息。

3.1.6 LSN

LSN(Log Sequence Number),代表日志序列号。在InnoDB中,LSN占用8个字节,且单调递增,它可以表示3中含义:

  • 重做日志写入的总量

    比如当前重做日志LSN的值为1000,有1个事务写入了100字节的日志,那么LSN的值变成1100。

  • checkpoint的位置

    表示已经刷新到磁盘页上的LSN。

  • 页的版本

    在每个页的头部,有一个值FIL_PAGE_LSN,记录了该页的LSN。在页中,LSN表示该页最后刷新时LSN的大小

通过SHOW ENGINE INNODB STATUS可以查看LSN的情况。

LSN的一个重要功能就是用于数据恢复。InnoDB存储引擎在启动时不管上次数据库连接是否正常关闭,都会尝试进行恢复操作。具体做法就是:对比checkpoint(即磁盘页上的LSN)和重做日志上的LSN,如果两者不一致,比如checkpoint为1000,而重做日志LSN为1300,则恢复1000~1300之间的数据。

3.2 undo log

undo log是回滚日志,用于事务执行失败或者用户发起ROLLBACK时的会滚操作。所有事务的修改操作都会先记录到这个回滚日志中,然后再进行操作。undo log具有以下特性:

  1. undo log是逻辑日志,只能将数据库逻辑地恢复到原来的样子,但是数据库的页结构可能发生了很大变化。在回滚时,InnoDB执行的其实是一个相反操作,比如要回滚一条INSERT语句时,会执行一条DELETE语句。
  2. undo log存放在数据库内部的一个特殊段,称之为undo segment,它位于共享表空间内。
  3. 在使用undo log进行回滚时,也会产生redo log。

undo log可以分为两种:insert undo和undate undo。insert undo log只对产生该条记录的事务可见,事务commit后即可删除,而undate undo 则不行。因为undate undo是对delete和update操作产生的日志,该日志可能需要提供MVCC机制,因此不能在事务提交后立即删除,而是由purge线程进行最后删除。

注:对于delete操作产生的undo log其实是将该条数据的del_flag字段置为1;对于update操作,是将该条数据的del_flag操作置为1,再插入一条新数据。

3.3 两阶段提交

两阶段提交是为了解决binglog和redo log之间一致性问题而提出的一种方案。

3.3.1 数据不一致问题

在执行更新语句过程,会记录redo logbinlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo logbinlog的写入时机不一样。

我们以update语句为例,假设id=2的记录,字段c值是0,把字段c值更新成1SQL语句为update T set c=1 where id=2

假设执行过程中写完redo log日志后,binlog日志写期间发生了异常,会出现什么情况呢?

由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。因此,之后用binlog日志恢复数据时,就会少这一次更新,恢复出来的这一行c值是0,而原库因为redo log日志恢复,这一行c值是1,最终数据不一致。

3.3.2 解决方案

为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。

原理很简单,将redo log的写入拆成了两个步骤preparecommit,这就是两阶段提交

使用两阶段提交后,写入binlog时发生异常也不会有影响,因为MySQL根据redo log日志恢复数据时,发现redo log还处于prepare阶段,并且没有对应binlog日志,就会回滚该事务。

注:如果redo log阶段更新出错,是否会回滚?不会,因为MySQL发现该事务存在于binlog中,即使redo log处于准备阶段,也不会进行回滚。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信