前言:
在事务那一篇文章提到事务的读现象,在本章节再进行补充说明,读现象是如何产生的,以及如何解决,其读现象也就是存在于不同事务内查询的结果差异,那么开始向下了解吧。
事务隔离机制
事务具有原子性、一致性、隔离性、持久性四大特性,而隔离性顾名思义指的就是事务彼此之间隔开,多个事务在同时处理一个数据时彼此之间互相不影响,如果隔离的不够好就有可能会产生脏读、不可重复读、幻读等读现象,为此,隔离性总共分为四种级别。不同级别会产生不同问题,由低到高依次为:
- Read uncommitted
- Read committed
- Repeatable read
- Serializable
这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题。
√可能出现,×不出现
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommitted | √ | √ | √ |
Read committed | × | √ | √ |
Repeatable read(mysql默认) | × | × | √ |
Serializable | × | × | × |
那这么说,直接使用最后一个?答案并不是这样的,我们先来逐个了解不同隔离级别的问题:
1、未提交读(Read uncommitted)
当我们隔离等级为Read uncommitted时,一个事务可以读取另一个事务未commit的数据。
未提交读的数据库锁情况(实现原理)
-
事务在读数据的时候并未对数据加锁。
-
务在修改数据的时候只对数据增加行级共享锁。
首先查询MySQL的隔离级别,早期版本MySQL使用:select @@tx_isolation;
,较高版本:select @@transaction_isolation;
MySQL默认为Rr级别
演示之前,我们先创建如下实验表:
CREATE TABLE `bank` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`age` int DEFAULT NULL,
`money` float(20,0) DEFAULT NULL,
PRIMARY KEY (`id`)
)
insert bank values(null,'jack',18,700);
insert bank values(null,'tom',19,200);
insert bank values(null,'jams',23,300);
设置事务等级为:Read uncommitted,演示脏读现象
set global transaction isolation level Read uncommitted;
修改之后退出再重新进入一下,因为是全局修改。
开启两个终端,注意笔者备注,终端每个SQL语句先后执行顺序
终端1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from bank; # 第一步
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 700 |
| 2 | tom | 19 | 200 |
| 3 | jams | 23 | 300 |
+----+------+------+-------+
3 rows in set (0.00 sec)
mysql> select * from bank; # 第三步
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 800 |
| 2 | tom | 19 | 200 |
| 3 | jams | 23 | 300 |
+----+------+------+-------+
3 rows in set (0.00 sec)
mysql> select * from bank; # 第五步,可以发现,事务2回滚之后,数据又变成了之前的样子
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 700 |
| 2 | tom | 19 | 200 |
| 3 | jams | 23 | 300 |
+----+------+------+-------+
3 rows in set (0.01 sec)
终端2
mysql> update bank set money = money + 100 where id = 1; # 第二步
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> rollback; # 第四步
Query OK, 0 rows affected (0.00 sec)
上序操作就是,终端1内的事务可以读取终端2事务未提交的数据,这个就是:脏读,需要解决这个问题我们只需要将事务级别调整为:Read committed即可解决,那么我们再来了解Read committed事务级别
2、提交读(Read committed)
提交读(READ COMMITTED)也可以翻译成读已提交,通过名字也可以分析出,在一个事务修改数据过程中,如果事务还没提交,其他事务不能读该数据。
这个事务级别虽然可以解决脏读问题,但是也会产生一个新的问题,不可重复读,也就是说设置该级别数据后,只要是已经commit的数据都可以读取
set global transaction isolation level Read committed;
实验开启两个终端,注意每个终端标注的SQL执行顺序
终端1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from bank; # 第一步
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 700 |
| 2 | tom | 19 | 200 |
| 3 | jams | 23 | 300 |
+----+------+------+-------+
3 rows in set (0.00 sec)
mysql> select * from bank; # 第三步,发现数据并未变化,因为对方事务未提交数据
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 700 |
| 2 | tom | 19 | 200 |
| 3 | jams | 23 | 300 |
+----+------+------+-------+
3 rows in set (0.00 sec)
mysql> select * from bank; # 第六步,这就是出现一个很奇怪的现象,与几秒钟前读取到的数据不符合
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 800 |
| 2 | tom | 19 | 200 |
| 3 | jams | 23 | 300 |
+----+------+------+-------+
3 rows in set (0.00 sec)
终端2
mysql> update bank set money = money + 100 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.00 sec)
上序操作演示出来的就是:不可重复读问题,我们两次读取到的结果不相同,这也违背了隔离性的,说好的互不干扰呢,那么需要解决这个问题我们只需要将事务级别调整为:Repeatable reads即可解决,那么我们再来了解Repeatable reads事务级别
三、可重复读(Repeatable reads)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。
实例演示:
时间点 | 事务A | 事务B |
---|---|---|
1 | 开启事务 | |
2 | 开启事务 | |
3 | 查询数据“jack”不存在 | |
4 | 插入数据“jack” | |
5 | 提交事务 | |
6 | 查询数据“jack”,不存在 | |
7 | 插入数据“jack”,不成功 |
事务A查询“jack”,查询不到,插入又不成功,“jack”这条数据就像幻觉一样出现。这就是所谓的“幻读”。
实操演示
终端1:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from bank; # 第一步
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 801 |
| 2 | tom | 19 | 200 |
+----+------+------+-------+
2 rows in set (0.00 sec)
mysql> select * from bank; # 第四步
+----+------+------+-------+
| id | name | age | money |
+----+------+------+-------+
| 1 | jack | 18 | 801 |
| 2 | tom | 19 | 200 |
+----+------+------+-------+
2 rows in set (0.00 sec)
mysql> insert bank values(3,'lolo',21,300); # 第五步
ERROR 1062 (23000): Duplicate entry '3' for key 'bank.PRIMARY'
终端2:
mysql> insert bank values(3,'hello',20,300); # 第二步
Query OK, 1 row affected (0.00 sec)
mysql> commit; # 第三步
Query OK, 0 rows affected (0.01 sec)
在我们事务1执行完查询语句后,事务2马上有插入了一条数据并提交了。此时我们事务2再次查询发现并无异议,然后插入id=3的主键值时发现该主键值已经存在,造成了一种幻觉。
但是我们通常不会明显出现幻读现象,因为MySQL中行锁算法内的Next-Key Lock帮助我们解决了这个问题。
四、可序列化(Serializable)
可序列化(Serializable)是最高的隔离级别,前面提到的所有的隔离级别都无法解决的幻读,在可序列化的隔离级别中可以解决。
我们说过,产生幻读的原因是事务一在进行范围查询的时候没有增加范围锁(range-locks:给SELECT 的查询中使用一个“WHERE”子句描述范围加锁),所以导致幻读。
可序列化的数据库锁情况
-
事务在读取数据时,必须先对其加 表级共享锁 ,直到事务结束才释放;
-
事务在更新数据时,必须先对其加 表级排他锁 ,直到事务结束才释放。
现象
-
事务1正在读取A表中的记录时,此时事务2也能读取A表,但不能对A表做更新、新增、删除,直到事务1结束。(因为事务一对表增加了表级共享锁,其他事务只能增加共享锁读取数据,不能进行其他任何操作)
-
事务1正在更新A表中的记录时,则事务2不能读取A表的任意记录,更不可能对A表做更新、新增、删除,直到事务1结束。(事务一对表增加了表级排他锁,其他事务不能对表增加共享锁或排他锁,也就无法进行任何操作)
虽然可序列化解决了脏读、不可重复读、幻读等读现象。但是序列化事务会产生以下效果:
-
事务1正在操作bank表中的数据,事务2直接不能select读取bank,因为已经被表级锁了
-
当某个事务对bank表进行select后,就会给bank表增加共享锁。
隔离等级越高,安全性越高,限制越大,但效率也会越低。
四种事务隔离级别从隔离程度上越来越高,但同时在并发性上也就越来越低。之所以有这么几种隔离级别,就是为了方便开发人员在开发过程中根据业务需要选择最合适的隔离级别。
如果本文对您有帮助,别忘一键3连,感谢支持!
技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请
点赞 收藏+关注
子夜期待您的关注,谢谢支持!