当前位置: 代码迷 >> 综合 >> MySQL【ACID+隔离级别+ redo log + undo log】
  详细解决方案

MySQL【ACID+隔离级别+ redo log + undo log】

热度:62   发布时间:2023-12-23 17:56:52.0

本文整理自尚硅谷MySQL数据库教程天花板

写在前面:
简单的思维导图:
并发的事务会导致数据不一致问题(脏写脏读等) ->
引申出四种隔离级别(解决问题的方案) ->
隔离级别由事务日志+锁+MVCC共同实现(解决问题的工具)
|
锁和MVCC不在此篇文章介绍

文章目录

      • MySQL数据库事务
        • 事务的ACID特性
        • 事务的状态
      • 如何使用事务
      • 事务隔离级别
        • 数据并发问题
        • SQL中的四种隔离级别
      • MySQL查看设置事务隔离级别
      • 事务原理-事务日志
        • 事务日志介绍(redo log、undo log)
        • redo log
          • 为什么需要 redo log
          • redo log记录的整体流程
          • redo log的三种刷盘策略
        • undo log
          • undo log的作用
          • undo log的生命周期

MySQL数据库事务

事务的ACID特性

  1. 原子性(atomicity): 原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。 通过undo log来保证一个事务能够成功回滚。

  2. 一致性(consistency): 根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。合法性状态说的通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的。

    举例1:A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就发现了此时数据是不一致的,为什么呢?因为你定义了一个状态,余额这列必须>=0。
    举例2:A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求A+B的总余额必须不变。
    举例3:在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。

  3. 隔离型(isolation): 事务的隔离性是指一个事务的执行不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性通过锁与MVCC来保证。

  4. 持久性(durability): 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的 ,接下来的其他操作和数据库 故障不应该对其有任何影响。持久性是通过重做日志(redo log)来保证的。

事务的状态

我们现在知道事务是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把事务大致划分成几个状态:

  1. 活动的(active) 事务对应的数据库操作正在执行过程中时,我们就说该事务处在活动的状态。
  2. 部分提交的(partially committed) 当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在部分提交的状态。
  3. 失败的(failed) 当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统 错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的状态。
  4. 中止的(aborted) 如果事务执行了一部分而变为失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为回滚 。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事 务处在了中止的状态。
  5. 提交的(committed) 当一个处在 部分提交的 状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了提交的状态。

如何使用事务

使用事务有两种方式,分别为 显式事务 和 隐式事务 。

  • 使用显示事务:

    步骤1: START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务。

    mysql> BEGIN;
    #或者
    mysql> START TRANSACTION;
    

    START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个修饰符 :

    1. READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
    2. READ WRITE (默认) :标识当前事务是一个读写事务 ,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
    3. WITH CONSISTENT SNAPSHOT :启动一致性读。

    步骤2:一系列事务中的操作(主要是DML,不含DDL)

    步骤3:提交事务 或 中止事务(即回滚事务)

    # 提交事务。当提交事务后,对数据库的修改是永久性的。
    mysql> COMMIT;
    # 回滚事务。即撤销正在进行的所有没有提交的修改
    mysql> ROLLBACK;
    # 将事务回滚到某个保存点。
    mysql> ROLLBACK TO [SAVEPOINT]
    
  • 隐示事务:

    隐式事务 MySQL中有一个系统变量 autocommit

    SHOW VARIABLES LIKE 'autocommit'; --默认是开启自动提交
    

    会隐式提交事务的情况:

    img

    ps:在Hibernate和MyBatis等持久层框架中,处理DML语句的时候会自动设置autocommit=0;如果DML中不进行手动提交事务,那么最后事务就会进行回滚。

事务隔离级别

事务有隔离性,理论上在某个事务对某个数据进行访问 时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样并发性不高 ,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高 ,那就看二者如何权衡取舍了。

数据并发问题

针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在不保证串行执行 (也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:

  1. 脏写( Dirty Write ) 对于两个事务 Session A、Session B,如果事务Session A 修改了另一个未提交事务Session B修改过 的数据,那就意味着发生了脏写

  2. 脏读( Dirty Read ) 对于两个事务 Session A、Session B,Session A 读取了已经被Session B 更新但还没有被提交的字段。 之后若 Session B 回滚 ,Session A 读取的内容就是临时且无效的。

  3. 不可重复读( Non-Repeatable Read ) 对于两个事务Session A、Session B,Session A 读取了一个字段,然后 Session B 更新了该字段。 之后 Session A 再次读取同一个字段, 值就不同了。那就意味着发生了不可重复读。

    我们在Session B中提交了几个隐式事务 (注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了studentno列为1的记录的列name的值,每次事务提交之后,如果Session A中的事务都可以查看到最新的值,这种现象也被称之为不可重复读 。

  4. 幻读( Phantom ) 对于两个事务Session A、Session B, Session A 从一个表中读取 了一个字段, 然后 Session B 在该表中 插 入 了一些新的行。 之后,如果 Session A 再次读取同一个表,就会多出几行。那就意味着发生了幻读。

    Session A中的事务先根据条件 studentno > 0这个条件查询表student,得到了name列值为’张三’的记录; 之后Session B中提交了一个隐式事务 ,该事务向表student中插入了一条新记录;之后Session A中的事务再根据相同的条件 studentno > 0查询表student,得到的结果集中包含Session B中的事务新插入的那条记录,这种现象也被称之为幻读 。我们把新插入的那些记录称之为幻影记录 。

SQL中的四种隔离级别

上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题 按照严重性来排一下序:

# 脏写 > 脏读 > 不可重复读 > 幻读

我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。 要解决这些并发问题,SQL标准中设立了4个隔离级别 :

  • READ UNCOMMITTED :读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。
  • READ COMMITTED :读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。
  • REPEATABLE READ :可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。
  • SERIALIZABLE :串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避 免脏读、不可重复读和幻读。

MySQL查看设置事务隔离级别

# 查看隔离级别
select @@transaction_isolation# 设置隔离级别
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
#其中,隔离级别格式:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE
# 或者
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
#其中,隔离级别格式:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE

事务原理-事务日志

事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?

  • 事务的隔离性由 锁机制 实现。

  • 而事务的原子性、一致性和持久性由事务的 redo 日志和 undo 日志来保证。

事务日志介绍(redo log、undo log)

  • REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。 是存储引擎层(innodb)生成的日志,保存在磁盘上,记录的是"物理级别"上的页修改操作,比如页号、偏移量、写入了某某数据。主要为了保证数据的可靠性;
  • UNDO LOG 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。 有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然。是存储引擎层(innodb)生成的日志,记录的是逻辑操作日志,比如对某一行数据进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操作。主要用于事务的回滚(undo log记录的是每个修改操作的逆操作)和一致性非锁定读(undo log回滚行记录到某种特定的版本—MVCC,即多版本并发控制)。

redo log

为什么需要 redo log

为什么需要redo log,从两个方面出发

  1. 当DML语句要修改表数据时,在真正修改磁盘中的页数据时,需要将页数据读入到内存中的缓存池buffer pool中,然后修改缓冲池中的页数据,然后缓冲池中的数据会以一定频率刷入到磁盘上(checkPoint机制);并不是执行一条DML就直接刷盘,这样效率太低。内存中页数据修改后,使用redo log记录有哪些页数据需要被修改,后续刷盘就根据redo log中的指令执行。
  2. 在一个事务中,redo log在commit之前就已记录好,如果事务commit后,还未进行刷盘时宕机,或是刷盘时宕机,mysql启动后也可根据redo log恢复数据,保证了事务的持久性。

REDO日志的好处、特点

  1. 好处:redo日志降低了刷盘频率、redo日志占用的空间非常小
  2. 特点:redo日志是顺序写入磁盘的 事务执行过程中,redo log不断记录
redo log记录的整体流程

在这里插入图片描述

第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝

第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值

第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式

第4步:定期将内存中修改的数据刷新到磁盘中

redo log的三种刷盘策略

这里的刷盘指内存中的redo log buffer刷到磁盘上的redo log file上。

注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存 (page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同 步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

InnoDB给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit 提交事务 时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  • 设置为0:表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)
  • 设置为1 (默认):表示每次事务提交时都将进行同步,刷盘操作
  • 设置为2:表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自 己决定什么时候同步到磁盘文件。

默认时redo log的刷盘流程
在这里插入图片描述

小结: innodb_flush_log_at_trx_commit=1时

只要事务提交成功,redo log记录就一定在硬盘里,不会有任何数据丢失。

如果事务执行期间MySQL挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。可以保证ACID的D,数据绝对不会丢失,但是效率最差的。

建议使用默认值,虽然操作系统宕机的概率理论小于数据库宕机的概率,但既然使用了事务那么数据的安全相对来说更重要些。

undo log

redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中更新数据的前置操作其实是要先写入一个 undo log

undo log如何保证事务原子性:

事务需要保证原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如: 情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误 , 操作系统错误 ,甚至是突然断电导致的错误。 情况二:程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前事务的执行。 以上情况出现,我们需要把数据改回原先的样子,这个过程称之为回滚 ,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性要求。

每当我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、 UPDATE) ,都需要"留一手",把回滚时所需的东西记下来。比如:

  • 你插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。(对于每个INSERT, InnoDB存储引擎会完成一个DELETE)
  • 你删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。(对于每 个DELETE, InnoDB存储引擎会执行一个INSERT)
  • 你修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。(对于每个UPDATE, InnoDB存储引擎会执行一个相反的UPDATE)

MySQL把这些为了回滚而记录的这些内容称之为撤销日志或者回滚日志(即undo log) 。注意,由于查询操作并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。

此外,undo log会产生redo log ,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。

undo log的作用
  • 作用1:回滚数据

    用户对undo日志可能有误解:undo用于将数据库物理地恢复到执行语句或事务之前的样子。但事实并非如此。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。

    这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。

  • 作用2:MVCC

    undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。

undo log的生命周期

以下是undo+redo事务的简化过程

假设有2个数值,分别为A=1和B=2,然后将A修改为3,B修改为4

  1. start transaction;
  2. 记录A=1到undo log;
  3. update A = 3;
  4. 记录A=3到redo log;
  5. 记录 B=2到undo log;
  6. update B = 4;
  7. 记录B = 4到redo log;
  8. 将redo log刷新到磁盘
  9. commit

在1-8步骤的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。

如果在8-9之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时redo log已经持久化。

若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘。
在这里插入图片描述

  相关解决方案