在MySQL中,锁是保证数据一致性的核心手段,但锁的粒度选择不当,往往会导致并发性能急剧下降。一条UPDATE语句执行时,到底是锁住了整张表,还是只锁住了被修改的那几行记录?这个问题的答案取决于存储引擎和SQL的写法。MySQL主流的两款存储引擎MyISAM和InnoDB采用了完全不同的锁策略,理解表锁和行锁的区别,是做好数据库优化的基本功。

表锁和行锁的核心区别
表级锁(Table Lock)是对整张表加锁,当一个会话获得表锁后,其他会话对这张表的读写操作都会被阻塞,必须等待锁释放。行级锁(Row Lock)则只锁定被操作的具体行记录,其他会话仍然可以并发读写同一张表中未被锁定的行。两者最本质的区别就是加锁的粒度:粒度越粗,锁的开销越小但并发度越低;粒度越细,并发度越高但锁管理的开销越大。
从性能角度看,表锁的加锁和解锁速度非常快,不会产生死锁(因为一次就锁住整张表,不存在循环等待),但在写密集场景下并发能力很差,一个长查询会阻塞所有写入。行锁支持高并发读写,多个事务可以同时操作不同行,但加锁需要维护更多锁信息,消耗更多内存,并且在事务相互持有对方需要的行锁时可能产生死锁。
| 对比维度 | 表锁 | 行锁 |
|---|---|---|
| 加锁粒度 | 整张表 | 单行或行区间 |
| 并发性能 | 低,读写互斥 | 高,不同行可并发操作 |
| 锁开销 | 小,锁数量少 | 大,锁信息占用内存 |
| 死锁概率 | 几乎不会死锁 | 可能出现死锁 |
| 是否依赖索引 | 不依赖 | 依赖,无索引会退化为表锁效果 |
MyISAM与InnoDB的锁机制差异
MyISAM引擎只支持表级锁,提供了读锁(LOCK TABLE ... READ)和写锁(LOCK TABLE ... WRITE)两种模式。多个会话可以同时持有同一张表的读锁,但写锁是排他的。MyISAM的表锁由SQL层自动管理,执行查询语句前会自动加读锁,执行更新语句前会自动加写锁。由于写入会阻塞读取、读取也会阻塞写入,MyISAM适合以读为主、写操作少的场景,比如报表类应用或数据仓库。
InnoDB支持行级锁,并且实现了标准的行锁模型:共享锁(S锁)和排他锁(X锁)。共享锁允许多个事务同时读取同一行,排他锁则阻塞其他事务的读写。此外,InnoDB还实现了意向锁(Intention Lock)这种表级锁,用于快速判断表中是否存在被锁定的行,避免加表锁时全表逐行检查。需要注意的是,InnoDB的行锁是加在索引项上的,如果UPDATE或DELETE语句的WHERE条件没有命中索引,InnoDB将无法精确定位行,只能扫描全表并对所有行加锁,效果等同于锁表。
在默认的可重复读(REPEATABLE READ)隔离级别下,InnoDB还提供了间隙锁(Gap Lock)和临键锁(Next-Key Lock)来防止幻读。间隙锁锁定的是索引记录之间的区间,临键锁则是记录锁加间隙锁的组合,锁定一个左开右闭的区间。这也是为什么范围更新操作可能锁定较大范围,导致其他事务插入失败或阻塞的原因。
InnoDB行锁基于索引的原理与验证
InnoDB的行锁是锁定索引记录的,这一点非常关键。当WHERE条件使用了主键或唯一索引时,InnoDB只锁定匹配的行;当使用普通索引时,会锁定匹配的索引范围;当没有索引可用时,将退化为对全部记录加锁。下面通过一个示例验证这个行为。
-- 创建测试表并插入数据
CREATE TABLE t_user (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
INDEX idx_age (age)
) ENGINE=InnoDB;
INSERT INTO t_user VALUES (1, '张三', 20), (2, '李四', 25), (3, '王五', 30);
-- 会话A:根据主键更新,只锁定 id=1 这一行
BEGIN;
UPDATE t_user SET name = 'test' WHERE id = 1;
-- 会话B:更新 id=2,不受影响,可以正常执行
UPDATE t_user SET name = 'test2' WHERE id = 2;
-- 会话A:WHERE 条件无索引,锁定全部记录,效果等同锁表
BEGIN;
UPDATE t_user SET name = 'x' WHERE name = '张三';
会话B中更新id=2的语句能立即执行成功,说明会话A只锁定了id=1这一行。但如果会话A使用无索引的name字段作为条件,会话B的任何更新操作都会被阻塞,直到会话A提交或回滚。这提醒我们在写UPDATE和DELETE语句时,WHERE条件必须保证有索引可用,否则并发性能会断崖式下降。
查看锁竞争情况可以借助SHOW ENGINE INNODB STATUS命令,其中LATEST DETECTED DEADLOCK部分会记录最近一次死锁的详细信息,包括两个事务各自持有和等待的锁。也可以查询information_schema.INNODB_TRX和performance_schema.data_locks视图,查看当前正在运行的事务以及它们持有的锁类型和锁定的索引。
常见锁问题与最佳实践
实际开发中最常见的锁问题有三个:一是无索引更新导致的锁范围扩大,解决办法是确保WHERE条件走索引;二是死锁,InnoDB会自动检测并回滚代价较小的事务,业务代码中应捕获死锁异常并实现重试逻辑;三是长事务持锁时间过长,导致其他事务大量阻塞,应尽量缩短事务范围,把耗时的网络调用、文件操作移到事务外部。
编写高并发SQL还有几点建议:更新语句尽量基于主键操作,锁粒度最小;避免在事务中执行SELECT后再UPDATE的读改写模式,必要时使用SELECT ... FOR UPDATE显式加排他锁,或采用乐观锁(版本号机制)代替悲观锁;批量更新时控制每批次的行数,避免一次锁定过多记录;对于热点行(如秒杀库存扣减),可以考虑将一行拆成多行分散压力,或使用Redis等缓存层做前置扣减。
总结一下:MyISAM只支持表锁,适合读多写少的场景;InnoDB支持行锁、间隙锁和临键锁,适合高并发的OLTP业务。行锁虽好,但依赖索引才能发挥作用,无索引条件下的行锁会退化为全表加锁。理解锁机制、控制事务粒度、保证索引命中,是提升数据库并发处理能力的三个关键点。