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 字段有非唯一索引。
通过非索引字段查询
什么是for update? 我们常使用的查询语句,比如 select * from t16 where b=1 属于快照读,是不会看到别的事务插入的数据的。而在查询语句后面加了 for update 显式给记录集加了排他锁,也就让查询变成了当前读。插入、更新、删除操作,都属于当前读。其实也就可以理解 select … for update 是为了让普通查询获得插入、更新、删除操作时所获得的锁。表面看起来 session1 只给了 b=1 这一行加了排他锁,但 session2 在请求其它行的排他锁时,却出现了锁等待。
由于 b 字段没有索引,因此只能走聚簇索引,进行全表扫描。从上图中可以看到,满足条件的记录有一条,但是聚簇索引上的所有记录,都被加上了 X 锁。 为什么不是只在满足条件的记录上加锁呢? 这是因为在 MySQL 中,如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由 server 层进行过滤。因此也就把所有记录都锁上了。
没有索引的情况下,InnoDB的当前读会对所有记录都加锁。所以在工作中应该特别注意 InnoDB 这一特性,否则可能会产生大量的锁冲突。
通过唯一索引查询
session1 给了 a=1 这一行加了排他锁,在 session2 中请求其他行的排他锁时,不会发生等待;但是在 session2 中请求 a=1 这一行的排他锁时,会发生等待。看下图:
由于 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需要在满足条件的唯一索引上加锁,并且会在对应的聚簇索引上加锁。
通过非唯一索引查询
在 a 字段的非唯一索引上,满足 c=3 的所有记录,都被加了锁。同时,对应的主键索引上的记录也都加上了锁。与通过唯一索引查询的情况相比,唯一索引查询最多有一行记录被锁,而非唯一索引将会把满足条件的所有记录都加上锁。
如果查询的条件是非唯一索引,那么SQL需要在满足条件的非唯一索引上都加上锁,并且会在它们对应的聚簇索引上加锁。
幻读
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 | ~ |
与 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提交后成功) |
所有记录都有 X 锁,除此之外,每个 GAP 也被加上了GAP锁。因此这张表在执行完 select * from t17 where b=1 for update; 到 commit 之前,除了不加锁的快照读,其它任何加锁的 SQL,都会等待。
RR 隔离级别下,非索引字段做条件的当前读不但会把每条记录都加上 X 锁,还会把每个GAP 加上 GAP 锁。再次说明,条件字段加索引的重要性。
RR 隔离级别下的唯一索引当前读
GAP 锁的目的是:为了防止同一事务两次当前读,出现幻读的情况。如果能确保索引字段唯一,那其实一个等值查询,最多就返回一条记录,而且相同索引记录的值,一定不会再新增,因此不会出现 GAP 锁。 因此以唯一索引为条件的当前读,不会有GAP锁。所以RR隔离级别下的唯一索引当前读加锁情况与 RC 隔离级别下的唯一索引当前读加锁情况一致。