Mysql随心杂谈 -- 让人头疼的“锁”
2026/5/14 1:51:34 网站建设 项目流程

一:什么是意向锁?

意向锁是数据库引擎内部使用的一种表级锁。它诞生的目的是为了解决一个核心矛盾:

如何高效判断“行锁”和表锁之间的冲突?

比如:事务A锁住了表中的某几行(行锁)。此时,事务B想来锁住整个表(表锁,比如LOCK TABLE ... WRITE)。如果没有意向锁,数据库就必须遍历表中每一行,检查是否有行锁存在,这效率极低。

意向锁就是用来快速解决这个问题的“路标”。

二、意向锁的几种类型

锁类型英文含义
意向共享锁IS Lock事务想要读取某些行时,先在表级别加上IS锁。
意向排他锁IX Lock事务想要修改某些行时,先在表级别加上IX锁。

三、自动加锁的实战场景(InnoDB引擎)

假设有一张user表,执行以下SQL:

场景1:查询一行(使用行级共享锁)

-- 在 serializable隔离级别或使用 LOCK IN SHARE MODE SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;

InnoDB的动作:

  1. 自动 对user表加意向共享锁(IS锁)
  2. 自动 对id= 1的数据行加 共享锁(行锁)

场景2:更新一行(需要行级排他锁)

UPDATE user SET name = "name" WHERE id = 1;

InnoDB的动作:

  1. 自动 对user表加意向排他锁(IX锁)
  2. 自动 对id=1的数据行加排他锁(行锁)

场景3:锁整张表(用户操作)

LOCK TABLES user WRITE;

数据库的冲突检查过程:

  • 看到用户想加表级排他锁

  • 立即检查user表上是否有意向锁(IS或IX)。

  • 如果发现有IX锁(代表有人在修改行),则冲突,事务B必须等待。

  • 这个过程是O(1)的简单检查,无需遍历行。

四:为什么不需要程序员手动加?

  1. 职责明确:意向锁是元数据级别的内部协调机制,用于在表锁和行锁之间建立沟通。程序员应该关注的是业务逻辑:SELECT...FOR UPDATE(行级排他锁),LOCK TABLES ...(表锁)等。
  2. 防止错误:如果允许用户手动加意向锁,极易破坏锁的兼容性矩阵,导致死锁或数据不一致

五、关键点总结

特性结论
加锁方式完全自动,由数据库引擎内部管理。
SQL语句用户无法写出任何语句来直接加意向锁。
可见性通过SHOW ENGINE INNODB STATUSperformance_schema可以观察到。
作用对象表级别
核心目的快速判断“表中是否有行锁”,避免遍历所有行。
兼容性意向锁之间互相兼容(IS和IS、IS和IX、IX和IX都不冲突)。只有表级排他锁才会阻塞意向锁。

六、一个形象的比喻

  • 整张表= 一栋宿舍楼。

  • 行锁= 某个具体的房间被锁上了。

  • 意向锁= 宿舍楼大门上挂的指示灯。

你想给整栋楼断电(加表锁),只需要看一眼大门口的指示灯(意向锁):

  • 如果指示灯亮IX(有人在使用房间),你就知道不能断电,得等着。

  • 你完全不用去每一层、每一个房间逐一检查门锁。

这个指示灯,就是数据库自动为你挂上去的。你只需要正常去锁房间(执行SQL),指示灯就会自动亮起。

问题一:为什么都说了是Innob是自动加意向锁,还要执行 LOCK IN SHARE MODE?

LOCK IN SHARE MODE加的是“行锁”,而意向锁是伴随这个行锁自动产生的“表级指示灯”。它们是两个不同层级、不同作用的锁。

可以理解为:

  • LOCK IN SHARE MODE:是你下达的业务指令(“我要读这几行,不让别人改”)。

  • 意向锁:是数据库收到指令后,为了高效管理而自动做的内部动作(“在表门口亮个灯,告诉别人这张表里有行锁”)。

详细拆解:执行一条SQL时发生了什么?

SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;

第1步:你下达指令(业务锁)

  • 你明确要求对id=1这一行加一个共享锁(行锁,S锁)

  • 目的:确保在你这笔事务期间,其他事务不能修改这一行。

  • 这是你主动的、显式的业务行为。

第2步:InnoDB自动响应(管理锁)

  • InnoDB引擎心想:“好的,你要锁行。但我为了整个数据库的性能,不能只顾这一行。我得在表级别做个标记,免得以后有人想锁整张表时,我还得去查每一行有没有锁。”

  • 于是,InnoDB自动user这张表加了一个意向共享锁(表锁,IS锁)

  • 这是数据库内部的、自动的管理行为,你无法控制,也无需知道。

问题二:核心对比:两种锁完全不同

特性LOCK IN SHARE MODE(你执行的)意向锁 (自动加的)
锁的粒度行级锁表级锁
加锁者用户(通过SQL)数据库系统(自动)
可见性用户明确知道并主动施加用户在正常操作中感觉不到,只能通过工具观察
锁的类型共享锁 (S)意向共享锁 (IS)
目的业务并发控制:防止其他事务修改或删除你读的数据。内部性能优化:快速检测表锁与行锁的冲突。
能否手动能,这是一条SQL指令不能,没有任何SQL能做到

问题三:为什么不能省略“显式的行锁”而只靠“自动的意向锁”?

因为它们的作用完全不同:

  • 意向锁只解决了“冲突检测”的效率问题,没有解决“数据保护”的业务问题。

    • 意向锁只是告诉你:“表里有人锁了行”。

    • 但它本身不保护任何一行数据。别的事务仍然可以绕过它,直接去修改那些没有被行锁锁住的行。

  • LOCK IN SHARE MODE/FOR UPDATE才是真正保护数据的锁。

    • 它们实实在在地锁住了特定的数据行,阻止其他事务的修改和删除。

问题四:既然有间隙锁(Gap Lock)和临键锁(Next-Key Lock),它们不也是“加锁”吗?那和我刚才说的行锁、意向锁是什么关系?

间隙锁和临键锁是“行锁的变体和组合”,他们依然属于“行级锁”的范畴,与“表级的意向锁”完全不同。而LOCK IN SHARE MODE加的是标准的行锁,在特定隔离级别和条件下,这个行锁会自动升级或扩展成临键锁

七:锁的完整层次结构:

表级锁 ├── 意向锁 (IS/IX) ← 自动加,管理用 ├── 表锁 (LOCK TABLES ...) ← 手动加 └── 元数据锁 (MDL) ← 自动加 行级锁 (InnoDB特有) ├── 记录锁 (Record Lock) ← 标准行锁,锁住索引记录 ├── 间隙锁 (Gap Lock) ← 锁住记录之间的间隙,防止插入 └── 临键锁 (Next-Key Lock) ← 记录锁 + 间隙锁(合体)

八:关键总结

你的认知实际情况
"我执行LOCK IN SHARE MODE是在加锁"✅ 对,你是在加行级锁
"数据库会自动加意向锁"✅ 对,这是表级的管理锁
"间隙锁和临键锁也是加锁"✅ 对,它们是行级锁的具体形态
"那我不执行SQL,间隙锁会自动加吗?"❌ 不会。必须先有DML或带锁的SELECT,才会产生间隙锁
"意向锁和间隙锁是一回事吗?"❌ 完全不同。意向锁是表级,间隙锁是行级间隙

问题五:一般业务中用什么锁?难道说实际操作中DML中,我简单的select查询都要加一个LOCK IN SHARE MODE?

一般业务中用什么锁?

绝大多数业务中,你根本不需要手动加任何锁。数据库的MVCC(多版本并发控制)帮你搞定了 99% 的场景。

难道简单 select 都要加 LOCK IN SHARE MODE?

绝对不需要!而且千万不要这么做!普通的SELECT * FROM user不加任何锁,它读的是快照数据,性能极高。

九:实际业务中,你到底什么时候才需要手动加锁?

我们来分三个层次看,你会发现自己平时基本都在第一层。

第一层:日常业务(90%的场景)—— 无锁,靠MVCC
-- 这才是你每天写的最多的SQL,它不加任何锁 SELECT * FROM orders WHERE user_id = 123; -- 更新操作会自动加行锁,但不用你操心 UPDATE orders SET status = 'paid' WHERE id = 456;
  • 原理:InnoDB 通过MVCC(多版本并发控制)让你读到的是一致性快照,不会读到未提交的数据,也不会被修改阻塞。

  • 你完全不用关心锁select畅快无比,update/delete数据库会自动加必要的行锁。

第二层:特定业务场景(9%的场景)—— 需要手动加锁

只有在为了满足特定业务逻辑,需要锁定正在读的数据,防止它被修改时,才手动加锁。

典型场景1:账户扣款(读书籍库存、抢券等)

-- 错误做法(并发会导致超卖) SELECT stock FROM product WHERE id = 1; -- 读到 stock=10 -- 此时另一个事务也读到了10 UPDATE product SET stock = stock - 1 WHERE id = 1; -- 两个事务都扣成9,超卖了 -- 正确做法:读的时候就把这行锁住 SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 加行级排他锁 -- 其他事务的更新/带锁的读会被阻塞 UPDATE product SET stock = stock - 1 WHERE id = 1;

典型场景2:读取后要基于这个值做决策(防止数据变化)

-- 比如:转账时检查余额是否足够,然后扣款 BEGIN; SELECT balance FROM account WHERE id = 1 FOR UPDATE; -- 锁住,防止期间被其他转账扣减 IF balance >= 1000 THEN UPDATE account SET balance = balance - 1000 WHERE id = 1; END IF; COMMIT;

什么时候用LOCK IN SHARE MODE

相对少见,通常用于“我只想确保读到的数据不被修改,但我自己也不改”的场景,比如生成报表时需要读取一组相关表并保证一致性。

你平时是怎么被“锁”的(而你完全没感觉)

这是最容易被误解的地方:你每天执行 SQL,InnoDB 在背后默默加了很多锁,但你无需知道。

你的操作数据库自动加的锁你写SQL的时候需要关心吗?
SELECT * FROM user无锁(读快照)❌ 不需要
INSERT INTO user ...行排他锁 + 意向排他锁❌ 不需要
UPDATE user SET ...行排他锁 + 意向排他锁 + 可能间隙锁❌ 不需要
DELETE FROM user ...行排他锁 + 意向排他锁 + 可能间隙锁❌ 不需要
SELECT ... FOR UPDATE行排他锁 + 意向排他锁需要,你明确加的
SELECT ... LOCK IN SHARE MODE行共享锁 + 意向共享锁需要,你明确加的

到底要不要手动加锁?

开始写SQL │ ├─ 只是查询数据展示(如列表页、详情页) │ └─ 用普通 SELECT,不加任何锁 ✅ │ ├─ 是 INSERT / UPDATE / DELETE 单条数据 │ └─ 直接写 DML,数据库自动加锁,不用操心 ✅ │ ├─ 需要“先读后写”,且写依赖于读到的值 │ ├─ 能否合并成一条 UPDATE SQL? │ │ ├─ 能 → 直接写 UPDATE ... SET ... WHERE ... ✅ │ │ └─ 不能(需要复杂逻辑判断)→ 用 SELECT ... FOR UPDATE ⚠️ │ │ │ └─ 是否超高并发(秒杀类)? │ └─ 考虑乐观锁或 Redis,避免数据库行锁竞争 ⚠️ │ └─ 需要读取多个表的数据生成报表,要求强一致 └─ 考虑 LOCK IN SHARE MODE 或 Serializable 隔离级别 ⚠️

十:乐观锁和悲观锁

乐观锁和悲观锁是解决并发问题的两种设计思想,是“怎么干活”的方法论。

两种锁的本质区别:

维度悲观锁乐观锁
核心思想"这行数据肯定会被别人改,我先锁住再说""这行数据大概率不会被别人改,更新时检查一下就行"
具体做法操作前先加锁,操作完再释放不加锁,更新时检查版本号或条件
数据库实现SELECT ... FOR UPDATE普通 UPDATE,加version字段
适用场景冲突很多(写多读少)冲突很少(读多写少)
性能低(有锁等待、死锁风险)高(无锁阻塞)

一句话总结:

悲观锁= 先抢厕所门锁,蹲完了再开。
乐观锁= 不锁门,上厕所前记一下门牌号,出来时看看门牌号变了没有,变了就重试。

实战代码:用手写SQL来感受区别:

假设有一个商品表,我们要扣库存(原库存 = 10)

方案一:悲观锁(手动加行锁)
-- 事务1(用户A买1件) BEGIN; SELECT stock FROM product WHERE id = 1 FOR UPDATE; -- 锁住这行,别人不能改 -- 假设读到 stock = 10 UPDATE product SET stock = 9 WHERE id = 1; COMMIT; -- 释放锁 -- 事务2(用户B同时买1件) -- 会被阻塞,等事务1提交后才能继续

特点

  • ✅ 数据绝对安全

  • ❌ 并发低,后面的请求都得排队

  • ❌ 可能死锁

方案2:乐观锁(用版本号)

首先表结构加一个字段

ALTER TABLE product ADD COLUMN version INT DEFAULT 0;
-- 先查出当前数据和版本号 SELECT stock, version FROM product WHERE id = 1; -- stock=10, version=5 -- 更新时检查版本号 UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5; -- 关键:版本号必须还是5 -- 检查 affected_rows -- 如果 = 1:更新成功 -- 如果 = 0:说明被别人改过了,重试整个流程

特点

  • ✅ 高并发,不阻塞其他事务

  • ❌ 需要写重试逻辑(通常循环3次)

  • ❌ 冲突多时(秒杀)会大量重试,反而更糟

十一:什么时候用哪个?(实战决策树)

你的业务场景 │ ├─ 冲突概率极高(秒杀、抢红包、热门商品扣库存) │ └─ 悲观锁也不够用,要上 Redis + 消息队列 │ ├─ 冲突概率中等(普通商品下单、普通账户扣款) │ ├─ 追求简单可靠 → 悲观锁(FOR UPDATE) │ └─ 追求极致性能 → 乐观锁 + 重试 │ ├─ 冲突概率低(更新用户昵称、修改订单备注) │ └─ 乐观锁 ✅ (几乎不会冲突,性能极佳) │ └─ 长事务 + 强一致性(金融转账、对账) └─ 悲观锁 ✅ (宁可慢,不能错)

十二:常见误区澄清(非常重要)

误区1:"MySQL 有个叫乐观锁的锁?"

错误。MySQL 本身没有"乐观锁"这个功能,它是你靠 version 字段自己实现的逻辑

误区2:"乐观锁一定比悲观锁好"

错误。冲突率高时,乐观锁会疯狂重试,性能雪崩。秒杀用乐观锁就是找死。

误区3:"用了悲观锁就不用管并发了"

错误。悲观锁可能死锁,可能锁升级成表锁,需要配合索引优化。

误区4:"乐观锁只能用一个 version 字段"

错误。你也可以用WHERE stock = old_stock(旧值条件),但 version 更通用。

十三:知识关联

【程序员主动加的锁(业务层)】 ├── 悲观锁思想 │ └── 实现方式:SELECT ... FOR UPDATE(行级锁) │ └── 底层触发:意向锁 + 记录锁/间隙锁(数据库自动) │ └── 乐观锁思想 └── 实现方式:version字段 + WHERE条件 └── 底层:普通UPDATE,无额外锁 【数据库自动加的锁(引擎层)】 ├── 意向锁(表级,管理用) ├── 记录锁、间隙锁、临键锁(行级,RR下自动) └── MDL锁(表结构稳定用) 【其他维度的锁】 ├── 共享锁 / 排他锁(读写属性) ├── 表锁 / 行锁(粒度) └── 死锁(你最终会遇到的问题)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询