美文网首页
事务专题(一)——数据库事务

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

作者: BugBean | 来源:发表于2018-07-21 10:14 被阅读0次

事务的基本要素

事务的四个基本要素: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是一样的,不会产生幻读!

参考文献:

相关文章

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

    事务的基本要素 事务的四个基本要素:ACID 原子性(A):整个事务中的操作,要么全部完成,要么全部不完成(全部撤...

  • 数据库事务简介(一)--- 所谓事务

    事务的概念 数据库事务简介(一)--- 所谓事务数据库事务简介(二)--- 故障恢复(未完成)数据库事务简介(三)...

  • 数据库笔记

    数据库 数据库⭐MySQL 默认存储引擎InnoDB(事务性存储引擎)一、事务 数据库事务? 数据库事务有什么作用...

  • Django之数据库事务编程

    在了解 Django 数据库事务编程前有必要先了解下数据库事务。 数据库事务(transaction) 数据库事务...

  • Spring学习之事务管理基础

    Spring学习之事务管理基础 数据库事务相关概念 事务的定义 事务,一般指的是数据库事务,是指数据库操作的时候,...

  • 事务及事务隔离级别

    事务及事务隔离级别 什么是事务 事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务...

  • 搞懂Redis(四)-Redis事务

    事务本质:一组命令的集合 数据库事务与Redis事务 数据库事务数据库事务通过ACID(原子性.一致性.隔离性.持...

  • spring事务

    1、什么是事务事务和数据库操作有关,commit事务提交,rollback事务回滚事务作用:保证一组和数据库有关的...

  • SpringBoot 数据库事务

    SpringBoot 数据库事务 事务: 数据库事务是 访问并可能更新 数据库中各种数据项的一个程序执行单元 事务...

  • SPRING的事务详解

    一、什么是事务 事务的概念 事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。事务的正...

网友评论

      本文标题:事务专题(一)——数据库事务

      本文链接:https://www.haomeiwen.com/subject/thocmftx.html