理解 Mysql 事务隔离级别
多个事务并发的情况下,如果没有任何限制,一个事务的操作可能会影响另一个事务的执行结果,从而造成脏读、不可重复读、幻读等并发一致性问题。
并发一致性问题
脏读
脏读是指一个事务读取到另一个事务未提交的记录。比如:
事务 A 对数据库的一条记录进行了修改,但是 还未提交
此时事务 B 从数据库查询该条记录,查到了 事务 A 修改后的数据
然后事务 A 因为某种原因回滚,导致修改未成功提交,该条记录还是原来的值
上述情况下,第 2 步事务 B 就存在脏读
不可重复读
不可重复读是指一个事务执行过程中,存在另外一个事务对数据进行了修改并提交,结果导致前一事务中两次读取到的值不一样。例如单条记录的情况:
事务 A 读取了一条记录的值是
before
事务 B 修改了该记录的值为
after
,并且提交成功事务 A 再次读该记录,得到的值是
after
上述情况下,事务 A 在同一次事务中两次读取同一记录的值不一样,这就是不可重复读
幻读
幻读是指在一个事务的执行过程中,存在另外一个事务对数据集进行了修改(新增或删除了记录),结果导致这两个事务查询到的数据集不一致。比如说
事务 A 根据主键 ID 统计 1 至 15 之间的记录数量,得到一个结果 15
事务 B 成功删除了主键 ID 为 13 的记录,统计 1 至 15 之间的记录数量,结果为14
此时事务 A 再次进行统计,结果还是 15
上述情况下,事务 A 第二次统计的结果不正确,在此基础上进行后续操作可能会出错
事务隔离
事务隔离用于解决事务的并发一致性问题,Mysql 提供了 4 种隔离级别,用于解决上文提到的问题。不同的隔离级别与一致性问题的关系如下表所示:
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
未提交读(read uncommitted) | 是 | 是 | 是 |
提交读(read committed) | 否 | 是 | 是 |
可重复读(repeatable read) | 否 | 否 | 是 |
可串行化(serializable) | 否 | 否 | 否 |
从上图可见级别从低到高为:未提交读 -> 提交读 -> 可重复读 -> 可串行化
未提交读(read uncommitted)
未提交读是最低的隔离级别,存在脏读情况,可以通过一个简单的例子验证下。先准备下测试用的数据
准备工作
当前使用的 mysql 版本如下
1 | ➜ ~ mysql --version |
首先创建一个测试用表 USER
,存储引擎选择 InnoDB
,注意不要使用 MyISAM
引擎,因为它不支持事务,测了也白测。
1 | CREATE TABLE `USER` ( |
新增两条测试数据,下面就操作这些数据验证并发问题
1 | INSERT INTO `USER` (`ID`, `USERNAME`, `USERID`) |
测试过程
按如下步骤操作,测试 read uncommitted 隔离级别下脏读问题。为方便说明问题,这里使用了表格,可按照序号往下看,主要关注事务1和事务2的执行命令
事务1 | 事务2 | |||||
---|---|---|---|---|---|---|
序号 | 执行命令 | 输出结果 | 说明 | 执行命令 | 输出结果 | 说明 |
1 | set session transaction isolation level read uncommitted; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别 | |||
2 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
3 | select * from user; | 1 admin 1 2 test 2 | 查看当前数据 | |||
4 | update user set userid = 3 where id = 2; | Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 | 更新id为2的数据 | |||
5 | set session transaction isolation level read uncommitted; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别 | |||
6 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
7 | select * from user where id = 2; | 2 test 3 | 可以看到事务1尚未提交的数据 | |||
8 | rollback; | Query OK, 0 rows affected (0.12 sec) | 事务1回滚 | |||
9 | select * from user where id = 2; | 2 test 2 | 看到的是原数据 |
第 4 步 事务1
修改了 id = 2
的记录,尚未提交事务。
第 7 步 事务2
可以查询到 事务1
尚未提交的修改。第 8 步 事务1
回滚,此时 事务2
再次查询看到的是修改前的结果。
提交读(read committed)
脏读问题
read committed 事务隔离级别解决了脏读的问题,也就是不会读到其他事务尚未提交的对单条记录的修改。同样是上面的操作顺序,区别是隔离级别不同,下面看下执行结果
事务1 | 事务2 | |||||
---|---|---|---|---|---|---|
序号 | 执行命令 | 输出结果 | 说明 | 执行命令 | 输出结果 | 说明 |
1 | set session transaction isolation level read committed; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
2 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
3 | select * from user; | 1 admin 1 2 test 2 | 查看当前数据 | |||
4 | update user set userid = 3 where id = 2; | Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 | 更新id为2的数据 | |||
5 | set session transaction isolation level read committed; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
6 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
7 | select * from user where id = 2; | 2 test 2 | 无法看到事务1尚未提交的数据 | |||
8 | commit; | Query OK, 0 rows affected (0.12 sec) | 事务1提交 | |||
9 | select * from user where id = 2; | 2 test 3 | 看到的是修改后的数据 |
注意第 7 步,这次 事务2
不能看到 事务1
未提交的修改了。因为事务1
还未提交,所以 事务2
查询到的还是修改前的数据
不可重复读问题
read committed 事务隔离级别下还存在不可重复读问题。
下面在 事务1
中两次读取同一记录,在这之间通过 事务2
对该记录进行修改并提交
事务1 | 事务2 | |||||
---|---|---|---|---|---|---|
序号 | 执行命令 | 输出结果 | 说明 | 执行命令 | 输出结果 | 说明 |
1 | set session transaction isolation level read committed; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
2 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
3 | select * from user where id = 2; | 2 test 3 | 查看当前数据 | |||
4 | set session transaction isolation level read committed; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
5 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
6 | select * from user where id = 2; | 2 test 3 | 查看当前数据 | |||
7 | update user set userid = 4 where id = 2; | Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 | 查看当前数据 | |||
8 | commit; | Query OK, 0 rows affected (0.12 sec) | 事务2提交 | |||
9 | select * from user where id = 2; | 2 test 4 | 查看当前数据,与上次查询结果不一致 |
事务1
在第 3 步 和第 9 步分别查询了 id = 2
的记录,在这中间 事务2
修改了该条记录并提交成功,导致前者在一次事务中两次查询同一记录的结果不一致。
可重复读(repeatable read)
不可重复读问题
将事务隔离级别修改为 repeatable read,重复上次操作,再看下 事务1
的两次查询结果
事务1 | 事务2 | |||||
---|---|---|---|---|---|---|
序号 | 执行命令 | 输出结果 | 说明 | 执行命令 | 输出结果 | 说明 |
1 | set session transaction isolation level repeatable read; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
2 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
3 | select * from user where id = 2; | 2 test 4 | 查看当前数据 | |||
4 | set session transaction isolation level repeatable read; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
5 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
6 | select * from user where id = 2; | 2 test 4 | 查看当前数据 | |||
7 | update user set userid = 5 where id = 2; | Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 | 查看当前数据 | |||
8 | commit; | Query OK, 0 rows affected (0.12 sec) | 事务2提交 | |||
9 | select * from user where id = 2; | 2 test 4 | 查看当前数据,与上次查询结果一致 |
注意第 3 步和第 9 步查询结果一致,这说明已经不存在「不可重复读」问题了
幻读问题
事务1
根据查询结果新增数据,在查询前 事务2
新增了一条记录
事务1 | 事务2 | |||||
---|---|---|---|---|---|---|
序号 | 执行命令 | 输出结果 | 说明 | 执行命令 | 输出结果 | 说明 |
1 | set session transaction isolation level repeatable read; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
2 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
3 | set session transaction isolation level repeatable read; | Query OK, 0 rows affected (0.00 sec) | 设置session事务隔离级别为提交读 | |||
4 | start transaction; | Query OK, 0 rows affected (0.00 sec) | 开启事务 | |||
5 | select * from user; | 1 admin 1 2 test 5 | 查看当前所有数据 | |||
6 | insert into user value(3, “aaa”, 3); | Query OK, 1 row affected (0.02 sec) | 新增id为3的记录 | |||
7 | commit; | Query OK, 0 rows affected (0.12 sec) | 事务2提交 | |||
8 | select * from user; | 1 admin 1 2 test 5 | 查看当前所有数据 | |||
9 | insert into user value(3, “bbb”, 3); | ERROR 1062 (23000): Duplicate entry ‘3’ for key ‘PRIMARY’ mysql> select * from user; | 见鬼了,明明没有id为3的记录 |
事务1
执行查询发现不存在 id = 3
的记录,然后执行新增操作,结果却失败了。
可串行化(serializable)
可串行化(serializable)级别下不存在幻读的问题。对于上面的情况,其结果是 事务2
在执行了第6步后进入等待,需 事务1
执行完后才能继续,也就是串行执行。
mysql命令
查看系统变量
1
show global variables \G
找到事务隔离级别变量,当前隔离级别为可重复读
1
2
3*************************** 518. row ***************************
Variable_name: transaction_isolation
Value: REPEATABLE-READ查看全局事务隔离级别
1
select @@global.transaction_isolation;
输出结果
1
2
3
4
5
6+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ |
+--------------------------------+
1 row in set (0.02 sec)查看 session 事务隔离级别
1
select @@session.transaction_isolation;
输出结果
1
2
3
4
5
6+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| READ-UNCOMMITTED |
+---------------------------------+
1 row in set (0.00 sec)设置事务隔离级别
1
set session transaction isolation level read uncommitted;
参考
https://segmentfault.com/a/1190000016566788
BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL