惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy International News Feed
Martin Fowler
Martin Fowler
D
Docker
Y
Y Combinator Blog
云风的 BLOG
云风的 BLOG
U
Unit 42
T
Tailwind CSS Blog
J
Java Code Geeks
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
美团技术团队
F
Fortinet All Blogs
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
The GitHub Blog
The GitHub Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recorded Future
Recorded Future
N
Netflix TechBlog - Medium
Google DeepMind News
Google DeepMind News
Hacker News: Ask HN
Hacker News: Ask HN
L
LINUX DO - 最新话题
Microsoft Security Blog
Microsoft Security Blog
N
News and Events Feed by Topic
I
Intezer
TaoSecurity Blog
TaoSecurity Blog
NISL@THU
NISL@THU
小众软件
小众软件
博客园 - 聂微东
博客园 - Franky
有赞技术团队
有赞技术团队
P
Palo Alto Networks Blog
爱范儿
爱范儿
H
Hacker News: Front Page
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cisco Blogs
P
Proofpoint News Feed
I
InfoQ
Google DeepMind News
Google DeepMind News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
H
Heimdal Security Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
量子位

Inno' Blog

将 what-digit-you-write 部署到 Heroku上 | Inno' Blog 论文浅析:GpSense: A GPU-friendy method for common-sense subgraph matching in massively parallel architectures Github项目clone速度过慢?试试这招 | Inno' Blog Hadoop分布式搭建全流程记录 | Inno' Blog Shell编程学习记录 | Inno' Blog 重学MySQL之视图 | Inno' Blog 重学MySQL之查询练习 | Inno' Blog 重学MySQL之范式 | Inno' Blog 重学MySQL之基本操作和约束 | Inno' Blog 为你的Web应用提供HTTPS服务 | Inno' Blog 一个关于切片的小问题 | Inno' Blog Go语言学习笔记 | Inno' Blog 关于英译中的个人技巧 | Inno' Blog 不知不觉已经写够了十万行代码 | Inno' Blog YARN 架构 | Inno' Blog HDFS 架构 | Inno' Blog 正则中关于环视(lookaround)的小例子 | Inno' Blog 设计模式(十八):访问者模式 | Inno' Blog 如何借助Github进行协同开发 | Inno' Blog
重学MySQL之事务 | Inno' Blog
Inno Fang · 2020-03-29 · via Inno' Blog

重新学习 MySQL 的基础知识,本章内容为事务的介绍

什么是事务

数据库事务(Database Transaction) ,事务是一系列作为一个逻辑单元来执行的操作集合。

事务具有四大特征(ACID):

  • 原子性(Atomicity):事务是最小的工作单位,不可以再分割,事务中的操作要么全部执行,要么全都不执行,不能只完成部分操作。原子性在数据库系统中,由恢复机制来实现。
  • 一致性(Consistency):同一事务中的 SQL 语句,必须保证同时成功或这失败,事务开始前和结束后的状态应该保持一致性。数据库一致性的定义是由用户负责的。
  • 隔离性(Isolation):系统必须保证事务不受其他并发执行事务的影响,即当多个事务同时运行时,各事务之间相互隔离,不可互相干扰。事务查看数据时所处的状态,要么是另一个并发事务修改它之前的状态,要么是另一个并发事务修改它之后的状态,事务不会查看中间状态的数据。隔离性通过系统的并发控制机制实现。
  • *持久性(Durability):事务一旦结束 ( COMMIT ) ,就不可以再返回了 ( ROLLBACK ) 。

事务的 ACID 原则保证了一个事务或者成功提交,或者失败回滚,二者必居其一。

为什么要使用事务

举例来说,银行转账的时候,用户 A 向用户 B 转账了 100 元,此时在数据库中,用户 A 的金额应当 -100,而用户 B 的金额应当 +100。倘若只有一条 SQL 语句执行成功,而另一条执行失败,那么就会出现数据前后不一致的情况。

因此在执行多条有关联的 SQL 语句时,事务要求这些 SQL 语句要么同时执行成功,要么都失败。

事务的流程

在 MySQL 中,要使用事务,首先就需要先开启事务,然后执行 SQL 语句,所有 SQL 语句都执行完确认无误后就可以提交;如果在提交之前及时发现了错误,可以使用回滚来撤销刚才执行的 SQL 语句,使其回到最后一次提交数据时的状态。

注意,一旦 SQL 执行结果提交成功,回滚操作就无法撤销刚才的操作。因此,提交(COMMIT)回滚(ROLLBACK)应当是一对并列操作而非递进操作。

在 MySQL 中,事务是默认自动提交的。所以,为了保证一致性,以便在 SQL 语句执行错误后及时回滚,我们可以选择手动关闭自动提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

mysql> SELECT @@AUTOCOMMIT;
+
| @@AUTOCOMMIT |
+
| 1 |
+
1 row in set (0.00 sec)


mysql> SET AUTOCOMMIT = 0;
Query OK, 0 rows affected (0.00 sec)


mysql> SELECT @@AUTOCOMMIT;
+
| @@AUTOCOMMIT |
+
| 0 |
+
1 row in set (0.00 sec)

此后,倘若我们需要提交数据,就需要手动提交,即在 @@AUTOCOMMIT = 0 时,使用 COMMIT 命令提交事务。

如果需要回滚,则在 @@AUTOCOMMIT = 0 时,使用 ROLLBACK 命令回滚事务。

但是,关闭自动提交后,就会使得数据库的操作更加繁琐,因为每次执行语句后都需要手动输入 COMMIT

因此,我们可以选择使用手动开启事务的方式

开启事务

在 MySQL 中,可以使用 BEGIN 或者 START TRANSACTION 来手动开启事务

以上面银行转账为例,创建一张 user 表,并插入两条记录

1
2
3
4
5
6
7
8
9
10
11
12
mysql> CREATE TABLE user(
-> id INT PRIMARY KEY AUTO_INCREMENT,
-> name VARCHAR(20) NOT NULL,
-> money INT DEFAULT 0
-> );
Query OK, 0 rows affected (0.33 sec)

mysql> INSERT INTO user(name, money) VALUES('Mike', 1000);
Query OK, 1 row affected (0.36 sec)

mysql> INSERT INTO user(name, money) VALUES('Amy', 2000);
Query OK, 1 row affected (0.08 sec)

开启事务,完成 Mike 向 Amy 转账 100 元的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE user SET money=money-100 WHERE name='Mike';
Query OK, 1 row affected (0.31 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> UPDATE user SET money=money+100 WHERE name='Amy';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 900 |
| 2 | Amy | 2100 |
+
2 rows in set (0.00 sec)

虽然我们还没有提交修改,但会发现数据表中的数据已经更改了,但是不要慌,此时发生变化的数据只是暂存在一张临时表中,要完成最终数据的修改,仍然需要手动提交。

提交事务

提交事务,意味着将事务开始以来所执行的所有数据修改成为数据库的永久部分,因此也标志着一个事务的结束。一旦执行了该命令,将不能回滚事务。只有在所有修改都准备好提交给数据库时,才执行这一操作。

1
2
3
4
5
6
7
8
9
10
11
mysql> COMMIT;
Query OK, 0 rows affected (0.09 sec)

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 900 |
| 2 | Amy | 2100 |
+
2 rows in set (0.00 sec)

此时,对数据的修改才正式生效。

撤销事务

正如前面所说,如果修改已经提交,那么回滚是无效的

1
2
3
4
5
6
7
8
9
10
11
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 900 |
| 2 | Amy | 2100 |
+
2 rows in set (0.00 sec)

开启事务,尝试执行 Amy 向 Mike 转账 200 元的操作,并在最后进行回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE user SET money=money-200 WHERE name='Amy';
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> UPDATE user SET money=money+200 WHERE name='Mike';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 1100 |
| 2 | Amy | 1900 |
+
2 rows in set (0.00 sec)

mysql> ROLLBACK;
Query OK, 0 rows affected (0.07 sec)

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 900 |
| 2 | Amy | 2100 |
+
2 rows in set (0.00 sec)

回滚成功。

不过有时候,我们并不需要回退所有操作,可以让事务回滚到指定位置,此时,需要在事务中设定保存点(SAVEPOINT)。

保存点的创建通过 SAVEPOINT <保存点名称> 语句来实现,再执行 ROLLBACK TO <保存点名称> 语句回滚到该保存点。

开启事务,完成以下操作:

  1. Mike 向 Amy 转账 200 元
  2. Amy 向 Mike 转账 100 元
  3. 回滚到第二步,并将最终修改提交

最终的结果是,Mike 仅仅只向 Amy 转账了 200 元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE user SET money=money-200 WHERE name='Mike';
Query OK, 1 row affected (0.32 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> UPDATE user SET money=money+200 WHERE name='Amy';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 700 |
| 2 | Amy | 2300 |
+
2 rows in set (0.00 sec)

mysql> SAVEPOINT mike2amy;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE user SET money=money-100 WHERE name='Amy';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> UPDATE user SET money=money+100 WHERE name='Mike';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 800 |
| 2 | Amy | 2200 |
+
2 rows in set (0.00 sec)

mysql> ROLLBACK TO mike2amy;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 700 |
| 2 | Amy | 2300 |
+
2 rows in set (0.00 sec)

mysql> COMMIT;
Query OK, 0 rows affected (0.08 sec)

mysql> SELECT * FROM user;
+
| id | name | money |
+
| 1 | Mike | 700 |
| 2 | Amy | 2300 |
+
2 rows in set (0.00 sec)

事务的隔离性

之前的例子中提到了原子性、一致性和持久性,但未提到隔离性,因此这里单独讲解

事务的隔离性分为四种(性能从高到低,隔离级别从低到高):

1. READ UNCOMMITTED ( 读取未提交 )

如果有多个事务,那么任意事务都可以看见其他事务的未提交数据。

设置系统隔离级别,LEVEL 后面表示要设置的隔离级别。

1
2
3
4
5
6
7
8
9
10
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@GLOBAL.TX_ISOLATION;
+
| @@GLOBAL.TX_ISOLATION |
+
| READ-UNCOMMITTED |
+
1 row in set (0.00 sec)

在查询隔离级别时,SELECT @@GLOBAL.TX_ISOLATION; 表示系统的隔离级别,而不加 GLOBAL 的语句 SELECT @@TX_ISOLATION; 则表示会话的隔离级别,设置隔离级别的时候也可以选择是否是 GLOBAL 的。

注意,以上是查询隔离级别的语法是 MySQL 5.x 的,如果是 MySQL 8.x 则需要略加修改

1
2
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
SELECT @@TRANSACTION_ISOLATION;

该级别的性能是最优的,但是隔离性是最差的。当一个事务可以读取另一个事务未提交的数据时,就会出现脏读(Dirty Read) 的情况,这在实际开发中是不允许出现的。

2. READ COMMITTED ( 读取已提交 )

只能读取到其他事务已经提交的数据。

需要注意的是,在有新的事务连接进来时,它们可以查询到已经提交过的事务数据。但是对于当前事务来说,它们看到的还是未提交之前的数据

1
2
3
4
5
6
7
8
9
10
11
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@GLOBAL.TX_ISOLATION;
+
| @@GLOBAL.TX_ISOLATION |
+
| READ-COMMITTED |
+
1 row in set (0.00 sec)

虽然 READ COMMITTED 让我们只能读取到其他事务已经提交的数据,但还是会出现问题,就是在读取同一个表的数据时,可能会发生前后不一致的情况。这被称为不可重复读现象 ( READ COMMITTED ) 。

3. REPEATABLE READ ( 可被重复读 )

MySQL 默认事务隔离级别

如果有多个连接都开启了事务,那么事务之间只能共享之前已提交的记录,比如事务 A 在读到一条数据之后,此时事务 B 对该数据进行了修改并提交,那么事务 A 再读该数据,读到的还是原来的内容。

1
2
3
4
5
6
7
8
9
10
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@GLOBAL.TX_ISOLATION;
+
| @@GLOBAL.TX_ISOLATION |
+
| REPEATABLE-READ |
+
1 row in set (0.00 sec)

不过,可重复读会导致 幻读(Phantom Read) 问题,它指的是事务 A 在读取某一范围的数据时,另一个事务在该范围内修改了新的数据,当事务 A 再读取该范围的数据时,会发现出现了新的数据,这导致了数据前后的不一致。而解决幻读的方式就是使用更高级别的隔离——串行化(SERIALIZABLE)。

4. SERIALIZABLE ( 串行化 )

假设把所有的事务都放在一个串行的队列中,那么所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作 ( 这意味着队列中同时只能执行一个事务的写入操作 ) 。

1
2
3
4
5
6
7
8
9
10
mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@GLOBAL.TX_ISOLATION;
+
| @@GLOBAL.TX_ISOLATION |
+
| SERIALIZABLE |
+
1 row in set (0.00 sec)

如果事务 A 开启后执行了 SQL 语句但未提交,此时事务 B 开启后也打算执行 SQL 语句,因为隔离级别设置为串行化,所以在事务 A 未手动提交之前,事务 B 的操作都将进入阻塞状态,或者出现等待超时