mysql进阶-理解mysql事务和隔离级别

1 你真的理解 MySQL 中的事务吗 ?

在解读事务隔离级别之前,我们先来看一看事务。这里我将会说明三个话题:事务是什么、事务的 ACID 特性、并发事务会带来什么样的问题。好的,我们先来解读下事务的概念吧。

1.1 事务是什么

事务是作为单个逻辑工作单元执行的一系列操作,它所表达的语义是:要么全部执行,要么全部不执行。我们在读书时可能就听过银行取款的例子,用它来解释事务就再合适不过了。假如 A 要去银行取款 100 元,这次的取款过程会涉及两个操作:

  • 将 A 的余额减少 100 元
  • A 获得 100 元取款

那么,这两个操作就是一次事务,因为这两个操作只能全部成功或全部失败,任何一个部分成功或失败,将会是非常严重的系统漏洞。事务的目标是保证数据库的完整性,避免各种原因引起的数据库内容不一致的问题。所以,事务可以保证数据安全,事务控制实际上就是在控制数据的安全访问。

1.2 事务的 ACID 特性

事务必须要有四个属性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。深刻理解这四个属性是理解事务的基础,下面,我们依次来看一看这四个属性的含义与要求。

  • 原子性:原子本身是化学中的一个名词,它指的是构成化学元素的最小粒子,即不能再更细的分割了。事务操作必须是原子的,对于一个事务中的所有操作,要么全部执行(COMMIT),要么全部不执行(ROLLBACK)
  • 一致性:一致性指的是数据的完整性,即执行事务的前后,数据整体应该是一致的。事务必须能够让数据库从一个一致性状态变换到另一个一致性状态。对于取款的案例来说,A 的数据总值就是一致的
  • 隔离性:它指的是一个事务的执行不能被其他事务所干扰,这里又涉及到并发的概念。一个事务内部的操作对其他并发的事务是隔离的,简单的说,每个事务都认为是自己独占数据库
  • 持久性:这个属性简单易懂,它是说一个事务一旦提交(COMMIT),它对数据库中数据的改变就是永久性的。任何操作甚至是系统故障都不应该对其产生影响

可以看到,事务有着严格的判定标准,想要同时实现它们又要求有很高的性能,可谓是难上加难。所以,在各大数据库厂商的实现中,真正能够满足这四个特性的事务寥寥无几。例如:InnoDB 存储引擎默认的事务隔离级别是可重复读,它不能满足隔离性要求;而 MySQL 的 NDB Cluster 事务则不满足持久性和隔离性。所以,与其说 ACID 是事务必须满足的条件,不如说它们是衡量事务的严谨性标准。

1.3 并发事务会带来什么样的问题

如果你写过多线程的程序,那就一定对并发的概念不会陌生了。并发事务的概念是多个事务并发运行,那么,如果在并发运行的过程中对相同的数据进行了修改,就可能会引起一些问题。总结下来,可能出现的问题一共有三种:脏读、不可重复读、幻读。

  • 脏读:事务 A 读取了事务 B 当前更新的数据,但是事务 B 出现了回滚或未提交修改,事务 A 读到的数据就被称为 “脏数据”。通常情况下,使用 “脏数据” 会造成系统数据不一致,出现错误
  • 不可重复读:事务 A 在执行过程中多次读取同一数据,但是事务 B 在事务 A 的读取过程中对数据做了多次修改并提交,则会导致事务 A 多次读取的数据不一致,进而无法做出准确性判断
  • 幻读:事务 A 在执行过程中读取了一些数据,但是事务 B 随即插入了一些数据,那么,事务 A 重新读取时,发现多了一些原本不存在的数据,就像是幻觉一样,称之为幻读

仔细品味,可以发现,不可重复读与幻读从概念上来说,是非常相似的。区分它们只要记住:不可重复读指的是对原来存在的数据做修改,而幻读指的是新增或者删除数据。

2 解读事务隔离级别

SQL 标准定义了四种隔离级别,由低到高依次为:READ-UNCOMMITTED(未提交读)、READ-COMMITTED(提交读)、REPEATABLE-READ(可重复读)、SERIALIZABLE(串行化)。它们可以逐个解决脏读、不可重复读、幻读这几类问题。

2.1 SQL 标准定义的四种事务隔离级别

这里,我将按照隔离级别由低到高的顺序依次对它们进行解读:

  • READ-UNCOMMITTED:它是最低的隔离级别,正如它的名称一样,它允许一个事务读取其他事务未提交的数据。这个隔离级别很少在工业环境中应用,因为它的性能并不会比其他高级别的性能好很多
  • READ-COMMITTED:它可以保证一个事务修改的数据提交之后才能被其他的事务读取。这个隔离级别是大多数数据库系统的默认隔离级别,但并不是 MySQL 默认的
  • REPEATABLE-READ:它的核心在于 “可重复”,即在一个事务内,对同一字段的多次读取结果都是相同的,也是 MySQL 的默认事务隔离级别
  • SERIALIZABLE:它是最高的隔离级别,花费的代价也是最高的,事务的处理是顺序执行的。在这个级别上,可能会导致大量的锁超时现象和锁竞争。同样,在工业级环境中,很少被使用

仔细分析这四种隔离级别,是不是发现:除 SERIALIZABLE 之外的另外三种都不能解决所有的问题。所以,在实际的应用中,一定是有所取舍的。

2.2 不同事务隔离级别可能会产生的问题

隔离级别越低,事务请求的锁也就越少,所以,可能出现的问题也就越多。而 SERIALIZABLE,它通过强制事务排序,并按顺序执行,使各个事务之间不可能会产生冲突,从而才能够解决脏读、不可重复读、幻读所有的问题。下面,我用一张表来总结各个事务隔离级别能够解决的问题(使用 Y 标识)与不能够解决的问题(使用 N 标识)。

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED N N N
READ-COMMITTED Y N N
REPEATABLE-READ Y Y N
SERIALIZABLE Y Y Y

具体选择哪一种隔离级别应该是多个维度的考虑,例如:事务请求锁的多少(性能问题)、能够解决什么问题、业务特点等等。一般情况下,使用 InnoDB 存储引擎,我们会选择 READ-COMMITTED。

3 实践不同事务隔离级别对事务的影响

理论知识总是不直观的,下面,我将使用示例的形式直观的感受下不同事务隔离级别对事务的影响。首先,我们先去看一看 InnoDB 存储引擎系统级的隔离级别和会话级的隔离级别(默认情况下的):

-- global.tx_isolation 代表系统级的隔离级别,即对所有的会话都生效
-- tx_isolation 代表会话级隔离级别,只对当前会话生效
mysql> SELECT @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+

修改隔离级别的方式也非常简单,示例如下:

-- 在 session1 中修改会话的隔离级别
mysql> SET session tx_isolation='read-uncommitted';
Query OK, 0 rows affected, 1 warning (0.00 sec)

-- session1 的 tx_isolation 修改成功
mysql> SELECT @@global.tx_isolation, @@tx_isolation;
+-----------------------+------------------+
| @@global.tx_isolation | @@tx_isolation   |
+-----------------------+------------------+
| REPEATABLE-READ       | READ-UNCOMMITTED |
+-----------------------+------------------+

-- 对 session2 并没有产生影响
mysql> SELECT @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+

默认情况下,SQL 语句是自动提交的,我们也可以将自动提交关闭:

-- 查看默认的 SQL 语句自动提交
mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+

-- 关闭自动提交
mysql> SET autocommit = off;

-- 检验自动提交是否已关闭
mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+

在实际操作中还会使用到一些并发控制语句:

  • start transaction:显式地开启一个事务
  • commit:提交事务,使得对数据库做的所有修改成为永久性
  • rollback:回滚结束用户的事务,并撤销正在进行的所有未提交的修改

最后,我们还需要有一张示例表并填充一些示例数据,如下所示 worker 表:

mysql> SELECT id, type, name, salary FROM worker;
+----+------+--------+--------+
| id | type | name   | salary |
+----+------+--------+--------+
|  1 | A    | tom    |   1800 |
|  2 | B    | jack   |   2100 |
|  3 | C    | pony   |   NULL |
|  4 | B    | tony   |   3600 |
|  5 | B    | marry  |   1900 |
|  6 | C    | tack   |   1200 |
|  7 | A    | tick   |   NULL |
|  8 | B    | clock  |   2000 |
|  9 | C    | noah   |   1500 |
| 10 | C    | jarvis |   1800 |
+----+------+--------+--------+

好的,准备工作已经就绪了,接下来,我们开启两个会话(MySQL 客户端),看一看数据库处于不同的事务隔离级别会有怎样的状况发生。

3.1 READ-UNCOMMITTED 级别

-- 修改 “会话 A” 的事务隔离级别
mysql> SET session tx_isolation='read-uncommitted';

-- 查看 “会话 A” 的事务隔离级别(之后将不再重复查看)
mysql> SELECT @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+

-- “会话 A” 开启事务
mysql> start transaction;

-- “会话 A” 查询 worker 表中 id 为1的记录
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   1800 |
+----+------+------+--------+

-- “会话 B” 开启事务
mysql> start transaction;

-- “会话 B” 修改 id 为1的记录,将 salary 修改为 2000,但是并不提交
mysql> UPDATE worker SET salary = 2000 WHERE id = 1;

-- “会话 A” 再次查询 worker 表中 id 为1的记录
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   2000 |
+----+------+------+--------+

3.2 READ-COMMITTED 级别

-- 修改 “会话 A” 的事务隔离级别
mysql> SET session tx_isolation='read-committed';

-- “会话 A” 开启事务
mysql> start transaction;

-- “会话 A” 查询 worker 表中 id 为1的记录
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   1800 |
+----+------+------+--------+

-- “会话 B” 开启事务
mysql> start transaction;

-- “会话 B” 修改 id 为1的记录,将 salary 修改为 2000,但是并不提交
mysql> UPDATE worker SET salary = 2000 WHERE id = 1;

-- “会话 A” 再次查询 worker 表中 id 为1的记录,数据并没有发生变化,因为 “会话 B” 并未提交
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   1800 |
+----+------+------+--------+

-- “会话 B” 提交
mysql> commit;

-- “会话 A” 再次查询 worker 表中 id 为1的记录
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   2000 |
+----+------+------+--------+

3.3 REPEATABLE-READ 级别

-- 修改 “会话 A” 的事务隔离级别
mysql> SET session tx_isolation='repeatable-read';

-- “会话 A” 开启事务
mysql> start transaction;

-- “会话 A” 查询 worker 表中 id 为1的记录
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   1800 |
+----+------+------+--------+

-- “会话 B” 开启事务
mysql> start transaction;

-- “会话 B” 修改 id 为1的记录,将 salary 修改为 2000,但是并不提交
mysql> UPDATE worker SET salary = 2000 WHERE id = 1;

-- “会话 A” 再次查询 worker 表中 id 为1的记录,数据并没有发生变化
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   1800 |
+----+------+------+--------+

-- “会话 B” 提交
mysql> commit;

-- “会话 A” 再次查询 worker 表中 id 为1的记录,数据仍没有发生变化
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   1800 |
+----+------+------+--------+

3.4 SERIALIZABLE 级别

-- 修改 “会话 A” 的事务隔离级别
mysql> SET session tx_isolation='serializable';

-- “会话 A” 开启事务
mysql> start transaction;

-- “会话 A” 查询 worker 表中 id 为1的记录
mysql> SELECT * FROM worker WHERE id = 1;
+----+------+------+--------+
| id | type | name | salary |
+----+------+------+--------+
|  1 | A    | tom  |   1800 |
+----+------+------+--------+

-- “会话 B” 开启事务
mysql> start transaction;

-- “会话 B” 修改 id 为1的记录,将 salary 修改为 2000,由于 “会话 A” 事务没有提交,“会话 B” 的事务一直处于等待状态,直到超时
mysql> UPDATE worker SET salary = 2000 WHERE id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

综上可以看到,每一种事务隔离级别有着不一样的操作效果。想要更好的理解它们,就应该立刻动手去实验,但是需要特别注意事务开启、提交、回滚顺序的正确性,否则,可能会得出不一样的结论。

4 总结

事务隔离级别是 MySQL 中的进阶知识点,想要把它完全搞明白并不是一件简单的事。同时,也正是由于它的存在,才保证了数据并发的正确性。虽然目前的各种 ORM 框架已经封装了事务的开启、提交、回滚过程,但是,完全的掌握还是非常有必要的。这将有利于你在工作中写出正确的代码,以及在面试中能够驾轻就熟。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容