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锁)的读写特性
独占锁允许一个事务 独占性地访问资源,其他事务既不能读取也不能修改该资源。具体特性如下:
读操作:独占锁通常应用于写操作,也就是说,只有当前持有独占锁的事务可以修改数据,其他事务在该数据上不能执行 任何类型的读写操作,包括 共享锁 和 独占锁。
写操作:当一个事务对数据加了独占锁时,其他事务既不能加共享锁,也不能加独占锁,因此当前数据无法被其他事务读取或修改。只有持有独占锁的事务可以修改该数据。
典型应用场景:独占锁通常用于 UPDATE 和 DELETE 操作,确保其他事务不能修改或读取数据,直到当前事务提交或回滚。
示例:
假设事务 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. 🙋♂️🙋♂️哪些操作会导致在表级别加锁呢?
前面讨论过数据库的 行锁 概念,它的理解相对简单。因为在讲解锁机制之前,我们已经深入探讨了多事务并发执行和事务隔离的问题,所以应该能轻松理解多事务并发时,如何通过行锁(独占锁)来避免脏写问题。
行锁与独占锁
在多个事务并发更新同一行数据时,通常会加 独占锁(也叫排他锁),这是行级别的锁。独占锁是互斥的,意味着:
一个事务提交后会释放独占锁,唤醒下一个事务。
在这个过程中,脏写问题不会发生。
读取数据的两种方式
当你查询数据时,如果该数据正在被另一个事务更新,有两种可能的处理方式:
基于 MVCC 机制:通过事务隔离级别,读取该数据的快照版本。这是常见的做法,适用于大多数场景。
加锁查询:使用特定的 SQL 语法手动加锁,来确保对数据的访问。比如:
独占锁:当你查询时加上独占锁 (FOR UPDATE),那么这个查询和其他事务更新操作的独占锁是互斥的。
共享锁:当你查询时加上共享锁 (LOCK IN SHARE MODE),则和其他查询的共享锁不互斥,但和独占锁(即更新操作)是互斥的。
关于分布式锁
虽然数据库的行锁机制能够确保数据的一致性,但在复杂的业务场景中,使用 数据库锁(尤其是行级锁)来实现锁机制,可能会让系统难以维护。因为这样的加锁逻辑往往隐藏在 SQL 语句中,使得 Java 业务层的锁逻辑变得复杂和难以调试。
因此,个人的建议是,在复杂的业务场景下,使用 分布式锁(例如基于 Redis 或 Zookeeper)来管理业务锁,而不是依赖数据库内的锁机制。分布式锁更适合处理跨多个服务、跨多个节点的锁需求,而且它的控制更加灵活,维护性也更好。
表级锁
在数据库中,不仅可以在查询语句中使用特定的语法加 行锁,例如 LOCK IN SHARE MODE 或 FOR UPDATE,还可以通过其他方式在 表级别 加锁。
有些人可能认为,在执行增删改操作时,数据库会自动加行锁;而在执行 DDL 语句(如 ALTER TABLE)时,会自动加表级锁。这个看法有一定道理,但并不完全正确。实际上,执行 DDL 语句时,MySQL 会通过 元数据锁(Metadata Locks,简称 MDL)来阻塞增删改操作;而在增删改操作时,也会阻塞 DDL 语句。
元数据锁 vs 表级锁
元数据锁:MySQL 的一种机制,用于确保在 DDL 操作期间,表的元数据不会被其他操作改变。
表级锁:这是 InnoDB 存储引擎的概念,用于控制整个表的访问权限。
虽然 DDL 操作和增删改操作是互斥的,但它们是通过 元数据锁 实现的,而不是传统意义上的表级锁。
以下是会导致表级锁的主要操作场景:
4.1. DDL(数据定义语言)操作
表结构修改
执行ALTER TABLE
、DROP TABLE
、TRUNCATE TABLE
等 DDL 语句时,MySQL 会通过 元数据锁(Metadata Lock,MDL) 锁定整个表 [1]。此时其他事务的增删改操作会被阻塞,直到 DDL 完成。
ALTER TABLE users ADD COLUMN age INT; -- 锁定表直到操作完成
索引操作
创建或删除索引时(如CREATE INDEX
、DROP INDEX
),同样会触发表级锁 [4]。
4.2. 显式表级锁命令
手动锁定表
使用LOCK TABLES
命令显式锁定表(如共享锁或独占锁):
LOCK TABLES users WRITE; -- 独占锁,禁止其他会话读写
LOCK TABLES users READ; -- 共享锁,允许读但禁止写
4.3. 无索引的写操作
全表扫描的更新/删除
在 InnoDB 中,如果UPDATE
或DELETE
语句未使用索引,会导致全表扫描并升级为 表级锁(即使引擎默认支持行锁)[4]。例如:
UPDATE users SET status=1 WHERE name LIKE '%test%'; -- 无索引字段导致表锁
隐式锁升级
当行锁数量超过阈值(如 InnoDB 行锁上限),系统会自动将锁升级为表级锁 [7]。
4.4. 长事务与并发控制
大事务或未提交事务
长时间未提交的事务可能持有表锁,导致其他事务阻塞 [6]。例如:
BEGIN;
UPDATE users SET balance=balance-100 WHERE id=1; -- 未提交,持有锁
-- 其他会话的 DDL 或写操作会被阻塞
高并发关联操作
涉及多表关联的复杂操作(如跨表更新)可能触发表级锁,尤其在未优化索引时 [4]。
4.5. 存储引擎差异
MyISAM 引擎
MyISAM 默认使用表级锁,所有写操作(INSERT
/UPDATE
/DELETE
)都会锁定整个表 [7]。InnoDB 引擎
通常支持行级锁,但以下情况会退化为表锁:
SQL 语句未命中索引。
显式使用
LOCK TABLES
命令。
4.6. 总结与建议
避免表级锁的方法:
排查锁表问题:
可通过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. 表级锁与意向锁的互斥关系
表级锁和意向锁并不是互斥的。举个例子,假设有两个事务:
事务 A 要更新 id=10 的数据,并且在表上加了一个 意向独占锁。
事务 B 要更新 id=20 的数据,也会在表上加一个 意向独占锁。
这两个事务是不会互斥的,因为他们在表级加的 意向独占锁 并不会互斥。它们各自在表中操作不同的行数据,互不干扰。
类似地,如果:
事务 A 要更新表中的数据,且在表上加了一个 意向独占锁。
事务 B 要读取表中的数据,且在表上加了一个 意向共享锁。
这两者也不会互斥,因为一个是更新操作,另一个是查询操作。它们加的 意向锁 不会产生冲突。
5.4. 手动加表锁与意向锁的互斥关系
虽然 表级锁 和 意向锁 在表级别的互斥关系比较简单,但手动加的 表级锁 和自动加的 意向锁 之间却是有一定的互斥关系的。
比如,表上加 表级独占锁(通过 LOCK TABLES xxx WRITE)时:
任何其他事务想要更新表中的数据都会被阻塞,因为它们加的 意向独占锁 与手动加的 表级独占锁 是互斥的。
同样,如果你手动加了 表级共享锁(通过 LOCK TABLES xxx READ):
任何想要更新表中数据的操作都会被阻塞,因为 意向独占锁 与 表级共享锁 也是互斥的。
5.5. 锁的互斥关系总结
表格总结了各种锁类型之间的互斥关系:
从表格中可以看出,手动加的 表级独占锁 和 表级共享锁 会阻塞其他事务的读写操作。具体来说:
如果你加了 表级独占锁,其他事务的更新和查询操作都会被阻塞。
如果你加了 表级共享锁,其他事务的更新操作也会被阻塞。
5.6. 总结
我们讲解了 表级锁 和 意向锁 的概念。实际上,手动加表级锁的情况非常少见,因为它对数据库性能的影响较大。通常我们会依赖 行级锁 来确保数据的并发访问,而 意向锁 主要用于确保行级操作与表级的协调。理论上,表级意向锁与行级独占锁的互斥性较弱,而手动加的表锁则会影响到事务的执行。
对于多数实际应用场景而言,行级独占锁 和 MVCC机制 足以满足大多数并发控制的需求。
评论区