
MySQL全局锁、表锁、行锁、间隙锁、临键锁超详细讲解
在数据库并发访问的场景中,锁机制是保证数据一致性和完整性的核心。MySQL中的锁机制层层递进,从全局到表级再到行级,不同粒度的锁适用于不同的业务场景。本文将从宏观到微观,详细剖析MySQL中的全局锁、表锁、行锁、间隙锁和临键锁,帮助你彻底搞懂MySQL的并发控制原理。
一、全局锁
全局锁是对整个数据库实例加锁,加锁后整个数据库处于只读状态,后续的所有写操作、DDL语句以及更新操作的事务提交语句都将被阻塞。
1. 使用场景
全局锁最典型的使用场景是全库逻辑备份。在备份过程中,不希望有数据变动,以保证备份的一致性。
2. 加锁与解锁方式
-- 加全局读锁 FLUSH TABLES WITH READ LOCK; -- 释放全局锁 UNLOCK TABLES;
3. 实际备份建议
在InnoDB引擎中,进行全库备份时推荐使用 mysqldump --single-transaction 参数,它利用MVCC机制实现一致性非锁定读,不会阻塞业务的正常读写,避免使用全局锁导致数据库长时间不可写入。
mysqldump -h www.ipipp.com -u root -p --single-transaction mydb > backup.sql
二、表级锁
表级锁会锁定整张表,锁粒度大,并发度低。MySQL中的表级锁主要包含:表锁、元数据锁(MDL)和意向锁。
1. 表锁(Table Lock)
表锁分为读锁(共享锁)和写锁(排他锁)。加锁后,当前会话和其他会话对该表的读写操作均会受到限制。
-- 加读锁(当前会话可读不可写,其他会话可读不可写) LOCK TABLES t1 READ; -- 加写锁(当前会话可读写,其他会话不可读不可写) LOCK TABLES t1 WRITE; -- 释放表锁 UNLOCK TABLES;
2. 元数据锁(MDL)
MDL不需要显式使用,在访问表时自动加上。其作用是保证并发环境下表结构的一致性。当对一个表做增删改查(DML)时,加MDL读锁;当对表结构做修改(DDL)时,加MDL写锁。
注意:长事务持有MDL读锁时,如果其他会话尝试修改表结构(申请MDL写锁),会被阻塞;进而后续所有对该表的DML操作也会被阻塞,导致连接数暴增甚至打满。因此,应避免在业务高峰期执行DDL操作。
3. 意向锁(Intention Lock)
意向锁是InnoDB自动加的表级锁,主要用于快速判断表里是否有行锁。当事务打算给某行加行级共享锁(S锁)前,必须先取得表的意向共享锁(IS锁);打算加行级排他锁(X锁)前,必须先取得表的意向排他锁(IX锁)。
意向锁与行锁的兼容性规则是:意向锁之间互相兼容;但意向锁与表级的共享锁/排他锁互斥。这样,当需要加表级写锁时,只需判断表上是否存在意向锁即可,无需逐行检查是否有行锁,极大提升了加锁效率。
三、行锁
行锁是InnoDB引擎特有的锁机制(MyISAM不支持),其锁粒度最小,只锁定匹配的索引记录,并发度最高。行锁是加在索引上的,如果没有走索引,行锁会退化为表锁。
1. Record Lock(记录锁)
记录锁是单独锁住某一条索引记录。例如,通过主键索引精准匹配一条记录时,就会加记录锁。
-- Session A BEGIN; -- id为主键,对id=1的记录加行级排他锁 UPDATE users SET age = 20 WHERE id = 1; -- Session B -- 尝试修改同一条记录,会被阻塞 UPDATE users SET age = 25 WHERE id = 1; -- 修改其他记录则正常执行 UPDATE users SET age = 30 WHERE id = 2;
2. 行锁的三大算法前提
InnoDB的行锁是通过给索引上的项加锁来实现的,这就意味着只有通过索引条件检索数据,InnoDB才使用行级锁。如果在没有索引的字段上执行更新,会导致全表扫描,从而将所有行的锁都加上,等效于表锁。
四、间隙锁
间隙锁是InnoDB在可重复读(REPEATABLE READ)隔离级别下为了解决幻读问题而引入的锁机制。它锁定的是索引记录之间的间隙(开区间),防止其他事务在间隙中插入新记录。
1. 幻读问题
在同一个事务中,先后执行相同的范围查询,后一次查询看到了前一次没有看到的记录,这就是幻读。间隙锁就是为了阻止在间隙中插入数据从而避免幻读。
2. 间隙锁的锁定逻辑
假设表中存在主键id为 1, 5, 10 的三条记录。对 id=5 的记录加锁时,不仅锁住了 id=5 这条记录,还会对 (1, 5) 和 (5, 10) 这两个间隙加间隙锁。
-- 假设表中有id=1, 5, 10的记录 -- Session A BEGIN; -- 对id=5加记录锁,同时会在左右两侧加间隙锁 SELECT * FROM users WHERE id = 5 FOR UPDATE; -- Session B -- 尝试在间隙(1,5)中插入,会被阻塞 INSERT INTO users (id, name) VALUES (3, 'test'); -- 尝试在间隙(5,10)中插入,也会被阻塞 INSERT INTO users (id, name) VALUES (8, 'test');
核心要点:间隙锁之间是不冲突的,间隙锁只与“插入意向锁”冲突。也就是说,事务A加了间隙锁,事务B也可以加间隙锁,但事务B不能在这个间隙中插入数据。
五、临键锁
临键锁是InnoDB行锁的默认算法,它是记录锁 + 间隙锁的组合,锁定的是一个左开右闭的区间。
1. 锁定范围
对于上面的记录 1, 5, 10。临键锁锁定的区间为:
(负无穷大, 1]、(1, 5]、(5, 10]、(10, 正无穷大]。
当执行 SELECT * FROM users WHERE id = 5 FOR UPDATE 时,加上的就是临键锁,它既锁住了 id=5 这条记录本身,也锁住了 (1, 5] 和 (5, 10] 的范围。
2. 退化机制
临键锁在不同场景下会退化,这是理解锁行为的关键:
等值查询,记录存在:临键锁会退化为记录锁,只锁定匹配的索引记录本身,不再锁定两侧的间隙。
等值查询,记录不存在:临键锁会退化为间隙锁,锁定寻找记录经过的间隙区间,不锁定任何实际记录。
范围查询:会对扫描到的范围加临键锁,命中记录的向右边界可能会退化为间隙锁。
-- 假设表中存在id为1, 5, 10的记录 -- Session A BEGIN; -- 等值查询id=5(存在),临键锁退化为记录锁,锁住id=5 SELECT * FROM users WHERE id = 5 FOR UPDATE; -- Session B -- 插入id=3成功,因为间隙(1,5)没有被锁 INSERT INTO users (id, name) VALUES (3, 'test1'); -- 插入id=4失败,因为id=5这条记录本身被锁住 UPDATE users SET name = 'test2' WHERE id = 5; ----------------------------------------- -- Session A BEGIN; -- 等值查询id=7(不存在),临键锁退化为间隙锁,锁住(5, 10) SELECT * FROM users WHERE id = 7 FOR UPDATE; -- Session B -- 插入id=6失败,在间隙(5,10)内 INSERT INTO users (id, name) VALUES (6, 'test3'); -- 插入id=15成功,不在间隙内 INSERT INTO users (id, name) VALUES (15, 'test4');
六、总结与优化建议
MySQL的锁机制层层递进:全局锁掌控全库,表锁控制整表,行锁精细到行,而间隙锁和临键锁则是为了在RR隔离级别下彻底解决幻读问题而生。在日常开发中,为减少锁冲突和死锁概率,应遵循以下原则:
合理使用索引:确保更新和删除操作走索引,避免行锁升级为表锁。
缩小事务粒度:事务不要包含过多的逻辑,尽量在完成业务操作后立即提交,减少锁持有的时间。
控制隔离级别:如果业务对幻读不敏感,可以将隔离级别降为读提交(RC),此时只会使用记录锁,不会使用间隙锁和临键锁,并发性能更好。
按固定顺序访问资源:在多个事务中操作多张表或不同行时,尽量保持相同的访问顺序,以避免死锁的产生。