聊聊不同隔离级别下,都会使用哪些锁?
1. MySQL 锁机制
对于 MySQL 来说,如果只支持串行访问的话,那么其效率会非常低。因此,为了提高数据库的运行效率,MySQL 需要支持并发访问。而在并发访问的情况下,会发生各种各样的问题,例如:脏读、不可重复读、幻读等问题。为了解决这些问题,就出现了事务隔离级别
本质上,事务隔离级别就是为了解决并发访问下的数据一致性问题的。不同的事务隔离级别,解决了不同程度的数据一致性
我们所说的全局锁、表锁、行级锁等等,其实都是事务隔离级别的具体实现。而 MVCC、意向锁,则是一些局部的性能优化
2. 事务隔离级别
MySQL 数据库有四大隔离级别:
- 读未提交(脏读):可以读取到其他事务还没提交的数据。 未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读
- 读已提交(不可重复读):该隔离级别的事务能读取到已经提交事务的数据。 因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读
- 可重复读(MySQL 的默认事务隔离级别:幻读):同一事务范围内读取到的数据是一致的。 但也会有新的问题,比如:此级别的事务正在执行时,另一个事务成功的插入了某条数据,两次查询结果记录条数不一样
- 串行化:所有事务串行执行。 不用去竞争,一个个去执行,但是效率也是最低的
- 当事务读取数据时,会获取共享锁,以确保数据的一致性。如果有其他事务已经持有排他锁,则读取操作需要等待排他锁释放
- 当事务修改数据时,会获取排他锁,以防止其他事务读取或修改相同的数据。如果有其他事务已经持有共享锁或排他锁,则修改操作需要等待相关锁释放
3. MySQL 锁类型
在 MySQL 中有全局锁、表级锁、行级锁三种类型,其中比较关键的是表级锁、行级锁
MySQL 锁类型:
- 全局锁
- 表级锁
- 表锁:在 Innodb 存储存储引擎中,表锁也用得比较少
- 元数据锁:基本上都是数据库自行操作
- 意向锁
- 行级锁
- 记录锁:某个索引记录的锁
- 间隙锁:两个索引记录之间的空隙锁
- 临键锁:记录锁 + 间隙锁
- 自增锁
在 Innodb 存储引擎中,我们可以通过下面的命令来查询锁的情况:
# 开启锁的日志
set global innodb_status_output_locks=on;
# 查看innodb引擎的信息(包含锁的信息)
show engine innodb status\G;
查询结果一般如下图所示:
上面几种不同类型的锁,其各自的关键字为:
- 表级的意向排它锁(IX):lock mode IX。
- 行级的插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention
- 行级的记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
- 行级的间隙锁(LOCK_GAP): lock_mode X locks gap before rec
- 行级的 Next-key 锁(LOCK_ORNIDARY): lock_mode X
通过上面的命令,我们就可以知道不同的事务隔离级别使用了哪些锁了
4. 准备数据测试
CREATE TABLE `2022`.`price_test` (
`id` BIGINT(64) NOT NULL AUTO_INCREMENT,
`name` varchar(32) not null,
`price` INTEGER(4) NULL,
PRIMARY KEY (`id`));
INSERT INTO price_test(name,price) values('apple', 10);
四种隔离级别:
READ UNCOMMITTED
:读未提交READ COMMITTED
:读已提交REPEATABLE READ
:可重复读SERLIALIZABLE
:序列化
5. 读未提交
打开两个命令行窗口,并且都修改事务隔离级别为「读未提交」
// 设置隔离级别
SET session TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
// 查看隔离级别
select @@transaction_isolation;
5.1 读写
- 事务 A 执行如下命令,查询出 id 为 1 记录的 price 值,不提交事务
begin;
select * from price_test where id = 1;
- 接着,事务 B 执行如下命令,修改 price 为 20
begin;
update price_test set price = 20 where id = 1;
- 接着,事务 A 再次读取 id 为 1 记录的 price 值。事务 A 读取到了事务 B 未提交的数据,这其实就是脏读了
select * from price_test where id = 1;
【结论】:在「读未提交」事务隔离级别下,读写是可以同时进行的,不会阻塞。
5.2 写写
事务 A 和 事务 B 同时对 id 为 1 的记录进行更新,看看是否能够更新成功
- 先用如下命令在事务 A(上边的窗口)执行,将 price 修改为 15,此时事务 A 还未提交。
begin;
update price_test set price = 15 where id = 1;
- 如下命令在事务 B(下边的窗口)执行,将 price 修改为 20,从图中可以看到,事务 B 阻塞卡住了
begin;
update price_test set price = 20 where id = 1;
【结论】:在「读未提交」事务隔离级别下,写写不可以同时进行的,会阻塞
通过查看锁信息可以看到,其是加上一个行级别的记录锁,如下图所示:
如果指定了非索引的列作为查询条件,是否会触发间隙锁呢?
插入一条数据:
INSERT INTO `2022`.`price_test` (`id`, `name`, `price`) VALUES (2, 'orange', 30);
-
在事务 A 执行如下命令:
select * from price_test where price > 15 for update;
,查询 price > 15 的记录: -
接着,我们在事务 B 执行如下命令:
select * from price_test where price > 5 for update;
,查询 price > 5 的记录。从如下结果可以看到,事务 B 阻塞住了:
3. 事务 A 查看锁的情况,如下图所示:
从上图可以看出,MySQL 只是加上了一个记录锁,并没有加间隙锁
5.3 总结
在「读未提交」隔离级别下,读写操作可以同时进行,但写写操作无法同时进行。与此同时,该隔离级别下只会使用行级别的记录锁,并不会用间隙锁
6. 读已提交
设置一下隔离级别为「读已提交」
SET session TRANSACTION ISOLATION LEVEL READ COMMITTED;
6.1 读写
- 事务 A 执行如下命令,查询出 id 为 1 记录的 price 值,不提交事务
begin;
select * from price_test where id = 1;
- 接着,事务 B 执行如下命令,修改 price 为 20
begin;
update price_test set price = 20 where id = 1;
- 接着,事务 A 再次读取 id 为 1 记录的 price 值。事务 A 未读取到了事务 B 未提交的数据,未产生脏读。
select * from price_test where id = 1;
6.2 写写
测试同时对 id 为 1 的数据进行更新:
- 事务 A 执行如下命令:
begin;
update price_test set price = 15 where id = 1;
- 接着事务 B 执行如下命令:
begin;
update price_test set price = 20 where id = 1;
- 事务 B 阻塞了。查看下锁信息,如下图所示
可以看到,其锁是一个行级别的记录锁,结果和「读未提交」的是一样的
继续看看范围的查询是否会触发间隙锁
- 事务 A 执行:
begin;
select * from price_test where price > 5 for update;
- 事务 B 执行:
begin;
select * from price_test where price > 15 for update;
- 事务 B 会阻塞,查看锁信息如下图所示
6.3 总结
在「读已提交」隔离级别下,只会使用行级别的记录锁,并不会用间隙锁。
6.4 读已提交隔离级别如何解决了【脏读】问题
脏读:是在并发事务中,一个事务可以读取到另一个未提交事务的数据
在“读已提交”隔离级别下,事务只能读取已经提交的数据,而不能读取未提交的数据。这意味着当一个事务正在进行修改时,其他事务无法读取到该事务所做的修改,直到该事务提交
为了实现"读已提交"隔离级别,数据库管理系统通常使用锁机制或多版本并发控制(MVCC)。锁机制可以确保一个事务在修改数据时对其他事务进行阻塞,以防止脏读。而MVCC则通过为每个事务创建不同的数据版本来实现隔离,并且只允许事务读取已提交的数据版本
7. 可重复读
我们设置一下隔离级别为「可重复读」
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ;
7.1 读写
- 事务 A 执行如下命令,查询出 id 为 1 记录的 price 值,不提交事务
begin;
select * from price_test where id = 1;
- 接着,事务 B 执行如下命令,修改 price 为 20
begin;
update price_test set price = 20 where id = 1;
- 接着,事务 A 再次读取 id 为 1 记录的 price 值。事务 A 未读取到了事务 B 未提交的数据,未产生脏读。
select * from price_test where id = 1;
7.2 写写
同时对 id 为 1 的数据进行更新,看看会发生什么
- 事务 A 执行如下命令:
begin;
update price_test set price = 15 where id = 1;
- 事务 B 执行如下命令
begin;
update price_test set price = 20 where id = 1;
- 事务 B 阻塞了。查看下锁信息,加上一个行级别的记录锁
继续看看范围的查询是否会触发间隙锁?
- 事务 A 执行:
begin;
select * from price_test where price > 5 for update;
- 事务 B 执行:
begin;
select * from price_test where price > 15 for update;
- 事务 B 会阻塞,查看锁信息如下图所示
可以看到,在这里就变成了 Next-Key 锁,就是记录锁和间隙锁结合体
7.3 总结
在「可重复读」隔离级别下,使用了记录锁、间隙锁、Next-Key 锁三种类型的锁
可重复读存在幻读的问题,但实际上在 MySQL 中,因为其使用了间隙锁,所以在「可重复读」隔离级别下,可用加锁解决幻读问题。因此,MySQL 将「可重复读」作为了其默认的隔离级别
8. 总结
对于任何隔离级别,表级别的表锁、元数据锁、意向锁都是会使用的,但对于行级别的锁则会有些许差别
- 在「读未提交」和「读已提交」隔离级别下,都只会使用记录锁,不会用间隙锁,当然也不会有 Next-Key 锁了
- 对于「可重复读」隔离级别来说,会使用记录锁、间隙锁和 Next-Key 锁