MySQL 行级锁:加锁机制、核心规则与实践指南
前言
在高并发MySQL场景中,行级锁是保障数据一致性、减少锁冲突的关键技术。与表级锁(如MyISAM引擎)相比,行级锁仅锁定操作涉及的行数据,能显著提升并发读写效率。
行级锁的两种核心类型InnoDB的行级锁基于锁类型 + 锁范围划分,核心类型分为共享锁(S锁)和排他锁(X锁),二者的兼容性直接影响事务并发:
锁类型
作用
与其他锁的兼容性
共享锁(S锁)
允许事务读取一行数据(SELECT ... LOCK IN SHARE MODE)
与S锁兼容,与X锁冲突
排他锁(X锁)
允许事务修改 / 删除一行数据(UPDATE/DELETE/INSERT)
与S锁、X锁均冲突
共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。排他锁又称写锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的锁。获得排他锁的事务既能读数据,又能修改数据。意向锁除了S锁和X锁之外,Innodb还有两种锁,是IX锁和IS锁,这里的I是Intention的意思,即意向锁。
IS锁: 表示事务打算在资源上设置共享锁(读锁)。这通常用于表示事务计划读取资源,并不希望在读取时有其他事务设置排它锁。IX锁: 表示事务打算在资源上设置排它锁(写锁)。这表示事务计划修改资源,并不希望有其他事务同时设置共享或排它锁。事务隔离级别未提交读(Read uncommitted)是最低的隔离级别。通过名字我们就可以知道,在这种事务隔离级别下,一个事务可以读到另外一个事务未提交的数据。这种隔离级别下会存在幻读、不可重复读和脏读的问题。提交读(Read committed)也可以翻译成读已提交,通过名字也可以分析出,在一个事务修改数据过程中,如果事务还没提交,其他事务不能读该数据。所以,这种隔离级别是可以避免脏读的发生的。可重复读(Repeatable reads),由于提交读隔离级别会产生不可重复读的读现象。所以,比提交读更高一个级别的隔离级别就可以解决不可重复读的问题。这种隔离级别就叫可重复读。但是这种隔离级别没办法彻底解决幻读。可串行化(Serializable)是最高的隔离级别,前面提到的所有的隔离级别都无法解决的幻读,在可串行化的隔离级别中可以解决。扩展脏读又称无效数据的读出,是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的。不可重复读是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。MySQL的行级锁锁的到底是什么?
数据库的行级锁根据锁的粒度不同,可以分为:
Record Lock表示记录锁,锁的是索引记录,没有定义主键,那么MySQL会默认选择一个唯一的非空索引作为聚簇索引。如果没有适合的非空唯一索引,则会创建一个隐藏的主键(row_id)作为聚簇索引Gap Lock是间隙锁,锁的是索引记录之间的间隙,间隙锁只有在Repeatable Reads这种隔离级别中才会起作用。Next-Key Lock是Record Lock和Gap Lock的组合,同时锁索引记录和间隙。他的范围是左开右闭的。加锁原则规则类型
适用场景
加锁单位变化
关键特点
原则 1
所有行级锁场景
始终是Next-Key Lock
行锁 + 间隙锁,防幻读
原则 2
所有查询
仅访问到的对象加锁
未访问的行 / 间隙不锁定
优化 1
索引等值查询(唯一索引加锁)
Next-Key Lock
→ 行锁
无间隙锁,锁范围最小
优化 2
索引等值查询(右遍历不满足)
Next-Key Lock
→ 间隙锁
无行锁,仅锁间隙
bug
唯一索引范围查询
额外锁定第一个不满足条件的行
锁范围扩大,易导致不必要阻塞
测试数据当我们执行update users set age=age+1 where id = 7的时候,由于表中没有id=7的记录,所以:
根据原则 1,加锁单位是Next-Key Lock,加锁范围就是(5,10];根据优化 2,这是一个等值查询id=7,而id=10不满足查询条件,Next-Key Lock退化成间隙锁,因此最终加锁的范围是(5,10)。案例二当我们执行select * from users where id>=10 and id<11 for update的时候:
根据原则 1,加锁单位是Next-Key Lock,会给(5,10]加上Next-Key Lock,范围查找就往后继续找,找到id=15这一行停下来根据优化 1,主键id上的等值条件,退化成行锁,只加了id=10这一行的行锁。根据原则 2,访问到的都要加锁,因此需要加Next-Key Lock(10,15]。因此最终加的是行锁id=10和Next-Key Lock(10,15]。案例三当我们执行select * from users where id>10 and id<=15 for update的时候:
根据原则 1,加锁单位是Next-Key Lock,会给(10,15]加上Next-Key Lock,并且因为id是唯一键,所以循环判断到id=15这一行就应该停止了。但是,InnoDB会往前扫描到第一个不满足条件的行为止,也就是id=20。而且由于这是个范围扫描,因此索引id上的(15,20]这个Next-Key Lock也会被锁上。案例四当我们执行select * from users where age>=25 and age<26 for update的时候:
根据原则 1,加锁单位是Next-Key Lock,会给(20,25]加上Next-Key Lock,范围查找就往后继续找,找到age=30这一行停下来根据原则 2,访问到的都要加锁,因此需要加Next-Key Lock(25,30]。由于索引age是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终加的锁是,索引age上的(20,25]和(25,30]这两个Next-Key Lock。总结
Record Lock表示记录锁,锁的是索引记录。Gap Lock是间隙锁,锁的是索引记录之间的间隙。Next-Key Lock是Record Lock和Gap Lock的组合,同时锁索引记录和间隙。他的范围是左开右闭的。
InnoDB的RR级别中,加锁的基本单位是Next-Key Lock,只要扫描到的数据都会加锁。唯一索引上的范围查询会访问到不满足条件的第一个值为止。
同时,为了提升性能和并发度,也有两个优化点:
索引上的等值查询,给唯一索引加锁的时候,Next-Key Lock退化为行锁。索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,Next-Key Lock退化为间隙锁。