26. 事务的基础知识


一、数据事务概述

??事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性,同时我们还能通过事务的机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。

1.1、存储引擎支持情况

??SHOW ENGINES;命令查看当前MySQL支持的存储引擎有哪些,以及这些存储引擎是否支持事务。

在MySQL中只有InnoDB是支持事务的!

1.2、基本概念

  • 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。
  • 事务处理的原则:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。
    • 当在一个事务中执行多个操作时,要么所有的事务都被提交( commit ),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改,整个事务回滚( rollback )到最初状态。

1.3、事务的ACID特性

  • 原子性(atomicity)
    • 原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
  • 一致性(consistency)
    • 一致性是指事务执行前后,数据从一个合法性状态变换到另外一个合法性状态。这种状态是语义上的而不是语法上的,跟具体的业务有关。
  • 隔离型(isolation)
    • 事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(durability)
    • 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
    • 持久性是通过事务日志来保证的。日志包括了重做日志回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

ACID 是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。
数据库事务,其实就是数据库设计者为了方便起见,把需要保证原子性隔离性一致性持久性的一个或多个数据库操作称为一个事务。

1.4、事务的状态

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

??由此可见,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算结束。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的修改都会被回滚到没执行该事务之前的状态。

二、如何使用事务

??使用事务有两种方式,分别为显示事务隐式事务

2.1、显示事务

  1. START TRANSACTION或者BEGIN,作用是显示开启一个事务
    • START TRANSACTION语句相较于BEGIN特别之处在于,后边能跟随几个修饰符 :
      • READ ONLY:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。只读事务只是不允许修改那些其它事务也能访问到的表中的数据,对于临时表来说(使用CREATE TEMPORARY TABLE创建的表),由于它们只能在当前会话中可见,所以只读事务其实也是可以对临时表进行增,删、改等操作。
      • READ WRITE(默认):标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
      • WITH CONSISTENT SNAPSHOT:启动一致性读。
  2. 一系列事务中的操作(主要是DML,不含DDL)
  3. COMMIT(提交事务)或 ROLLBACK(中止事务,即回滚事务)
    • 将事务回滚到某个保存点:ROLLBACK TO 保存点名称;
    • 在事务中创建保存点:SAVE SAVEPOINT 保存点名称;
    • 删除某个保存点:REPLACE 保存点名称;

2.2、隐式事务

??默认情况下,如果我们不显示的使用START TRANSACTION或者BEGIN语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交。如果我们想要关闭这种自动提交的功能,可以显示的使用START TRANSCATION或者BEGIN语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。或者把系统变量autocommit的值设置为OFF

2.3、隐式提交数据的情况

  • 数据定义语言(Data definition language,缩写为:DDL)
    • 数据库对象,指的就是数据库视图存储过程等结构。当我们使用CREATEALTERDROP等语句取修改数据库对象是,就会隐式的提交前面语句所属的事务。
  • 隐式使用或修改mysql数据库中的表
    • 当我们使用ALTER USERCREATE USERDROP USERGRANT USERRENAME USERREVOKESET PASSWORD等语句也会隐式的提交前面语句所属于的事务。
  • 事务控制或关于锁定的语句
    • 当我们在一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务。
    • 当前的autocommit系统变量的值为OFF,我们手动把它调为ON时,也会隐式的提交前边语句所属的事务。
    • 使用LOCK TABLESUNLOCK TABLES等关于锁定的语句也会隐式的提交前边语句所属的事务。
  • 加载数据的语句
    • 使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前面语句所属的事务。
  • 关于MySQL复制的一些语句
    • 使用START SLAVECREATE SLAVERESET SLAVECHANGE MASTER TO等语句时会隐式的提交前面语句所属的事务。
  • 其它的一些语句
    • 使用ANALYZE TABLECACHE INDEXCHECK TABLEFLUSHLOAD INDEX INTO CACHEOPTIMIZE TABLERAPAIR TABLERESET等语句也会隐式的提交前面语句所属的事务。

三、例程

情况一:

CREATE DATABASE test26;
USE test26;

CREATE TABLE user(name VARCHAR(15) PRIMARY KEY);
SELECT * FROM user;

BEGIN;
INSERT INTO user VALUES('张三');	-- 此时不会自动提交数据
COMMIT;

SELECT * FROM user;

BEGIN;
INSERT INTO user VALUES('李四');
INSERT INTO user VALUES('李四');
SELECT * FROM user;
ROLLBACK;
SELECT * FROM user;

情况二:

TRUNCATE TABLE user;							-- DDL操作会自动提交数据,不受autocommit变量的影响
SELECT * FROM user;

BEGIN;
INSERT INTO user VALUES('张三');	
COMMIT;

INSERT INTO user VALUES('李四');	-- 默认情况下(即autocommit为true),DML操作也会自动提交数据
INSERT INTO user VALUES('李四');	-- 事务的失败的状态
ROLLBACK;
SELECT * FROM user;

情况三:

TRUNCATE TABLE user;
SELECT * FROM user;

BEGIN 
INSERT INTO user VALUES('张三');
INSERT INTO user VALUES('李四');
SELECT * FROM user;
SAVEPOINT s1;											-- 设置保存点
INSERT INTO user VALUES('王五');
SELECT * FROM user;
ROLLBACK TO s1;										-- 回滚到保存点,不是事务的结束状态
SELECT * FROM user;
ROLLBACK;													-- 回滚操作
SELECT * FROM user;

情况四:

TRUNCATE TABLE user;	
SELECT * FROM user;

SELECT @@completion_type;
SET @@completion_type = 1;

BEGIN;
INSERT INTO user VALUES('张三');
COMMIT;
SELECT * FROM user;

INSERT INTO user VALUES('李四');
INSERT INTO user VALUES('李四');
ROLLBACK;
SELECT * FROM user;
  • completion = 0,这是默认情况。当我们执行COMMIT的时候会提交事务,在执行下一个事务时,还需要使用START TRANSACTION或者BEGIN来开启。
  • completion = 1,这种情况下,当我们提交事务后,相当于执行了COMMIT AND CHAIN,也就是开启一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务。
  • completion = 2,这种情况下COMMIT=COMMIT AD RELEASE,也就是当我们提交后,会自动于服务器断开连接。

当我们设置autocommit=0时,不论是否采用START TRANSACTION或者BEGIN的方式来开启事务,都需要用COMMIT进行提交,让事务生效,使用ROLLBACK对事务进行回滚。
当我们设置autocommit=1时,每条SQL语句都会自动进行提交。 不过这时,如果你采用START TRANSACTION或者BEGIN的方式来显式地开启事务,那么这个事务只有在COMMIT时才会生效,在ROLLBACK时才会回滚。

四、常见的事务分类

  • 扁平事务
    • 在扁平事务中,所有操作都处于同一层次,其由 BEGIN WORK 开始或 ROLLBACK WORK 结束,其间的操作是原子的,要么都执行,要么都回滚。因此,扁平事务是应用程序成为原子操作的基本组成模块。扁平事务的主要限制是不能提交或者回滚事务的某一部分,或者分几个步骤提交。扁平事务一般由三种不同的结果:①事务成功完成;②应用程序要求停止事务;③外界因素强制终止事务;
  • 带有保存点的扁平事务
    • 除了支持扁平事务支持的操作外,还允许在事务执行过程中回滚到同一事务中较早的一个状态。这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销太大。
      • 保存点(Savepoint)用来通知事务系统应该记住事务的当前状态,以便当之后发生错误时,事务能回到保存点当时的状态。对于扁平的事务来说,隐式的设置了一个保存点,然而在整个事务中,只有这一个保存点。因此,回滚只能会回滚到事务开始的状态。
  • 链事务
    • 链事务是指一个事务由多个子事务链式组成,它可以被视为保存点模式的一个变种。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务,前一个子事务的提交操作和下一个子事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。这样,在提交子事务时就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。
  • 嵌套事务
    • 嵌套事务是一种层次结构框架,有一个顶层事务(Top-Level Transaction)控制着各个层次的事务,顶层事务之下嵌套的事务被称为子事务(Subtransaction),其控制着每一个局部的变换,子事务本身也可以是嵌套事务。因此,嵌套事务的层次结构可以看成是一棵树。
  • 分布式事务
    • 分布式事务通常是在分布式环境下运行的扁平事务,因此,需要根据数据所在位置访问网络中不同节点的数据库资源。

链式事务与带有保存点的扁平事务的不同之处:

  • 带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚操作仅限于当前事务,即只能恢复到最近的一个保存点。
  • 对于锁的处理,两者也不相同。链事务在执行COMMIT后即释放了当前锁持有的锁,而带有保存点的扁平事务不影响迄今为止所持的锁。

五、事务隔离级别

??MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session )。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。事务有隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对性能影响太大,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时 性能尽量高些 ,那就看二者如何权衡取舍了。

5.1、数据并发问题

  • 脏写( Dirty Write )
    • 对于两个事务 Session A、Session B,如果事务Session A修改了另一个未提交事务Session B修改过的数据,那就意味着发生了脏写
  • 脏读( Dirty Read )
    • 对于两个事务 Session A、Session B,Session A读取了已经被 Session B更新但还没有被提交的字段。之后若 Session B回滚,Session A 读取 的内容就是临时且无效的。
  • 不可重复读( Non-Repeatable Read )
    • 对于两个事务Session A、Session B,Session A读取了一个字段,然后 Session B更新了该字段。 之后Session A再次读取同一个字段,值就不同了。那就意味着发生了不可重复读。
  • 幻读( Phantom )
    • 对于两个事务Session A、Session B, Session A 从一个表中读取了一个字段, 然后 Session B 在该表中插入了一些新的行。之后, 如果 Session A再次读取同一个表,就会多出几行。那就意味着发生了幻读

如果Session B中删除了一些符合条件的记录而不是插入新的记录,那Session A再根据此条件读取的记录变少了,这种现象不属于幻读。幻读强调的是一个事务按照某个相同条件多次读取记录时,后读到时读到了之前没有读到的数据

5.2、SQL中的四种隔离级别

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

SQL标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题

隔离级别 脏读可能性 不可重复读可能性 幻读可能性 加锁读
READ UNCONMITED YES YES YES NO
READ COMMITED NO YES YES NO
REPEATABLE READ MO NO YES NO
SERIALIZABLE NO NO NO YES

5.3、MySQL支持的四种隔离级别

5.3.1、查看隔离级别

SELECT @@transaction_isolation; 
SHOW VARIABLES LIKE 'transaction_isolation';

5.3.2、设置隔离级别

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 */

/*
  使用 GLOBAL 关键字(在全局范围影响):
    当前已经存在的会话无效;
    只对执行完该语句之后产生的会话起作用;

  使用 SESSION 关键字(在会话范围影响):
    对当前会话的所有后续的事务有效;
    如果在事务之间执行,则对后续的事务有效;
    该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务
  
  如果再服务器启动时想要改变事务的隔离级别,可以修改启动参数 transaction_isolation 的值。
*/