事务专题(一)——数据库事务

事务的基本要素

事务的四个基本要素:ACID

原子性(A):整个事务中的操作,要么全部完成,要么全部不完成(全部撤销)

一致性(C):事务开始之前和结束之后,数据库的完整性没有遭到破坏

隔离性(I):在同一时间,只允许一个事务请求同一数据

持久性(D):事务完成以后,该事务对数据库所做的操作持久化在数据库中,并不会被回滚

原子性与一致性

在数据库实现的场景中,一致性可以分为数据库外部的一致性和数据库内部的一致性。前者由外部应用的编码来保证,即某个应用在执行转帐的数据库操作时,必须在同一个事务内部调用对帐户A和帐户B的操作。如果在这个层次出现错误,这不是数据库本身能够解决的,也不属于我们需要讨论的范围。后者由数据库来保证,即在同一个事务内部的一组操作必须全部执行成功(或者全部失败)。这就是事务处理的原子性。

为了实现原子性,需要通过日志:将所有对数据的更新操作都写入日志,如果一个事务中的一部分操作已经成功,但以后的操作,由于断电/系统崩溃/其它的软硬件错误而无法继续,则通过回溯日志,将已经执行成功的操作撤销,从而达到“全部操作失败”的目的。最常见的场景是,数据库系统崩溃后重启,此时数据库处于不一致的状态,必须先执行一个crash
recovery的过程:读取日志进行REDO(重演将所有已经执行成功但尚未写入到磁盘的操作,保证持久性),再对所有到崩溃时尚未成功提交的事务进行UNDO(撤销所有执行了一部分但尚未提交的操作,保证原子性)。crash
recovery结束后,数据库恢复到一致性状态,可以继续被使用。

其中一致性还可以分为:

  • 强一致性:读操作可以立即读到提交的更新操作。
  • 弱一致性:提交的更新操作,不一定立即会被读操作读到,此种情况会存在一个不一致窗口,指的是读操作可以读到最新值的一段时间。
  • 最终一致性:是弱一致性的特例。事务更新一份数据,最终一致性保证在没有其他事务更新同样的值的话,最终所有的事务都会读到之前事务更新的最新值。如果没有错误发生,不一致窗口的大小依赖于:通信延迟,系统负载等。

隔离性

本文重点讨论事务的隔离性,隔离性是为了保证并发情况下数据库的强一致性,下面探讨一下并发情况下带来的问题。

事务中经常出现的并发问题

脏读: 一个事务读取了另一个事务操作但未提交的数据

比如A、B两个事务,都操作同一张表,A刚刚对数据进行了操作(插入、修改等)但还没有提交,这时B读取到了A刚刚操作的数据,因为A有可能回滚,所以这部分数据有可能只是临时的、无效的,即脏数据。

不可重复读:一个事务中的多个相同的查询返回了不同数据

比如A、B两个事务,A中先后有两次查询相同数据的操作,第一次查询完之后,B对相关数据进行了修改,造成A事务第二次查询出的数据与第一次不一致。

幻读:事务并发执行时,其中一个事务对另一个事务中操作的结果集的影响

比如A、B两个事务,事务A操作表中符合条件的若干行。事务B插入符合A操作条件的数据行,然后再提交。后来发现事务A并没有如愿对“所有”符合条件的数据行做了修改。

请注意不可重复读和幻读的区别:不可重复读和幻读都是在另一个事务提交之后才操作,但不可重复读只针对某一条特定数据,而幻读指的是整个操作结果集;下面提到的Repeatable read隔离级别可以防止脏读、不可重复读,但是不能防止幻读,是因为该隔离级别只应用了行锁锁住了某一行特定数据,而没有用表锁把整个表锁住,而Serializable隔离级别就可以锁住整个表,从而防止幻读。

SQL规范定义的四个事务隔离级别

以上都是事务中经常发生的问题,所以为了兼顾并发效率和异常控制,SQL规范定义了四个事务隔离级别:

Read uncommitted(读未提交):如果设置了该隔离级别,则当前事务可以读取到其他事务已经修改但还没有提交的数据。这种隔离级别是最低的,会导致上面所说的脏读

Read committed(读已提交):如果设置了该隔离级别,当前事务只可以读取到其他事务已经提交后的数据,这种隔离级别可以防止脏读,但是会导致不可重复读和幻读。这种隔离级别最效率较高,并且不可重复读和幻读在一般情况下是可以接受的,所以这种隔离级别最为常用

<span id="repeatableRead">Repeatable read(可重复读)</span>:如果设置了该隔离级别,可以保证当前事务中多次读取特定记录的结果相同。可以防止脏读、不可重复读,但是会导致幻读

<span id="serializable">Serializable(串行化)</span>:如果设置了该隔离级别,所有的事务会放在一个队列中执行,当前事务开启后,其他事务将不能执行,即同一个时间点只能有一个事务操作数据库对象。这种隔离级别对于保证数据完整性的能力是最高的,但因为同一时刻只允许一个事务操作数据库,所以大大降低了系统的并发能力

引用一张很经典的表格:

事务隔离级别 是否存在脏读 是否存在不可重复读 是否存在幻读
Read uncommitted
Read committed ×
Repeatable read × ×
Serializable × × ×

并且隔离级别越高,并发性能越弱:

隔离级别和并发性能的关系图

事务隔离级别测试

read uncommitted测试

打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:

mysql> set session transaction isolation level read uncommitted;
mysql> select @@tx_isolation;
+--------------------+
| @@tx_isolation     |
+--------------------+
| READ-UNCOMMITTED   |
+--------------------+
1 row in set, 1 warning (0.01 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

在客户端A的事务提交之前,打开另一个客户端B,更新表account:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance=balance+1000 where id=1;
Query OK, 1 rows affected (0.01 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    2000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

这时,虽然客户端B的事务还没提交,打开客户端A查询数据,发现已经可以查询到客户端B更新的数据:

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    2000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

客户端B回滚,所有的操作都将会被撤销:

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

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

客户端A再次查询:

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

所以一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据

read committed测试

针对上面的问题,把客户端A的事务隔离级别设置为read committed,再重复上面的步骤,会发现B在事务结束之前,A并不能查询到B所做的操作。

客户端A设置事务隔离级别为read committed,然后开启事务:

mysql> set session transaction isolation level read committed;
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-COMMITTED   |
+------------------+
1 row in set, 1 warning (0.01 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

客户端B开启事务并更新数据:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

mysql> update account set balance=balance+1000 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    2000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

客户端A在客户端B未提交事务前读取数据:

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

正如上文所说,这种隔离级别下可能导致前事务中多次读取特定记录的结果不相同,比如客户端A事务隔离级别为read committed,在A的一个事务中,执行两次相同的查询,在这两次查询的中间,客户端B对数据进行更改并提交事务,那么会导致客户端A的两次查询结果不一致,导致“不可重复读”的麻烦。

客户端B提交事务:

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

客户端A再次读取数据:

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    2000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

repeatable read测试

同理,如果设置了repeatable read隔离级别,就可以保证在当前事务中多次执行相同查询的结果集相同,实现“可重复读”。

客户端A设置事务隔离级别为repeatable read并开启事务:

mysql> set session transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

客户端B开启事务,修改数据并提交事务:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance=5000 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    5000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

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

客户端A查询,数据没有发生变化:

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

可以发现,在同一个事务A里,无论事务B是否提交,读到的数据都是相同的。

但是,这种隔离级别会导致“幻读”,比如客户端A中事务操作表中符合条件的若干行,同时客户端B中事务插入符合A操作条件的数据行,然后再提交。

客户端A设置事务隔离级别为repeatable read并开启事务:

mysql> set session transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
+----+------+---------+
3 rows in set (0.00 sec)

客户端B开启事务,插入数据并提交事务:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values(4, 'd', 8888);
Query OK, 1 row affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
|  4 | d    |    8888 |
+----+------+---------+
4 rows in set (0.00 sec)

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

客户端A更新全表数据:

mysql> update account set balance=9999;
Query OK, 4 rows affected (0.00 sec)
Rows matched: 4  Changed: 4  Warnings: 0

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    9999 |
|  2 | b    |    9999 |
|  3 | c    |    9999 |
|  4 | d    |    9999 |
+----+------+---------+
4 rows in set (0.00 sec)

奇怪,这样操作并不会发生幻读,那如果事务B在事务A更新之后插入数据,会不会产生幻读呢,我们再来做一个测试:

客户端A设置事务隔离级别为repeatable read并开启事务,并在开启事务后更新全表数据

mysql> set session transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
|  4 | d    |    8888 |
+----+------+---------+
3 rows in set (0.00 sec)

mysql> update account set balance=9999;
Query OK, 4 rows affected (0.00 sec)
Rows matched: 4  Changed: 4  Warnings: 0

客户端B开启事务,插入数据:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
|  4 | d    |    8888 |
+----+------+---------+
4 rows in set (0.00 sec)

mysql> insert into account values(5, 'e', 5555);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction  //阻塞超时

这个时候客户端B阻塞超时,可以发现,事务A正在执行写操作时,事务B是阻塞的(第一次测试事务B没有阻塞是因为事务A没有进行写操作)

为了不阻塞,在客户端A提交事务:

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

客户端A提交事务后,客户端B再次插入数据:

mysql> insert into account values(5, 'e', 5555);  //事务A已提交,没有阻塞
Query OK, 1 row affected (2.92 sec)

mysql> select * from account; //因为事务B的默认隔离级别是REPEATABLE-READ,即使事务A提交后也读不到更新后的数据
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    1000 |
|  2 | b    |    1200 |
|  3 | c    |    6666 |
|  4 | d    |    8888 |
|  5 | e    |    5555 |
+----+------+---------+
5 rows in set (0.00 sec)

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

mysql> select * from account; //事务B提交后,就能读到事务A更新的数据了
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    9999 |
|  2 | b    |    9999 |
|  3 | c    |    9999 |
|  4 | d    |    9999 |
|  5 | e    |    5555 |
+----+------+---------+
5 rows in set (0.00 sec)

客户端A再次读取数据:

mysql> select * from account; //两个事务都提交后客户端A读到的数据,但这不算是幻读,因为事务A已经提交
+----+------+---------+
| id | name | balance |
+----+------+---------+
|  1 | a    |    9999 |
|  2 | b    |    9999 |
|  3 | c    |    9999 |
|  4 | d    |    9999 |
|  5 | e    |    5555 |
+----+------+---------+
5 rows in set (0.00 sec)

可以发现,事务A正在执行写操作时,事务B是阻塞的(第一次测试事务B没有阻塞是因为事务A没有进行写操作),可见mysql的repeatable read隔离级别和Serializable是一样的,不会产生幻读!

serializable测试

笔者再次把事务隔离级别设置为serializable,实验结果与repeatable read的第二测试一样。

再次证明,mysql的repeatable read隔离级别和Serializable是一样的,不会产生幻读!

参考文献:

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

推荐阅读更多精彩内容