MySQL的锁机制是保证并发场景下数据一致性的重要手段,通过对数据或资源加锁,控制多个事物对数据的访问顺序,避免因并发导致数据不一致。
在多事务并发访问时,锁用于解决“多个事务同时操作同一资源”的冲突问题,确保事务ACID的特性(尤其是隔离性和一致性)。
一、锁的分类
MySQL的锁可按粒度、功能和实现方式等维度进行分类。
1.1 按粒度划分
按锁定的范围(粒度),分为表锁和行锁。
1.1.1 表锁
定义:锁定整张表,事务操作时对整个表加锁。
特点:
- 开销小,加锁块(无需逐行判断)
- 无死锁(表级锁冲突直接等待,不会循环等待)
- 锁粒度大,并发度低(锁定期间其他事务无法操作表中任何数据)
支持的存储引擎:MyISAM(默认)、InnoDB(也支持表锁)
常见表锁类型:
- 表级共享锁(读锁):加锁后,当前事务可读表,其他事务也可读表,但是不可以写表
- 表级排他锁(写锁):加锁后,当前事务可读写表,其他事务不可读也不可写
1.1.2 行锁
定义:仅锁定表中的某一行(或多行)数据,不影响其他行
特点:
- 开销大,加锁慢(需要定位具体行)
- 可能产生死锁(多事务交叉锁定不同行)
- 锁粒度小,并发程度高(适用高并发写场景)
支持的存储引擎:仅InnoDB
核心依赖:行锁给予索引实现,若查询未命中索引,InnoDB会自动升级为表锁(导致并发下降)
1.2 按功能划分(锁的兼容性)
按照事物对数据的操作类型(读/写),分为共享锁(S)和排他锁(X),二者的兼容性决定了并发访问规则。
1.2.1 共享锁(S锁,Shared Lock)
定义:允许事务读取数据的锁
特性:多个事务可以同时对同一数据加S锁(读不互斥);但是S锁与排他锁(X锁)互斥(读时不能写,写时不能读)
手动加锁方式:SELECT ... LOCK IN SHARE MODE;(事务结束后自动释放)
1.2.2 排他锁(X锁,Exclusive Lock)
定义:允许事务修改数据的锁
特性:同一数据只能有一个X锁(写互斥);X锁与S锁,X锁和X锁均互斥。
自动加锁的场景:InnoDB中,Update/Delete/Insert操作会自动对涉及的行加X锁
手动加锁方式:SELECT ... FOR UPDATE;(事务结束后自动释放)
二、InnoDB特有的行锁细化
InnoDB的行锁并非简单锁定“行记录”,而是基于索引的“范围锁定”,具体可分为如下:
2.1 记录锁(Record Lock)
定义:锁定单行记录(索引对应的具体行),仅阻止其他事务修改或删除该记录。
场景:精准命中唯一索引(如主键)时处罚,例如WHERE id=1000;(id是主键)
2.2 间隙锁(Gap Lock)
定义:锁定索引之间的“间隙”(不包含记录本身),防止其他事务在间隙中插入新记录(解决幻读的核心方法)
场景:使用范围查询(如WHERE age > 14 AND age < 18;)且未命中具体记录时,InnoDB会锁定满足条件的索引间隙
示例:表中已有age=15,age=16的记录,如果事务执行“SELECT * FROM user WHRER age >14 AND age < 18 FOR UPDATE;”,则InnoDB会锁定(14,18)的间隙,其他事务无法插入age=17的新纪录(防止事务再次查询出现新的内容,即幻读)
2.3 临键锁(Next-Key Lock)
定义:记录锁+间隙锁的组合,锁定范围包含“记录本身”和“前一个间隙”,是InnoDB默认的行锁方式(可重复读隔离级别下)。
作用:同时防止“修改已有记录”和“插入新纪录”,彻底解决幻读。
示例:索引列有10,20,30,临键锁会锁定范围(-∞,10]、(10,20]、(20,30]、(30,+∞),每个范围既包含记录(如20),也包含前一个间隙(如10到20之间)
三、其他特殊锁
3.1 意向锁(Intention Lock)
定义:表级锁,用于标识“事务将要对表中的行加何种锁”(S锁或X锁),提高表锁与行锁的交互效率。
类型:
- 意向共享锁(IS):事务计划对表中的某些行加S锁
- 意向排他锁(IX):事务计划对表中的某些行加X锁
作用:当需要加表级锁时,无需住行检查行锁(效率低),只需要检查意向锁即可。
示例:若表上有IX锁,说明已有事务打算对行加X锁,此时其他事务无法加表级S锁(避免冲突)
3.2 自增锁(AUTO-INC Lock)
定义:特殊的表级锁,用于自增列(AUTO_INCREMENT)的插入操作,确保自增值的唯一性和连续性
机制:事务插入自增列时,InnoDB会锁定自增计数器,分配一个唯一自增值后释放(而非等到事务结束),平衡效率和唯一性
3.3 元数据锁(MDL锁,Metadata Lock)
定义:表级锁,用于保护表结构(元数据)的修改,防止“事务读写数据时,表结构被并发修改”导致的不一致。
机制:
- 事务读取表时,自动加MDL读锁
- 事务修改表结构时,自动加MDL写锁
- MDL读锁之间兼容,读锁与写锁,写锁与写锁互斥
注意:长时间持有MDL读锁时,会阻塞ALTER TABLE(需等待读锁释放),可能导致业务卡顿
四、锁与隔离级别的关系
InnoDB的隔离级别通过锁机制实现,不同级别对锁的使用不同:
读未提交(RU):几乎不加锁,可能出现脏读;
读已提交(RC):使用记录锁,不使用间隙锁/临键锁,可能出现不可重复读;
可重复读(RR,默认):使用临键锁,解决幻读;
串行化(Serializable):强制所有事务串行执行,相当于全表加锁,并发度极低;
五、锁的常见问题和优化
死锁:多事务较差锁定不同的行(如事务A锁行1等待行2,事务B锁行2等待行1)。
解决:设置innodb_deadlock_detect = ON(自动检查死锁并会滚其中一个事务);优化事务逻辑,按固定顺序访问资源。
锁升级:行锁因未命中索引(或锁范围过大)升级为表锁,导致并发下降。
解决:优化,确保查询命中索引,缩小锁范围。
长事务锁等待:事务持有锁时间过长,导致其他事务阻塞。
解决:优化,控制事务时长,避免在事务中执行非数据库操作(如RPC调用)。
六、拓展:为什么在大厂中更偏向使用RC(读已提交)而非RR(可重复读)?
核心原因:RC在高并发场景下的性能优势更明显,能避免RR带来的锁冲突和复杂度
我们从一下几个角度出发分析问题:
6.1 锁机制差异
InnoDB的隔离模式与锁机制深度绑定,RR和RC的锁行为差异直接影响并发能力:
RC级别:仅使用记录锁(Record Lock),不使用间隙锁和临键锁。锁范围严格限制在“实际存在的记录”上,间隙允许插入新数据,因此锁冲突更少,并发写入能力更强。对于高并发场景(如电商订单,支付系统),这种特性能显著提升吞吐量
RR级别:为了保证“可重复读”和“防止幻读”,InnoDB会使用临键锁(Next-Key Lock)——一种“记录锁+间隙锁”的组合,不仅确定当前记录,还会锁定索引间隙(防止其他事务插入新记录)。这种锁机制虽然能够避免幻读,但会导致锁范围扩大,增加锁冲突概率。
例如:对范围查询加锁时,RR会锁定整个范围的间隙,即使该范围没有记录,其他事务也无法插入符合条件的数据,容易引发死锁等待甚至死锁。
那么就有人问了:虽然RC隔离级别低,并发程度高,但是会出现幻读的现象,那该怎么办?
6.2 解决幻读的方式:更依赖于应用层控制
RR级别通过临键锁“被动”防止幻读,但是这种方式的代价锁开销大,而RC虽然理论上可能出现幻读,但是大厂通过应用逻辑主动规避,更灵活高效:
幻读的实际影响有限:幻读指“同一个事务内,多次范围查询的结果集行数不一致”,但实际业务中,这种场景并不常见。多数业务更关注“读已提交的数据”,而非“严格重复读”。
应用层规避方法:若需避免幻读,可以在应用层加锁(如对查询范围加行所SELECT ... FOR UPDATE),或通过版本号,状态机控制,而非依赖数据库层的临键锁。这种方式更灵活,且能精准控制锁范围,减少不必要的阻塞。
6.3 binlog格式与主从一致性:RC更安全
早期MySQL中,RR级别若使用STATEMENT格式的binlog(日志记录SQL),可能导致主从数据不一致。RR下的间隙锁在从库重放binlog时可能失效(从库用SQL语句重放,无法完全复现主库的所行为),导致数据不一致。
而RC级别配合ROW格式的binlog(日志记录行级变更),能天然避免这个问题——ROW格式记录的是“数据最终状态”,与锁行为无关,主从一致性更可靠。
6.4 业务需求:多数场景不需要“可重复读”
RR的“可重复读”特性(同一事务内多次读取统一数据结果一致),在多数业务场景中并非必需。
例如电厂订单查询:用户在一个事务内两次次查询订单状态,若期间订单被其他事务更新(如支付成功),用户更希望看到最新状态(已支付),而非旧状态(待支付)。这种场景下,RC的“读已提交”反而更符合业务预期。