Mysql随心杂谈 -- 让人头疼的“锁”
一什么是意向锁意向锁是数据库引擎内部使用的一种表级锁。它诞生的目的是为了解决一个核心矛盾如何高效判断“行锁”和表锁之间的冲突比如事务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锁自动 对id1的数据行加排他锁行锁。场景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 MODELOCK IN SHARE MODE加的是“行锁”而意向锁是伴随这个行锁自动产生的“表级指示灯”。它们是两个不同层级、不同作用的锁。可以理解为LOCK IN SHARE MODE是你下达的业务指令“我要读这几行不让别人改”。意向锁是数据库收到指令后为了高效管理而自动做的内部动作“在表门口亮个灯告诉别人这张表里有行锁”。详细拆解执行一条SQL时发生了什么SELECT * FROM user WHERE id 1 LOCK IN SHARE MODE;第1步你下达指令业务锁你明确要求对id1这一行加一个共享锁行锁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; -- 读到 stock10 -- 此时另一个事务也读到了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相对少见通常用于“我只想确保读到的数据不被修改但我自己也不改”的场景比如生成报表时需要读取一组相关表并保证一致性。你平时是怎么被“锁”的而你完全没感觉这是最容易被误解的地方你每天执行 SQLInnoDB 在背后默默加了很多锁但你无需知道。你的操作数据库自动加的锁你写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; -- stock10, version5 -- 更新时检查版本号 UPDATE product SET stock stock - 1, version version 1 WHERE id 1 AND version 5; -- 关键版本号必须还是5 -- 检查 affected_rows -- 如果 1更新成功 -- 如果 0说明被别人改过了重试整个流程特点✅ 高并发不阻塞其他事务❌ 需要写重试逻辑通常循环3次❌ 冲突多时秒杀会大量重试反而更糟十一什么时候用哪个实战决策树你的业务场景 │ ├─ 冲突概率极高秒杀、抢红包、热门商品扣库存 │ └─ 悲观锁也不够用要上 Redis 消息队列 │ ├─ 冲突概率中等普通商品下单、普通账户扣款 │ ├─ 追求简单可靠 → 悲观锁FOR UPDATE │ └─ 追求极致性能 → 乐观锁 重试 │ ├─ 冲突概率低更新用户昵称、修改订单备注 │ └─ 乐观锁 ✅ 几乎不会冲突性能极佳 │ └─ 长事务 强一致性金融转账、对账 └─ 悲观锁 ✅ 宁可慢不能错十二常见误区澄清非常重要误区1MySQL 有个叫乐观锁的锁❌错误。MySQL 本身没有乐观锁这个功能它是你靠 version 字段自己实现的逻辑。误区2乐观锁一定比悲观锁好❌错误。冲突率高时乐观锁会疯狂重试性能雪崩。秒杀用乐观锁就是找死。误区3用了悲观锁就不用管并发了❌错误。悲观锁可能死锁可能锁升级成表锁需要配合索引优化。误区4乐观锁只能用一个 version 字段❌错误。你也可以用WHERE stock old_stock旧值条件但 version 更通用。十三知识关联【程序员主动加的锁业务层】 ├── 悲观锁思想 │ └── 实现方式SELECT ... FOR UPDATE行级锁 │ └── 底层触发意向锁 记录锁/间隙锁数据库自动 │ └── 乐观锁思想 └── 实现方式version字段 WHERE条件 └── 底层普通UPDATE无额外锁 【数据库自动加的锁引擎层】 ├── 意向锁表级管理用 ├── 记录锁、间隙锁、临键锁行级RR下自动 └── MDL锁表结构稳定用 【其他维度的锁】 ├── 共享锁 / 排他锁读写属性 ├── 表锁 / 行锁粒度 └── 死锁你最终会遇到的问题