mysql的锁

54 阅读12分钟

MySQL 的锁是 并发控制的核心,用于解决多用户同时操作数据时的冲突(如脏读、不可重复读、幻读、数据不一致)。其核心逻辑是:通过 “锁定” 数据资源,限制不同事务对数据的操作权限,确保事务隔离性(ACID 中的 I)。

锁的设计与存储引擎强相关(InnoDB 是默认且唯一支持事务和行锁的主流引擎),下面结合实际场景,从「分类→核心锁类型→引擎差异→常见问题」逐步拆解:

一、锁的核心分类(4 个维度)

MySQL 锁可从不同维度划分,核心分类如下,先建立整体认知:

分类维度具体类型核心作用
锁粒度表锁(Table Lock)、页锁(Page Lock)、行锁(Row Lock)控制锁定的范围(粒度越小,并发性能越好,开销越高)
锁模式共享锁(S 锁 / 读锁)、排他锁(X 锁 / 写锁)控制操作权限(读 / 写冲突控制)
InnoDB 特有意向锁(IS/IX)、记录锁、间隙锁、临键锁(Next-Key Lock)适配事务和索引,解决幻读、优化锁检查效率
并发策略悲观锁(Pessimistic Lock)、乐观锁(Optimistic Lock)锁的使用思路(悲观:先锁再操作;乐观:先操作后校验)

二、基础:锁粒度(表锁 vs 页锁 vs 行锁)

锁粒度决定了 “锁定数据的范围”,是影响并发性能的关键因素。

1. 表锁(Table Lock):最粗粒度的锁

  • 定义:锁定整张表,事务操作时,整张表无法被其他事务修改(读锁除外)。

  • 支持引擎:所有引擎(MyISAM 默认,InnoDB 也支持,但不常用)。

  • 核心特点

    • 优点:开销极小(无需定位行 / 页)、加锁解锁速度快;
    • 缺点:并发性能极差(写操作会阻塞所有其他读写,读操作会阻塞写操作)。
  • 触发场景

    • MyISAM 引擎的所有写操作(INSERT/UPDATE/DELETE)默认加表级排他锁,读操作加表级共享锁;

    • InnoDB 执行无索引条件的 UPDATE/DELETE(如 UPDATE user SET age=20,无 WHERE 或 WHERE 条件无索引),会升级为表锁;

    • 手动加表锁:

      LOCK TABLES user READ; -- 加表级共享锁(自己可读,其他事务可读不可写)
      LOCK TABLES user WRITE; -- 加表级排他锁(自己可读写,其他事务不可读写)
      UNLOCK TABLES; -- 释放锁(事务结束也会自动释放)
      

2. 行锁(Row Lock):最细粒度的锁

  • 定义:仅锁定需要操作的行数据,其他行不受影响(InnoDB 核心锁类型)。

  • 支持引擎:仅 InnoDB(依赖聚簇索引,通过索引定位行)。

  • 核心特点

    • 优点:并发性能极好(多事务可同时操作不同行);
    • 缺点:开销大(需定位行、维护锁结构)、可能引发死锁。
  • 触发场景

    • InnoDB 中,带索引条件的 SELECT/INSERT/UPDATE/DELETE 自动加行锁(如 UPDATE user SET age=20 WHERE id=1,id 是主键索引,仅锁 id=1 的行);
    • 关键:行锁依赖索引!若 WHERE 条件无索引(或索引失效),会升级为表锁(因无法定位具体行)。

3. 页锁(Page Lock):中间粒度的锁

  • 定义:锁定数据页(InnoDB 页大小默认 16KB),介于表锁和行锁之间。
  • 支持引擎:BDB 引擎(极少用),InnoDB 仅在特定场景间接使用(如页分裂时的页级锁)。
  • 核心特点:开销和并发性能介于两者之间,因实用性低,无需重点关注。

粒度对比总结

锁粒度并发性能开销适用场景代表引擎
表锁极低极小读多写少、单表数据量小(如配置表)MyISAM
行锁极高极大高并发读写、多表关联(如订单表)InnoDB
页锁中等中等极少使用BDB

三、核心:锁模式(共享锁 S vs 排他锁 X)

所有锁的本质都是 “控制操作权限”,S 锁和 X 锁是最基础的锁模式,核心规则是 “读写冲突、读读兼容”

1. 共享锁(S 锁 / 读锁)

  • 作用:允许事务读取数据,但禁止修改数据。

  • 加锁方式

    • 自动:InnoDB 中 SELECT ... FOR SHARE(MySQL 8.0+)或 SELECT ... LOCK IN SHARE MODE(低版本);
    • 注意:普通 SELECT 语句默认不加锁(InnoDB 快照读机制),仅在显式声明或事务隔离级别为 SERIALIZABLE 时加 S 锁。
  • 兼容性:与其他 S 锁兼容(多个事务可同时读),与 X 锁互斥(读时禁止写)。

2. 排他锁(X 锁 / 写锁)

  • 作用:允许事务修改数据(INSERT/UPDATE/DELETE),禁止其他事务读写。

  • 加锁方式

    • 自动:InnoDB 中 INSERT/UPDATE/DELETE 语句默认加 X 锁;
    • 显式:SELECT ... FOR UPDATE(查询时加 X 锁,阻塞其他事务读写)。
  • 兼容性:与所有锁(S 锁、X 锁)互斥(写时禁止其他读写)。

锁模式兼容性矩阵(关键!)

已持有锁 \ 请求锁S 锁(读)X 锁(写)
S 锁(读)兼容(可共存)互斥(阻塞)
X 锁(写)互斥(阻塞)互斥(阻塞)

实战示例:S 锁与 X 锁的冲突

-- 事务A(读操作,加S锁)
START TRANSACTION;
SELECT * FROM user WHERE id=1 FOR SHARE; -- 加S锁
-- 此时事务B可执行 SELECT * FROM user WHERE id=1 FOR SHARE(兼容)
-- 但事务B执行 UPDATE user SET age=20 WHERE id=1(需X锁)会被阻塞,直到事务A提交/回滚

-- 事务A提交后,事务B的UPDATE才会执行
COMMIT;
-- 事务A(写操作,加X锁)
START TRANSACTION;
UPDATE user SET age=20 WHERE id=1; -- 自动加X锁
-- 此时事务B执行 SELECT * FROM user WHERE id=1 FOR SHARE(需S锁)会被阻塞
-- 事务B执行 UPDATE user SET age=21 WHERE id=1(需X锁)也会被阻塞

COMMIT; -- 事务A提交,锁释放,事务B执行

四、InnoDB 特有锁:解决事务与索引的复杂场景

InnoDB 作为事务安全引擎,在 S/X 锁基础上扩展了 4 类特有锁,核心是「适配聚簇索引」和「解决幻读」。

1. 意向锁(Intention Lock:IS/IX)

  • 核心问题:表锁和行锁需要快速判断兼容性(如事务 A 持有行锁,事务 B 申请表锁,如何避免逐行检查行锁?)。

  • 定义:意向锁是「表级锁」,用于标识 “表中是否有行锁”,无需手动加锁,InnoDB 自动生成。

  • 类型

    • 意向共享锁(IS):事务计划对表中某些行加 S 锁(执行 SELECT ... FOR SHARE 前自动加);
    • 意向排他锁(IX):事务计划对表中某些行加 X 锁(执行 INSERT/UPDATE/DELETE 或 SELECT ... FOR UPDATE 前自动加)。
  • 兼容性

    • IS 锁:仅与表级 S 锁兼容,与表级 X 锁互斥;
    • IX 锁:与表级 S/X 锁都互斥(因表中已有行锁,禁止全表操作);
  • 作用:减少锁检查开销(判断表锁是否可行时,只需检查意向锁,无需逐行检查行锁)。

2. 记录锁(Record Lock):锁定具体行

  • 定义:锁定索引对应的「具体行数据」,仅锁定存在的记录,不锁定间隙(解决 “不可重复读”)。

  • 适用场景:InnoDB 隔离级别为 READ COMMITTED(RC)时,或查询条件是「唯一索引等值查询」(如主键 id=1)。

  • 示例

    -- id 是主键(唯一索引),隔离级别 RC
    UPDATE user SET age=20 WHERE id=1; -- 仅锁定 id=1 的行(记录锁)
    -- 其他事务可操作 id≠1 的行,无冲突
    

3. 间隙锁(Gap Lock):锁定索引间隙

  • 定义:锁定「索引之间的空白区域」(不包含记录本身),用于防止插入新数据(解决 “幻读”)。

  • 适用场景:InnoDB 隔离级别为 REPEATABLE READ(RR,默认隔离级别),查询条件是「范围查询」(如 id BETWEEN 1 AND 5)。

  • 示例

    -- id 是主键,隔离级别 RR
    SELECT * FROM user WHERE id BETWEEN 1 AND 5 FOR UPDATE; -- 加间隙锁
    -- 锁定的范围是 (负无穷,1)、(1,2)、(2,3)、(3,4)、(4,5)、(5, 正无穷)
    -- 其他事务无法插入 id 在 1~5 之间的新数据(如 id=3),避免幻读
    
  • 注意:间隙锁仅在 RR 级别生效,RC 级别会关闭间隙锁(通过 innodb_locks_unsafe_for_binlog=1 控制)。

4. 临键锁(Next-Key Lock):记录锁 + 间隙锁

  • 定义:InnoDB RR 级别下的「默认行锁类型」,是 “记录锁 + 间隙锁” 的组合,锁定范围是「左开右闭」。

  • 核心规则:锁定索引记录及其左侧的间隙(如索引值为 1、3、5,临键锁范围是 (-∞,1]、(1,3]、(3,5]、(5,+∞))。

  • 示例

    -- id 是主键,隔离级别 RR(默认)
    SELECT * FROM user WHERE id > 3 FOR UPDATE; -- 加临键锁
    -- 锁定范围是 (3,5]、(5,+∞)(假设存在 id=5 的记录)
    -- 其他事务无法插入 id=4、6 的数据(避免幻读),也无法修改 id=5 的行(记录锁)
    
  • 作用:RR 级别通过临键锁解决了 “幻读”(事务多次查询同一范围,结果一致),这是 InnoDB 默认隔离级别的核心优势。

五、并发策略:悲观锁 vs 乐观锁

这是锁的 “使用思路”,而非具体锁类型,适用于不同并发场景。

1. 悲观锁(Pessimistic Lock)

  • 核心思路:假设并发冲突一定会发生,先加锁再操作(“先锁后做”)。

  • 实现方式:InnoDB 的行锁(S/X 锁)、表锁都是悲观锁(自动或手动加锁)。

  • 适用场景:写多读少(如订单支付、库存扣减,冲突概率高)。

  • 示例(库存扣减,避免超卖)

    -- 事务A:扣减库存(加X锁,阻塞其他事务修改)
    START TRANSACTION;
    -- 显式加X锁,防止其他事务同时扣减
    SELECT stock FROM product WHERE id=1 FOR UPDATE; 
    UPDATE product SET stock=stock-1 WHERE id=1 AND stock>0; -- 扣减库存
    COMMIT; -- 释放锁
    

2. 乐观锁(Optimistic Lock)

  • 核心思路:假设并发冲突不会发生,先操作数据,最后通过 “版本校验” 判断是否冲突(“先做后验”)。

  • 实现方式:无需数据库锁,通过业务字段实现(如版本号 version、时间戳 update_time)。

  • 适用场景:读多写少(如商品详情查询、用户信息查看,冲突概率低)。

  • 示例(更新用户信息,避免覆盖)

    -- 1. 表结构添加版本号字段
    CREATE TABLE user (
      id INT PRIMARY KEY AUTO_INCREMENT,
      username VARCHAR(50) NOT NULL,
      version INT DEFAULT 1 -- 版本号,初始为1
    );
    
    -- 2. 事务A:更新用户信息(先查版本,后校验)
    START TRANSACTION;
    -- 查询数据时获取当前版本号
    SELECT username, version FROM user WHERE id=1; -- 假设版本号=1
    -- 更新时校验版本号(仅当版本号匹配时才更新,同时版本号+1)
    UPDATE user SET username='zhangsan_new', version=version+1 WHERE id=1 AND version=1;
    -- 判断更新行数,若为0则说明版本已被修改(冲突),回滚
    IF ROW_COUNT() = 0 THEN ROLLBACK; END IF;
    COMMIT;
    
  • 优点:无锁开销,并发性能极高;缺点:冲突时需重试,增加业务复杂度。

六、InnoDB 与 MyISAM 锁机制核心差异

对比维度InnoDB(默认)MyISAM(已淘汰)
锁粒度行锁(为主)+ 表锁仅表锁
事务支持支持(依赖锁实现隔离性)不支持
锁模式S/X 锁、意向锁、临键锁等仅表级 S/X 锁
并发性能高(多事务可操作不同行)低(写阻塞读、读阻塞写)
死锁可能发生(行锁 + 事务)不可能(表锁,加锁顺序唯一)
适用场景高并发读写、事务场景(电商、支付)只读场景(报表、日志)

七、锁的常见问题与解决方案

1. 死锁(最常见问题)

  • 定义:两个或多个事务互相持有对方需要的锁,导致永久阻塞(如事务 A 持有行 1 的 X 锁,申请行 2 的 X 锁;事务 B 持有行 2 的 X 锁,申请行 1 的 X 锁)。

  • 示例

    -- 事务A
    START TRANSACTION;
    UPDATE user SET age=20 WHERE id=1; -- 持有id=1的X锁
    UPDATE user SET age=30 WHERE id=2; -- 申请id=2的X锁(被事务B阻塞)
    
    -- 事务B
    START TRANSACTION;
    UPDATE user SET age=25 WHERE id=2; -- 持有id=2的X锁
    UPDATE user SET age=35 WHERE id=1; -- 申请id=1的X锁(被事务A阻塞)
    -- 双方互相阻塞,死锁发生
    
  • 解决方案

    1. 统一事务加锁顺序(如所有事务先操作 id=1,再操作 id=2);
    2. 减少事务长度(避免长事务持有锁过久);
    3. 避免一次性锁定大量数据(拆分 SQL);
    4. 排查死锁:SHOW ENGINE INNODB STATUS;(查看最近死锁日志)。

2. 锁等待超时

  • 定义:事务申请锁时,超过指定时间(innodb_lock_wait_timeout,默认 50 秒)仍未获取到锁,触发超时。

  • 解决方案

    1. 优化 SQL(确保索引有效,避免行锁升级为表锁);
    2. 缩短事务执行时间(避免事务中包含非数据库操作,如 RPC 调用);
    3. 调整超时参数(根据业务设置合理值,如 SET GLOBAL innodb_lock_wait_timeout=10;)。

3. 行锁升级为表锁

  • 原因:WHERE 条件无索引(或索引失效),InnoDB 无法定位具体行,只能锁定整张表。

  • 示例(错误用法)

    -- username 无索引,导致行锁升级为表锁
    UPDATE user SET age=20 WHERE username='zhangsan'; -- 锁定整张表
    
  • 解决方案

    1. 为 WHERE 条件字段建立索引(如 CREATE INDEX idx_user_username ON user(username););
    2. 避免使用无索引的范围查询(如 WHERE age>20 若 age 无索引,会锁全表)。

八、锁的使用建议(实战总结)

  1. 优先使用 InnoDB 引擎:仅 InnoDB 支持行锁和事务,是高并发场景的唯一选择;

  2. 合理设计索引:索引是行锁的基础,避免无索引导致表锁;

  3. 控制事务粒度:事务尽量 “短平快”,避免持有锁过久;

  4. 避免锁冲突

    • 读多写少用乐观锁(版本号),写多读少用悲观锁(SELECT ... FOR UPDATE);
    • 批量操作拆分小事务(如批量更新 1000 行,拆分为 10 个事务,每个更新 100 行);
  5. 隔离级别选择

    • 默认 RR 级别(临键锁解决幻读,并发性能平衡);
    • 读多写少场景可改为 RC 级别(关闭间隙锁,减少死锁概率,但允许幻读);
  6. 避免显式加表锁:InnoDB 中尽量不用 LOCK TABLES,依赖行锁实现并发控制。

总结

MySQL 锁的核心是「粒度 + 模式 + 引擎」:

  • 粒度:行锁(并发优)> 页锁 > 表锁(并发差);
  • 模式:S/X 锁控制读写冲突,InnoDB 扩展意向锁、临键锁适配事务;
  • 引擎:InnoDB 是唯一支持事务和行锁的主流引擎,MyISAM 已淘汰。

实际开发中,锁问题的根源多是「索引设计不合理」或「事务管理不当」。只要确保索引有效、事务简短、锁粒度足够细,就能最大程度避免锁冲突,提升并发性能。