一:什么是意向锁?
意向锁是数据库引擎内部使用的一种表级锁。它诞生的目的是为了解决一个核心矛盾:
如何高效判断“行锁”和表锁之间的冲突?
比如:事务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的动作:
- 自动 对user表加意向共享锁(IS锁)
- 自动 对id= 1的数据行加 共享锁(行锁)
场景2:更新一行(需要行级排他锁)
UPDATE user SET name = "name" WHERE id = 1;InnoDB的动作:
- 自动 对
user表加意向排他锁(IX锁) - 自动 对
id=1的数据行加排他锁(行锁)。
场景3:锁整张表(用户操作)
LOCK TABLES user WRITE;数据库的冲突检查过程:
看到用户想加表级排他锁。
立即检查
user表上是否有意向锁(IS或IX)。如果发现有IX锁(代表有人在修改行),则冲突,事务B必须等待。
这个过程是O(1)的简单检查,无需遍历行。
四:为什么不需要程序员手动加?
- 职责明确:意向锁是元数据级别的内部协调机制,用于在表锁和行锁之间建立沟通。程序员应该关注的是业务逻辑:SELECT...FOR UPDATE(行级排他锁),LOCK TABLES ...(表锁)等。
- 防止错误:如果允许用户手动加意向锁,极易破坏锁的兼容性矩阵,导致死锁或数据不一致
五、关键点总结
| 特性 | 结论 |
|---|---|
| 加锁方式 | 完全自动,由数据库引擎内部管理。 |
| SQL语句 | 用户无法写出任何语句来直接加意向锁。 |
| 可见性 | 通过SHOW ENGINE INNODB STATUS或performance_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锁(表结构稳定用) 【其他维度的锁】 ├── 共享锁 / 排他锁(读写属性) ├── 表锁 / 行锁(粒度) └── 死锁(你最终会遇到的问题)