MySQL十部曲之八:InnoDB事务模型及其操作语句

文章目录

  • 什么是事务
  • ACID特性
    • InnoDB原子性的实现
    • InnoDB一致性的实现
    • InnoDB隔离性的实现
      • 并发事务之间存在的问题
      • 隔离性的实现方式
        • 多版本并发控制 (Multi-Version Concurrency Control)
        • 一致性非锁定读
        • 非一致性锁定读
          • 行锁
          • 表锁
            • 意向锁
            • AUTO-INC锁
      • MVCC和锁在不同事务隔离级别中的应用
    • InnoDB持久性的实现
  • 事务调度
  • 事务操作语句
    • START TRANSACTION、 COMMIT和ROLLBACK
    • 不可回滚的语句
    • 导致隐式提交的语句
    • SAVEPOINT、ROLLBACK TO SAVEPOINT、和RELEASE SAVEPOINT

什么是事务

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。数据库事务通常包含了一个序列的对数据库的读/写操作。包含有以下两个目的:

  • 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  • 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

当事务被提交给了数据库管理系统(DBMS),则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要回滚,回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。

ACID特性

并非任意的对数据库的操作序列都是数据库事务。数据库事务拥有以下四个特性,习惯上被称之为ACID特性。

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

InnoDB原子性的实现

在InnoDB中,所有的用户活动都发生在事务中。如果启用了自动提交模式,则每个SQL语句单独形成一个事务。默认情况下,MySQL为每个新连接启动会话时都启用了自动提交模式,因此MySQL在每个SQL语句没有返回错误时都会在该语句之后进行提交。如果语句返回错误,则提交或回滚行为取决于错误。

启用了自动提交功能的会话可以执行多语句事务,方法是使用显式的START TRANSACTION语句开始,并以COMMITROLLBACK语句结束。

如果使用SET autocommit = 0在会话中禁用自动提交模式,则该会话始终有一个打开的事务。COMMITROLLBACK语句结束当前事务,并开始一个新事务。

如果一个会话在没有显式提交最后一个事务的情况下结束,MySQL回滚该事务。

有些语句隐式地结束事务,就好像在执行语句之前执行了COMMIT

COMMIT意味着在当前事务中所做的更改是永久的,并且对其他会话可见。另一方面,ROLLBACK语句取消当前事务所做的所有修改。COMMITROLLBACK都会释放当前事务中设置的所有InnoDB锁。

InnoDB一致性的实现

ACID模型的一致性方面主要涉及InnoDB内部处理,以防止数据崩溃。相关的MySQL特性包括:

  • InnoDB双写缓冲区。
  • InnoDB崩溃恢复。

InnoDB隔离性的实现

并发事务之间存在的问题

  • 脏读:所谓脏读,就是指事务A读到了事务B还没有提交的数据,比如同一个账户银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。
  • 不可重复读:所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。还是以银行取钱为例,事务A开启事务,查出银行卡余额为1000元,此时切换到事务B事务B开启事务,取走100元,提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。
  • 幻读:所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。比如学生信息,事务A开启事务,修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务,插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。

隔离性的实现方式

在InnoDB中,通过锁和MVCC来实现事务的隔离性,并且InnoDB致力于将MVCC与传统的锁相结合。一般情况下,我们不需要以手动的方式保证事务的隔离性,而是通过设置事务的隔离级别来保证。InnoDB提供了SQL:1992标准中描述的所有四种事务隔离级别:

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

InnoDB默认的隔离级别是REPEATABLE READ。用户可以使用SET TRANSACTION语句更改单个会话或所有后续连接的隔离级别。要为所有连接设置服务器的默认隔离级别,请在命令行或选项文件中使用--transaction-isolation选项。

多版本并发控制 (Multi-Version Concurrency Control)

InnoDB是一个多版本的存储引擎。它保留有关已更改行的旧版本的信息,以支持并发和回滚等事务性特性。这些信息存储在undo表空间中称为回滚段的数据结构中。在内部,InnoDB在数据库中存储的每一行添加三个字段:

  • 一个6字节的DB_TRX_ID字段:表示插入或更新该行的最后一个事务的事务标识符。
  • 一个7字节的DB_ROLL_PTR字段,称为回滚指针:回滚针指向该行写入回滚段的undo日志记录。
  • 一个6字节的DB_ROW_ID字段包含一个行ID:随着新行插入而单调增加。

在一个事务中:

  • 执行UPDATEDELETEINSERT操作时:
    • InnoDB会为要修改的数据行创建一个快照,并将修改后的数据写入这个快照。
    • 在快照的DB_TRX_ID字段存储当前事务的标识符,以便其他事务能够正确读取相应版本的数据。
    • 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响。
  • 执行SELECT操作时:根据某种原则选择相应的版本进行读取 (见下文一致性非锁定读)。
  • 提交后:它所做的修改将成为数据库的最新版本,并且对其他事务可见。
  • 回滚后:它所做的修改将被撤销,对其他事务不可见。

上述这一过程就称为MVCC。

一致性非锁定读

一致性读(也就是一个普通的SELECT语句)是InnoDB在READ COMMITTEDREPEATABLE READ隔离级别下处理SELECT语句的默认模式。一致性读不会在它访问的表上设置任何锁,因此其他会话可以在对表执行一致性读的同时自由地修改这些表。一致性读意味着InnoDB使用多版本来为查询提供数据库在某个时间点的快照。查询会看到在该时间点之前提交的事务所做的更改,而不会看到后面或未提交的事务所做的更改。该规则的例外情况是,查询在同一事务中看到前面的语句所做的更改。此异常会导致以下异常:

  • 如果更新表中的某些行,SELECT可以看到更新行的最新版本。
  • 如果其他会话同时更新同一张表,这种异常意味着你可能会看到这张表处于数据库中从未有过的状态(也就是幻读)。

假设您以默认的REPEATABLE READ隔离级别运行。则同一事务中的所有一致性读都读取由该事务中的第一个一致性读建立的快照。当你发出一个一致性读时,InnoDB会给你的事务一个查询看到数据库的时间点。如果另一个事务在您指定的时间点之后删除了一行并提交,则不会看到该行已被删除。插入和更新的处理方式类似。您可以通过提交事务,然后执行另一个SELECTSTART TRANSACTION WITH CONSISTENT SNAPSHOT来提前您的时间点。

在下面的示例中,会话A只有在B提交了插入并且会话A也提交了插入时才会看到B插入的行,因此时间点提前到B提交之后。

             Session A              Session B

           SET autocommit=0;      SET autocommit=0;
time
|          SELECT * FROM t;
|          empty set
|                                 INSERT INTO t VALUES (1, 2);
|
v          SELECT * FROM t;
           empty set
                                  COMMIT;

           SELECT * FROM t;
           empty set

           COMMIT;

           SELECT * FROM t;
           ---------------------
           |    1    |    2    |
           ---------------------
非一致性锁定读

如果在同一个事务中查询数据,然后插入或更新相关数据,则常规SELECT语句无法提供足够的保护。其他事务可以更新或删除您刚刚查询的相同行。InnoDB支持两种类型的锁读,提供额外的安全性:

  • SELECT ... FOR SHARE:在读取的任何行上设置共享锁。其他会话可以读取这些行,但在事务提交之前不能修改它们。如果这些行中的任何一行被另一个尚未提交的事务更改,则查询将等待,直到该事务结束,然后使用最新的值。
  • SELECT ... FOR UPDATE
    • 对于搜索遇到的索引记录,锁定行和任何关联的索引项,就像为这些行发出UPDATE语句一样。
    • 其他事务无法更新这些行,无法执行SELECT…FOR SHARE,或者从某些事务隔离级别读取数据。

所有由FOR SHAREFOR UPDATE查询设置的锁在事务提交或回滚时被释放。

外部语句中的锁定读子句不会锁定嵌套子查询中的表行,除非在子查询中也指定了锁定读子句。例如,下面的语句不会锁定表t2中的行:

SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2) FOR UPDATE;

要锁定表t2中的行,在子查询中添加一个锁读子句:

SELECT * FROM t1 WHERE c1 = (SELECT c1 FROM t2 FOR UPDATE) FOR UPDATE;

如果一行被事务锁住,SELECT…FOR UPDATESELECT…FOR SHARE事务请求同一行锁时,必须等待阻塞事务释放行锁。此行为可防止事务更新或删除由其他事务查询更新的行。但是,如果您希望查询在所请求的行被锁定时立即返回,或者从结果集中排除锁定的行是可以接受的,则没有必要等待行锁被释放。

为了避免等待其他事务释放行锁,可以使用NOWAITSKIP LOCKED选项“

  • NOWAIT:使用NOWAIT的锁读永远不会等待获取行锁。查询立即执行,如果所请求的行被锁定,则会失败并出现错误。
  • SKIP LOCKED:使用SKIP LOCKED的锁读从不等待获取行锁。查询立即执行,从结果集中删除锁定的行。

NOWAITSKIP LOCKED仅适用于行级锁。

下面的示例演示了NOWAITSKIP LOCKED。会话1启动一个事务,该事务对单个记录使用行锁。会话2尝试使用NOWAIT选项对同一条记录进行锁定读取。由于所请求的行被会话1锁定,锁定读操作立即返回一个错误。在会话3中,使用SKIP LOCKED的锁定读取返回请求的行,除了被会话1锁定的行。

# Session 1:

mysql> CREATE TABLE t (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;

mysql> INSERT INTO t (i) VALUES(1),(2),(3);

mysql> START TRANSACTION;

mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE;
+---+
| i |
+---+
| 2 |
+---+

# Session 2:

mysql> START TRANSACTION;

mysql> SELECT * FROM t WHERE i = 2 FOR UPDATE NOWAIT;
ERROR 3572 (HY000): Do not wait for lock.

# Session 3:

mysql> START TRANSACTION;

mysql> SELECT * FROM t FOR UPDATE SKIP LOCKED;
+---+
| i |
+---+
| 1 |
| 3 |
+---+

InnoDB支持多粒度锁,允许行锁和表锁共存。

  • 表锁针对整个表加锁,实现简单加锁快,不会出现死锁,但锁冲突的概率高。
  • 行锁针对索引记录加锁,实现复杂加锁慢、会出现死锁,但并发高。
行锁

InnoDB实现了标准的行级锁,其中有两种类型的锁,共享锁(S)和独占锁(X)。

  • 共享锁允许持有该锁的事务读取一行。
  • 独占锁允许持有该锁的事务更新或删除行。

如果事务T1持有行r上的共享锁,那么来自不同事务T2的请求对行r上的锁的处理如下:

  • T2S锁的请求可以立即被授予。因此,T1T2都对r持有S锁。
  • T2X锁的请求不能立即被授予。

如果事务T1持有行r上的独占锁,则来自某个不同事务T2的对行r上任意类型锁的请求不能立即被授予。相反,事务T2必须等待事务T1释放对行r的锁。

根据锁的粒度可以将行锁划分为以下几种:

  • 记录锁:
    • 记录锁是索引记录上加的锁。
    • 对于使用唯一索引搜索唯一行的语句会加记录锁(但不包括搜索条件只包括多列唯一索引的一些列的情况,在这种情况下,会加间隙锁)。
    • 例如:SELECT c1 FROM t WHERE c1 = 10 For UPDATE;将防止任何其他事务插入、更新或删除t.c1值为10的行。
  • 间隙锁:
    • 间隙锁是在索引记录之间的间隙上加的锁,或者在第一个索引记录之前或最后一个索引记录之后的间隙上加的锁。
    • 如果搜索列没有索引、有一个非唯一索引或搜索条件只包括多列唯一索引的一些列时会加间隙锁。
    • InnoDB中的间隙锁是纯抑制的,这意味着它们的唯一目的是防止其他事务插入到间隙中。间隙锁可以共存。一个事务使用的间隙锁不会阻止另一个事务在相同的间隙上使用间隙锁。共享间隙锁和独占间隙锁之间没有区别。它们彼此不冲突,并且它们执行相同的功能。允许冲突的间隙锁的原因是,如果从索引中清除一条记录,则必须合并由不同事务持有的记录上的间隙锁。
    • 例如:SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 For UPDATE;将防止其他事务将值15插入到列t.c1中,无论列中是否已经存在这样的值,因为范围中所有现有值之间的间隙被锁定。
  • 临键锁:临键锁是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合。
  • 插入意向锁:插入意向锁是一种间隙锁,该锁表示插入的意图,插入到相同索引间隙的多个事务如果不在间隙内的相同位置插入,则无需等待对方。
表锁
意向锁

意向锁是表级别的锁,它指示事务将对表中的某一行需要哪种类型的锁。意向锁不阻塞任何东西,意向锁的主要目的是显示某人正在锁定表中的一行,或者将要锁定表中的一行。有两种类型的意向锁:

  • 意向共享锁(IS)表明事务打算对表中的单个行设置共享锁。
  • 意向独占锁(IX)指示事务打算对表中的单个行设置独占锁。

例如,SELECT…FOR SHARE设置IS锁,SELECT…FOR UPDATE设置一个IX锁。意向锁的协议如下:

  • 在事务获得表中某一行的共享锁之前,它必须首先获得表上的IS锁或更强的锁。
  • 在事务获得表中某一行的独占锁之前,它必须首先获得该表上的IX锁。

表级锁类型兼容性如下表所示:

X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

如果锁与现有锁兼容,则将锁授予请求事务,但如果它与现有锁冲突,则不会授予。事务等待,直到冲突的现有锁被释放。如果锁请求与现有锁冲突,并且由于会导致死锁而无法授予,则会发生错误。

AUTO-INC锁

AUTO-INC锁是一种特殊的表级锁,用于在具有AUTO_INCREMENT列的表中插入事务。在最简单的情况下,如果一个事务正在向表中插入值,那么任何其他事务都必须等待对该表进行自己的插入,以便第一个事务插入的行接收连续的主键值。

MVCC和锁在不同事务隔离级别中的应用

InnoDB使用不同的锁策略支持这里描述的每一种事务隔离级别。下文详细描述了MySQL如何支持不同的事务级别:

  • REPEATABLE READ
    • 一致性读:同一事务中的所有一致性读都读取由该事务中的第一个一致性读建立的快照。这意味着,如果在同一个事务中发出几个非锁定SELECT语句,这些SELECT语句彼此之间也是一致的。
    • 锁定SELECTUPDATEDELETE语句:锁的使用取决于语句使用的查询索引和查询条件:
      • 对于具有唯一查询条件的唯一索引,InnoDB只使用记录锁锁定找到的索引记录。
      • 对于其他情况,InnoDB将使用间隙锁或临键锁锁定扫描的索引范围,来阻止其他会话插入到范围所覆盖的间隙中。
  • READ COMMITTED
    • 一致性读:即使在同一个事务中,每个一致性读会设置和读取自己的新快照。
    • 锁定SELECTUPDATE语句和DELETE语句:InnoDB只使用记录锁锁定找到的索引记录,间隙锁仅用于外键约束检查和重复键检查。
  • READ UNCOMMITTED
    • 一致性读:不使用一致性读处理SELECT语句
    • SELECT语句:以非锁定方式执行,因此可能使用行的早期版本,所以使用这个隔离级别,这样的读取是不一致的,这也被称为脏读。
    • 锁定SELECTUPDATEDELETE语句:与READ COMMITTED类似。
  • SERIALIZABLE
    • 一致性读:不使用一致性读处理SELECT语句
    • SELECT语句:
      • 如果仅用了自动提交, 则InnoDB隐式地将所有SELECT语句转换为SELECT…FOR SHARE语句。
      • 如果启用了自动提交,则SELECT拥有自己的事务。
    • 锁定SELECTUPDATEDELETE语句:与REPEATABLE READ类似。

InnoDB持久性的实现

ACID模型的持久性方面涉及MySQL软件特性与特定硬件配置的交互。由于有许多可能性取决于您的CPU、网络和存储设备的能力,因此这方面是最复杂的,无法提供具体的指导方针。

事务调度

InnoDB使用争用感知事务调度(CATS)算法对等待锁的事务进行优先级排序。当多个事务等待同一对象上的锁时,CATS算法确定哪个事务首先收到锁。

CATS算法通过分配调度权重来确定等待事务的优先级,调度权重是根据事务阻塞的事务数量计算的。例如,如果两个事务正在等待同一对象上的锁,那么阻塞事务最多的事务将被分配更大的调度权重。如果权重相等,则优先考虑等待时间最长的事务。

事务操作语句

START TRANSACTION、 COMMIT和ROLLBACK

START TRANSACTION
    [transaction_characteristic [, transaction_characteristic] ...]

transaction_characteristic: {
    WITH CONSISTENT SNAPSHOT
  | READ WRITE
  | READ ONLY
}

COMMIT
ROLLBACK
SET autocommit = {0 | 1}

这些语句提供对事务使用的控制:

  • START TRANSACTION开始一个新的事务。
  • COMMIT提交当前事务,使其更改永久保存。
  • ROLLBACK回滚当前事务,取消其更改。
  • SET autocommit禁用或启用当前会话的默认自动提交模式。

默认情况下,MySQL运行时启用自动提交模式。这意味着,当不在事务内时,每个语句都是原子的,就像被START transactionCOMMIT包围一样。您不能使用ROLLBACK来撤消该效果;但是,如果在语句执行期间发生错误,则回滚语句。

要隐式地禁用自动提交模式,使用START TRANSACTION语句。使用START TRANSACTION,自动提交将保持禁用状态,直到使用COMMITROLLBACK结束事务。然后自动提交模式恢复到以前的状态。

START TRANSACTION允许几个控制事务特征的修饰符。要指定多个修饰符,用逗号分隔它们。

  • WITH CONSISTENT快照修饰符对能够创建快照的存储引擎启动一致性读操作。这只适用于InnoDB。其效果与发出START事务,然后从任何InnoDB表中进行SELECT是一样的。WITH CONSISTENT快照修饰符不更改当前事务隔离级别,因此仅当当前隔离级别允许一致性读取时,它才提供一致性快照。
  • READ WRITEREAD ONLY修饰符设置事务访问模式。它们允许或禁止对事务中使用的表进行更改。READ ONLY限制防止事务修改或锁定对其他事务可见的事务表和非事务表。
    • 当事务为只读时,MySQL会对InnoDB表上的查询进行额外的优化。指定READ ONLY可确保在无法自动确定只读状态的情况下应用这些优化。
    • 如果未指定访问方式,则采用默认方式READ WRITE

不可回滚的语句

某些语句无法回滚。通常,这些语句包括数据定义语言(DDL)语句,例如那些创建或删除数据库的语句,那些创建、删除或修改表或存储例程的语句。

您应该设计您的事务不包含这样的语句。如果您在事务的早期发出了一条不能回滚的语句,然后另一条语句失败,那么在这种情况下,不能通过发出ROLLBACK语句回滚事务的全部效果。

导致隐式提交的语句

本节中列出的语句(以及它们的同义词)隐式地结束当前会话中活动的任何事务,就像在执行语句之前执行了COMMIT一样。

这些语句中的大多数在执行后还会导致隐式提交。其目的是在其自己的特殊事务中处理每个这样的语句。事务控制和锁定语句是例外:如果隐式提交发生在执行之前,则不会在执行之后发生另一次提交。

  • 定义或修改数据库对象的数据定义语言语句。
  • 事务控制和锁定语句。
  • 数据加载语句。
  • 复制控制语句。

SAVEPOINT、ROLLBACK TO SAVEPOINT、和RELEASE SAVEPOINT

SAVEPOINT identifier
ROLLBACK TO [SAVEPOINT] identifier
RELEASE SAVEPOINT identifier

SAVEPOINT语句设置一个名称为identifier的命名事务保存点。如果当前事务具有同名的保存点,则删除旧的保存点并设置新保存点。

ROLLBACK TO SAVEPOINT语句将事务回滚到指定的保存点,而不终止事务。当前事务在设置保存点之后对行所做的修改在回滚中被撤销,但是InnoDB不会释放保存点之后存储在内存中的行锁。(对于新插入的行,锁信息由该行存储的事务ID携带;锁不会单独存储在内存中。在这种情况下,行锁在撤消操作中被释放。)晚于指定保存点设置的保存点将被删除。

RELEASE SAVEPOINT语句从当前事务的保存点集中删除指定的保存点。不发生提交或回滚。如果保存点不存在,则会产生错误。

如果执行COMMITROLLBACK操作而不指定保存点,则会删除当前事务的所有保存点。

当调用存储函数或激活触发器时,将创建新的保存点级别。先前关卡的保存点将不可用,因此不会与新关卡的保存点发生冲突。当函数或触发器终止时,它创建的所有保存点都会被释放,并且恢复到以前的保存点级别。

你可能感兴趣的:(MySQL,mysql,oracle,数据库)