MySQL 事务隔离级别解析和实战
1、MySQL 隔离界别查看
- 查看回话隔离级别
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;
- 查看系统隔离级别
SELECT @@global.tx_isolation;
2、MySQL 隔离级别修改
- MySQL 默认的隔离级别是可重复读( REPEATABLE READ)
- 在 my.inf 文件中修改隔离级别
transaction-isolation = {READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE}
image.png
- 用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。语法如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
image.png
3、MySQL 四种隔离级别
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
3.1、未提交读
- 允许脏读,也就是说一个事务有可能读到另一个事务未提交的数据
3.2、已提交读
- 只能读到已经提交的数据,Oracle等多数数据库的默认隔离级别
3.3、可重复读
- 存在幻读
3.4、可串行化
- 完全串行化,每次读都需要获得表级共享锁,读写阻塞
4、实例操作
- 新建一个表用来测试
CREATE TABLE `test` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名字',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测试';
4.1、脏读
- 当一个事务访问一个数据,并且进行了修改。另一个事务读到了被修改的数据,并且使用了这个数据。
- sessoin1 (插入数据但不提交事务)
mysql> SELECT @@session.tx_isolation; // 查询会话隔离级别可重复读
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set, 1 warning (0.04 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> insert into test(`name`) values("qiu"); //插入数据成功,此时事务还没有提交
Query OK, 1 row affected (0.01 sec)
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | qiu |
+----+------+
1 row in set (0.00 sec)
- sessoin2(可重复读,证明不会出现脏读)
mysql> SELECT @@session.tx_isolation; //会话隔离级别为可重复读
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT @@tx_isolation; //系统隔离级别为可重复读
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> select * from test; //查询不到 sessoin1 未提交的数据,不会出现脏读现象
Empty set (0.00 sec)
- sessoin3(为提交读出现脏读现象)
mysql> SELECT @@session.tx_isolation;//会话隔离级别为未提交读
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED | --------读到了 session1 未提交的数据,出现脏读现象
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select * from test;//读到了 session1 未提交的数据,此为脏读
+----+------+
| id | name |
+----+------+
| 1 | qiu |
+----+------+
1 row in set (0.00 sec)
4.2、不可重复读
-
在同一个事务内,多次读取同一个数据,此时事务还没有完成。另一个事务在前一个事务两次读取之间修改了数据,由于修改了数据,前一个事务读到的数据不一样,因此称为不可重复读。
-
sessoin1(事务内第一次读)
mysql> SELECT @@session.tx_isolation; //隔离级别为提交读
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-COMMITTED |
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> start transaction; //开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test; //开启事务内的第一次查询
+----+------+
| id | name |
+----+------+
| 2 | qiu |
+----+------+
1 row in set (0.01 sec)
- sessoin2
mysql> SELECT @@session.tx_isolation;//隔离级别为可重复读
+------------------------+
| @@session.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 test;
+----+------+
| id | name |
+----+------+
| 2 | qiu |
+----+------+
1 row in set (0.01 sec)
mysql> insert into test(`name`) values ("hello"); //在sessoin1第一次查询后修改了数据
Query OK, 1 row affected (0.01 sec)
mysql> select * from test;
+----+-------+
| id | name |
+----+-------+
| 2 | qiu |
| 3 | hello |
+----+-------+
2 rows in set (0.00 sec)
mysql> commit; //提交事务
Query OK, 0 rows affected (0.01 sec)
- sessoin1(事务内第二次读)
mysql> select * from test; //在事务内第二次读,读到了 sessoin2 提交的数据
+----+-------+
| id | name |
+----+-------+
| 2 | qiu | ---------------READ-COMMITTED级别出现不可重复读现象
| 3 | hello |
+----+-------+
2 rows in set (0.00 sec)
4.3、可重复读
- 验证 REPEATABLE-READ 级别下的可重复读
- sessoin1(事务内第一次读)
mysql> SELECT @@session.tx_isolation;//隔离级别为可重复读
+------------------------+
| @@session.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 test;
+----+-------+
| id | name |
+----+-------+
| 2 | qiu |
| 3 | hello |
+----+-------+
2 rows in set (0.00 sec)
- sessoin2
mysql> SELECT @@session.tx_isolation; //隔离级别为可重复读
+------------------------+
| @@session.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 test;
+----+-------+
| id | name |
+----+-------+
| 2 | qiu |
| 3 | hello |
+----+-------+
2 rows in set (0.00 sec)
mysql> insert into test (`name`) values ("hi"); //sessoin1 第一次读之后改变数据
Query OK, 1 row affected (0.01 sec)
mysql> select * from test;
+----+-------+
| id | name |
+----+-------+
| 2 | qiu |
| 3 | hello |
| 4 | hi |
+----+-------+
3 rows in set (0.00 sec)
mysql> commit; //提交事务
Query OK, 0 rows affected (0.00 sec)
- sessoin1(事务内第二次读)
mysql> select * from test;
+----+-------+
| id | name |
+----+-------+
| 2 | qiu |
| 3 | hello | -------------sessoin1 没有读到 sessoin2 提交的数据,出现可重复读现象
+----+-------+
2 rows in set (0.00 sec)
4.4、幻读
-
第一个事务对表中的所有数据进行修改,第二个事务往表里面插入一条数据。此时第一个事务发现表中还有未修改的数据,好像出现了幻觉一样。
-
幻读现象1:
session1: session2:
mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+-----------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 2 warnings (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
Empty set (0.00 sec)
mysql> insert into test (`id`, `name`) values (1, "hi~~~");
Query OK, 1 row affected (0.00 sec)
mysql> select * from test;
Empty set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from test;
Empty set (0.00 sec)
mysql> insert into test (`id`, `name`) values (1, "hello");
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
//what fuck ???刚刚查询,告诉我没有数据。等我插入的时候就告诉我主键冲突了。此乃幻读现象
- 幻读现象2:
session1: session2:
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from test;
+----+-------+
| id | name |
+----+-------+
| 1 | hi~~~ |
+----+-------+
1 row in set (0.00 sec)
mysql> insert into test (`id`, `name`) values (2, "hello~~");
Query OK, 1 row affected (0.01 sec)
mysql> select * from test;
+----+-------+
| id | name |
+----+-------+
| 1 | hi~~~ |
+----+-------+
1 row in set (0.04 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+-------+
| id | name |
+----+-------+
| 1 | hi~~~ |
+----+-------+
1 row in set (0.00 sec)
mysql> update test set name = "up";
Query OK, 2 rows affected (0.01 sec)
Rows matched: 2 Changed: 2 Warnings: 0
//what fuck ???刚出查询不是只有一条数据吗?怎么更新了两条。此乃幻读现象
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
| 2 | up |
+----+------+
2 rows in set (0.00 sec)
- innodb_locks_unsafe_for_binlog:设定InnoDB是否在搜索和索引扫描中使用间隙锁(gap locking)
- 当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读。
4.5、加锁
- 通过加锁来防止幻读
session1: session2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
//加锁锁住了 id <= 1 的范围
mysql> select * from test where id <= 1 for update;
+----+------+
| id | name |
+----+------+
| 1 | up |
+----+------+
1 row in set (0.18 sec)
//id 不在锁内,允许插入
mysql> insert into test (`id`, `name`) values (3, "lock");
Query OK, 1 row affected (0.15 sec)
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
| 2 | up |
+----+------+
2 rows in set (0.01 sec)
//id = 1 已经加了写锁,事务等待锁释放
mysql> insert into test(`id`, `name`) values (1, "lock");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
| 2 | up |
+----+------+
2 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
| 2 | up |
+----+------+
2 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
| 2 | up |
| 3 | lock | ------------session2 插入的数据
+----+------+
4 rows in set (0.00 sec)
- 通过加锁读来获得其他事务提交的结果
session1: session2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
+----+------+
4 rows in set (0.01 sec)
mysql> insert into test (`id`, `name`) values (7, "hello");
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.08 sec)
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
+----+------+
4 rows in set (0.00 sec)
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
+----+------+
4 rows in set (0.00 sec)
//读到了 session2 提交的数据
mysql> select * from test lock in share mode;
+----+-------+
| id | name |
+----+-------+
| 1 | up |
| 7 | hello |
+----+-------+
5 rows in set (0.00 sec)
//读到了 session2 提交的数据
mysql> select * from test for update;
+----+-------+
| id | name |
+----+-------+
| 1 | up |
| 7 | hello |
+----+-------+
5 rows in set (0.00 sec)
//读不到 session2 提交的数据
mysql> select * from test;
+----+------+
| id | name |
+----+------+
| 1 | up |
+----+------+
4 rows in set (0.00 sec)
- 可重复读和提交读本身就是互相矛盾的。保证了可重复读,就读不到其他事务的提交;保证了提交读,两次读取的数据可能会出现不一致。
- MySQL 默认的隔离级别是可重复读,可通过加锁读来获取其他事务的提交。
- MySQL 的可重复读并不能避免幻读,可通过加 Next-Key Lock 来避免幻读现象。
- Next-Key Lock:锁定一个范围,包括记录本身。
总结
- 每种数据库隔离级别都解决了一个问题。数据库隔离级别依次增强,性能也依次变差。大部分环境中使用 READ-COMMITTED 是可行的。
参考文献
关注公众号
-
大家可以关注我的公众号【学霸的一天】,更多有趣、有用的知识等你来发现
宣传二维码.png
网友评论