MySQL中表锁、行锁和页锁哪种锁粒度在并发环境下对性能影响最大?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1242个文字,预计阅读时间需要5分钟。
MySQL的MyISAM引擎仅支持表级锁,执行INSERT、UPDATE、DELETE时,会锁定整个表。这意味着同时进行的两个事务修改的是不同行的数据,也会相互等待。典型表现是使用SHOW PROCESSLIST查看时,会看到大量LOCKED状态的线程。
但它在全表扫描类操作(如 ALTER TABLE、OPTIMIZE TABLE)或一次性导入百万级数据时反而更高效:没有行锁开销,也无需维护锁队列。如果你的应用以读多写少、且写操作集中在离线任务中(比如凌晨 ETL),MyISAM 表锁未必是瓶颈。
常见误用点:
- 在高并发 OLTP 场景下误选
MyISAM,导致写请求排队雪崩 - 没意识到
SELECT ... FOR UPDATE在MyISAM下不生效(它根本不支持事务和行锁) - 用
LOCK TABLES t1 WRITE手动加锁后忘记UNLOCK TABLES,造成后续所有操作挂起
InnoDB 默认行锁,但容易因索引失效退化为表锁
InnoDB 的行锁实际是“索引记录锁”,只对满足条件且命中索引的行加锁。一旦 WHERE 条件无法使用索引(比如对无索引字段查询、或用了函数如 WHERE YEAR(create_time) = 2024),InnoDB 就会升级为全表扫描+全表加锁——效果等同于表锁。
另一个关键点:UPDATE 或 DELETE 若影响多行,InnoDB 会为每行单独加锁,但锁之间不冲突;而如果事务中混合了范围查询(如 SELECT ... WHERE id BETWEEN 100 AND 200 FOR UPDATE),可能触发间隙锁(Gap Lock)或临键锁(Next-Key Lock),进一步扩大锁定范围。
实操建议:
- 用
EXPLAIN确认 DML 语句是否走了索引,避免隐式类型转换(如字符串字段传数字) - 避免在高频更新的表上建太多二级索引——每条更新都要维护所有相关索引记录的锁
- 长事务会持续持有行锁,用
SELECT * FROM information_schema.INNODB_TRX定期检查运行超 5 秒的事务
页锁是 MariaDB Aria 引擎的折中方案,MySQL 原生不支持
严格来说,标准 MySQL(包括官方 8.0+)**没有页锁**。所谓“页锁”常被误传,实际是 Aria(MariaDB 的引擎)或某些老版本 Berkeley DB 的特性。它把数据按页(通常 4KB 或 8KB)分组加锁,粒度介于表锁和行锁之间。
它的价值在于:比行锁节省内存(不用为每行存锁结构),又比表锁并发高。但在 MySQL 生态里,你无法通过配置启用页锁——innodb_locks_unsafe_for_binlog 这类参数控制的是间隙锁行为,不是锁粒度切换。
如果你看到文档提到“MySQL 支持页锁”,大概率是混淆了存储引擎。当前唯一可靠选择仍是:
- 高并发、短事务、主键/索引明确 → 用
InnoDB行锁 - 只读为主、数据量小、需压缩或崩溃恢复快 → 可考虑
MyISAM表锁 - 需要页级控制?得换引擎(如
Aria)或应用层分片
死锁检测开销随行锁数量指数增长,别让单个事务锁太多行
InnoDB 死锁检测是基于 wait-for graph 的实时算法,当一个事务要获取新锁时,会遍历所有被锁行的持有者及其等待链。如果单个 UPDATE 影响 10 万行,不仅锁管理内存暴涨,死锁检测耗时也可能从微秒级跳到毫秒级,拖慢整个事务吞吐。
更隐蔽的问题是锁升级不存在——InnoDB 不会把 1000 个行锁自动合并成一个页锁或表锁。所以业务代码里要警惕:
- 用
IN子句批量更新时,WHERE id IN (1,2,...,5000)实际产生 5000 个独立行锁 - 未加
LIMIT的DELETE FROM t WHERE status = 'pending'可能锁住全表符合条件的行 - 循环中逐条
UPDATE比批量INSERT ... ON DUPLICATE KEY UPDATE更容易引发锁竞争
锁粒度不是越小越好,而是要匹配访问模式。一个 update 语句锁 5 行和锁 500 行,对并发的影响可能差两个数量级——这点在压测时容易被忽略,直到上线后突发慢查询。
本文共计1242个文字,预计阅读时间需要5分钟。
MySQL的MyISAM引擎仅支持表级锁,执行INSERT、UPDATE、DELETE时,会锁定整个表。这意味着同时进行的两个事务修改的是不同行的数据,也会相互等待。典型表现是使用SHOW PROCESSLIST查看时,会看到大量LOCKED状态的线程。
但它在全表扫描类操作(如 ALTER TABLE、OPTIMIZE TABLE)或一次性导入百万级数据时反而更高效:没有行锁开销,也无需维护锁队列。如果你的应用以读多写少、且写操作集中在离线任务中(比如凌晨 ETL),MyISAM 表锁未必是瓶颈。
常见误用点:
- 在高并发 OLTP 场景下误选
MyISAM,导致写请求排队雪崩 - 没意识到
SELECT ... FOR UPDATE在MyISAM下不生效(它根本不支持事务和行锁) - 用
LOCK TABLES t1 WRITE手动加锁后忘记UNLOCK TABLES,造成后续所有操作挂起
InnoDB 默认行锁,但容易因索引失效退化为表锁
InnoDB 的行锁实际是“索引记录锁”,只对满足条件且命中索引的行加锁。一旦 WHERE 条件无法使用索引(比如对无索引字段查询、或用了函数如 WHERE YEAR(create_time) = 2024),InnoDB 就会升级为全表扫描+全表加锁——效果等同于表锁。
另一个关键点:UPDATE 或 DELETE 若影响多行,InnoDB 会为每行单独加锁,但锁之间不冲突;而如果事务中混合了范围查询(如 SELECT ... WHERE id BETWEEN 100 AND 200 FOR UPDATE),可能触发间隙锁(Gap Lock)或临键锁(Next-Key Lock),进一步扩大锁定范围。
实操建议:
- 用
EXPLAIN确认 DML 语句是否走了索引,避免隐式类型转换(如字符串字段传数字) - 避免在高频更新的表上建太多二级索引——每条更新都要维护所有相关索引记录的锁
- 长事务会持续持有行锁,用
SELECT * FROM information_schema.INNODB_TRX定期检查运行超 5 秒的事务
页锁是 MariaDB Aria 引擎的折中方案,MySQL 原生不支持
严格来说,标准 MySQL(包括官方 8.0+)**没有页锁**。所谓“页锁”常被误传,实际是 Aria(MariaDB 的引擎)或某些老版本 Berkeley DB 的特性。它把数据按页(通常 4KB 或 8KB)分组加锁,粒度介于表锁和行锁之间。
它的价值在于:比行锁节省内存(不用为每行存锁结构),又比表锁并发高。但在 MySQL 生态里,你无法通过配置启用页锁——innodb_locks_unsafe_for_binlog 这类参数控制的是间隙锁行为,不是锁粒度切换。
如果你看到文档提到“MySQL 支持页锁”,大概率是混淆了存储引擎。当前唯一可靠选择仍是:
- 高并发、短事务、主键/索引明确 → 用
InnoDB行锁 - 只读为主、数据量小、需压缩或崩溃恢复快 → 可考虑
MyISAM表锁 - 需要页级控制?得换引擎(如
Aria)或应用层分片
死锁检测开销随行锁数量指数增长,别让单个事务锁太多行
InnoDB 死锁检测是基于 wait-for graph 的实时算法,当一个事务要获取新锁时,会遍历所有被锁行的持有者及其等待链。如果单个 UPDATE 影响 10 万行,不仅锁管理内存暴涨,死锁检测耗时也可能从微秒级跳到毫秒级,拖慢整个事务吞吐。
更隐蔽的问题是锁升级不存在——InnoDB 不会把 1000 个行锁自动合并成一个页锁或表锁。所以业务代码里要警惕:
- 用
IN子句批量更新时,WHERE id IN (1,2,...,5000)实际产生 5000 个独立行锁 - 未加
LIMIT的DELETE FROM t WHERE status = 'pending'可能锁住全表符合条件的行 - 循环中逐条
UPDATE比批量INSERT ... ON DUPLICATE KEY UPDATE更容易引发锁竞争
锁粒度不是越小越好,而是要匹配访问模式。一个 update 语句锁 5 行和锁 500 行,对并发的影响可能差两个数量级——这点在压测时容易被忽略,直到上线后突发慢查询。

