MySQL 的四种事务隔离级别
MySQL 事务 面试 About 12,066 words事务的基本要素(ACID)
原子性(Atomicity)
事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
一致性(Consistency)
事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A
向B
转账,不可能A
扣了钱,B
却没收到。
隔离性(Isolation)
同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A
正在从一张银行卡中取钱,在A
取钱的过程结束前,B
不能向这张卡转账。
持久性(Durability)
事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务的并发问题
脏读
事务A
读取了事务B
更新的数据,然后B
回滚操作,那么A
读取到的数据是脏数据。
不可重复读
不能多次读,多次读数据就不一样了。
事务A
多次读取同一数据,事务B
在事务A
多次读取的过程中,对数据作了更新并提交,导致事务A
多次读取同一数据时,结果不一致。
幻读
系统管理员A
将数据库中所有学生的成绩从具体分数改为ABCDE
等级,但是系统管理员B
就在这个时候插入了一条具体分数的记录,当系统管理员A
改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(READ-UNCOMMITTED) | 是 | 是 | 是 |
读已提交(READ-COMMITTED) | 否 | 是 | 是 |
可重复读(REPEATABLE-READ) | 否 | 否 | 是 |
串行化(SERIALIZABLE) | 否 | 否 | 否 |
MySQL默认事务隔离级别
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
读未提交
第一步:打开一个客户端A
,并设置当前事务模式为read uncommitted
(读未提交),查询表account
的记录:
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 100 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第二步:打开另一个客户端B
,并设置当前事务模式为read uncommitted
(读未提交),更新表account
:
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance=balance-50 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 | jack | 50 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第三步:客户端B
的事务还没提交,但是客户端A
就可以查询到B
已经更新的数据:
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 100 | -- 此处为第一步读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql>
mysql>
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 | -- 此处为新读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第四步:客户端B
的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A
查询到的数据其实就是脏数据:
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> rollback; -- 客户端B执行回滚
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 100 | -- 客户端B中已经是最新的数据了
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第五步:客户端A
在客户端B
事务回滚后执行更新语句,发现并不是预想的50-50=0
,解决此脏读现象可使用读已提交(READ-COMMITTED)隔离级别。
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> update account set balance=balance-50 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 | jack | 50 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
读已提交
第一步:打开一个客户端A
,并设置当前事务模式为read committed
(读已提交),查询表account
的记录:
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第二步:打开另一个客户端B
,并设置当前事务模式为read committed
(读已提交),更新表account
:
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance=balance-50 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 | jack | 0 |
| 2 | smith | 150 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第三步:客户端B
的事务还没提交,客户端A
不能查询到B
已经更新的数据,解决了脏读问题,在客户端A
查询:
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 | -- 此处为第一步读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 | -- 此处为新读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第四步:客户端B
的事务提交
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance=balance-50 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 | jack | 0 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
第五步:客户端A
执行与上一步(第三步)相同的查询,结果与上一步(第三步)不一致,即产生了不可重复读的问题(每次读取数据不一致问题)。
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 | -- 此处为第一步读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 50 | -- 此处为第三步读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql>
mysql>
mysql> select * from account; -- 此处客户端B已经提交了事务
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 0 | -- 此处为新读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
可重复读
第一步:打开一个客户端A
,并设置当前事务模式为repeatable read
(可重复读),查询表account
的记录:
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 0 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第二步:打开另一个客户端B
,并设置当前事务模式为repeatable read
(可重复读),更新表account
并提交:
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance=balance-50 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 | jack | -50 |
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
第三步:在客户端A
查询表account
的记录,与第一步查询结果一致,没有出现不可重复读的问题:
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 0 | -- 此处为第一步读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 0 | -- 此处为新取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
第四步:在客户端A
执行更新语句,发现并不是0-50=-50
,而是根据客户端B
已提交的事务(第二步)得到的数据值-50-50=-100
。数据的一致性是没有被破坏。可重复读的隔离级别下使用了MVCC
机制,select
操作不会更新版本号,是快照读(历史版本);insert
、update
和delete
会更新版本号,之后再进行select
操作则会得到最新的版本号。
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | 0 | -- 此处为第三步读取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql> update account set balance=balance-50 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | -100 | -- 此处为新取到的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
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,'peter', 400);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
第六步:在客户端A
查询表account
的记录,没有查出新增的数据,所以没有出现幻读:
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | -100 | -- 此处为第四步读取的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
mysql>
mysql>
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | -100 | -- 此处为新读取的值
| 2 | smith | 200 |
| 3 | ray | 300 |
+----+-------+---------+
3 rows in set (0.00 sec)
串行化
第一步:打开一个客户端A
,并设置当前事务模式为serializable
(串行化),查询表account
的记录:
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | jack | -100 |
| 2 | smith | 200 |
| 3 | ray | 300 |
| 4 | peter | 400 |
+----+-------+---------+
4 rows in set (0.00 sec)
第二步:打开一个客户端B
,并设置当前事务模式为serializable
,插入一条记录报错,表被锁了一直等待插入,MySQL
中事务隔离级别为serializable
时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(5,'kitty', 500);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
补充
- 事务隔离级别为读提交时,写数据只会锁住相应的行;
- 事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是
next-key
锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读; - 事务隔离级别为串行化时,读写数据都会锁住整张表;
- 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大;
MySQL MVCC
实现机制参考链接:https://blog.csdn.net/whoamiyang/article/details/51901888- 关于
next-key
锁可以参考链接:https://www.jianshu.com/p/bf862c37c4c9
原文
————        END        ————
Give me a Star, Thanks:)
https://github.com/fendoudebb/LiteNote扫描下方二维码关注公众号和小程序↓↓↓