MySQL-15丨MySQL行级锁

Posted by jiefang on October 28, 2019

MySQL行级锁

MySQL 5.5 之前的默认存储引擎是 MyISAM,5.5 之后改成了 InnoDB。InnoDB 后来居上最主要的原因就是:

  • InnoDB 支持事务:适合在并发条件下要求数据一致的场景。
  • InnoDB 支持行锁:有效降低由于删除或者更新导致的锁定。

两阶段锁

两阶段锁:锁操作分为两个阶段,加锁阶段和解锁阶段。

InnoDB 行锁模式

InnoDB实现了两种类型的行锁:

  • 共享锁(S):允许事务去读一行,阻止其它事务获得相同数据集的排他锁
  • 排他锁(X):允许获得排他锁的事务更新数据,阻止其它事务取得相同数据集的共享锁和排他写锁。

对于普通select语句,InnoDB不会加任何锁,事务可以显式加共享锁或排他锁:

  • 共享锁(S):select * from table where …lock in share mode;
  • 排他锁(X):select * from table where …for update;

InnoDB 行锁算法

InnoDB 行锁的三种算法:

  • Record Lock:单个索引的记录加锁。
  • Gap Lock:间隙锁,对索引之间的间隙加锁,但不包括记录本身。
  • Next-Key Lock:Gap Lock+ Record Lock,锁定一个范围,并且锁定记录本身。

InnoDB行锁实现特点意味着:如果不通过索引条件检索数据,那么InnoDB将对表中所有记录加锁,实际效果跟表锁一样。

RC级别行锁

1
2
//设置当前session隔离级别为提交读
set session transaction_isolation ='READ-COMMITTED';
  • RC 隔离级别,a 字段没索引。
  • RC 隔离级别,a 字段有唯一索引。
  • RC 隔离级别,a 字段有非唯一索引。

通过非索引字段查询

image

什么是for update? 我们常使用的查询语句,比如 select * from t16 where b=1 属于快照读,是不会看到别的事务插入的数据的。而在查询语句后面加了 for update 显式给记录集加了排他锁,也就让查询变成了当前读。插入、更新、删除操作,都属于当前读。其实也就可以理解 select … for update 是为了让普通查询获得插入、更新、删除操作时所获得的锁。表面看起来 session1 只给了 b=1 这一行加了排他锁,但 session2 在请求其它行的排他锁时,却出现了锁等待。 image

由于 b 字段没有索引,因此只能走聚簇索引,进行全表扫描。从上图中可以看到,满足条件的记录有一条,但是聚簇索引上的所有记录,都被加上了 X 锁。 为什么不是只在满足条件的记录上加锁呢? 这是因为在 MySQL 中,如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由 server 层进行过滤。因此也就把所有记录都锁上了。

没有索引的情况下,InnoDB的当前读会对所有记录都加锁。所以在工作中应该特别注意 InnoDB 这一特性,否则可能会产生大量的锁冲突。

通过唯一索引查询

image

session1 给了 a=1 这一行加了排他锁,在 session2 中请求其他行的排他锁时,不会发生等待;但是在 session2 中请求 a=1 这一行的排他锁时,会发生等待。看下图: image

由于 a 是唯一索引,因此 select * from t16 where a=1 for update;(后面称为 SQL2) 语句会选择走 a 列的索引进行条件过滤,在找到 a=1 的记录后,会将唯一索引上 a=1 索引记录上加X 锁,同时,会根据读取到的 id 列,回到聚簇索引,然后将 id=1 对应的聚簇索引项加 X 锁。 为什么聚簇索引上的记录也要加锁? 比如,并发的一条SQL,是通过主键来更新:update t16 set b=10 where id=1;如果SQL2没有将主键索引上的记录加锁,那么并发的 update 并不知道 SQL2 在执行,所以如果update执行了,就违背了同一记录上的更新或者删除需要串行执行的约束。

如果查询的条件是唯一索引,那么SQL需要在满足条件的唯一索引上加锁,并且会在对应的聚簇索引上加锁。

通过非唯一索引查询

image image

在 a 字段的非唯一索引上,满足 c=3 的所有记录,都被加了锁。同时,对应的主键索引上的记录也都加上了锁。与通过唯一索引查询的情况相比,唯一索引查询最多有一行记录被锁,而非唯一索引将会把满足条件的所有记录都加上锁

如果查询的条件是非唯一索引,那么SQL需要在满足条件的非唯一索引上都加上锁,并且会在它们对应的聚簇索引上加锁。

幻读

image image

RC隔离级别下,只锁住了满足c=3的当前行,而不会对后面的位置加锁。

RR级别行锁

1
2
//设置当前session隔离级别为可重复读
set session transaction_isolation ='REPEATABLE-READ';

RR隔离级别下的非唯一索引查询

session1 session2
set session transaction_isolation =’REPEATABLE-READ’;/* 设置会话隔离级别为 RC*/ set session transaction_isolation =’REPEATABLE-READ’;
begin; begin;
~ select * from t16 where c=4 for update;
insert into t16(a,b,c) values (5,5,4);(等待) ~
~ select * from t16 where c=4 for update;
insert into t16(a,b,c) values (5,5,4);(成功) commit;
commit ~

image

与 RC 隔离级别下的图相似,但是有个比较大的区别是:RR 隔离级别多了 GAP 锁。 首先需要考虑哪些位置可以插入新的满足条件 c=4 的项:

  • 由于 B+ 树索引是有序的,因此 [2,2](代表 c 和 id 的值,后面就不一一说明了)前面的记录,不可能插入 c=4 的记录了;
  • [2,2] 与 [4,4] 之间可以插入 [4,3];
  • [4,4] 与 [4,6] 之间可以插入 [4,5];
  • [4,6] 之后,可以插入的值就很多了:4,n

为了保证这几个区间不会插入新的满足条件 c=4 的记录,MySQL RR 隔离级别选择了 GAP锁,将这几个区间锁起来。

RR 隔离级别下的非索引字段查询

session1 session2
set session transaction_isolation =’REPEATABLE-READ’;* set session transaction_isolation =’REPEATABLE-READ’;
begin; ~
select * from t17 where b=1 for update; ~
~ select * from t17 where b=1 for update;(等待)
commit select * from t16 where c=3 for update; (session1提交后成功)

image

所有记录都有 X 锁,除此之外,每个 GAP 也被加上了GAP锁。因此这张表在执行完 select * from t17 where b=1 for update; 到 commit 之前,除了不加锁的快照读,其它任何加锁的 SQL,都会等待。

RR 隔离级别下,非索引字段做条件的当前读不但会把每条记录都加上 X 锁,还会把每个GAP 加上 GAP 锁。再次说明,条件字段加索引的重要性。

RR 隔离级别下的唯一索引当前读

GAP 锁的目的是:为了防止同一事务两次当前读,出现幻读的情况。如果能确保索引字段唯一,那其实一个等值查询,最多就返回一条记录,而且相同索引记录的值,一定不会再新增,因此不会出现 GAP 锁。 因此以唯一索引为条件的当前读,不会有GAP锁。所以RR隔离级别下的唯一索引当前读加锁情况与 RC 隔离级别下的唯一索引当前读加锁情况一致。