innoDB存储引擎中主要有行锁、间隙锁、临建锁、插入意向锁、表锁。
接下来会逐一解释使用的场景及锁的效果,并通过事务模拟一些场景来验证效果。
实验MySQL版本是5.7,innoDB存储引擎
总结论
- 行锁锁定单行记录,禁止更新/删除操作,适用于唯一索引等值查询;
- 间隙锁锁定索引范围间隙,禁止插入数据,解决幻读问题;
- 临建锁结合行锁和间隙锁,既禁止修改又禁止插入;
- 插入意向锁声明插入意图,与间隙锁互斥,但不同插入意向锁相互兼容;
- 表锁直接锁定整表,用于无索引或索引失效场景。
select xxx for update、update和delete加锁情况是一样的
不同插入意向锁之间是非互斥关系
插入意向锁和间隙锁、临建锁是互斥关系
简单好记方式:都是针对索引的
唯一索引等值条件命中加行锁,其他情况把条件圈定的范围作为一个整体A区间,给A区间内的记录加行锁,往区间A的左边找到第一个不匹配的索引B,往区间A的右边找到第一个不匹配的索引C,间隙锁锁住索引区间(B,C)。
加锁过程
当对非唯一索引进行等值条件命中时,就会给命中的记录加临建锁,锁住该记录和前一个索引的区间。当找到第一个大于条件的记录的时候,会给该记录加一个间隙锁,锁住上一个记录到当前记录的间隙。
有索引age=5,10,10,15,20 查:age=10 ;命中了记录10,往左找到记录5,往右找到记录15
两个临建锁+一个间隙锁:(5,10],(10,10],(10,15)
查:age>9 and age <15 ;命中了记录10,往左找到记录5,往右找到记录15一个临建锁+一个间隙锁:(5,10],(10,10],(10,15)
查:age>9 and age <=15 ;命中了记录10,15,往左找到记录5,往右找到记录20两个临建锁+一个间隙锁:(5,10],(10,10],(10,15],(15,20)
无论查询条件如何变化,只要是走同一条索引,且命中索引或命中区间的情况是一样的,那么加锁情况也是一样的。
使用场景
- 行锁:查询唯一索引的字段,等值条件命中。事务中插入一条数据之后
- 间隙锁:查询索引字段,条件不命中(等值条件、范围条件)。
- 临建锁:查询索引字段,范围查询命中或非唯一索引等值查条件命中
- 插入意向锁:插入数据的时候会先扫描索引,然后检查要插入的区间是否有间隙锁,没有就给该区间加一个插入意向锁
- 表锁:没有使用索引或优化器觉得不使用索引更快(各种索引失效场景)
行锁
行锁是innoDB存储引擎实现并发读写的关键,行锁实际是加在索引上的,锁住单行记录。
在开启事务,插入一条数据之后会给该新插入的数据加行锁,防止其他事务在当前事务提交之前操作该行。(会阻塞)
使用场景:
查询唯一索引的字段,等值条件命中索引。
验证:
数据库现有数据如下:

索引如下:一个主键索引,一个普通索引:age

后续的验证均采用上述数据,且每种场景验证完都恢复到如上数据。
唯一索引等值条件命中
步骤如下,后续操作也是类似步骤:
1.先启动事务1,启动事务2
2.然后执行事务1的SQL
3.执行事务2的SQL
4.观看事务2是否被事务1阻塞。
5.提交事务1
6.观看事务2的SQL是否立刻就成功了
7.提交事务2
具体某一步操作的效果会注释放在操作后方
注意SQL字段的数据类型,如果类型不对需要强制转换可能导致索引失效。
事务1如下:
sql
start transaction
-- 命中索引id=15,给该记录加行锁
update newtable set name = "888888" where id =15;
commit
事务2,如下:
sql
start transaction
--和事务1一样,禁止修改该行
update newtable set name = "99999" where id =15; --阻塞
delete from newtable where id = 15 --阻塞
--更新附近有记录的行
update newtable set name = "99999" where id =10; --不阻塞
update newtable set name = "99999" where id =20; --不阻塞
--更新附近无记录的行
update newtable set name = "99999" where id =14; --不阻塞
update newtable set name = "99999" where id =16; --不阻塞
--附近插入数据(间隙范围内)
insert into newtable values(12,"lisi",20) --不阻塞
insert into newtable values(16,"lisi",10) --不阻塞
commit
结论
唯一索引等值条件命中加的确实是行锁,锁住了单行记录,禁止修改该行记录。
唯一索引等值条件不命中
事务1如下:
sql
start transaction
-- 不命中索引,id=13处于区间(10,15)
update newtable set name = "888888" where id =13;
commit
事务2,如下:
sql
start transaction
--和事务1一样条件,记录都不存在,自然不应该会阻塞(后续不再验证变更不存在的数据)
--因为在where条件查询的时候没有数据就已经结束本次查询了。
update newtable set name = "99999" where id =13; --不阻塞
delete from newtable where id = 13 --不阻塞
--更新最近的有记录的行
update newtable set name = "99999" where id =10; --不阻塞
update newtable set name = "99999" where id =15; --不阻塞
--插入附近无记录的行(间隙范围内)
insert into newtable values(12,"lisi",20) --阻塞
insert into newtable values(13,"lisi",20) --阻塞
insert into newtable values(14,"lisi",20) --阻塞
--插入附近无记录的行(间隙范围外,间隙范围内阻塞才有测的必要)
insert into newtable values(9,"lisi",20) --不阻塞
insert into newtable values(16,"lisi",20) --不阻塞
commit
结论
唯一索引等值条件不命中加的是间隙锁,锁住的是最近的整个间隙,id=13,按照索引离得最近的是10和15,也就是(10,15),不允许插入数据。但更新删除操作都可以。
非唯一索引等值条件命中
age字段加的是普通索引
事务1如下:
sql
start transaction
-- 命中索引age=15,锁住该行记录,往两边扩大,区间(10,20)加间隙锁
update newtable set name = "888888" where age =15;
commit
事务2,如下:
sql
start transaction
--和事务1一样条件
update newtable set name = "99999" where age =15; --阻塞
insert into newtable values(32,"lisi",15) --阻塞
--更新插入附近有记录的行
update newtable set name = "99999" where age =10; --不阻塞
update newtable set name = "99999" where age =20; --不阻塞
insert into newtable values(1,"lisi",10) --不阻塞
insert into newtable values(33,"lisi",10) --阻塞
insert into newtable values(2,"lisi",20) --阻塞
insert into newtable values(34,"lisi",20) --不阻塞
-- 插入附近无记录的行(间隙范围内)
insert into newtable values(35,"lisi",13) --阻塞
insert into newtable values(36,"lisi",16) --阻塞
-- 插入无记录的行(间隙范围外)
insert into newtable values(37,"lisi",9) --不阻塞
insert into newtable values(38,"lisi",21) --不阻塞
commit
结论
非唯一索引等值条件命中,加的是临建锁(间隙锁+行锁)+间隙锁。
加锁过程:
扫描索引,找到第一个匹配条件的行记录,先给该行加临建锁,锁住该记录和前一个区间,也就是(10,15];因为后面可能还有匹配的记录,继续往右找,如果还有age=15的记录则继续给该记录加临建锁;继续往右,直到找到第一个非命中的记录,也就是age=20,给它加间隙锁,因为需要防止其他事务插入age=15的数据从而引发幻读,也就是加一个(15,20)的间隙锁【这里左侧的15是命中的最后一个行记录】。得到的总的不可插入数据的区间是(10,20)。
特别注意
两个端点需要特别注意,这里的不包含10,20分别指的是索引上左右离命中记录最近的记录。而在非唯一索引中排序是先按照索引,相同再按照其他条件排序的(没有指明就是主键),因此无论是age=10还是age=20的记录都有可能可以插入,也有可能不可以插入。重点看二次排序字段(没指明就是主键)。上面的插入数据正是验证了这一点。
非唯一索引等值条件不命中
事务1如下:
sql
start transaction
-- 不命中索引,age=13处于区间(10,15),间隙锁锁住该区间
update newtable set name = "888888" where age =13;
commit
事务2,如下:
sql
start transaction
--和事务1一样条件
update newtable set name = "99999" where age =13; --不阻塞
insert into newtable values(39,"lisi",13) --阻塞
--更新插入附近有记录的行
update newtable set name = "99999" where age =10; --不阻塞
update newtable set name = "99999" where age =15; --不阻塞
insert into newtable values(3,"lisi",10) --不阻塞
insert into newtable values(40,"lisi",10) --阻塞
insert into newtable values(4,"lisi",15) --阻塞
insert into newtable values(41,"lisi",15) --不阻塞
-- 插入附近无记录的行(间隙范围内)
insert into newtable values(42,"lisi",13) --阻塞
insert into newtable values(43,"lisi",16) --阻塞
-- 插入无记录的行(间隙范围外)
insert into newtable values(44,"lisi",9) --不阻塞
insert into newtable values(45,"lisi",21) --不阻塞
commit
结论
不命中索引会给命中的区间加间隙锁,如上述例子,age=13,在区间(10,15)里面,给该区间加间隙锁,不允许插入数据。
间隙锁
上面的两个非命中索引的场景。
唯一索引范围查询命中(非唯一索引也是类似的)
事务1如下:
sql
start transaction
-- 索引没有命中,间隙锁锁住所在区间(10,15)
-- 加间隙锁是为了防止其他事务在(13,14)之间插入数据引起幻读
update newtable set name = "888888" where id > 13 and id < 14;
commit
事务2,如下:
sql
start transaction
--和事务1一样条件
update newtable set name = "99999" where id > 13 and id < 14; --不阻塞,没有数据
--更新附近有记录的行
update newtable set name = "99999" where id =10; --不阻塞
update newtable set name = "99999" where id =15; --不阻塞
-- 插入附近无记录的行(间隙范围内,间隙确实扩大到(10,15))
insert into newtable values(14,"lisi",11) --阻塞
insert into newtable values(16,"lisi",14) --阻塞
-- 插入无记录的行(间隙范围外)
insert into newtable values(9,"lisi",55) --不阻塞
insert into newtable values(16,"lisi",55) --不阻塞
commit
结论
走索引却没有命中数据加的确实是间隙锁,锁住查询范围所在的间隙,禁止其他事务插入。
临建锁
唯一索引范围查询命中(和非唯一索引等值查询命中一样)
事务1如下:
sql
start transaction
-- 命中索引id=15,给该记录加行锁,间隙锁锁住区间(10,20)
-- 加间隙锁是为了防止其他事务在(13,17)之间插入数据引起幻读
update newtable set name = "888888" where id > 13 and id < 17;
commit
事务2,如下:
sql
start transaction
--和事务1一样条件
update newtable set name = "99999" where id > 13 and id < 17; --阻塞
--更新附近有记录的行
update newtable set name = "99999" where id =10; --不阻塞
update newtable set name = "99999" where id =20; --不阻塞
-- 插入附近无记录的行(间隙范围内,间隙确实扩大到(10,20))
insert into newtable values(14,"lisi",13) --阻塞
insert into newtable values(16,"lisi",16) --阻塞
insert into newtable values(12,"lisi",13) --阻塞
insert into newtable values(18,"lisi",13) --阻塞
-- 插入无记录的行(间隙范围外)
insert into newtable values(9,"lisi",9) --不阻塞
insert into newtable values(21,"lisi",21) --不阻塞
commit
结论
和非唯一索引等值条件命中类似,也是加临建锁,只不过这里的记录有唯一索引限制少了一些需要验证的场景。因为需要防止其他事务在(13,17)之间插入数据导致幻读,而实际的间隙只能按照索引走,也就是扩大了范围变成(10,20)
非唯一索引范围查询命中(和非唯一索引等值查询命中一样)
事务1如下:
sql
start transaction
-- 命中索引age=15,20,给该记录加行锁,间隙锁锁住区间(10,25)
update newtable set name = "888888" where age > 13 and age <= 20;
commit
事务2,如下:
sql
start transaction
--和事务1一样条件
update newtable set name = "99999" where age > 13 and age <= 20; --阻塞
update newtable set name = "99999" where age =15 ; --阻塞
update newtable set name = "99999" where age =20 ; --阻塞
insert into newtable values(46,"lisi",15) --阻塞
insert into newtable values(47,"lisi",20) --阻塞
--更新插入附近有记录的行
update newtable set name = "99999" where age =10; --不阻塞
update newtable set name = "99999" where age =25; --不阻塞
--在间隙端点的左边和右边的区别
insert into newtable values(3,"lisi",10) --不阻塞
insert into newtable values(40,"lisi",10) --阻塞
insert into newtable values(4,"lisi",25) --阻塞
insert into newtable values(41,"lisi",25) --不阻塞
-- 插入附近无记录的行(间隙范围内)
insert into newtable values(42,"lisi",13) --阻塞
insert into newtable values(43,"lisi",16) --阻塞
insert into newtable values(43,"lisi",20) --阻塞
insert into newtable values(43,"lisi",23) --阻塞
-- 插入无记录的行(间隙范围外)
insert into newtable values(44,"lisi",9) --不阻塞
insert into newtable values(45,"lisi",26) --不阻塞
commit
结论
和非唯一索引等值条件命中类似,也是加临建锁,因为需要防止其他事务在(13,17)之间插入数据导致幻读。这里多了同样命中索引的场景验证。
插入意向锁
上面已经把各种插入场景都尝试了。
提炼一下插入过程:
插入之前会先做校验,遍历所有的索引。以唯一索引为例子:
- 遍历索引,一开始都是比要插入的数据小的,一直往下走
- 知道遇到第一个大于等于当前数据的索引,如果等于则报错
- 如果大于要插入的字段,则判断该位置是否有间隙锁,如果有则阻塞等待,如果没有则判断是否和已有的插入意向锁冲突,如果不冲突则插入一个插入意向锁。(这里可能两个事务插入唯一键冲突的数据,后一个事务会阻塞;如果前一个提交,则后一个冲突;如果前一个回滚,则后一个成功)
- 遍历完全部索引确认没问题之后,再插入数据,再给该新插入的记录加上行锁。
- 提交事务之后释放行锁
非唯一索引的在遇到相同数据的时候就需要根据第二个排序字段来找到要插入的位置。
表锁
查询条件没有索引,或者优化器优化之后不走索引,就会锁表。
事务1如下:
sql
start transaction
-- 没有索引可用,锁表
update newtable set age= 18 where name = "zhangsan";
commit
事务2,如下:
sql
start transaction
--更新数据
update newtable set name = "99999" where age=5; --阻塞
update newtable set name = "99999" where age =15 ; --阻塞
update newtable set name = "99999" where age =20 ; --阻塞
--插入数据
insert into newtable values(3,"lisi",10) --阻塞
insert into newtable values(40,"lisi",10) --阻塞
insert into newtable values(4,"lisi",25) --阻塞
insert into newtable values(41,"lisi",25) --阻塞
--删除数据
delete from newtable where age=5;--阻塞
commit
结论
SQL不走索引会加表锁,表锁会把整个表锁住,无法变更里面的数据,也无法新增或删除数据。
因此写SQL的时候记得给条件加上索引