Mysql 数据库事物及隔离级别
- 事务的基本要素(ACID)
- 事务的隔离级别问题
- 事务的隔离级别
- READ UNCOMMITTED 读未提交
- READ COMMITED 读已提交
- REPEATABLE READ 可重复读
- SERIALIZABLE 可串行化
- 数据准备
- READ UNCOMMITTED
- READ COMMITED
- REPEATABLE READ
- SERIALIZABLE
事务的基本要素(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 UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称作脏读。这个级别会导致很多问题,从性能上来说,READ UNCOMMITTED不会比其他的级别好太多,但确缺乏其他级别的很多好处,除非真的有必要的理由,在实际应用中一般很少使用。
READ COMMITED 读已提交
大多数数据库系统的默认隔离级别是 READ COMMITED(但mysql不是)。READ COMMITED满足前面提到的隔离性简单定义:一个事务开始时,只能看见已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。
REPEATABLE READ 可重复读
MySQL的默认事务隔离级别。
REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样的记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另一个幻读的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。InnoDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。
SERIALIZABLE 可串行化
SERIALIZABLE 是最高的隔离级别。它通过强事务串行执行,避免了前面说的幻读的问题。简单来说,SERIALIZABLE会在曲度的每一行数据都加上锁,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据一致性而且可以接受没有并发的情况下,才考虑使用该级别。
数据准备
CREATE TABLE `s_test` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态。0-删除,1-可用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
insert into s_test(`name`) values ('a'),('b'),('c'),('d');
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | a | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
READ UNCOMMITTED
分别开启2个数据库连接终端,模拟2个事物操作。
终端A 设置事物隔离级别Read UnCommited 然后读取测试表中的数据
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | a | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.02 sec)
终端B设置事物隔离级别Read UnCommited 然后修改测试表中的数据,并不提交事物
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 s_test set name = 'x' where id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
此时再去终端A中读取数据库中的数据
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | x | 2019-05-07 18:42:37 | 2019-05-08 10:46:50 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
可以发现事物A已经可以读到事物B未提交的修改,在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。
此时,我们将终端B的事物回滚
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
再到终端A查询数据库
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | a | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
发现数据已经回滚,则事物A中间那次读到的数据就是脏读。
READ COMMITED
同样,我们终端A 设置事物隔离级别Read Commited 然后读取测试表中的数据
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 s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | a | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
然后在终端B设置事物隔离级别Read Commited 然后修改测试表中的数据,并不提交事物
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 s_test set name = 'x' where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
此时再次到终端A查询数据库记录
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | a | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
此时我们会发现,我们终端B未提交的修改,我们并没有读到,即不会产生脏读。
此时,我们到终端B将刚做的修改提交
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
再次回到终端A查询数据库数据
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | x | 2019-05-07 18:42:37 | 2019-05-08 10:59:54 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
此时我们发现,我们已经读到了终端B事物提交的事物,但是这也产生了一个新的问题,即事物A的2次提交读到的不同的数据。即产生了重复读的问题。
REPEATABLE READ
同样,我们终端A 设置事物隔离级别Repeatable Read然后读取测试表中的数据
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 s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | x | 2019-05-07 18:42:37 | 2019-05-08 10:59:54 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
然后 在终端B 设置事物隔离级别Repeatable Read然后然后修改测试表中的数据,并提交事物
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 s_test set name = 'y' where id = 1;
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 s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | y | 2019-05-07 18:42:37 | 2019-05-08 11:08:51 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
此时,再到终端A查看数据
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | x | 2019-05-07 18:42:37 | 2019-05-08 10:59:54 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
发现事物B一提交的修改不回影响事物A的重复读取,即不回产生重复读的问题。
此时,再到终端B中插入一条记录
mysql> insert into s_test(`name`) values ('e');
Query OK, 1 row affected (0.02 sec)
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | y | 2019-05-07 18:42:37 | 2019-05-08 11:08:51 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 5 | e | 2019-05-08 11:13:54 | 2019-05-08 11:13:54 | 1 |
+----+------+---------------------+---------------------+--------+
5 rows in set (0.00 sec)
然后再回到终端A,插入一条看起来不存在的记录
mysql> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | x | 2019-05-07 18:42:37 | 2019-05-08 10:59:54 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
+----+------+---------------------+---------------------+--------+
4 rows in set (0.00 sec)
mysql> insert into s_test(id,name) values(5,'e');
ERROR 1062 (23000): Duplicate entry '5' for key 'PRIMARY'
即产生了幻读,读不出来已经存在的数据。
SERIALIZABLE
同样,我们在终端A中 设置事物隔离级别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> select * from s_test;
+----+------+---------------------+---------------------+--------+
| id | name | create_time | update_time | status |
+----+------+---------------------+---------------------+--------+
| 1 | y | 2019-05-07 18:42:37 | 2019-05-08 11:08:51 | 1 |
| 2 | b | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 3 | c | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 4 | d | 2019-05-07 18:42:37 | 2019-05-07 18:42:37 | 1 |
| 5 | e | 2019-05-08 11:13:54 | 2019-05-08 11:13:54 | 1 |
+----+------+---------------------+---------------------+--------+
5 rows in set (0.00 sec)
然后在终端B中设置事物隔离级别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 s_test(`name`) values ('f');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
然后会发现,终端B事务被挂住了,然后过了一段时间,提示了错误 (1205, u’Lock wait timeout exceeded; try restarting transaction’) ,说等待锁超时。
在串行化隔离级别中,事物会在读取的每一行数据都加上锁,也就是说,上面A事务在读取时,已经加了锁,此时B事务在插入操作时,得等待锁的放开,时间一长,A锁未放开,B就报错了。