侧边栏壁纸
博主头像
ProSayJ 博主等级

Talk is cheap. Show me the code.

  • 累计撰写 41 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

31-MySQL的锁机制

YangJian
2025-06-28 / 0 评论 / 0 点赞 / 14 阅读 / 0 字

1. 锁机制概述

之前我们已经详细讨论了在多个事务并发执行时,如何协调读写操作,尤其是当多个事务同时访问同一批数据时,如何避免脏读、不可重复读和幻读等问题。简单来说,这些问题的根本原因是事务在读取数据时可能会读取到不一致的值,导致数据的不准确或冲突。

脏读、不可重复读和幻读的问题,核心就在于事务读数据的方式是否正确。如果你读的数据是其他事务未提交的修改(脏读),或者数据在两次读取之间发生了变化(不可重复读),或者你读取到的记录本应不存在(幻读),这些问题就会发生。因此,解决这些问题的关键是设计出正确的读策略,而这正是我们之前提到的基于undo log多版本链条和ReadView机制的MVCC(多版本并发控制)方案所要解决的核心问题。

接下来,我们要探讨一个新的问题——当多个事务同时并发更新同一行数据时,如何避免“脏写”的问题。

我们之前提到过,脏写是数据库中绝对不允许的,这种问题的防范就依赖于数据库的锁机制。锁机制的作用是确保多个事务在更新同一数据时,能够按顺序进行,避免出现并发冲突。今天我们就来初步了解一下MySQL中的锁机制。

假设数据库中有一行数据,在没有任何事务进行更新时,这行数据是空闲的。如果此时有一个事务想要更新这行数据,它首先会检查这行数据是否已经被其他事务加锁。如果没有人加锁,这个事务就可以“捷足先登”了。

此时,这个事务会创建一个锁,这个锁包含了事务的ID(trx_id)以及该事务的状态,并将这个锁与数据行关联起来。要注意,更新数据时,需要将数据所在的磁盘页面加载到内存缓存中进行修改。因此,数据行和与之关联的锁结构都是存储在内存中的。

上图,事务A在这行数据上加锁,意味着这行数据当前处于锁定状态,无法被其他事务访问或修改。

这里的锁机制与Java中的加锁概念类似。对于不了解加锁概念的人来说,可以把它理解为在编程中常见的互斥锁(mutex),用于保证在同一时刻,只有一个线程(或事务)能够访问某个资源。对于数据库来说,锁是防止多个事务并发修改同一数据时产生冲突的关键。

接着,假设现在有另一个事务B,它也希望更新同一行数据。事务B会首先检查该数据是否已被加锁。一旦它发现事务A已经加锁了这行数据,事务B就会意识到自己无法立刻修改数据,只能排队等待。

在这种情况下,事务B会创建一个新的锁数据结构,其中包含它的trx_id以及它的等待状态。由于事务B需要等待,因此它的状态会被标记为true,表示它在等待事务A释放锁。这就意味着事务B无法立即进行数据更新,必须等待事务A完成操作并释放锁。

这时候,事务B处于排队状态,直到事务A提交或回滚,释放了对数据的锁,事务B才能继续进行它的操作。

当事务A完成数据更新后,它会释放自己对这行数据的锁。锁一旦被释放,事务A会检查一下是否还有其他事务对该数据加锁。

这时,事务A发现事务B之前已经创建了锁并处于等待状态,因此它会将事务B锁中的“等待状态”标记为false,表示事务B不再需要等待,并允许事务B继续执行。

此时,事务B就获取到了锁,可以继续执行它的操作,比如更新数据。事务B可以在事务A释放锁后进行修改,这样就避免了冲突和脏写的情况。

这种机制保证了即使有多个事务并发执行,只要通过加锁控制并发访问,就能确保每个事务在合适的时机获取到所需的资源,从而避免脏写、并发冲突等问题。

总结起来,这种基于锁的机制,就是数据库通过串行化多个事务对同一数据的访问,确保了事务的安全性与一致性。

2. 共享锁(S) 和独占锁(X)

在之前的讨论中,我们提到多个事务同时更新一行数据时,都会加锁并排队等待,必须等一个事务执行完毕并提交锁释放后,其他事务才能继续执行。那么在这个过程中,事务加的到底是什么锁呢?

其实,这些事务加的主要是 独占锁(X锁),也称为排他锁。独占锁的特点是:当一个事务对数据加了独占锁后,其他事务想要更新这行数据时,也必须加独占锁,但只能排队等待。

2.1. 读操作是否需要加锁?

那么问题来了:当某个事务正在更新数据时,其他事务还能读取这行数据吗?并且读取操作需要加锁吗?

答案是:不需要。

在默认情况下,MySQL 允许事务在更新数据时,其他事务可以读取这行数据,而不需要加锁。之所以能做到这一点,是因为 MySQL 的 MVCC(多版本并发控制)机制的支持。通过这个机制,读取操作并不会与更新操作产生互斥。

具体来说,查询操作会根据 ReadView 从 undo log 的版本链条中找到一个适合当前事务的快照版本进行读取,而不需要担心其他事务是否正在更新该数据。因此,读取操作默认是不加锁的,也不会影响其他事务的执行。

2.2. 如果强制要求加锁呢?

假设在执行查询操作时,确实需要加锁怎么办?MySQL 提供了一种 共享锁(S锁)机制。如果你想在查询时加锁,可以使用 LOCK IN SHARE MODE 语法。

SELECT * FROM table LOCK IN SHARE MODE;

这个语句会在查询时为数据加上共享锁。共享锁允许多个事务同时读取同一行数据,只要这些事务都是加共享锁的。

共享锁与独占锁的互斥关系:

  • 如果一个事务已经对某行数据加了 独占锁,其他事务就无法加 共享锁,因为共享锁和独占锁是互斥的。

  • 反之,如果你先加了共享锁,其他事务来更新这行数据时,也必须加独占锁,无法直接进行更新操作,必须排队等待。

  • 然而,共享锁和共享锁之间是 不互斥的,也就是说,多个事务都可以在同一行数据上加共享锁,彼此不会影响。

2.3. ♨♨🙋‍♂️🙋‍♂️独占锁和共享锁的读写特性总结

在 MySQL 中,共享锁S锁)和 独占锁X锁)是两种常见的锁类型,它们在数据的 读写特性 上具有不同的行为。理解这些锁的行为有助于更好地设计并发控制策略,避免死锁或性能瓶颈。

1. 共享锁(S锁)的读写特性

共享锁允许多个事务 并发读取 同一资源,但 不允许修改 该资源。具体特性如下:

  • 读操作:当一个事务对数据加共享锁时,其他事务也可以对同一数据加共享锁(即多个事务都能读取该数据),但不能对其加独占锁(即不能修改该数据)。

  • 写操作:当一个事务对数据加了共享锁时,其他事务既不能对该数据加共享锁,也不能加独占锁。所以,共享锁本身并不允许数据被修改

  • 典型应用场景:共享锁通常用于 SELECT … FOR SHARE 查询语句,允许其他事务继续查询数据,但不允许修改。

示例:

假设事务 A 对某条数据加了共享锁(SELECT ... FOR SHARE),那么事务 B 可以执行类似的查询操作(SELECT ... FOR SHARE)读取数据,但事务 B 无法对这条数据执行 UPDATE 或 DELETE 操作,直到事务 A 释放共享锁。

2. 独占锁(X锁)的读写特性

独占锁允许一个事务 独占性地访问资源,其他事务既不能读取也不能修改该资源。具体特性如下:

  • 读操作:独占锁通常应用于写操作,也就是说,只有当前持有独占锁的事务可以修改数据,其他事务在该数据上不能执行 任何类型的读写操作,包括 共享锁独占锁

  • 写操作:当一个事务对数据加了独占锁时,其他事务既不能加共享锁,也不能加独占锁,因此当前数据无法被其他事务读取或修改。只有持有独占锁的事务可以修改该数据。

  • 典型应用场景:独占锁通常用于 UPDATEDELETE 操作,确保其他事务不能修改或读取数据,直到当前事务提交或回滚。

示例:

假设事务 A 对某条数据加了独占锁(UPDATE 或 DELETE),那么其他事务 B 无法对该数据进行任何操作,包括查询(SELECT)或更新(UPDATE)。事务 B 必须等待事务 A 提交或回滚,才能获得对该数据的锁。

3. 锁的互斥关系

  • 共享锁共享锁:多个事务可以 并发 获取共享锁,且可以互不干扰,只要没有事务想要修改数据。

  • 共享锁独占锁:共享锁不允许与独占锁同时存在,因此,当一个事务持有共享锁时,其他事务无法对该数据加独占锁(无法修改)。

  • 独占锁独占锁:如果两个事务都持有独占锁,则这两个事务无法并发访问同一数据,因为独占锁是排他的。


4. 示例场景:

假设有一个表 employees,有字段 salary。事务 A 和事务 B 同时运行,以下为两种情况的示例:

共享锁场景:

  • 事务 A 执行 SELECT salary FROM employees WHERE id = 1 FOR SHARE,获取共享锁。

  • 事务 B 执行 SELECT salary FROM employees WHERE id = 1 FOR SHARE,也可以获取共享锁,事务 A 和事务 B 都可以查询数据,但不能修改。

  • 事务 A 或事务 B 不能执行 UPDATE salary SET salary = 5000 WHERE id = 1,因为它们需要独占锁来执行更新操作。

独占锁场景:

  • 事务 A 执行 UPDATE employees SET salary = 5000 WHERE id = 1,获取独占锁。

  • 事务 B 执行 SELECT salary FROM employees WHERE id = 1,会被阻塞,因为事务 A 已经获取了独占锁,事务 B 必须等待事务 A 提交或回滚,才能获取共享锁。

  • 事务 C 执行 DELETE FROM employees WHERE id = 1,也会被阻塞,直到事务 A 提交或回滚。

总结:

  • 共享锁:允许多个事务同时读取数据,但不允许修改数据。适用于 读取共享数据 的场景。

  • 独占锁:允许一个事务独占访问和修改数据,其他事务无法读取或修改该数据。适用于 更新或删除数据 的场景。

通过理解共享锁和独占锁的读写特性,可以帮助我们设计更高效的事务隔离策略,减少锁竞争,避免死锁,同时提高并发性能。

2.4. 规律总结:

  • 更新操作:必然加独占锁,独占锁是互斥的,其他事务不能同时更新这行数据。

  • 查询操作:默认情况下,不加锁,使用 MVCC 机制读快照,避免频繁加锁。

  • 共享锁和独占锁:互斥,不能同时存在。

  • 共享锁和共享锁:不互斥,多个事务可以共享访问同一行数据。

锁类型

独占锁

共享锁

独占锁

互斥

互斥

共享锁

互斥

不互斥

这个机制通过控制不同锁之间的互斥关系,确保了数据库在高并发情况下的安全性与一致性。

2.5. FOR UPDATE

其实在大多数业务系统开发中,主动在查询操作时加共享锁的情况相对较少。尽管数据库提供了行级锁的功能,但一般情况下,我们不会在数据库层面进行复杂的手动加锁操作。更多时候,开发者会使用像 Redis 或 Zookeeper 这样的分布式锁来控制系统中的业务锁逻辑。

不过,有一个特殊的情况值得注意,那就是当查询操作需要读取数据并且后续有更新需求时,可以通过加 独占锁 来确保数据的完整性。具体的语法是:

SELECT * FROM table FOR UPDATE;

2.5.1. FOR UPDATE 的作用

当你在查询时加上 FOR UPDATE,表示你查出来的数据后续会被更新,并且在你提交事务之前,其他事务是无法更新这些数据的。这是一种独占锁,确保在当前事务提交前,其他事务不能对这行数据进行修改。

2.5.2. 工作原理

  • 查询 + 更新:通过 FOR UPDATE 语法,事务会加上独占锁,确保在当前事务提交之前,其他事务无法修改这些数据。

  • 锁的持有:只有当前持有锁的事务才能进行更新操作,直到该事务提交并释放锁,其他事务才能对这些数据进行更新。

3. 总结

  • 默认更新操作:使用独占锁,确保数据一致性。

  • 默认查询操作:使用 MVCC 机制,通过快照读取数据,不涉及加锁。

  • 共享锁和独占锁的互斥规则:共享锁和独占锁互斥,多个共享锁之间不互斥。

  • FOR UPDATE 用法:通过查询加独占锁,确保数据在事务内可更新,避免其他事务修改。

理解这些概念后,大家就能更好地应对不同场景下的并发控制问题。

4. 🙋‍♂️🙋‍♂️哪些操作会导致在表级别加锁呢?

前面讨论过数据库的 行锁 概念,它的理解相对简单。因为在讲解锁机制之前,我们已经深入探讨了多事务并发执行和事务隔离的问题,所以应该能轻松理解多事务并发时,如何通过行锁(独占锁)来避免脏写问题。

行锁与独占锁

在多个事务并发更新同一行数据时,通常会加 独占锁(也叫排他锁),这是行级别的锁。独占锁是互斥的,意味着:

  • 一个事务提交后会释放独占锁,唤醒下一个事务。

  • 在这个过程中,脏写问题不会发生。

读取数据的两种方式

当你查询数据时,如果该数据正在被另一个事务更新,有两种可能的处理方式:

  1. 基于 MVCC 机制:通过事务隔离级别,读取该数据的快照版本。这是常见的做法,适用于大多数场景。

  2. 加锁查询:使用特定的 SQL 语法手动加锁,来确保对数据的访问。比如:

  • 独占锁:当你查询时加上独占锁 (FOR UPDATE),那么这个查询和其他事务更新操作的独占锁是互斥的。

  • 共享锁:当你查询时加上共享锁 (LOCK IN SHARE MODE),则和其他查询的共享锁不互斥,但和独占锁(即更新操作)是互斥的。

关于分布式锁

虽然数据库的行锁机制能够确保数据的一致性,但在复杂的业务场景中,使用 数据库锁(尤其是行级锁)来实现锁机制,可能会让系统难以维护。因为这样的加锁逻辑往往隐藏在 SQL 语句中,使得 Java 业务层的锁逻辑变得复杂和难以调试。

因此,个人的建议是,在复杂的业务场景下,使用 分布式锁(例如基于 RedisZookeeper)来管理业务锁,而不是依赖数据库内的锁机制。分布式锁更适合处理跨多个服务、跨多个节点的锁需求,而且它的控制更加灵活,维护性也更好。


表级锁

在数据库中,不仅可以在查询语句中使用特定的语法加 行锁,例如 LOCK IN SHARE MODEFOR UPDATE,还可以通过其他方式在 表级别 加锁。

有些人可能认为,在执行增删改操作时,数据库会自动加行锁;而在执行 DDL 语句(如 ALTER TABLE)时,会自动加表级锁。这个看法有一定道理,但并不完全正确。实际上,执行 DDL 语句时,MySQL 会通过 元数据锁(Metadata Locks,简称 MDL)来阻塞增删改操作;而在增删改操作时,也会阻塞 DDL 语句。

元数据锁 vs 表级锁

  • 元数据锁:MySQL 的一种机制,用于确保在 DDL 操作期间,表的元数据不会被其他操作改变。

  • 表级锁:这是 InnoDB 存储引擎的概念,用于控制整个表的访问权限。

虽然 DDL 操作和增删改操作是互斥的,但它们是通过 元数据锁 实现的,而不是传统意义上的表级锁。

以下是会导致表级锁的主要操作场景:


4.1. DDL(数据定义语言)操作

  1. 表结构修改
    执行
    ALTER TABLEDROP TABLETRUNCATE TABLE 等 DDL 语句时,MySQL 会通过 元数据锁(Metadata Lock,MDL) 锁定整个表 [1]。此时其他事务的增删改操作会被阻塞,直到 DDL 完成。

ALTER TABLE users ADD COLUMN age INT; -- 锁定表直到操作完成
  1. 索引操作
    创建或删除索引时(如
    CREATE INDEXDROP INDEX),同样会触发表级锁 [4]。


4.2. 显式表级锁命令

  1. 手动锁定表
    使用
    LOCK TABLES 命令显式锁定表(如共享锁或独占锁):

LOCK TABLES users WRITE; -- 独占锁,禁止其他会话读写 
LOCK TABLES users READ;  -- 共享锁,允许读但禁止写

4.3. 无索引的写操作

  1. 全表扫描的更新/删除
    在 InnoDB 中,如果
    UPDATEDELETE 语句未使用索引,会导致全表扫描并升级为 表级锁(即使引擎默认支持行锁)[4]。例如:

UPDATE users SET status=1 WHERE name LIKE '%test%'; -- 无索引字段导致表锁
  1. 隐式锁升级
    当行锁数量超过阈值(如 InnoDB 行锁上限),系统会自动将锁升级为表级锁 [
    7]。


4.4. 长事务与并发控制

  1. 大事务或未提交事务
    长时间未提交的事务可能持有表锁,导致其他事务阻塞 [
    6]。例如:

BEGIN;
UPDATE users SET balance=balance-100 WHERE id=1; -- 未提交,持有锁
-- 其他会话的 DDL 或写操作会被阻塞
  1. 高并发关联操作
    涉及多表关联的复杂操作(如跨表更新)可能触发表级锁,尤其在未优化索引时 [
    4]。


4.5. 存储引擎差异

  1. MyISAM 引擎
    MyISAM 默认使用表级锁,所有写操作(
    INSERT/UPDATE/DELETE)都会锁定整个表 [7]。

  2. InnoDB 引擎
    通常支持行级锁,但以下情况会退化为表锁:

  • SQL 语句未命中索引。

  • 显式使用 LOCK TABLES 命令。


4.6. 总结与建议

  • 避免表级锁的方法

  1. 为频繁查询的字段添加索引 [4]。

  2. 优化事务逻辑,减少长事务 [6]。

  3. 避免在高峰时段执行 DDL 操作 [1]。

  • 排查锁表问题
    可通过
    SHOW OPEN TABLES 或查询 information_schema.INNODB_LOCKS 定位锁冲突 [5]。


5. 🙋‍♂️🙋‍♂️表锁和行锁互相之间的关系以及互斥规则是什么

我们继续深入讨论 MySQL 中的 表锁。事实上,表锁的使用场景非常少见,通常情况下,大家不会主动去加表锁,表锁的使用相对较为“鸡肋”,我们来看看 MySQL 中表锁的两种类型以及相关的锁机制。

5.1. 表锁的两种类型

表锁可以通过如下两种语法来手动加:

  • LOCK TABLES xxx READ:加 表级共享锁

  • LOCK TABLES xxx WRITE:加 表级独占锁

然而,实际上,表锁 很少被开发者主动使用,往往是因为它对性能的影响较大,所以一般都不会在日常的开发中遇到。我们通常使用的是 意向锁

5.2. 意向锁的概念

意向锁 是 MySQL 用于在行级操作时,表级加锁的一种机制。它的作用并不是直接锁住表中的某一行数据,而是标记当前事务是否会对某行数据进行修改。

  • 意向独占锁(Intent Exclusive Lock,简称 IX 锁):当一个事务准备在某行数据上加独占锁时,会首先在表级加一个意向独占锁。

  • 意向共享锁(Intent Shared Lock,简称 IS 锁):当一个事务准备在某行数据上加共享锁时,会首先在表级加一个意向共享锁。

5.3. 表级锁与意向锁的互斥关系

表级锁和意向锁并不是互斥的。举个例子,假设有两个事务:

  1. 事务 A 要更新 id=10 的数据,并且在表上加了一个 意向独占锁

  2. 事务 B 要更新 id=20 的数据,也会在表上加一个 意向独占锁

这两个事务是不会互斥的,因为他们在表级加的 意向独占锁 并不会互斥。它们各自在表中操作不同的行数据,互不干扰。

类似地,如果:

  • 事务 A 要更新表中的数据,且在表上加了一个 意向独占锁

  • 事务 B 要读取表中的数据,且在表上加了一个 意向共享锁

这两者也不会互斥,因为一个是更新操作,另一个是查询操作。它们加的 意向锁 不会产生冲突。

5.4. 手动加表锁与意向锁的互斥关系

虽然 表级锁意向锁 在表级别的互斥关系比较简单,但手动加的 表级锁 和自动加的 意向锁 之间却是有一定的互斥关系的。

比如,表上加 表级独占锁(通过 LOCK TABLES xxx WRITE)时:

  • 任何其他事务想要更新表中的数据都会被阻塞,因为它们加的 意向独占锁 与手动加的 表级独占锁 是互斥的。

同样,如果你手动加了 表级共享锁(通过 LOCK TABLES xxx READ):

  • 任何想要更新表中数据的操作都会被阻塞,因为 意向独占锁表级共享锁 也是互斥的。

5.5. 锁的互斥关系总结

表格总结了各种锁类型之间的互斥关系:

锁类型

独占锁

意向独占锁

共享锁

意向共享锁

独占锁

互斥

互斥

互斥

互斥

意向独占锁

互斥

不互斥

互斥

不互斥

共享锁

互斥

互斥

不互斥

不互斥

意向共享锁

互斥

不互斥

不互斥

不互斥

从表格中可以看出,手动加的 表级独占锁表级共享锁 会阻塞其他事务的读写操作。具体来说:

  • 如果你加了 表级独占锁,其他事务的更新和查询操作都会被阻塞。

  • 如果你加了 表级共享锁,其他事务的更新操作也会被阻塞。

5.6. 总结

我们讲解了 表级锁意向锁 的概念。实际上,手动加表级锁的情况非常少见,因为它对数据库性能的影响较大。通常我们会依赖 行级锁 来确保数据的并发访问,而 意向锁 主要用于确保行级操作与表级的协调。理论上,表级意向锁与行级独占锁的互斥性较弱,而手动加的表锁则会影响到事务的执行。

对于多数实际应用场景而言,行级独占锁MVCC机制 足以满足大多数并发控制的需求。

0

评论区