索引
核心基础:B+树
下图是B+树的数据结构,帮助更好的了解 B+树 www.cs.usfca.edu/~galles/vis...
InnoDB 的所有索引类型都基于 B+树 数据结构。这是理解所有索引行为的关键。B+树的特点:
- 多叉平衡树: 高度平衡,查找效率稳定(时间复杂度
O(log n)
)。 - 数据只存叶子节点: 非叶子节点(内部节点)仅存储索引键值和指向子节点的指针。叶子节点存储了完整的索引键值和关联的数据(对于主键索引,就是实际的行数据;对于二级索引,是主键值)。
- 叶子节点双向链表: 所有叶子节点按索引键值顺序首尾相连,形成一个双向链表。这使得范围查询(
BETWEEN
,>
,<
) 和全表扫描极其高效,只需遍历链表即可。 - 高扇出度: 每个节点能容纳大量键值/指针,大大减少了树的深度(通常只需 3-4 层),减少磁盘 I/O。
InnoDB 索引类型详解
-
聚簇索引(Clustered Index / Primary Index)
-
定义: InnoDB 表的数据存储本身是按主键值顺序组织的。聚簇索引的叶子节点包含了完整的行数据 。每个 InnoDB 表有且只有一个聚簇索引。
-
如何选择:
- 如果表定义了
PRIMARY KEY
,则该主键就是聚簇索引。 - 如果没有主键,InnoDB 会选择一个第一个所有列都非空的
UNIQUE
索引作为聚簇索引。 - 如果两者都没有,InnoDB 会自动创建一个隐藏的、名为
GEN_CLUST_INDEX
的 6 字节ROWID
作为聚簇索引。
- 如果表定义了
-
特点:
- 数据即索引,索引即数据。数据文件就是聚簇索引文件。
- 基于主键查找极快(一次 B+树查找就能获取数据)。
- 主键顺序插入最快(避免页分裂)。
- 主键范围查找或排序很快(叶子节点顺序存储,链表连接)。
- 更新主键代价高(可能导致数据行移动或页分裂)。
- 二级索引都需要引用聚簇索引(稍后解释)。
-
-
二级索引(Secondary Indexes)
-
定义: 用户创建在主键(聚簇索引)之外的列上的索引。也称为非聚簇索引。
-
结构: 二级索引的叶子节点不存储完整的行数据。它存储了两部分内容:
- 二级索引定义的索引列的值。
- 对应行的主键值(即聚簇索引的键值)。
-
查询过程(回表查询 - Bookmark Lookup):
- 在二级索引的 B+树中查找目标索引键值。
- 找到后,获取叶子节点上存储的主键值。
- 使用这个主键值回到聚簇索引的 B+树中进行查找。
- 最终在聚簇索引的叶子节点找到完整的行数据。
-
特点:
- 创建目的是为了加速非主键列的查询。
- 通常比基于主键的查询多一次 B+树查找(回表)。
- 覆盖索引可以避免回表,提高性能(见下文)。
- 叶子节点按二级索引列值排序,包含主键值。
- 二级索引占用额外空间。
-
-
唯一索引(Unique Indexes)
- 定义: 索引键值在表中必须唯一(允许存在 NULL 值,但唯一索引对 NULL 的处理有特殊性,多个 NULL 值在 MySQL 中是允许的,因为标准 SQL 规定 NULL 是不可比较的)。主键索引天然是唯一索引。
- 行为: 在插入或更新索引列时,会强制检查唯一性约束。如果违反唯一性,操作会失败。
- 结构: 在物理存储上,唯一索引和普通二级索引的结构完全相同(都是 B+树,叶子节点存索引键值+主键值)。唯一的区别是是否启用了唯一性约束检查。
-
联合索引(Composite Indexes / Compound Indexes / Multi-column Indexes)
-
定义: 在多个列上创建的索引。索引键由多个列的值按顺序组合而成。
-
结构: B+树按定义的列顺序排序。首先按第一列排序,第一列相同时按第二列排序,依此类推。
-
核心原则:最左前缀匹配(Leftmost Prefixing):
-
查询时,索引可以用于只涉及索引定义中最左边连续列的条件。
-
INDEX idx_col1_col2_col3 (col1, col2, col3)
- 有效:
WHERE col1 = val
,WHERE col1 = val AND col2 = val2
,WHERE col1 = val AND col2 = val2 AND col3 = val3
,WHERE col1 = val AND col3 = val3
(只用到 col1),WHERE col1 LIKE 'prefix%'
。 - 无效 :
WHERE col2 = val2
,WHERE col2 = val2 AND col3 = val3
,WHERE col3 = val3
。这些无法利用索引的有序性。
- 有效:
-
-
优点:
- 可以覆盖涉及多个列的查询条件。
- 可以避免回表查询(如果查询的列都在索引中)。
- 可以优化
ORDER BY col1, col2, col3
或GROUP BY col1, col2, col3
。 - 一个联合索引有时可以替代多个单列索引(但需遵循最左前缀)。
-
-
覆盖索引(Covering Index)
-
定义: 一个索引包含了查询所需的所有字段(即 SELECT 的列和 WHERE/JOIN/ORDER BY/GROUP BY 中涉及判断的列)。
-
原理: 由于二级索引的叶子节点包含了索引列值和主键值,如果一个查询只需要这些值,那么数据库无需回表查询 聚簇索引就能直接返回结果,查询计划中会有
Using index
提示。 -
优势:
- 避免代价高昂的回表操作,显著提升性能(减少 I/O 和 CPU)。
- 通常比查询聚簇索引本身要快,因为二级索引通常更小。
-
设计: 在创建联合索引时考虑查询语句的字段需求,有目的地使索引能"覆盖"常用查询。
-
-
全文索引(Full-Text Index)
-
定义: 专门为文本字段(
CHAR
,VARCHAR
,TEXT
)设计的索引类型,用于实现高效的自然语言全文搜索 (MATCH () ... AGAINST ()
)。 -
结构: MySQL 8.0 中,InnoDB 的全文索引使用了一种特殊的结构(基于倒排索引的原理),与标准的 B+树不同。它存储了单词(或词干)到文档列表的映射。
-
特点:
- 用于关键词搜索、相关性排序、词干提取等。
- 支持自然语言模式和布尔模式。
- 有自己的分析器(分词器)来处理文本。
- 需要在特定文本列上显式创建
FULLTEXT INDEX
。
-
-
空间索引(Spatial Index - R-Tree)
-
定义: 为空间数据类型(如
GEOMETRY
,POINT
,LINESTRING
,POLYGON
)设计的索引类型,用于高效地进行空间关系查询(如ST_Contains()
,ST_Within()
,ST_Distance()
等)。 -
结构: 使用 R-Tree 数据结构,专门优化了存储和查询地理空间数据的范围。MySQL 的 R-Trees 支持在
MEMORY
和InnoDB
引擎上使用。 -
特点:
- 需要显式创建
SPATIAL INDEX
。 - 用于地理信息系统(GIS)、位置服务等场景。
- 需要显式创建
-
MySQL 8.0 新增特性与优化:
-
降序索引(Descending Indexes):
- 在 8.0 之前,索引默认是升序(ASC)的。如果要按
DESC
排序,数据库即使用了索引,也常常需要额外的排序操作。 - 8.0 允许在创建索引时为单个列指定
DESC
(例如:CREATE INDEX idx ON t1 (col1 DESC, col2 ASC);
)。 - 优化点: 优化器可以利用索引本身的降序存储来直接满足
ORDER BY ... DESC
或混合方向的ORDER BY
(如ORDER BY col1 DESC, col2 ASC
),避免昂贵的文件排序(filesort)。底层 B+树结构变化不大,但扫描方向可以利用信息优化。
- 在 8.0 之前,索引默认是升序(ASC)的。如果要按
-
倒序索引(InnoDB Reverse Key Indexes - 需谨慎使用):
- 一个特定场景下的优化,主要用于缓解
AUTO_INCREMENT
主键在高并发插入时的热点竞争(比如并发写入大量数据时所有写入都集中在最后一个数据页)。 - 创建语法:
CREATE TABLE t (id INT AUTO_INCREMENT PRIMARY KEY) ENGINE=InnoDB KEY_BLOCK_SIZE=4 REVERSE;
(表级设置) 或在特定索引上CREATE INDEX ... USING HASH
(旧方式,现已不推荐,新方式直接用REVERSE
关键字作用于表)。 - 原理: 将索引键值的字节进行倒序存储(例如,1 变成 0x80000001),使得连续插入的新行分散到不同的索引页上。
- 注意事项: 会显著降低范围查询效率(因为物理顺序被打乱),并可能导致更大的表空间占用。仅在明确需要解决热点竞争问题时才考虑使用,8.0 后通常有更好的热点优化机制(如
innodb_autoinc_lock_mode=2
)。
- 一个特定场景下的优化,主要用于缓解
-
不可见索引(Invisible Indexes):
-
允许将索引设置为对优化器"不可见"。
-
创建/修改语法:
CREATE INDEX ... INVISIBLE;
或ALTER TABLE ... ALTER INDEX ... INVISIBLE/VISIBLE;
-
用途:
- 安全删除前的测试:设置不可见后,观察数据库运行是否正常,确认无影响后再真正删除。
- 临时禁用索引:用于性能诊断,分析某索引是否真的被用到或有负面作用。
- 优化器提示增强。
-
优点: 避免直接删除带来的风险;操作是元数据级修改,非常快。
-
索引最佳实践与设计原则:
-
选择性原则:
- 选择性高的列优先建索引。选择性 =
COUNT(DISTINCT column) / COUNT(*)
,越接近 1(唯一)越好。选择性低的列(如性别、状态标志),索引过滤的数据比例小,可能不如全表扫描。
- 选择性高的列优先建索引。选择性 =
-
覆盖索引优先:
- 尽量设计出能够覆盖常用查询的联合索引,避免回表查询带来的性能损耗。
-
考虑索引列顺序(联合索引):
- 将最常用作查询条件的列放在索引的最左侧。
- 将选择性高的列尽可能放在左侧(但也要兼顾查询条件出现的频率)。
- 考虑
ORDER BY
和GROUP BY
的需求。
-
避免过度索引:
- 索引会增加写入开销(维护B+树结构需要额外I/O和CPU)和磁盘空间占用。删除不必要的冗余索引。
-
使用前缀索引(谨慎使用):
- 对大文本(
VARCHAR
/TEXT
/BLOB
)列,可以使用列值的前缀创建索引(INDEX idx (col_name(N))
),节省空间。 - 缺点: 选择性降低;无法用作覆盖索引(除非查询只需要前缀);不支持
ORDER BY
或GROUP BY
。 - 需要仔细选择前缀长度,保证足够的选择性。
- 对大文本(
-
使用
EXPLAIN
分析:-
在执行查询前使用
EXPLAIN
或EXPLAIN FORMAT=TREE
(8.0)来分析优化器的查询计划。关注:type
:访问类型(system/const > eq_ref > ref > range > index > ALL
)。possible_keys
:可能用到的索引。key
:实际使用的索引。key_len
:使用的索引长度(间接看出使用了联合索引的多少部分)。ref
:使用的列或常量与索引的比较。rows
:预估要扫描的行数(越小越好)。filtered
:存储引擎层过滤后剩余行的百分比(估算值)。Extra
:重要提示(如Using where
,Using index
,Using filesort
,Using temporary
)。
-
-
关注
ORDER BY
优化:- 如果
ORDER BY
顺序和索引顺序一致,且WHERE
条件能利用最左前缀,通常能避免filesort
。 - 利用 8.0 的降序索引优化混合排序。
- 如果
-
主键设计:
- 使用自增整数 (
AUTO_INCREMENT
) 通常是最优选择(插入快、空间小、范围查询快)。 - 业务主键要确保唯一且尽可能短(减少二级索引占用的空间,减少回表比较的次数)。
- 使用自增整数 (
-
定期分析表 (
ANALYZE TABLE
):- 更新表的索引统计信息,帮助优化器生成更准确的执行计划。
常见索引失效情况分析:
- 违反最左前缀原则(联合索引): 查询条件从索引的第二列或后面的列开始,不使用索引定义的第一列。
WHERE col2 = ? AND col3 = ?
(索引为(col1, col2, col3)
)。 - 在索引列上使用函数或表达式:
WHERE YEAR(date_column) = 2023
(需改用范围查询:WHERE date_column BETWEEN '2023-01-01' AND '2023-12-31'
)。WHERE col1 * 2 > 10
。WHERE UPPER(name) = 'JOHN'
。 - 使用通配符
%
开头的LIKE
查询:WHERE name LIKE '%Doe'
(无法利用索引的有序性)。'John%'
可以使用索引。 - 类型转换导致失效:
WHERE varchar_column = 123
(数字比较字符串列,隐含类型转换)。应将值转换为与列定义一致的字符串:WHERE varchar_column = '123'
。 - OR 连接非索引列条件(部分情况):
WHERE indexed_col = 10 OR non_indexed_col = 20
。优化器可能选择全表扫描而非index_merge
。尝试重写为UNION
或确保所有条件列都索引。 - 索引列参与计算:
WHERE col + 1 > 10
。 - 使用
NOT
或!=
/<>
:WHERE col != 10
。可能不会使用索引(优化器选择全表扫描可能更快)。对于枚举类型,可以重写为WHERE col IN (...
排除不需要的值)。 - 连接字段类型/字符集/排序规则不匹配: 在多表 JOIN 时,连接字段如果类型、字符集或排序规则不一致,会导致索引失效(需要隐式转换)。
- 表中数据量很小: 优化器认为全表扫描(可能只涉及很少的页)比使用索引(需要访问索引页和数据页)更快。
- 使用了
IS NULL
/IS NOT NULL
(不绝对失效): 主要看该列允许 NULL 值的比例(选择性)以及优化器的评估(SELECT COUNT(*)
)。允许 NULL 值的列上建索引,IS NULL
有时可用ref_or_null
访问方法。
深入理解物理结构:索引页(Index Pages)
- InnoDB 存储数据在表空间(Tablespace)中,基本单位是页(Page),通常是 16KB。
- 数据页(索引页)包含页头(Page Header)、行记录(Row Records)、页目录(Page Directory)和页尾(Page Trailer)。
- 页目录(Page Directory): B+树中的页(非叶子节点和叶子节点)内部使用槽(Slot) 来组织记录。非叶子节点存储(键值, 子页指针);叶子节点存储键值和实际数据(或主键值)。页目录(一个槽数组)指向页内的记录,类似一个内部的小索引,加速页内二分查找。
- 页分裂(Page Split): 当新行需要插入到一个已满(达到
FIL_PAGE_FREE
阈值)的页时,InnoDB 会将该页分裂成两个页,并将部分行移动到新页。页分裂是昂贵的操作(涉及 I/O 和页调整),会导致碎片。有序插入(如自增ID)可以大大减少页分裂。OPTIMIZE TABLE
可以重组数据和索引以减少碎片。
总结:
掌握 InnoDB 索引的核心在于深刻理解 B+树的结构以及不同类型索引(聚簇、二级、联合等)在 B+树上的具体组织形式。牢记聚簇索引的核心地位(数据按主键组织)和二级索引依赖主键的特性(回表查询)。联合索引的最左前缀原则和覆盖索引的概念是优化查询性能的关键利器。
MySQL 8.0 引入了降序索引、不可见索引等特性,提供了更多的优化手段和调试工具。优化索引需要结合业务查询模式、数据选择性,并利用 EXPLAIN
工具进行分析。定期审视和调整索引是保障数据库长期高性能运行的必要工作。避免常见的索引失效错误也能显著提升查询效率。
锁
InnoDB 锁的核心目标:
- 并发控制: 允许多个事务同时读写数据库,最大化资源利用率。
- 数据一致性: 保证事务看到符合其隔离级别的数据视图,防止脏读、不可重复读和幻读。
- 事务隔离: 实现 ANSI SQL 标准定义的
READ UNCOMMITTED
,READ COMMITTED
,REPEATABLE READ
(InnoDB 默认),SERIALIZABLE
等隔离级别。
InnoDB 锁的主要类型:
锁可以按两个维度分类:锁的意图/类型 (加什么锁)和 锁的粒度/范围(锁住多大范围的数据)。
一、 按锁的意图/类型 (Lock Mode)
-
共享锁 (Shared Lock / S Lock)
-
目的: 用于读取操作。允许多个事务同时持有同一资源的共享锁。
-
行为:
- 如果一个事务持有某行(或间隙)的 S 锁,其他事务也能获得该行(或间隙)的 S 锁。
- 如果一个事务持有某行(或间隙)的 S 锁,其他事务不能获得该行(或间隙)的 X 锁。
-
如何获取:
- 普通
SELECT
语句默认不加 S 锁(使用 MVCC)。 SELECT ... LOCK IN SHARE MODE
语句显式请求 S 锁。- 部分
INSERT ... SELECT
场景中的SELECT
子查询也可能加 S 锁(取决于隔离级别和唯一性约束)。 FOREIGN KEY
约束检查可能会在检查父表行时加 S 锁。
- 普通
-
-
排他锁 (Exclusive Lock / X Lock)
-
目的: 用于写入操作(更新/删除/插入)。一次只允许一个事务持有特定资源的排他锁。
-
行为:
- 如果一个事务持有某行(或间隙)的 X 锁,其他事务不能获得该行(或间隙)的任何锁(S 或 X)。
-
如何获取:
UPDATE
,DELETE
语句在找到要修改的行后会加 X 锁(无论是精确匹配还是扫描)。INSERT
语句在插入新行时会隐式地对该行加 X 锁。SELECT ... FOR UPDATE
语句显式请求 X 锁。REPLACE INTO
/INSERT ... ON DUPLICATE KEY UPDATE
在操作行时加 X 锁。
-
-
意向锁 (Intention Lock)
-
目的: 表级锁 !它们表明一个事务打算在表中的某些行 上获取什么类型的锁(S 或 X)。它们是表锁与行锁之间的协调者。 意向锁是兼容性检查的快速机制。
-
原理:
- 意向锁是 弱锁 。它们不与行级锁冲突(
IS
/IX
彼此或与其他表级锁之间的冲突有特定规则,见下文),只与其他表级锁(包括其他意向锁)做兼容性检查。 - 意向共享锁 (Intention Shared Lock / IS Lock): 表示事务打算在表中的某些行上加 S 锁。
- 意向排他锁 (Intention Exclusive Lock / IX Lock): 表示事务打算在表中的某些行上加 X 锁(或者要修改其中某些行)。
- 意向锁是 弱锁 。它们不与行级锁冲突(
-
如何获取:
- 事务在获取某行的 S 锁 之前,必须先获取该表的 IS 锁(或更强的锁)。
- 事务在获取某行的 X 锁 之前,必须先获取该表的 IX 锁(或更强的锁)。
SELECT ... LOCK IN SHARE MODE
请求行级 S 锁前会自动请求表级 IS 锁。SELECT ... FOR UPDATE
/UPDATE
/DELETE
/INSERT
(影响行)请求行级 X 锁前会自动请求表级 IX 锁。
-
意义: 想象一下,没有意向锁:事务 A 持有表锁,事务 B 想修改某行,B 需要遍历检查表中的每一行是否被 A 锁住才能加行锁。效率极低!意向锁让这个检查变得高效:事务 B 只需要看表上有没有与它的 IX 锁冲突的表级锁(如 X 锁)就知道能不能继续加行锁了。见兼容性表。
-
锁兼容性矩阵:
当前锁 | 请求锁类型 IS |
请求锁类型 IX |
请求锁类型 S |
请求锁类型 X |
---|---|---|---|---|
IS |
✅ 兼容 | ✅ 兼容 | ✅ 兼容 | ⛔ 冲突 |
IX |
✅ 兼容 | ✅ 兼容 | ⛔ 冲突 | ⛔ 冲突 |
S |
✅ 兼容 | ⛔ 冲突 | ✅ 兼容 | ⛔ 冲突 |
X |
⛔ 冲突 | ⛔ 冲突 | ⛔ 冲突 | ⛔ 冲突 |
IS
/IX
:彼此兼容,也兼容已有的IS
或IX
。因为它们只是"意向",不是真正的锁行。允许多个事务同时持有IX
或IS
,然后各自去尝试锁不同的行。S
/IX
:冲突!因为S
是一个强表锁(暗示整个表只读),而IX
表示事务要修改其中某些行,这是不允许的。X
/ 任何其他锁 :冲突!X
是独占表锁(暗示整个表要被修改),不允许有任何并发操作(包括意向锁暗示的操作)。
二、 按锁的粒度/范围 (Lock Scope)
-
行级锁 (Row-Level Locks)
-
目的: 锁住单行记录。这是实现高并发的关键!也是 InnoDB 的核心优势之一。锁的对象是索引记录(即使是聚簇索引锁数据行)。
-
类型:
-
记录锁 (Record Lock / LOCK_REC_NOT_GAP):
锁住
索引记录本身
(不是间隙)。
- 例如:
SELECT * FROM t WHERE id = 10 FOR UPDATE;
在id=10
这条记录的聚簇索引上放一个 X 型的记录锁。
- 例如:
-
间隙锁 (Gap Lock / LOCK_GAP):
锁住索引记录之间的间隙
(区间),防止其他事务在范围内插入。
-
锁的是一个区间范围
(l, h)
,不包括边界点l
和h
本身。 -
目标: 防止幻读 (Phantom Read) 。
-
如何触发:
- 在 可重复读 (REPEATABLE READ) 隔离级别(默认),当查询使用
非唯一索引
或扫描唯一索引但无明确主键
的范围条件
(WHERE id > 10 AND id < 20
)时,除了锁住找到的记录本身,还会锁住这些记录之间的间隙和边界外的第一个间隙(取决于条件)。 SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
在未命中任何记录的唯一索引条件查询时(WHERE unique_col = non_exist_value
),会锁住该值所在的间隙。
- 在 可重复读 (REPEATABLE READ) 隔离级别(默认),当查询使用
-
共享与排他: 间隙锁只有一种类型(有时称为 LOCK_GAP ),但其意向是共享还是排他会受请求的锁类型影响。多个事务可以持有同一间隙的锁(
S型间隙锁
彼此兼容),但只要有一个事务持有S型间隙锁
,就不允许插入(因为插入需要X型间隙锁
,而S GAP
和X GAP
是冲突的)。
-
-
临键锁 / Next-Key Lock (LOCK_ORDINARY / LOCK_X | LOCK_S + LOCK_GAP):
-
定义: 记录锁 + 记录前面的间隙锁 。锁住的是一个左开右闭区间
(previous_record, current_record]
。 -
目的: InnoDB 在 REPEATABLE READ 级别防止幻读的主要机制就是通过 next-key locking。
-
行为:
- 当扫描到一条记录并加锁时,实际上加的是一个 next-key lock,既锁住了记录本身,也锁住了该记录之前的那段间隙。
- 例如:表有记录
id: 5, 10, 15
。SELECT * FROM t WHERE id = 10 FOR UPDATE;
不仅在id=10
上加了记录锁(LOCK_REC_NOT_GAP),还可能在其左侧间隙加锁((5, 10]
),组成一个 next-key lock。这会防止id=10
的更新和5 < id < 10
的插入(如id=7
)。
-
-
插入意向锁 (Insert Intention Lock / LOCK_INSERT_INTENTION):
-
定义: 特殊的间隙锁 。由
INSERT
操作在插入行之前设置。 -
目的: 表明一个事务打算在某个间隙插入新行。是信号量,用于多个事务想在同一个间隙插入时的等待协调。本身不阻止其他事务也在该间隙插入(只要插的不是同一个位置),但需要兼容性检查。
-
行为:
- 如果另一个事务在该间隙上持有不兼容的锁(如普通间隙锁
LOCK_GAP
或 next-key lock),INSERT
操作会被阻塞,并设置插入意向锁(表现为等待)。 - 多个插入意向锁彼此不冲突 。它们可以在同一个间隙中等待不同的插入位置。这允许多个并发的
INSERT
操作在同一个间隙中排队等待,而不是完全阻塞。 - 一旦间隙上原有的阻止插入的锁(如其他事务的间隙锁或 next-key lock)被释放,所有等待在该间隙上的插入意向锁会被唤醒并按 FIFO 顺序竞争执行插入。
- 如果另一个事务在该间隙上持有不兼容的锁(如普通间隙锁
-
-
-
-
表级锁 (Table-Level Locks)
-
目的: 锁住整张表。
-
类型:
-
表读锁 / 共享锁 (TABLE S LOCK):
- 显式加锁:
LOCK TABLES ... READ
。 - 允许其他会话读取,但禁止任何会话写入(包括加写锁)。
- 显式加锁:
-
表写锁 / 排他锁 (TABLE X LOCK):
- 显式加锁:
LOCK TABLES ... WRITE
。 - 禁止其他会话读或写。
- 显式加锁:
-
意向锁 (IS, IX): 如前所述,表级意向锁也是表级锁的一种,但它们的主要作用是协调行锁。
-
自增锁 (AUTO-INC Locks):
特定于有
AUTO_INCREMENT
列的表。-
目的: 保证多事务并发插入时,每个插入的行都能获得唯一且连续的自增值。
-
类型: 特殊的 表级锁。
-
行为 (旧模式 /
innodb_autoinc_lock_mode=0
):- 在
INSERT
开始执行时获取,直到该INSERT
语句(注意是语句级别)结束时才释放。 - 影响并发插入性能。
- 在
-
现代模式 (
innodb_autoinc_lock_mode=2
):- MySQL 8.0 默认
innodb_autoinc_lock_mode=2
("交错"锁)。 - 提供更细粒度的锁:只有在分配自增值的瞬间加轻量级互斥锁 (mutex),分配完成后立即释放。
- 优势: 显著提高高并发插入的性能(如多值
INSERT
/INSERT ... SELECT
)。 - 代价: 无法保证多个并发
INSERT
语句生成的自增值是连续分配的(但分配是唯一且单调递增的)。对于依赖连续性的复制(Statement-Based Replication, SBR)可能存在风险(实际风险较小,Row-Based Replication / RBR 不受影响)。
- MySQL 8.0 默认
-
传统模式 (
innodb_autoinc_lock_mode=1
): 混合模式(8.0 默认不是这个),简单的INSERT
使用轻量级锁,INSERT ... SELECT
等批量插入使用表锁。尽量使用2
。
-
-
-
注意事项: 在 InnoDB 中,一般不建议 使用
LOCK TABLES
,因为它会严重限制并发性,并且与事务 (BEGIN
/COMMIT
) 不兼容。InnoDB 的细粒度行锁提供了更好的并发控制。
-
三、 MySQL 8.0 锁相关特性与增强
-
NOWAIT 与 SKIP LOCKED (SELECT ... FOR UPDATE/SHARE 选项):
-
目的: 提供更灵活的锁定读取行为。
-
NOWAIT
: 如果请求的行(或间隙)已被其他事务锁定,查询立即返回错误ER_LOCK_WAIT_TIMEOUT
(等价于设置了一个无限小的innodb_lock_wait_timeout
)。 -
SKIP LOCKED
: 跳过那些已被其他事务锁定的行,只返回未被锁定的行。 -
应用场景: 构建高效的并发队列(如工作队列或任务分配)、避免死锁或减少锁等待。
-
示例:
sql
sql
复制
sql-- 尝试获取第1个未被锁定的任务,如果都不空闲则立即失败 SELECT * FROM task_queue WHERE status = 'PENDING' FOR UPDATE SKIP LOCKED LIMIT 1; -- 尝试获取第1个未被锁定的任务,不等待已被锁的任务 SELECT * FROM task_queue WHERE status = 'PENDING' FOR UPDATE SKIP LOCKED LIMIT 1;
-
-
数据字典改进:
- MySQL 8.0 使用原子 DDL (Atomic DDL) 并重构了数据字典(存储在 InnoDB 表空间中)。这显著影响了像
ALTER TABLE
这类 DDL 操作所需持有的锁类型和时间。通常目标是减少锁争用和不可用时间,但加锁行为变得更加复杂(涉及元数据锁和表定义的协调)。
- MySQL 8.0 使用原子 DDL (Atomic DDL) 并重构了数据字典(存储在 InnoDB 表空间中)。这显著影响了像
-
InnoDB Locking
章节文档改进: 官方手册对锁的解释更详细和清晰。
四、 死锁 (Deadlocks)
-
定义: 两个或多个事务相互等待对方释放锁,形成一个循环等待链条,导致它们都无法继续执行。
-
InnoDB 的处理:
- 死锁检测(默认开启): InnoDB 使用等待图 (wait-for graph) 算法检测死锁。当检测到死锁时,它会选择其中一个事务(通常是回滚代价最小的,如持有最少行锁或修改最少数据的那个)作为牺牲品 (victim)。
- 回滚: 牺牲品事务会收到
ER_LOCK_DEADLOCK
错误(MySQL 错误号1213
),其当前语句(或整个事务)被自动回滚。其他事务得以继续执行。 - 死锁超时 (
innodb_lock_wait_timeout
): 设置单个锁请求的最大等待时间(秒,默认 50 秒)。超时后请求的事务会收到ER_LOCK_WAIT_TIMEOUT
错误并被回滚(仅限于当前语句失败的隐式事务回滚,具体取决于语句)。注意:这不是死锁!这是单个锁等待超时。 死锁检测通常更快解决死锁。
-
分析与避免:
- 使用
SHOW ENGINE INNODB STATUS
命令查看最新的死锁信息 (LATEST DETECTED DEADLOCK
部分),了解哪些事务和锁导致了死锁。 - 开启
innodb_print_all_deadlocks
将所有死锁信息输出到错误日志。 performance_schema
表(如data_lock_waits
,data_locks
) 提供了更强大的锁监控能力(需要先启用相关setup_instruments
)。sys.innodb_lock_waits
视图可方便地查看当前阻塞的锁等待。- 调整事务逻辑/顺序:尽量按相同的顺序访问表和行(资源排序)。
- 减小事务大小:尽快提交事务。
- 合理使用索引:减少锁的范围和时间。
- 考虑使用
NOWAIT
/SKIP LOCKED
。
- 使用
五、 锁监控与诊断
-
SHOW ENGINE INNODB STATUS
:-
关键输出部分:
TRANSACTIONS
: 当前活动事务(包含锁等待信息)。LATEST DETECTED DEADLOCK
: 最近检测到的死锁详情(如果有)。---TRANSACTION <id>, ACTIVE <sec> sec
: 具体事务信息,包含锁列表 (held locks
) 和等待锁 (waits for lock
)。
-
-
performance_schema
(性能模式):-
8.0 推荐方式!提供动态视图:
performance_schema.data_locks
: 显示事务持有 和等待 的所有锁(行锁、间隙锁、意向锁、表锁等详细信息)。替代旧版INFORMATION_SCHEMA.INNODB_LOCKS
(已弃用)。performance_schema.data_lock_waits
: 显示当前锁阻塞信息(哪个事务在等待哪个事务持有的哪个锁)。替代旧版INFORMATION_SCHEMA.INNODB_LOCK_WAITS
(已弃用)。
-
使用前提:确保
setup_consumers
和setup_instruments
表中相关的消费者和工具已启用(通常在较新版本默认启用相关配置)。
-
-
sys
Schema (系统库):-
提供对
performance_schema
的更友好封装视图:
sys.innodb_lock_waits
: 最常用!清晰列出阻塞链:哪些事务阻塞了哪些事务?被阻塞的事务在等待什么锁?阻塞的事务持有的是什么锁?
-
-
INFORMATION_SCHEMA
:-
旧版本监控方式:
INFORMATION_SCHEMA.INNODB_TRX
: 当前运行的事务(状态、隔离级别、锁等待时间等)。INFORMATION_SCHEMA.INNODB_LOCKS
和INFORMATION_SCHEMA.INNODB_LOCK_WAITS
: MySQL 8.0 已弃用!强烈推荐迁移到performance_schema
。
-
锁相关优化建议:
- 避免长事务: 尽快提交/回滚事务!长事务会持锁时间长,增加冲突和死锁风险。
- 索引设计: 合适的索引可以让查询快速定位精确的行,使用行锁 (record lock) 而不是范围锁 (next-key lock) 或全表扫描。全表扫描会锁住大量行(甚至全表)。
- 主键选择: 优先使用自增整数主键 (高并发插入,顺序加锁减少页分裂和锁冲突)。避免随机值主键(如 UUIDv4)。
- 合理使用隔离级别: 了解默认的
REPEATABLE READ
会使用间隙锁(防幻读)。如果应用能容忍幻读(或通过其他手段避免),且对并发要求很高,可以考虑使用READ COMMITTED
隔离级别(它通常不使用间隙锁,仅使用记录锁)。 - 慎用
LOCK IN SHARE MODE
: 除非有充分理由(如特定的一致性读需求),否则优先考虑无锁的普通SELECT
(利用 MVCC)或FOR UPDATE
。 - 警惕
SELECT ... FOR UPDATE
/UPDATE
/DELETE
的条件: 不使用索引或使用非唯一索引的范围扫描,都会导致 锁定范围远大于期望的行数(大量记录、间隙甚至整个区间)。 - 使用
NOWAIT
/SKIP LOCKED
: 在适用场景下(如工作队列)可大幅提升并发性。 - 避免或优化批量 DML: 大量
INSERT
/UPDATE
/DELETE
不仅时间长,持锁范围也大。考虑分批执行。 - 调整
innodb_lock_wait_timeout
: 根据应用容忍度调整锁等待超时时间(不是默认值越小越好)。
总结:
MySQL 8.0 InnoDB 的锁机制是一个极其精密的并发控制体系,融合了:
- 细粒度行锁(记录锁、间隙锁、临键锁) :提供了高并发能力。
- 表级锁 / 意向锁:协调全局行为、辅助 DDL。
- MVCC (多版本并发控制) :为只读查询提供非锁定一致性视图。
- 锁增强特性 (
NOWAIT
,SKIP LOCKED
) :提供更灵活的并发选项。 - 完善的监控工具 (
performance_schema
,sys
) : 方便诊断锁冲突与死锁。
理解锁的类型(S/X/IS/IX)、粒度(行/表)和工作原理(包括间隙锁和 next-key locking 如何防止幻读),结合强大的锁监控工具,是诊断数据库性能问题、优化 SQL 语句、设计高性能并发应用以及处理死锁的核心技能。记住 "高并发 + 强一致性 + 防止锁冲突" 的不可能三角,合理选择策略(隔离级别、锁请求方式、事务大小等)进行平衡。