MySQL(五):锁的升级,你不可不知的关键点
2023-12-12 16:17:28
- 行锁为什么会升级?
在MySQL(四):锁的原理,讲解了为什么会在一个事务里面使用锁,而锁又分为表锁和行锁,行锁只有InnoDB存储引擎才有,因为InnoDB支持事务,而事务涉及到四个特性,其中一个是隔离性。
隔离性有不同的级别,如RR、RC、READ COMMITTED、READ UNCOMMITTED,每个级别的隔离性都会有不同的锁策略,这个不是本文重点,本文主要讲解行锁,行锁是怎么升级的。
为什么会在RR级别下出现行锁升级呢?在RR级别下,在查询一条数据的时候,会先给这行数据加上一个行锁,如果该行数据不存在,InnoDB会自动给索引的范围加上一个间隙锁,这个是因为RR级别的隔离性是最高的,如果在查询一个范围的时候,又有一个事务准备往这个范围插入数据,如果范围没有锁,那么这个查询会查到这些插入的数据,这样就造成了幻读。
间隙锁只存在RR级别下,间隙锁实际上是一个范围锁,它会把索引范围内所有的行都锁定,包括索引范围内的空隙,比如:
select * from user where id between 1 and 10000;
如果在RR级别下,执行这条SQL,InnoDB引擎就会给1和10000之间的索引加上一个间隙锁,此时如果另一个事务要插入一条id为5000的数据,就会被阻塞。
如果在RC级别下,执行上面的SQL,InnoDB引擎就不会给索引加上间隙锁,所以另外一个事务是可以往1和10000之间插入数据的,这样查询就可能会查询到这些新插入的数据,这就是幻读。
为了避免RR级别下的间隙锁,MySQL会把间隙锁升级为表锁,这样就不会出现幻读。
2. 锁升级的危害
锁升级的危害有很多,主要有以下几点:
- 性能下降: 锁升级会增加数据库的锁开销,导致数据库的性能下降。
- 死锁: 锁升级可能会导致死锁,死锁是指两个或多个事务互相等待对方释放锁,导致两个事务都无法继续执行。
- 系统崩溃: 锁升级可能会导致系统崩溃,当数据库锁冲突严重时,系统可能会崩溃。
3. 锁升级怎么办
为了避免锁升级,我们可以采取以下措施:
- 使用更低的隔离级别: 如果对数据的一致性要求不高,我们可以使用更低的隔离级别,如RC或READ COMMITTED,这样可以避免锁升级。
- 使用索引: 使用索引可以减少锁的范围,从而避免锁升级。
- 避免在事务中执行长时间的操作: 如果在事务中执行长时间的操作,可能会导致锁升级。
- 合理设计数据库表结构: 合理设计数据库表结构可以减少锁冲突,从而避免锁升级。
4. 案例讲解
我们通过几个案例来讲解锁升级是如何产生的,以及如何优化解决锁升级的问题。
案例1:
-- 事务一
begin;
select * from user where id = 1;
update user set name = 'zhangsan' where id = 1;
commit;
-- 事务二
begin;
select * from user where id = 1;
update user set name = 'lisi' where id = 1;
commit;
在案例1中,事务一先查询了id为1的用户,然后更新了id为1的用户的姓名,事务二也查询了id为1的用户,然后更新了id为1的用户的姓名。
在RR级别下,事务一查询id为1的用户时,会给id为1的行加上一个行锁,然后事务一更新id为1的用户时,会把行锁升级为表锁,此时事务二查询id为1的用户时,就会被事务一的表锁阻塞。
案例2:
-- 事务一
begin;
select * from user where id between 1 and 100;
update user set name = 'zhangsan' where id between 1 and 100;
commit;
-- 事务二
begin;
select * from user where id = 50;
update user set name = 'lisi' where id = 50;
commit;
在案例2中,事务一查询了id在1和100之间的所有用户,然后更新了id在1和100之间的所有用户的姓名,事务二查询了id为50的用户,然后更新了id为50的用户的姓名。
在RR级别下,事务一查询id在1和100之间的所有用户时,会给id在1和100之间的所有索引加上一个间隙锁,然后事务一更新id在1和100之间的所有用户时,会把间隙锁升级为表锁,此时事务二查询id为50的用户时,就会被事务一的表锁阻塞。
案例3:
-- 事务一
begin;
select * from user where id = 1;
-- 执行一个长时间的操作
sleep(100);
update user set name = 'zhangsan' where id = 1;
commit;
-- 事务二
begin;
select * from user where id = 1;
update user set name = 'lisi' where id = 1;
commit;
在案例3中,事务一查询了id为1的用户,然后执行了一个长时间的操作,事务二查询了id为1的用户,然后更新了id为1的用户的姓名。
在RR级别下,事务一查询id为1的用户时,会给id为1的行加上一个行锁,然后事务一执行长时间的操作时,行锁一直被持有,此时事务二查询id为1的用户时,就会被事务一的行锁阻塞。
5. 总结
本文主要讲解了行锁升级的原因、危害以及优化方法,并通过三个案例讲解了锁升级是如何产生的,以及如何优化解决锁升级的问题。