进阶篇(13) 事务基础知识

256 阅读18分钟

1. 数据库事务概述

事务让数据库始终保持一致性,同时还能通过事务机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。

目前只有InnoDB支持事务

1.1 基本概念

事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。

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

1.2 事务的ACID特性

  • 原子性(atomicity):

事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。

  • 一致性(consistency):

事务执行前后,数据从一个合法性状态变换到另一个合法性状态。这种状态是语义上的而不是语法上的,跟具体的业务有关。

什么是合法性状态?即满足 预定的约束 的状态。通俗一点,这状态是由你自己来定义的。满足这个状态,数据就是一致的。

举例1:A账户有200元,转账300元出去,此时A账户余额为-100元。此时数据是不一致的,为什么呢?因为,余额必须>=0是一个合法性状态。

举例2:A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?A+B的总余额必须不变 是一个合法性状态。

举例3:在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或回滚时,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。

  • 隔离型(isolation):

事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

如果无法保证隔离性会怎么样?假设A账户有200元,B账户0元。A账户往B账户转账两次,每次金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形:

image.png

  • 持久性(durability):

事务一旦提交,它对数据库中数据的改变就是永久性的

持久性通过事务日志来保证的。包括重做日志回滚日志。通过事务修改数据时,先将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

总结

ACID是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。

事务其实就是数据库设计者为了方便,把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称为一个事务。

1.3 事务的状态

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

  • 活动的

事务对应的操作正在执行。

  • 部分提交的

事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时。

  • 失败的

事务处在活动的或者部分提交的状态时,遇到错误无法继续执行或人为的停止当前事务的执行。

  • 中止的

如果事务执行了一部分而变为失败的状态,就需要把已经修改的操作还原到事务执行前的状态。即撤销失败事务对当前数据库造成的影响。撤销的过程称为回滚回滚完成时,即数据库恢复到了执行事务前的状态,我们就说该事务处在了中止的状态。

  • 提交的

处在部分提交的状态的事务将修改过的数据都同步到磁盘上之后。 image.png

2. 如何使用事务

2.1 显式事务

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

BEGIN; 
#或者 
START TRANSACTION;

START TRANSACTION语句特别之处在于后边能跟几个修饰符

READ ONLY:标识当前事务是一个只读事务,属于该事务的数据库操作只能读取数据,而不能修改数据。

补充:只读事务中只是不允许修改那些其他事务也能访问到的表中的数据,但临时表只能在当前会话中可见,所以只读事务能进行修改。

READ WRITE:标识当前事务是一个读写事务,属于该事务的数据库操作既可以读取和修改数据。

WITH CONSISTENT SNAPSHOT:启动一致性读。

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

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

# 提交事务。当提交事务后,对数据库的修改是永久性的。
COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改 
ROLLBACK; 

# 将事务回滚到某个保存点。 
ROLLBACK TO [SAVEPOINT]

其中关于SAVEPOINT相关操作有:

# 在事务中创建保存点,方便后续针对保存点进行回滚。一个事物中可以存在多个保存点。
SAVEPOINT 保存点名称;
# 删除某个保存点
RELEASE SAVEPOINT 保存点名称;

2.2 隐式事务

MySQL中有一个系统变量 autocommit :事务自动提交设置,默认为1(ON),即除非显示声明事务的开始,否则每一个语句都会被当做独立的事务被处理

SHOW VARIABLES LIKE 'autocommit';

关闭 自动提交 功能的两种方法:

  • 显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务。这样在本次事务提交或者回 滚前会暂时关闭掉自动提交的功能。
  • 把系统变量 autocommit 的值设置为 OFF
SET autocommit = OFF/0;

2.3 隐式提交数据的情况

  • 数据定义语言(Data definition language,DDL)

数据库对象指数据库、表、视图、存储过程等结构。当我们使用CREATE、ALTER、DROP等语句去修改数据库对象时,就会隐式的提交前边语句所属于的事务。即:

BEGIN;

SELECT ...#事务中的一条语句
UPDATE ... #事务中的一条语句
...#事务中的其它语句

CREATE TABLE ...#此语句会隐式的提交前边语句所属于的事务
  • 隐式使用或修改mysql数据库中的表

当我们使用ALTER USER、 CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE 、SET、PASSWORD等语句时也会隐式的提交前边的语句所属于的事务

  • 事务控制或关于锁定的语句
    • 在事务还没提交或回滚时又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务。
      BEGIN;
    
      SELECT ...#事务中的一条语句
      UPDATE ... #事务中的一条语句
      ...#事务中的其它语句
    
      BEGIN; #此语句会隐式的提交前边语句所属于的事务
    
    • 当前的autocommit系统变量的值为OFF,我们手动把它调为ON时,也会隐式的提交前边语句所属的事务。
    • 使用LOCK TABLESUNLOCK TABLES等关于锁定的语句也会隐式的提交前边语句所属的事务。
  • 加载数据的语句
    使用LOAD DATA语句批量导入数据时会隐式的提交上一个事务。
  • 关于MySQL复制的一些语句
  • 其他的一些语句 使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。

2.4 使用举例:

建议在终端运行

案例1:commit、rollback、completion

建表并把自动提交打开:

SET autocommit = TRUE; 
CREATE TABLE user3(NAME VARCHAR(15) PRIMARY KEY);
BEGIN;
INSERT INTO user3 VALUES('张三');  
COMMIT;
SELECT * FROM user3;

BEGIN; #开启一个新的事务
INSERT INTO user3 VALUES('李四');  
ROLLBACK;

SELECT * FROM user3;

由于回滚我们第二次插入的记录无效: image.png

清空表:

TRUNCATE TABLE user3; 

两次插入由于没有在显式事务内,每条插入都是一个事务:

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

INSERT INTO user3 VALUES('李四'); 
INSERT INTO user3 VALUES('王五');  

ROLLBACK; 

SELECT * FROM user3;

回滚到上一次提交的情况,即插入李四后: image.png

我们再次清空表并检查一个变量:

TRUNCATE TABLE user3;

SELECT @@completion_type;

image.png

SET @@completion_type = 1; # 设为chain

再次执行一下上次的插入:

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

INSERT INTO user3 VALUES('李四');
INSERT INTO user3 VALUES('王五'); 

ROLLBACK;

SELECT * FROM user3;

相同的SQL代码,结果却不一样。这是为什么呢? image.png

completion_type的作用:

  1. completion=0,默认设置。COMMIT时提交事务,下一个事务还需要使用START TRANSACTION 或者BEGIN来开启。
  2. completion=1,提交事务后,相当于执行了COMMIT AND CHAIN,开启一个链式事务,即提交事务后会开启一个相同隔离级别的事务。
  3. completion=2COMMIT AND RELEASE,提交后会自动与服务器断开连接。

案例2:体会savepoint 创建表并插入数据:

CREATE TABLE user4(NAME VARCHAR(15),balance DECIMAL(10,2));
INSERT INTO user4(NAME,balance) VALUES('张三',1000);

开启事务更改数据并设置保存点,最后回滚到保存点:

BEGIN;
UPDATE user4 SET balance = balance - 100 WHERE NAME = '张三';
UPDATE user4 SET balance = balance - 100 WHERE NAME = '张三';

SAVEPOINT s1;#设置保存点

UPDATE user4 SET balance = balance + 1 WHERE NAME = '张三';

ROLLBACK TO s1; #回滚到保存点

SELECT * FROM user4;

image.png

此时如果需要保存点的内容可直接提交,不需要则可回滚到上次提交的地方:

ROLLBACK; #回滚操作

SELECT * FROM user4;

image.png

3. 事务隔离级别

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

3.1 数据并发问题

先看访问相同数据的事务在 不保证串行执行 (执行完一个再执行另一个)的情况下可能会出现哪些问题:

1. 脏写(Dirty Write

对于两个事务 a、 b,如果a事务修改了未提交的b事务修改过的数据,即发生了脏写

一开始姓名为王五,第6步时回滚为王五了。 image.png

2. 脏读(Dirty Read

对于两个事务 a、b,事务a读取了已经被事务 b更新但还没有被提交的字段。之后若事务b回滚,事务A读取的内容就是临时且无效的。

image.png

3. 不可重复读(Non-Repeatable Read

对于两个会话 A、B,会话A开启的事务a读取了一个字段,然后 会话 B更新了该字段(没有显式开启事务,更新字段直接提交)。 之后事务a再次读取同一个字段,值就不同了。即发生了不可重复读。

image.png

4. 幻读(Phantom

对于两个会话A、B, 会话A的事务a 从一个表中读取了一个字段, 然后 会话 B 在表中插入一些新的行。 如果 事务a再次读取同一个表, 就会多出几行。即发生了幻读。新插入的记录被称为幻影记录

image.png

注意1:

如果会话 B中剔除了一些符合studentno > 0的记录而不是插入新记录,那么事务a之后再根据studentno > 0的条件读取的记录变少了,这种现象算幻读吗?不算,幻读强调的是事物按照某个相同条件多次读取记录时,后续读取时读到了之前没有读到的记录

注意2:

那对于先前已经读到,之后又读取不到这种情况,算啥呢?这相当于对记录发生了不可重复读的现象。幻读重点强调读取到之前读取没有获取到的记录。

3.2 SQL中的四种隔离级别

以上问题的严重性排序:

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

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

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

image.png 脏写怎么没涉及到?脏写问题太严重了,哪种隔离级别都不允许脏写的情况发生。

3.3 MySQL支持的四种隔离级别

MySQL的默认隔离级别为REPEATABLE READ,我们可以手动修改一下事务的隔离级别。

SELECT @@transaction_isolation;

3.4 如何设置事务的隔离级别

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的影响:

  • 使用 GLOBAL 关键字:
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
#或
SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';

则: 当前已经存在的会话无效。只对执行完该语句之后产生的会话起作用

  • 使用 SESSION 关键字:
    • 对当前会话的所有后续的事务有效
    • 如果在事务里执行,则只对后续的事务有效

3.5 不同隔离级别举例

模拟两个会话对一张表的操作:
image.png

创建表

CREATE TABLE account(id INT PRIMARY KEY,name VARCHAR(10), balance INT);
INSERT INTO account VALUES (1,'张三', 100);
INSERT INTO account VALUES (2,'李四', 0);

每次演示完一个问题都会恢复初始状态:张三100 李四0

3.5.1 读未提交之脏读

将两个会话都设为读未提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT @@transaction_isolation;

我们在会话1开启一个事务,将id=1的balance增加100,不急着提交:

BEGIN;
UPDATE account SET balance = balance + 100 WHERE id = 1;
SELECT * FROM account WHERE id = 1;

image.png

但是此时会话2也能查询到200:

image.png

这就是脏读

那有什么危害呢?

比如张三有给李四转100块钱的想法(事务),但是李四能看到张三的想法,而李四又不想要这100块钱,于是直接给张三转回去了。但是没想到张三返悔了(ROLLBACK),李四白白损失了100块

3.5.2 读已提交

将两个会话设为读已提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

和上面的操作一样:

BEGIN;
UPDATE account SET balance = balance + 100 WHERE id = 1;
SELECT * FROM account WHERE id = 1;

此时会话2就不会发生脏读的情况了:
image.png

但此时的不可重复读问题还存在:

会话1:

BEGIN;
SELECT * FROM account WHERE id = 1;

image.png

此时在会话2中修改记录(隐式事务,即更新完直接提交)

UPDATE account SET balance = balance + 100 WHERE id = 1;

此时会话1就出现了不可重复读image.png

3.5.3 可重复读

两个会话全改为可重复读:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

再次进行上面的不可重复读的演示,会发现会话1在事务中读该记录永远不会发生改变

3.5.4 幻读

会话1:

BEGIN;
SELECT COUNT(*) FROM account WHERE id = 3;

因为我们本来就没有id为3的记录:
image.png

在会话2:

INSERT INTO account VALUES(3,'王五',0);

此时会话1再读一次:发生了幻读:
image.png

SERIALIZABLE:可以解决幻读问题,自己去试试。

4. 事务的常见分类

  • 扁平事务
    最简单的一种,在实际生产环境使用最频繁的事务。所有操作处于同一层次,由BEGIN开始,COMMIT或ROLLBACK结束,其间的操作是原子的,要么都执行,要么都回滚。
    一般有三种结果:①事务成功完成。②应用程序要求停止事务。比如应用程序在捕获到异常时会回滚事务。③外界因素强制终止事务。如连接超时或连接断开。
  • 带有保存点的扁平事务
    在事务执行过程中回滚到较早的一个状态。这是因为某些事务可能在执行时出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销太大。
    保存点(Savepoint)通知事务系统记住事务当前状态,以便之后发生错误时,事务能回到保存点当时的状态。扁平事务只隐式的设置了一个保存点,因此,回滚只能会滚到事务开始时的状态。
  • 链事务
    一个事务由多个子事务链式组成。带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。链事务的思想:提交事务时释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务,前一个事务的提交操作和下一个事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。这样,在提交子事务时就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。其工作方式如下: image.png 链事务与带有保存点的扁平事务的不同:

①带有保存点的扁平事务能回滚到任意保存点,而链事务中的回滚仅限于当前事务,即只能恢复到最近的一个保存点。

②对于锁的处理,两者也不相同,链事务在COMMIT后释放当前所持有的锁,而带有保存点的扁平事务不影响迄今为止的所持有的锁。

  • 嵌套事务
    层次结构框架,由一个顶层事务控制着各个层次的事务,顶层事务之下嵌套的事务被称为子事务,控制着每一个局部的变换,子事务本身也可以是嵌套事务。因此,嵌套事务的层次结构可以看成是一棵树。
  • 分布式事务 在分布式环境下运行的扁平事务,因此,需要根据数据所在位置访问网络中不同节点的数据库资源。例如,一个用户从A银行的账户向B银行的账户转账1000元,这里需要用到分布式事务,因为不能仅调用某一家银行的数据库就完成任务。