技术原理:MySQL数据库事务浅析

  1. 云栖社区>
  2. 博客>
  3. 正文

技术原理:MySQL数据库事务浅析

adoryn 2018-06-01 16:24:23 浏览2065
展开阅读全文
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/80539558

在SQL编程中,事务编程已然成为必不可少的一个组成部分。本文将基于MySQL对数据库事务进行简单的介绍和分析。

一、事务概述

事务可以由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。事务是访问并更新数据库中各种数据项的一个程序执行单元。事务能保证数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保其要么对所有修改都已经保存,要么对所有修改操作都不保存。

事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元,狭义上的事务特指数据库事务。一方面,当多个应用程序并发访问数据库时,事务可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。另一方面,事务为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持数据一致性的方法。

下面,我们将聚焦于事务的ACID特性进行讲解与分析。

在MySQL的InnoDB存储引擎中,其默认的事务隔离级别是Read Repeatable(可重复读),完全遵循和满足事务的ACID特性。

下面,我们就具体介绍事务的ACID特性,具体如下:

1、原子性(Atomic)

事务的原子性是指整个数据库事务是不可分割的工作单位。只有是事务中所有的数据库操作都执行成功,整个事务的执行才算成功。事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

事务的原子性要求事务中包含的各项操作在一次执行过程中,只允许出现以下两种状态之一:

  • 全部成功执行;
  • 全部不执行。

举个例子,一个用户在ATM机前取款,其取款流程为:

  • 登录ATM机平台,验证密码;
  • 从远程银行的数据库中取得账户的信息;
  • 用户在ATM机上输入要提取的金额;
  • 从远程银行的数据库中更新账户信息;
  • ATM机出款;
  • 用户取钱。

整个取款的操作过程应该视为原子操作,即要么都做,要么都不做。不能出现用户钱未从ATM机上取得而银行卡上的钱已经被扣除的情况。通过事物模型,可以保证该操作的原子性。

2、一致性(Consistency)

事务的一致性是指整个数据库事务将数据库从一种一致的状态转变为另一种一致的状态。在事务开始之前和事务结束之后,数据库的完整性约束不会被破坏。因此,事务是一致性的单位,如果事务中的某个动作失败了,系统可以自动地撤销事务使其返回初始化的状态。

举个例子,在表中有一个字段为姓名,它是一个唯一约束,即在表中姓名不能重复。如果一个事务对表进行了修改,但是在事务提交或当事务操作发生回滚后,表中的数据姓名变得非唯一了,那么就破坏了事务的一致性要求,即事务将数据库从一种一致性状态转变为了一种不一致的状态。

3、隔离性(Isolation)

事务的隔离性要求每个读写事务的对象与其他事务的操作对象能相互分离,即该事物提交前对其他事务都不可见。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

通常情况下,实现事务的隔离性需要使用锁机制。当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之间的并发性。

4、持久性(Durability)

事务的一致性是指事务一旦提交,其结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。持久性保证的是事务系统的高可靠性(high reliability),而不是高可用性(high availability)。

二、事务分类

从理论上的角度来说,可以把事务分为以下几种类型:

  • 扁平事务(flat transactions)
  • 带有保存点的扁平事务(flat transactions with savepoints)
  • 链事务(chained transactions)
  • 嵌套事务(nested transactions)
  • 分布式事务(distributed transactions)

1、扁平事务

扁平事务是事务类型中最简单的一种,也是实际生产环境中使用最为频繁的事务。

在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束,处于之间的操作是原子的,即要么都执行,要么都回滚。

扁平事务是应用程序成为原子操作的基本组成模块。

扁平事务的三种不同情况:

mysql_transaction_flat_example.png

如上图所示,扁平事务的执行分为三种情况:

  1. 成功完成(96%)
  2. 应用程序要求停止事务(3%)
  3. 由于外界原因强制终止事务(1%)

分析扁平事务的特点,其主要限制就是不能提交或回滚事务的某一部分,或者分几个步骤进行提交

2、带有保存点的扁平事务

前面我们知道,扁平事务存在不能回滚到同一事务中较早的一个状态的限制,因此带有保存点的扁平事务应运而生。

保存点(savepoint)用于通知系统应该记住事务当前的状态,以便以后发生错误时,事务能够回到该状态。对于扁平事务而言,其隐式地设置了一个保存点,即只能回滚到事务开始时的状态。

保存点用SAVE WORK函数来建立,通知系统记录当前的处理状态。当出现问题时,保存点能用做内部的重启动点,根据应用逻辑,决定是回到最近一个保存点还是其他更早的保存点。

带保存点的扁平事务有一个特点,那就是当系统发生崩溃时,所有的保存点都将消失,因为保存点是易失的(volatile),而非持久的(persistent)。

3、链事务

链事务的思想是,在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。此处,提交事务操作和开始下一个事务操作将合并为一个原子操作。链事务中的回滚仅限于当前事务,即只能恢复到最近一个保存点。

其工作方式如下图所示:

mysql_transaction_chain_example.png

4、嵌套事务

嵌套事务是一个层次结构框架。在嵌套事务框架中,有一个顶层事务(top-level transaction),其控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制着每一个局部的变换。

嵌套事务的层次结构如下图所示:

mysql_transaction_iteration_example.png

在嵌套事务中,其基本特性如下:

  • 嵌套事务是由若干事务组成的一棵树,子树可以是嵌套事务,也可以是扁平事务。

  • 处在叶子节点的事务是扁平事务,但是每个子事务从根到叶节点的举例可以是不同的。

  • 位于根节点的事务称为顶层事务,其他事务称为子事务。

  • 事务的前驱(predecessor)称为父事务(parent),事务的下一层称为儿子事务(child)。

  • 子事务既可以提交也可以回滚,但是它的提交操作并不马上生效,除非由其父事务提交。由此可以推论,任何子事务都是在顶层事务提交后才真正提交。

  • 树中的任意一个事务的回滚会引起它的所有子事务一同回滚。

在嵌套事务中,实际的工作交由叶子节点来完成,即只有叶节点的事务才能访问数据库、发送消息、获取其他类型的资源。而高层的事务仅负责逻辑控制,决定何时调用相关的子事务。

5、分布式事务

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常,一个分布式事务中会涉及对多个数据源或业务系统的操作。

对于分布式事务而言,其内容错综复杂,基本理论包括CAP和BASE理论。在后续博客中,我们将详细分析分布式事务。

三、事务的隔离级别

1、事务问题

在数据库中,并发事务之间可能会存在相互影响的问题。在事务进行并发操作时,可能会出现下面一些问题:

  • 脏读

事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的数据就是脏数据。

  • 不可重复读

在同一事务中,对同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果和提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录。要避免这种情况,最简单的方式就是对要修改的记录加锁,但这会导致锁竞争加剧,影响性能。

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

  • 幻读

在同一事务中,同一个查询多次返回的结果不一致。比如,事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的。对于这种情况,不能通过记录加锁解决,因为对于新增的记录根本无法加锁,解决方式就是将事务串行化,这样就能避免幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

2、隔离级别

下面,我们着重讲解一下事务的隔离级别,分别是:

读未提交(Read Uncommitted)

对于读未提交,该隔离级别允许脏读取,其隔离级别最低。换句换说,如果一个事务正在处理某一数据,并对其进行了更新,但同时尚未完成事务,因此还没有进行事务提交;与此同时,允许另一个事务也能够访问该数据。在这种情况下,一个事务可以读到另一个事务未提交的结果,因此,所有的并发事务问题都会发生。该隔离级别可以通过“排他写锁”实现。

举个例子,事务A和事务B同时进行,事务A在整个执行阶段,会将某数据项的值从1开始,做一系列的加1操作直到变成10之后进行事务提交,此时,事务B能够看到这个数据项在事务A操作过程中的所有中间值(如1变成2等),而对这一系列的中间值的读取就是未授权的。

读未提交的实现原理:

  • 事务在读数据时并未对数据加锁;
  • 事务在修改数据时只对数据增加排他写锁。

举例:

mysql_transaction_isolation_level_read_u

事务一共查询了两次,在两次查询的过程中,事务二对数据进行了修改,并未提交(commit)。但是事务一的第二次查询查到了事务二的修改结果。在数据库的读现象浅析中我们介绍过,这种现象我们称之为脏读。

所以,未提交读会导致脏读

读已提交(Read Committed)

对于读已提交,其仅允许读取已经被提交的数据。也就是说,只有在事务提交后,其更新结果才会被其他事务读取。基于读已提交隔离级别,可以解决脏读问题。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

同样举上一个例子,事务A和事务B同时进行,事务A进行与上述同样的操作,此时,事务B无法看到这个数据项在事务A操作过程中的所有中间值,只能看到最终的结果10。

读已提交的实现原理:

  • 事务对当前被读取的数据加行级共享读锁(当读到时才加锁),一旦读完改行,立即释放该行级共享读锁;

  • 事务在更新某个数据时,必须先对其加行级排他写锁,直到事务结束才释放。

举例:

mysql_transaction_isolation_level_read_c

在读已提交隔离级别中,在事务二提交之前,事务一不能读取数据。只有在事务二提交之后,事务一才能读数据。从上面的例子中我们也看到,事务一两次读取的结果并不一致,所以提交读不能解决不可重复读的读现象。

简而言之,读已提交这种隔离级别保证了读到的任何数据都是提交的数据,避免了脏读(dirty reads)。但是不保证事务重新读的时候能读到相同的数据,因为在每次数据读完之后其他事务可以修改刚才读到的数据。

可重复读(Repeatable Read)

对于可重复读,其保证在事务处理过程中,多次读取同一个数据时,其值和事务开始时刻是一致的。基于可重复读隔离级别,可以解决脏读和不可重复读问题。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。

可重复读的实现原理:

  • 事务在读取某数据时,必须先对其加行级共享读锁,直到事务结束才释放;
  • 事务在更新某数据时,必须先对其加行级排他写锁,直到事务结束才释放。

举例:

mysql_transaction_isolation_level_repeat

在上面的例子中,只有在事务一提交之后,事务二才能更改该行数据。所以,只要在事务一从开始到结束的这段时间内,无论它读取该行数据多少次,结果都是一样的。

从上面的例子中我们可以得到结论:可重复读隔离级别可以解决不可重复读的读现象。但是可重复读这种隔离级别中,还有另外一种读现象解决不了,那就是幻读。看下面的例子:

mysql_transaction_isolation_level_repeat

上面的两个事务执行情况及现象如下:

1、事务一的第一次查询条件是age BETWEEN 10 AND 30;如果这是有十条记录符合条件。这时,它会给符合条件的这十条记录增加行级共享锁。任何其他事务无法更改这十条记录。

2、事务二执行一条sql语句,语句的内容是向表中插入一条数据。因为此时没有任何事务对表增加表级锁,所以,该操作可以顺利执行。

3、事务一再次执行SELECT * FROM users WHERE age BETWEEN 10 AND 30;时,结果返回的记录变成了十一条,比刚刚增加了一条,增加的这条正是事务二刚刚插入的那条。

所以,事务一的两次范围查询结果并不相同。这也就是我们提到的幻读

串行化(Serializable)

串行化是最严格的事务隔离级别,它要求所有事务都被串行执行,即事务只能一个接一个地处理,不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

串行化的实现原理:

  • 事务在读取数据时,必须先对其加表级共享读锁,直到事务结束才释放;
  • 事务在更新数据时,必须先对其加表级排他写锁,直到事务结束才释放。

虽然可序列化解决了脏读、不可重复读、幻读等读现象。但是序列化事务会产生以下效果:

  1. 无法读取其它事务已修改但未提交的记录。

  2. 在当前事务完成之前,其它事务不能修改目前事务已读取的记录。

  3. 在当前事务完成之前,其它事务所插入的新记录,其索引键值不能在当前事务的任何语句所读取的索引键范围中。

对比

下图展示了不同隔离级别下事务访问数据的差异。

mysql_transaction_compare_table.png

以上4个隔离级别的隔离性依次增强,分别解决不同的问题。具体对比如下图所示:

mysql_transaction_level_compare.png

事务的隔离级别越高,就越能保证数据的完整性和一致性,但同时对并发性能的影响也越大。

通常,对于绝大多数的应用程序来说,可以优先考虑将数据库的隔离级别设置为读已提交,这能够避免脏读的同时保证较好的并发性能。但是,这种隔离级别会导致不可重复读、幻读等并发问题,不过,较为科学的做法是在可能出现这类问题的个别场合中,由应用程序主动采用悲观锁或乐观锁来进行事务控制

在MySQL中,InnoDB存储引擎默认的支持隔离级别是Repeatable Read。与标准SQL不同的是,InnoDB存储引擎在Repeatable Read事务隔离级别下,使用Next-Key Lock的锁算法,因此避免了幻读的产生。另外,对于大多数数据库,其默认的事务隔离级别是Read Committed

四种事务隔离级别从隔离程度上越来越高,但同时在并发性上也就越来越低。之所以有这么几种隔离级别,就是为了方便开发人员在开发过程中根据业务需要选择最合适的隔离级别。

3、MySQL隔离级别配置

  • 在MySQL库启动时设置事务的默认隔离级别,可以修改MySQL的配置文件my.cnf:
[mysqld]
transaction-isolation = READ-COMMITTED
  • 查看当前会话的事务隔离级别,可以使用:
mysql> SELECT @@tx_isolation;
  • 查看全局的事务隔离级别,可以使用:
mysql> SELECT @@global.tx_isolation;

四、事务控制语句

1、事务SQL

在MySQL命令行的默认设置下,事务都是自动提交(auto commit)的,即执行SQL语句后就会马上执行commit操作。

显式开启一个事务的命令:

  • BEGIN && START TRANSACTION
  • SET AUTOCOMMIT=0

事务控制的基本命令:

  • START TRANSACTION | BEGIN:显式地开启一个事务。

  • COMMIT:提交事务,并使已对数据库进行的所有修改成为永久性的。

  • ROLLBACK:回滚事务,并撤销正在进行的所有未提交的修改。

  • SAVEPOINT identifier:SAVEPOINT允许在事务中创建一个保存节点,一个事务中可以有多个SAVEPOINT。

  • RELEASE SAVEPOINT identifier:删除一个事务的保存点,当没有一个保存点时,会抛出异常。

  • ROLLBACK TO [SAVEPOINT]:配合SAVEPOINT一起使用,可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。

  • SET TRANSACTION:用于设置事务的隔离级别。

2、不好的事务编程

下面,我们关注一些不好的事务编程习惯,主要包括:

  • 在循环中提交
  • 使用自动提交
  • 使用自动回滚

在循环中提交

开发人员非常喜欢在循环中进行事务的提交,如下:

CREATE PROCEDURE load1(count INT UNSIGNED)
BEGIN
    DECLARE s INT UNSIGNED DEFAULT 1;
    DECLARE c CHAR(80) DEFAULT REPEAT('a', 80);
    WHILE s <= count DO
        INSERT INTO table1 SELECT NULL,c;
        COMMIT;
        SET s = s+1;
    END WHILE;
END;

在上述例子中,是否加上提交命令COMMIT并不是关键,因为InnoDB存储引擎默认为自动提交。

上述方法的问题在于两方面:

  • 当发生错误时,数据库会停留在一个未知的位置。
  • 性能问题。

改进方法:

CREATE PROCEDURE load2(count INT UNSIGNED)
BEGIN
    DECLARE s INT UNSIGNED DEFAULT 1;
    DECLARE c CHAR(80) DEFAULT REPEAT('a', 80);
    START TRANSACTION;
        WHILE s <= count DO
            INSERT INTO table1 SELECT NULL,c;
            SET s = s+1;
        END WHILE;
    COMMIT;
END;

对比两种方法的执行时间,有:

mysql> CALL load1(10000);  ——> 1 min 3.15sec
mysql> TRUNCATE TABLE table1;
mysql> CALL load2(10000);  ——> 0.63 sec

原因何在?每一次提交都会写一次重做日志,但是使用事务只用写一次重做日志。

使用自动提交

使用如下语句来改变当前自动提交的方式:

mysql> SET autocommit=0;  ——> 关闭自动提交
Query OK, 0 rows affected (0.00sec)

同时,也可使用START TRANSACTION或BEGIN来显式地开启一个事务。显式开启事务后,在默认设置下,MySQL会自动执行SET autocommit=0的命令,并在COMMIT或者ROLLBACK结束一个事务后执行SET autocommit=1命令。

使用自动回滚

对于下面一个存储过程:

CREATE PROCEDURE sp_auto_rollback_demo()
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
    START TRANSACTION;
        INSERT INTO b SELECT 1;
        INSERT INTO b SELECT 2;
        INSERT INTO b SELECT 3;
    COMMIT;
END

存储过程sp_auto_rollback_demo定义了一个EXIT类型的HANDLER,当捕获到错误时进行回滚。但是自动回滚会导致一些不明的问题出现。

五、小结

在本文,我们了解了事务如何工作以及如何使用事务,同时讲解了操作事务的SQL语句以及怎样在应用程序中正确地使用事务。切记:MySQL数据库总是自动提交的。在应用程序中,最好的做法是把事务地START TRANSACTION、COMMIT、ROLLBACK操作交给程序端来完成,而不是在存储过程内完成。

网友评论

登录后评论
0/500
评论
adoryn
+ 关注