InnoDB锁机制全景图:全局锁、表锁、行锁、意向锁到底怎么用?

概述

带着问题来学习 1、mysql 中的锁分类有那些? 2、在一个事务中 锁是自动开启的么? 3、read view 是干什么用的,当前读和快照读有什么区别? 4、执行普通的 sql 语句 自动触发的锁有哪些 ,需要手动触发的有哪些?

事务

事务的特性(ACID)

1、A --- Atomicity 原子性

核心:事务是最小单元,不可拆分

  • 事务里所有 SQL,全部成功提交,或者全部回滚,不会出现执行一半的情况。
  • 实现:undo log 回滚日志,失败时根据 undo log 恢复数据。

2、C --- Consistency 一致性

核心:事务执行前后,数据的业务规则状态保持合法

  • 数据库从一个合法状态,变成另一个合法状态,不会出现非法数据。
  • 原子性、隔离性、持久性最终都是为了保证一致性。

3、I --- Isolation 隔离性

核心:多个并发事务之间互相隔离,互不干扰 并发事务如果不隔离,会产生三类问题:脏读、不可重复读、幻读。

4、D --- Durability 持久性

核心:事务一旦提交,修改永久写入磁盘,崩溃也不会丢失

  • 提交成功后,数据持久化保存。
  • 实现:redo log 重做日志,崩溃恢复时重放 redo log。

事务的隔离级别

MySQL 4 种隔离级别(从低到高):

隔离级别 脏读 不可重复读 幻读
读未提交 READ UNCOMMITTED ✅存在 ✅存在 ✅存在
读已提交 READ COMMITTED (RC) ❌不存在 ✅存在 ✅存在
可重复读 REPEATABLE READ (RR)【MySQL InnoDB 默认】 ❌不存在 ❌不存在 ✅存在(InnoDB 通过 MVCC + 间隙锁解决幻读)
串行化 SERIALIZABLE ❌不存在 ❌不存在 ❌不存在

三个并发问题解释

  1. 脏读 :读到其他事务未提交的数据,如果对方回滚,读到的数据就是脏数据。
  2. 不可重复读 :同一个事务内,两次读取同一行数据,结果不一样 ,因为别的事务修改并提交了这行。(针对行更新)
  3. 幻读 :同一个事务内,相同查询条件,前后返回行数不一致,其他事务插入 / 删除了满足条件的数据。(针对新增 / 删除行)

InnoDB RR 级别:通过 MVCC 解决快照读的幻读;配合间隙锁 + 临键锁解决当前读的幻读。

锁

在并发访问数据库时,对共享资源(行、表等)做访问控制,保障事务的隔离性,防止并发问题(脏写、脏读、不可重复读、幻读),同时尽可能平衡并发性能。

MVCC(快照读)可以做到不加行锁,解决脏读、不可重复读;但MVCC 只能解决读的问题,写操作无法绕开锁。写操作必须使用锁来防止脏写。

锁的分类

1. 按锁粒度划分

  • 全局锁 :对整个数据库实例加锁。FLUSH TABLES WITH READ LOCK(FTWRL),所有表只读,一般用于全库备份。
  • 表级锁:锁定整张表

1、表共享读锁(S 锁):LOCK TABLES ... READ,多个事务可同时读,不能写。

2、表排他写锁(X 锁):LOCK TABLES ... WRITE,只有持有锁的事务读写,其他事务阻塞读写。

注意:LOCK TABLES 是 Server 层锁,不是 InnoDB 事务锁,会绕过事务隔离机制,生产尽量不用。

  • 行级锁(InnoDB 核心):只锁定索引记录,InnoDB 行锁是基于索引实现,如果不走索引会退化成表锁。

1、记录锁(Record Lock):锁定索引中的某一行记录。

2、间隙锁(Gap Lock):锁定索引记录之间的间隙,防止幻读,仅在可重复读 RR隔离级别生效。

3、临键锁(Next-Key Lock):记录锁 + 间隙锁,RR 默认行锁算法,左开右闭区间。

4、插入意向锁(Insert Intention Lock):间隙锁的一种,插入时判断间隙是否被占用,提升并发插入性能。

2. 按锁兼容性划分

  • 共享锁 S(读锁) :事务加 S 锁后,可以读该行;其他事务可以加 S 锁,但不能加 X 锁。
  • 排他锁 X(写锁) :事务加 X 锁后,可以读写该行;其他事务不能加 S/X 锁。

3.意向锁(表级别,InnoDB 自动维护)

意向锁是表锁,用来快速判断表上是否存在行锁,避免遍历所有行判断锁冲突。

  • 意向共享锁 IS:事务给某行加 S 锁前,先在表上加 IS 锁。
  • 意向排他锁 IX:事务给某行加 X 锁前,先在表上加 IX 锁。

意向锁之间互相兼容;意向锁和普通 S/X 表锁互斥。

ReadView、当前读 & 快照读

ReadView 是什么?

ReadView(读视图)是 InnoDB 在快照读时用来做 MVCC 多版本并发控制的核心数据结构。 它保存了执行快照读瞬间的活跃事务列表,包含 4 个核心字段:

  1. m_ids:当前系统中,所有未提交的活跃事务 ID 集合
  2. min_trx_id:m_ids 最小事务 ID
  3. max_trx_id:下一个将要分配的事务 ID
  4. creator_trx_id:生成这个 ReadView 的当前事务 ID

RC 和 RR 隔离级别 ReadView 创建时机差异:
RC(读已提交):每次快照读都会生成新 ReadView,可以读到其他事务已提交数据。
RR(可重复读):事务内第一次快照读时生成 ReadView,整个事务复用这个 ReadView,保证可重复读,避免不可重复读。

快照读(Snapshot Read)

读取的是 Undo 日志里的历史版本数据,不加行锁 ,并发友好。普通 SELECT 属于快照读。

sql 复制代码
-- 快照读,不加行锁,基于MVCC ReadView读取历史版本
SELECT * FROM user WHERE id = 1;

当前读(Current Read)

读取数据库最新版本数据,读取时必须加锁(S/X 锁),保证读到最新数据。 以下语句都是当前读:

  • SELECT ... LOCK IN SHARE MODE(加 S 锁)
  • SELECT ... FOR UPDATE(加 X 锁)
  • UPDATE / DELETE / INSERT
sql 复制代码
-- 当前读,加共享S锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
​
-- 当前读,加排他X锁
SELECT * FROM user WHERE id = 1 FOR UPDATE;
​
-- UPDATE也是当前读,自动加X锁
UPDATE user SET name = 'new' WHERE id = 1;
类型 是否加锁 读取数据 底层
快照读 不加行锁 历史版本 MVCC ReadView + Undo log
当前读 加 S/X 行锁 最新版本 行锁机制

RR 级别下,快照读解决不可重复读;临键锁解决幻读。

锁的触发

锁的触发的前提条件:InnoDB,事务内执行;行锁依赖索引,无索引会退化表锁。

1、自动触发锁(不需要额外关键字,语句执行自动加锁)

  1. INSERT:插入记录,加记录锁;间隙会加插入意向锁。
  2. UPDATE:当前读,命中索引行加X 排他行锁;RR 下会附带间隙锁 / 临键锁。
  3. DELETE:当前读,命中索引行加X 排他行锁;RR 下会附带间隙锁 / 临键锁。
sql 复制代码
BEGIN;
-- INSERT 自动加记录锁
INSERT INTO user(id,name) VALUES(10,'张三');
​
-- UPDATE 当前读,自动加X行锁
UPDATE user SET name='李四' WHERE id=10;
​
-- DELETE 当前读,自动加X行锁
DELETE FROM user WHERE id=10;
COMMIT;

普通 SELECT(快照读)不加任何行锁。

2、手动显式触发锁(需要写特定 SQL 关键字)

  1. SELECT ... LOCK IN SHARE MODE:手动加 S 共享行锁,其他事务可读,写阻塞。
  2. SELECT ... FOR UPDATE:手动加 X 排他行锁,其他事务读写都阻塞。
  3. LOCK TABLES ... READ / WRITE:MySQL Server 层表锁,不是 InnoDB 事务行锁,生产慎用。
sql 复制代码
BEGIN;
-- 手动加S共享锁,其他事务可以读,不能修改id=1
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
​
-- 手动加X排他锁,其他事务读写id=1都会阻塞
SELECT * FROM user WHERE id = 1 FOR UPDATE;
COMMIT;

锁的使用

原则:能不用显式锁就不用;优先 MVCC 快照读;只有业务需要 "当前读,保证读到最新数据,防止并发修改" 时才使用锁

1、需要强一致性的业务场景(悲观锁场景)

业务上需要:查询之后,在当前事务内,这条记录不允许被其他事务修改。

典型场景:库存扣减、订单状态变更、分布式本地事务资源锁定。 使用:SELECT ... FOR UPDATE(X 排他锁)

sql 复制代码
BEGIN;
-- 锁定库存记录,防止其他事务同时扣减
SELECT stock_num FROM goods_stock WHERE id = 1 FOR UPDATE;
-- 判断库存
UPDATE goods_stock SET stock_num = stock_num -1 WHERE id =1;
COMMIT;

注意:FOR UPDATE 一定要在事务内执行;提交才释放锁。

2.乐观锁方案(替代悲观锁,减少锁冲突)

不使用数据库锁,业务层通过 version 版本号控制,适合高并发、读多写少场景

ini 复制代码
-- 更新时带上版本号,版本不匹配更新行数为0,代表已经被别人修改
UPDATE goods_stock 
SET stock_num = stock_num -1, version = version +1 
WHERE id =1 AND version = 1;

3. 全库逻辑备份场景

需要保证备份期间全库一致性,防止 DDL/DML 破坏快照。 使用:FLUSH TABLES WITH READ LOCK;(全局读锁)

现在一般用 mysqldump --single-transaction(RR+MVCC,不需要全局锁),FTWRL 只在不支持事务的 MyISAM 场景使用。

锁优化方案

锁优化核心思路一句话:尽可能缩小锁范围、缩短锁持有时间,降低锁冲突概率

1、保证 SQL 命中索引,避免行锁退化为全表扫描锁

InnoDB 行锁基于索引。UPDATE / DELETE 如果 WHERE 条件不走索引 / 索引失效,会全聚簇索引扫描,所有扫描行加 X 锁,锁范围爆炸。

❌ 坏示例(name 无索引)

sql 复制代码
BEGIN;
UPDATE user SET age=20 WHERE name='test'; -- 全表扫描,所有行加X锁
COMMIT;

2、减少事务持有锁的时间:避免长事务

锁在事务提交 / 回滚前一直持有。事务越长,锁持有时间越长,锁冲突概率越高。 优化手段:

  1. 事务内只放必要 DML,不要在事务中包含业务耗时逻辑(HTTP 调用、文件 IO、复杂计算);
  2. 事务尽量小而快,尽早 commit;
  3. 监控长事务,杀掉长时间未提交事务。

3、合理选择锁策略:乐观锁优先于悲观锁

  • 冲突概率低:乐观锁(版本号),无数据库锁开销;
  • 冲突概率很高:悲观锁FOR UPDATE,但要控制粒度;

乐观锁缺点:并发极高时大量更新失败,需要业务重试。

4. 减少锁粒度,锁定尽可能少的数据

  • 尽量精确 WHERE 条件,只锁定需要修改的行;
  • 避免大范围UPDATE ... WHERE id > 100,RR 下会加临键锁,锁住一大片索引区间,容易幻读 + 大范围锁等待。

5. 调整业务逻辑,避免锁等待与死锁

死锁成因:多个事务,以相反顺序锁定资源。 优化:统一资源加锁顺序,所有事务都按相同顺序更新表 / 行。

相关推荐
newerp1 小时前
pprof 火焰图(Flame Graph)阅读与热点代码重构实战
后端·程序员·go
坊钰1 小时前
【LangChain框架入门级】10. 文本向量与向量数据库(Embedding / Redis / Pinecone / MMR)
数据库·python·langchain·embedding
Geek漫游指南1 小时前
PRD 写得越来越漂亮,需求怎么越来越糊涂?
后端·敏捷开发
小兔子1 小时前
Filebeat、Logstash、Fluent Bit 怎么选:采集层的边界与背压机制
后端
Omics Pro1 小时前
研究证实AI虚拟细胞可用于药物靶点发现
数据库·人工智能·算法·机器学习·自然语言处理
好奇的菜鸟1 小时前
GORM 入门(二):从 database/sql 平滑过渡到 GORM
数据库·sql
长春的KAKA2 小时前
GitLab 项目页面报 500 错误的完整修复记录:PostgreSQL 数据库损坏、统计表缺失及数据库迁移
数据库·postgresql·gitlab
徐小黑ACG2 小时前
Golang 基础01
开发语言·后端·golang
天空鸟_时光不老2 小时前
06-给AI流程加一道人工闸门
java·人工智能·spring boot·后端·spring·spring cloud·架构