返回

MySQL(五):锁的升级,你不可不知的关键点

后端

  1. 行锁为什么会升级?

在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. 锁升级的危害

锁升级的危害有很多,主要有以下几点:

  1. 性能下降: 锁升级会增加数据库的锁开销,导致数据库的性能下降。
  2. 死锁: 锁升级可能会导致死锁,死锁是指两个或多个事务互相等待对方释放锁,导致两个事务都无法继续执行。
  3. 系统崩溃: 锁升级可能会导致系统崩溃,当数据库锁冲突严重时,系统可能会崩溃。

3. 锁升级怎么办

为了避免锁升级,我们可以采取以下措施:

  1. 使用更低的隔离级别: 如果对数据的一致性要求不高,我们可以使用更低的隔离级别,如RC或READ COMMITTED,这样可以避免锁升级。
  2. 使用索引: 使用索引可以减少锁的范围,从而避免锁升级。
  3. 避免在事务中执行长时间的操作: 如果在事务中执行长时间的操作,可能会导致锁升级。
  4. 合理设计数据库表结构: 合理设计数据库表结构可以减少锁冲突,从而避免锁升级。

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. 总结

本文主要讲解了行锁升级的原因、危害以及优化方法,并通过三个案例讲解了锁升级是如何产生的,以及如何优化解决锁升级的问题。