灯下哥谭 灯下哥谭
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

灯下哥谭

灯还亮着
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL

    • MySQL 运维知识地图:从入门配置到高可用排障
    • MySQL8 配置文件 my.cnf 重要参数解读
    • MySQL 导出 CSV 中文乱码:字符集链路从头讲一遍
    • MySQL 角色管理
    • MySQL网络抓包审计
    • MySQL 性能压测:Sysbench 1.0 实战
    • MySQL Router 实现读写分离
    • Gh-ost重建表,清除表碎片率
    • MySQL MGR配合MySQL-router实现innodb-cluster
    • MySQL 快速分析binlog定位问题
    • MySQL执行计划分析
    • DBA常用SQL和命令整理备查
    • 单表数据同步方案选型:为什么不该用 mysqldump 做「实时同步」
    • MySQL的事务隔离级别
    • MySQL存储过程批量生成数据
    • MySQL insert on duplicate key update,replace into , insert ignore的理解
    • MySQL不同字符集之间的区别和选择
    • MySQL为什么有时候会选错索引
    • MySQL死锁问题
    • MySQL使用SQL语句查重去重
    • MySQLdump逻辑备份
    • MySQL 基于 GTID 主从复制:跳过异常事务的正确姿势
    • MySQL8快速克隆插件使用指南
    • MySQL8双1设置保障安全
    • MySQL锁
      • MySQL 锁
      • 锁的类型
      • 锁的粒度
      • 死锁
      • 乐观锁与悲观锁
        • 乐观锁
        • 悲观锁
      • 锁升级:InnoDB 没有这个机制
      • MySQL 并发更新数据的加锁处理
      • SELECT 显式加锁
      • 使用乐观锁
      • 坑与边界
    • innodb cluster安装
    • OPTIMIZE TABLE 和 ANALYZE TABLE 的区别:用实测数据说话
    • MySQLReplicaSet 安装
    • MySQL 的 Left join、Right join 和 Inner join 的区别
    • ORDER BY 配合 LIMIT 触发的索引选择陷阱
  • Redis

  • 高性能KV

  • TiDB

  • Elasticsearch

  • 数据管道

  • 其他数据库

  • 数据库
  • MySQL
灯下哥谭
2022-09-12
目录

MySQL锁

  • MySQL 锁
    • 锁的类型
    • 锁的粒度
    • 死锁
    • 乐观锁与悲观锁
      • 乐观锁
      • 悲观锁
    • 锁升级
  • MySQL 并发更新数据的加锁处理
    • SELECT 显式加锁
    • 使用乐观锁

# MySQL 锁

# 锁的类型

  • InnoDB实现了两种标准的行级锁:
    1. 共享锁(S Lock)
      • 语法:SELECT * FROM table FOR SHARE(8.0 起的写法;LOCK IN SHARE MODE 是 5.7 的旧语法,8.0 仍兼容但不建议在新代码里用)。
    2. 排他锁(X Lock)
      • 语法:SELECT * FROM table FOR UPDATE。
  • 如果一个事务T1已经获取了行r的共享锁,另一个事务T2可以立即获得行r的共享锁。因为读取不会改变行的数据,所以多个事务可以同时获取共享锁,这称为锁兼容。
  • 但是,如果有其他事务T3想获得行r的排他锁,则必须等待事务T1和T2释放行r上的共享锁,这称为锁不兼容。
X S
X 不兼容 不兼容
S 不兼容 兼容

版本说明

本文写于 2022-09,基于 MySQL 8.0 InnoDB。
行锁/表锁/间隙锁等锁机制未变,经 2026-07 复核仍适用。

  • 普通的 SELECT 语句默认不加锁(快照读,走 MVCC);INSERT / UPDATE / DELETE 会对扫描到的行加排他锁——注意是「扫描到的」而不是「最终改动的」,条件没走索引时两者差别巨大。

# 锁的粒度

锁级别 说明
表级锁 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
页面锁 开销和加锁时间介于表锁和行锁之间。InnoDB 和 MyISAM 都不提供页面锁,这一行是数据库通论里的分类,MySQL 里只有早已移除的 BDB 引擎用过,实际选型时不用考虑。
  • 以下情况表锁反而更划算(注意这是 MyISAM 语境,InnoDB 上一般不需要手动 LOCK TABLES):
    • 表的大部分行用于读取。
    • 对严格的关键字进行读取和更新:
      • UPDATE tbl_name SET column=value WHERE unique_key_col=key_value;
      • DELETE FROM tbl_name WHERE unique_key_col=key_value;
    • SELECT 结合并行的 INSERT 语句,并且只有很少的 UPDATE 或 DELETE 语句。
    • 在整个表上有许多扫描或 GROUP BY 操作,没有任何写操作。

# 死锁

InnoDB 默认开启死锁检测(innodb_deadlock_detect=ON),检测到环路后会主动回滚其中一个事务来打破死锁,被选中的是代价最小的那个——判据是该事务在 undo 里累积的修改量(插入/更新/删除的行数),不是「持有锁最少」。被回滚方收到 ERROR 1213 (40001): Deadlock found,应用侧必须有重试逻辑,把它当成正常会发生的事件处理,而不是异常告警。

死锁细节与典型场景见 MySQL死锁问题。

# 乐观锁与悲观锁

# 乐观锁

  • 通过数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。
    • 为数据增加一个版本标识,通常通过为数据库表增加一个数字类型的 version 字段来实现。
    • 当读取数据时,将 version 字段的值一同读出,每次更新数据时,对 version 值加 1。
    • 当提交更新时,判断数据库表对应记录的当前版本信息与第一次取出的 version 值是否相等,如果相等,则予以更新,否则认为是过期数据。
-- 读取数据和版本号
SELECT id, value, version FROM TABLE WHERE id = <id>;

-- 按特定版本号更新
UPDATE TABLE
SET value = 2, version = version + 1
WHERE id = <id> AND version = <version>;
1
2
3
4
5
6
7

# 悲观锁

  • 即上面提到的共享锁和排他锁。

# 锁升级:InnoDB 没有这个机制

网上大量中文资料会写「InnoDB 的锁升级阈值是 5000 行」「锁内存超过 40% 触发升级」——这是 SQL Server 的锁升级(Lock Escalation)机制,InnoDB 没有。照着这个结论去解释线上现象,会把根因找错。

InnoDB 的实际做法是:行锁在页级用位图管理。一个事务在同一个页上锁 1 行还是锁 100 行,锁结构的内存开销基本一致,所以 InnoDB 不需要「锁太多了就升级成表锁」这种降级保护。

那为什么线上确实会看到「明明只更新一行,却像锁了整张表」?真实原因是下面这三种,都不是锁升级:

  1. 没走索引,退化成全表加锁。UPDATE t SET c=1 WHERE no_index_col=5 会扫全表,扫到的每一行都要加锁——现象上等同于锁表,但机制是「扫了多少行就锁多少行」。这也是「更新条件一定要走索引」的硬性理由。
  2. 间隙锁 / next-key lock 把范围整段锁住。RR 隔离级别下范围条件会锁住索引记录之间的间隙,阻塞该范围内的插入,影响面远大于「几行」。
  3. 元数据锁(MDL)。DDL 会持有 MDL 写锁,与之冲突的查询全部排队,SHOW PROCESSLIST 里表现为大量 Waiting for table metadata lock——这是最常被误认成「锁升级」的一类。

验证「到底锁了什么」不要靠猜,直接查(MySQL 8.0):

-- 当前持有和等待中的锁
SELECT ENGINE_TRANSACTION_ID, OBJECT_NAME, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA
FROM performance_schema.data_locks;

-- 谁在等谁
SELECT * FROM sys.innodb_lock_waits\G
1
2
3
4
5
6

LOCK_MODE 里出现 X,GAP、X,REC_NOT_GAP、X(即 next-key)能直接区分上面第 2 种情况;LOCK_TYPE=TABLE 且 LOCK_MODE=IX 只是意向锁,不代表表被锁住了——意向锁之间互相兼容,这也是一个高频误读。

# MySQL 并发更新数据的加锁处理

  • MySQL 支持给数据行加锁(InnoDB),并且在 UPDATE/DELETE 等操作时会自动加上排他锁。
  • 但并非只要有 UPDATE 关键字就会全程加锁,例如:
UPDATE table1 SET num = num + 1 WHERE id = 1;
1
  • 这条语句实际上相当于两条 SQL 语句(伪代码):
a = SELECT * FROM table1 WHERE id = 1;
UPDATE table1 SET num = a.num + 1 WHERE id = 1;
1
2
  • 其中执行 SELECT 语句时没有加锁,只有在执行 UPDATE 时才加锁,这会导致并发操作时的数据更新不一致。
  • 解决方法有两种:
    • 通过事务显式对 SELECT 加锁。
    • 使用乐观锁机制。

# SELECT 显式加锁

  • 对 SELECT 进行加锁的方式有两种:
SELECT ... FOR SHARE;           -- 共享锁(8.0 语法,旧写法 LOCK IN SHARE MODE 仍兼容)
SELECT ... FOR UPDATE;          -- 排他锁
1
2

「其它事务不可读写」这个说法是错的,这是关于 FOR UPDATE 最常见的误解:它只挡住当前读(其他事务的 FOR UPDATE / FOR SHARE / UPDATE / DELETE),挡不住普通 SELECT——普通 SELECT 是快照读,走 MVCC 读 undo 里的历史版本,不需要加锁也不会被阻塞。所以:

  • 想让并发更新串行化 → 用 FOR UPDATE,有效
  • 想让别人「读不到旧值」→ FOR UPDATE 做不到,那是隔离级别的事(见 MySQL的事务隔离级别)

8.0 还补了两个实用修饰符,避免锁等待把连接堆死:

SELECT ... FOR UPDATE NOWAIT;        -- 拿不到锁立刻报错 ERROR 3572,不等
SELECT ... FOR UPDATE SKIP LOCKED;   -- 跳过已被锁住的行,做队列表分发很好用
1
2
  • 对于上述场景,必须使用排它锁。
  • 上述两种语句只有在事务中才能生效,否则不会生效。在 MySQL 命令行中使用事务的方式如下:
SET AUTOCOMMIT = 0;
BEGIN WORK;
    a = SELECT num FROM table1 WHERE id = 2 FOR UPDATE;
    UPDATE table1 SET num = a.num + 1 WHERE id = 2;
COMMIT WORK;
1
2
3
4
5
  • 这样在更新数据时使用事务操作,可以在并发情况下通过锁将并发改为顺序执行。

# 使用乐观锁

  • 乐观锁是锁实现的一种机制,它假定所有需要修改的数据都不会冲突。
  • 在更新之前不加锁,而是查询数据行的版本号(版本号是自定义字段,需在业务表上增加,每次更新时自增或更新)。
  • 在具体更新数据时,更新条件中添加版本号信息。当版本号未变化时,说明数据行未被更新过,满足更新条件,因此更新成功。
  • 当版本号变化时,条件不满足,需要重新查询数据行,再次使用新的版本号进行更新。

原则上,两种方式都可支持,具体使用哪一种取决于实际业务场景,对哪种支持更好,并且对性能影响最小。

# 坑与边界

  • data_locks 只看得到当前还持有的锁。事务提交/回滚后锁立即消失,事后排查锁冲突要靠 SHOW ENGINE INNODB STATUS(最近一次死锁)或提前打开 innodb_print_all_deadlocks=ON 把每次死锁都记进错误日志。
  • 意向锁(IS/IX)不是表锁。performance_schema.data_locks 里几乎每个写事务都会有一条 LOCK_TYPE=TABLE, LOCK_MODE=IX,它只是「我打算在这张表里加行锁」的声明,彼此兼容,看到它不代表表被锁了。
  • 乐观锁不是数据库提供的锁。本文「乐观锁」一节讲的 version 字段是应用层协议,MySQL 对此一无所知——它没有加任何锁,靠的是 UPDATE ... WHERE version=? 的影响行数为 0 来发现冲突。所以它只能保护走这套约定的写入路径:任何绕过 version 条件的 SQL(手工改数、别的服务直写、导数任务)都会悄悄破坏它。
  • autocommit=1 下单条 SELECT ... FOR UPDATE 等于白加锁:语句结束事务就提交,锁立刻释放。加锁读必须包在显式事务里。
  • RR 与 RC 下的加锁范围不同。RC 没有间隙锁,锁冲突面小得多,但代价是不可重复读;如果线上大量因间隙锁产生的死锁,把隔离级别降到 RC 是常见解法,但要先确认业务能接受。
#锁与并发#MySQL
上次更新: 9/11/2026

← MySQL8双1设置保障安全 innodb cluster安装→

最近更新
01
DeepSeek Harness 实战 06|学习笔记:插件、工具、技能不在同一个维度上 原创
09-11
02
DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆 原创
09-09
03
DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力 原创
09-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式