MySQL数据库事务的隔离级别

MySQL数据库事务的隔离级别

一、事务隔离级别的概念

数据库资源可以被多个用户同时访问,数据库在并发访问时,如果不采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性,此时需要为事务设置隔离级别。事务隔离级别是指在处理同一个数据的多个事务中,一个事务修改数据后,其他事务何时能看到修改后的结果。在MySQL数据库中事务有四种隔离级别,由低到高依次为:
(1)Read uncommitted(读取未提交):这是事务中最低的隔离级别,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。该隔离级别可能出现脏读,一般不使用;
(2)Read committed(读取已提交):其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值。该隔离级别可以避免脏读,但不能避免不可重复读和幻读;
(3)Repeatable read(可重复读):是MySQL的默认事务隔离级别。在该隔离级别,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响,可以避免脏读和不可重复读的问题,但不能避免幻读问题;
(4)Serializable(可串行化):事务中最高的隔离级别,通过强制事务排序使之不可能相互冲突,但性能很低,一般很少使用。在该隔离级别,事务顺序执行,可以避免脏读、不可重复读和幻读问题。

各种事务隔离级别存在的问题:

隔离级别 脏读 不可重复读 幻读
read uncommitted(读取未提交)
read committed(读取已提交) ×
repeatable read(可重复读) × ×
serializable(可串行化) × × ×

查看MySQL的事务隔离级别:

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

设置MySQL的事务隔离级别,语法如下:

set session transaction isolation level
{read uncommitted | read committed | repeatable read | serializable}

二、数据准备

创建用户账户(account)表并输入数据:

create table account(
account_id int primary key,
name char(20) not null default '',
balance decimal(16,2) not null default 0
) engine=innoDB;

insert into account values(1001,'Zhangsan',5000),(1002,'Lisi',5000),
(1003,'Wangwu',1000),(1004,'Liuping',500);

三、脏读

(一)造成脏读的原因分析

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

当事务的隔离级别设置为read uncommitted时可能出现脏读问题。使用两个客户端来模拟并发操作,将客户端A(登录用户为zhang)和客户端B(登录用户为liu)的事务隔离级别设置为read uncommitted,结果如下:

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

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

--在客户端A查看account表的信息
mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 4000.00 |
|       1002 | Lisi     | 6000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

1、在客户端B(登录用户为liu)中开启事务,然后进行转账操作:

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 4000.00 |
|       1002 | Lisi     | 6000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

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

mysql> update account set balance=balance-500 where account_id=1001;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> update account set balance=balance+500 where account_id=1002;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

2、在客户端B没有提交事务的情况下,在客户端A中查看account表的数据:

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 3500.00 |
|       1002 | Lisi     | 6500.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

3、发现已经转账成功!但此时客户端B中的事务并没有提交,这就是【脏读】。

当客户端B回滚了事务,此时在客户端A查看账户信息:

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 4000.00 |
|       1002 | Lisi     | 6000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

可以看出,客户端A查询到的数据发生了变化,变成了转账之前的情况。

(二)脏读的危害

对于脏读,客户端A如果仅仅是查看数据,则客户端B事务提交前和提交后,客户端A看到的数据会不一致,数据库中的数据不受影响。但如果在客户端B没有提交的情况下,客户端A对数据进行了更改,就会出现数据不一致问题。

将客户端A(登录用户为zhang)和客户端B(登录用户为liu)的事务隔离级别设置为read uncommitted。

1、在客户端B(登录用户为liu)中开启事务,然后进行转账操作:

客户端B的操作过程如下:

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 4000.00 |
|       1002 | Lisi     | 6000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

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

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

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

2、在客户端B没有提交事务的情况下,在客户端A中查看account表的数据,并对数据进行修改:

客户端A的操作过程如下,此时客户端A的更新语句被阻塞。

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 2000.00 |
|       1002 | Lisi     | 8000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

mysql> update account set balance=balance-8000 where account_id=1002;

3、在客户端B中的执行事务的回滚:

客户端B的操作过程如下:

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

4、在客户端A中查看账户信息

mysql> update account set balance=balance-8000 where account_id=1002;
Query OK, 1 row affected (5.81 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from account;
+------------+----------+----------+
| account_id | name     | balance  |
+------------+----------+----------+
|       1001 | Zhangsan |  4000.00 |
|       1002 | Lisi     | -2000.00 |
|       1003 | Wangwu   |  1000.00 |
|       1004 | Liuping  |   500.00 |
+------------+----------+----------+
4 rows in set (0.00 sec)

可以看到,在客户端B中执行rollback回滚之后,客户端A中的update命令执行成功了!Lisi的账户余额变成了负值。

四、不可重复读

(一)造成不可重复读原因分析

不可重复读是指在事务A中,读取了一个数据,在事务A还没有结束时,事务B读取访并修改了这个数据,并提交。紧接着,事务A又读取这个数据,但由于事务B已经修改了这个数据,导致事务A两次读到的的数据是不一样的,称为不可重复读。

当事务的隔离级别设置为read committed时可能出现不可重复读的问题。使用两个客户端来模拟并发操作,将客户端A(登录用户为zhang)和客户端B(登录用户为liu)的事务隔离级别设置为read committed,结果如下:

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

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

1、在客户端A中开启事务,查看accout表的账户信息:

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

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 4000.00 |
|       1002 | Lisi     | 6000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

2、在客户端B中开启事务,然后进行转账操作,并提交事务

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

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

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

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

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 3500.00 |
|       1002 | Lisi     | 6500.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

3、在客户端A中查看account表的数据

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 3500.00 |
|       1002 | Lisi     | 6500.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

此时,客户端A查询到了客户端B修改后的数据,而客户端A中的事务还没有结束。这意味着,客户端A在同一个事务中查询同一个表,两次查询的结果不一致。不过在大多数场合,这种问题是可以接受的,因此大部分数据库管理系统使用该隔离级别,比如Oracle。

该隔离级别不会发生脏读,因为在客户端B没有提交数据的情况下,客户端A是无法看到该数据的。

(二)把隔离级别设置为可重复读(repeatable read)

使用两个客户端来模拟并发操作,将客户端A(登录用户为zhang)和客户端B(登录用户为liu)的事务隔离级别设置为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)

1、在客户端A中开启事务,查看accout表的账户信息:

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

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 4000.00 |
|       1002 | Lisi     | 6000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

2、在客户端B中开启事务,然后进行转账操作,并提交事务

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

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

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

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

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 3500.00 |
|       1002 | Lisi     | 6500.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

3、在客户端A中查看account表的数据

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 4000.00 |
|       1002 | Lisi     | 6000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

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

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 3500.00 |
|       1002 | Lisi     | 6500.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
4 rows in set (0.00 sec)

可以看出,即使客户端B提交了事务,由于客户端A的事务还没有结束,客户端A看到的account表仍然是转账之前的信息。然后客户端A提交事务(也可以回滚事务),才能看到最终的更新结果。

五、幻读

当事务的隔离级别为repeatable read(可重复读)时,可能出现幻读的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。即在一个事务内两次查询的数据行数不一致,与不可重复读的问题类似,这都是因为在查询过程中其他事务做了更新操作。
不可重复读和幻读两者有些相似,但不可重复读重点在于update和delete,而幻读的重点在于insert。

举个例子:就是事务A查询id<10的记录时,返回了2条记录,接着事务B插入了一条id为3的记录,并提交。接着事务A查询id<10的记录时,返回了3条记录,结果却多了一条数据。

InnoDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。

使用两个客户端来模拟并发操作,将客户端A(登录用户为zhang)和客户端B(登录用户为liu)的事务隔离级别设置为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)

1、在客户端A中开启事务,查看accout表中account_id<1003的账户信息:

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

mysql> select * from account where account_id<1003;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 3000.00 |
|       1002 | Lisi     | 7000.00 |
+------------+----------+---------+
2 rows in set (0.00 sec)

2、在客户端B中开启事务,然后插入一条新记录,并提交事务

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

mysql> insert into account values(1000,'Tom',1000);
Query OK, 1 row affected (0.00 sec)

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

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1000 | Tom      | 1000.00 |
|       1001 | Zhangsan | 3000.00 |
|       1002 | Lisi     | 7000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
5 rows in set (0.00 sec)

3、在客户端A中查看accout表中account_id<1003的账户信息

mysql> select * from account where account_id<1003;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1001 | Zhangsan | 3000.00 |
|       1002 | Lisi     | 7000.00 |
+------------+----------+---------+
2 rows in set (0.00 sec)

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

mysql> select * from account where account_id<1003;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1000 | Tom      | 1000.00 |
|       1001 | Zhangsan | 3000.00 |
|       1002 | Lisi     | 7000.00 |
+------------+----------+---------+
3 rows in set (0.00 sec)

可以看出,客户端A并没有出现幻读,这是因为InnoDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。

六、可串行化(serializable)

当事务的隔离级别为可串行化(serializable)时,在每一行读取的数据上都会加锁,不会出现相互冲突,但这样会导致严重的性能问题,在实际应用中,一般不使用此隔离级别。

使用两个客户端来模拟并发操作,将客户端A(登录用户为zhang)和客户端B(登录用户为liu)的事务隔离级别设置为serializable,结果如下:

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

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

1、在客户端A中开启事务,查看accout表的账户信息:

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

mysql> select * from account;
+------------+----------+---------+
| account_id | name     | balance |
+------------+----------+---------+
|       1000 | Tom      | 1000.00 |
|       1001 | Zhangsan | 3000.00 |
|       1002 | Lisi     | 7000.00 |
|       1003 | Wangwu   | 1000.00 |
|       1004 | Liuping  |  500.00 |
+------------+----------+---------+
5 rows in set (0.00 sec)

2、在客户端B中进行更新操作

mysql> update account set balance=balance+500 where account_id=1002;

此时,客户端B的更新操作处于阻塞状态,只有当客户端A的事务提交之后,其他客户端才能更新。当客户端A长时间没有提交事务时,其他客户端的更新操作就会出现超时,使更新操作无法完成。

发布了44 篇原创文章 · 获赞 48 · 访问量 5398

猜你喜欢

转载自blog.csdn.net/weixin_44377973/article/details/103281577