云计算、AI、云原生、大数据等一站式技术学习平台

网站首页 > 教程文章 正文

MySQL锁机制:从表锁到MVCC,一场数据库的“锁”事大戏

jxf315 2025-05-10 22:46:33 教程文章 5 ℃

引言

在数据库的世界里,锁机制就像是一场精心编排的“锁”事大戏。每个角色都有自己的戏份,表锁、元数据锁、行锁、间隙锁、MVCC等轮番登场,共同维护着数据库的秩序。今天,我们就来揭开这场大戏的帷幕,看看这些锁是如何在MySQL中演绎出精彩的故事。

1. 表锁:数据库的“大门保安”

表锁是MySQL中最基本的锁机制,它就像是一个尽职尽责的“大门保安”,负责守护整个表的安全。当一个事务需要对表进行写操作时,表锁会毫不犹豫地将整个表锁住,确保没有其他事务能够干扰。

1.1 表锁的类型

  • 读锁(共享锁):多个事务可以同时持有读锁,但任何事务都不能对表进行写操作。
  • 写锁(排他锁):只有一个事务可以持有写锁,其他事务既不能读也不能写。

1.2 代码示例

sql

-- 事务1
LOCK TABLES my_table WRITE;
-- 执行写操作
UNLOCK TABLES;

-- 事务2
LOCK TABLES my_table READ;
-- 执行读操作
UNLOCK TABLES;

1.3 思考题

  • 表锁的优缺点是什么?在什么场景下使用表锁比较合适?

优点:

  • 简单粗暴:表锁的实现非常简单,适合对并发要求不高的场景。
  • 开销小:锁的粒度大,管理锁的开销较小。

缺点:

  • 并发性差:表锁会锁住整个表,导致其他事务无法访问,严重影响并发性能。
  • 不适合高并发场景:在高并发环境下,表锁会成为性能瓶颈。

适用场景:

  • 数据量小:表锁适合数据量较小的表,比如配置表、字典表等。
  • 写操作少:如果表的写操作非常少,读操作较多,表锁也可以接受。

2. 元数据锁:数据库的“档案管理员”

元数据锁(MDL)是MySQL中用于保护表结构的锁机制,它就像是一个严谨的“档案管理员”,确保表的结构不会被随意修改。

2.1 MDL的作用

  • 防止DDL操作冲突:当一个事务正在执行DDL操作(如ALTER TABLE)时,其他事务不能对表进行任何操作。
  • 防止读操作冲突:当一个事务正在执行读操作时,其他事务不能对表进行DDL操作。

2.2 代码示例

sql

-- 事务1
ALTER TABLE my_table ADD COLUMN new_column INT;

-- 事务2
SELECT * FROM my_table;

2.3 思考题

  • 元数据锁与表锁的区别是什么?在什么情况下会出现元数据锁的冲突?

区别:

    • 表锁:锁住的是整个表的数据,防止其他事务对表进行读写操作。
    • 元数据锁:锁住的是表的结构(元数据),防止其他事务修改表结构(如ALTER TABLE)或执行DDL操作。

元数据锁冲突的场景:

  • DDL与DML冲突:当一个事务在执行DDL操作(如ALTER TABLE)时,另一个事务尝试执行DML操作(如SELECT、INSERT),就会发生元数据锁冲突。
  • DDL与DDL冲突:两个事务同时尝试修改表结构(如都执行ALTER TABLE),也会发生元数据锁冲突。

3. 行锁:数据库的“精细管家”

行锁是MySQL中更为精细的锁机制,它就像是一个“精细管家”,能够精确地锁定表中的某一行,而不是整个表。

3.1 行锁的类型

  • 共享锁(S锁):多个事务可以同时持有共享锁,但不能持有排他锁。
  • 排他锁(X锁):只有一个事务可以持有排他锁,其他事务不能持有任何锁。

3.2 代码示例

sql

-- 事务1
START TRANSACTION;
SELECT * FROM my_table WHERE id = 1 FOR UPDATE;
-- 执行写操作
COMMIT;

-- 事务2
START TRANSACTION;
SELECT * FROM my_table WHERE id = 1 LOCK IN SHARE MODE;
-- 执行读操作
COMMIT;

3.3 思考题

  • 行锁与表锁相比,有哪些优势?在什么场景下使用行锁更为合适?

优势:

  • 并发性高:行锁只锁住特定的行,其他事务可以访问表中的其他行,大大提高了并发性能。
  • 粒度细:锁的粒度更小,适合高并发场景。

适用场景:

  • 高并发读写:比如电商系统中的订单表、库存表,需要频繁更新某一行数据。
  • 事务复杂:在复杂的事务中,可能需要对多行数据进行操作,行锁可以避免锁住整个

4. 间隙锁:数据库的“隐形守护者”

间隙锁是MySQL中用于防止幻读的锁机制,它就像是一个“隐形守护者”,默默地守护着数据之间的间隙。

4.1 间隙锁的作用

  • 防止幻读:当一个事务执行范围查询时,间隙锁会锁定查询范围内的所有间隙,防止其他事务插入新的数据。

4.2 代码示例

sql

-- 事务1
START TRANSACTION;
SELECT * FROM my_table WHERE id BETWEEN 1 AND 10 FOR UPDATE;
-- 执行写操作
COMMIT;

-- 事务2
START TRANSACTION;
INSERT INTO my_table (id) VALUES (5);
-- 事务2会被阻塞,直到事务1提交
COMMIT;

4.3 思考题

  • 间隙锁是如何防止幻读的?在什么情况下会出现间隙锁的冲突?

防止幻读的原理:

  • 锁定间隙:间隙锁会锁定一个范围(比如id BETWEEN 1 AND 10),不仅锁住已有的数据行,还会锁住这些行之间的“间隙”,防止其他事务插入新的数据。
  • 避免幻读:通过锁定间隙,确保在事务执行期间,不会有新的数据插入到查询范围内,从而避免幻读。

间隙锁冲突的场景:

  • 插入冲突:当一个事务锁定了某个范围的间隙(如id BETWEEN 1 AND 10),另一个事务尝试在这个范围内插入数据(如INSERT INTO my_table (id) VALUES (5)),就会发生间隙锁冲突。
  • 范围查询冲突:两个事务同时尝试锁定同一个范围的间隙,也会发生冲突。

5. MVCC:数据库的“时间旅行者”

MVCC(多版本并发控制)是MySQL中用于实现高并发的机制,它就像是一个“时间旅行者”,能够在不加锁的情况下实现事务的并发执行。

5.1 MVCC的原理

  • 版本链:每个数据行都有一个版本链,记录着该行的历史版本。
  • 快照读:事务在执行读操作时,会读取该事务开始时的数据快照,而不是当前的数据。

回到MVCC,MVCC的底层实现是有三部分组成的:隐藏字段、Undolog、ReadView。其实就是利用了undolog实现多个版本,然后结合ReadView进行比较。

① 隐藏字段

DB_TRX_ID: 创建这条记录或者最后一次修改改记录的事务的ID值

DB_ROLL_PTR: 回滚指针,指的是上一个数据的版本

DB_ROW_ID: 隐藏主键,如果数据表没有设置主键的话,会生成一个6字节的ROW_ID作为隐藏主键

② Undolog

Undolog会变成一个链表,链首是最新的旧记录,链尾是最旧的旧记录,链表中的内容不可能无限增加,所以在MySQL后台服务中有一个线程叫purge来进行清理操作。


③ ReadView(读视图是在事务进行快照读的时候产生的读视图,保存的并不是数据的信息,而是事务的相关信息)

  • m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。
  • m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID。
  • m_idsRead View 创建时其他未提交的活跃事务 ID 列表。
  • m_creator_trx_id:创建该 Read View 的事务 ID
  1. 如果被访问的记录版本号 DB_TRX_ID 等于 Readview 中的 creator_trx_id, 表明当前事务访问的是自己修改的记录,可以被访问;
  2. 如果被访问的记录版本号 DB_TRX_ID 小于 Readview 中的 min_trx_id, 表明该版本的数据在当前事务生成Readview 之前已经提交,可以被访问;
  3. 如果被访问的记录版本号 DB_TRX_ID 大于或等于 Readview 中的 max_trx_id, 表明生成该版本数据的事务在当前事务生成Readview后才开启,不可以被访问,需要沿着回滚指针寻找该记录的历史版本继续判断;
  4. 如果被访问的记录版本号 DB_TRX_ID 介于 min_trx_id 和 max_trx_id之间, 需要进一步判断 DB_TRX_ID 是不是在活跃事务列表 m_ids 中:如果在:说明创建 Readview 时生成该版本的事务尚未提交,该版本数据不可以被访问,需要沿着回滚指针寻找该记录的历史版本继续判断;如果不在:说明创建 Readview 时生成该版本的事务已经被提交,可以被访问,直接返回。

READ-UNCOMMITTED(读取未提交) :读未提交无需锁无需 MVCC,因为修改数据直接改源数据,会出现脏读。

READ-COMMITTED(读取已提交) :每次查询都会创建 Readview 读取数据

REPEATABLE-READ(可重复读) :同样的查询只会第一次创建 Readview 读取数据

SERIALIZABLE(可串行化) :表锁


5.2 代码示例

-- 事务1
START TRANSACTION;
SELECT * FROM my_table WHERE id = 1;
-- 执行读操作
COMMIT;

-- 事务2
START TRANSACTION;
UPDATE my_table SET value = 'new_value' WHERE id = 1;
-- 执行写操作
COMMIT;

5.3 思考题

  • MVCC是如何实现高并发的?在什么情况下会出现MVCC的冲突?

实现高并发的原理:

  • 版本链:MVCC通过为每行数据维护一个版本链,记录数据的历史版本。每个事务在读取数据时,会根据自己的事务ID选择合适的数据版本。
  • 快照读:事务在执行读操作时,读取的是事务开始时的数据快照,而不是当前的数据。这样,读操作不会阻塞写操作,写操作也不会阻塞读操作。

MVCC冲突的场景:

  • 写-写冲突:两个事务同时尝试更新同一行数据,后提交的事务会失败。
  • 读-写冲突:如果一个事务在读取数据的同时,另一个事务更新了该数据,可能会导致读取到的数据不一致(取决于隔离级别)。

6. 锁机制的综合应用

在实际应用中,锁机制往往是多种锁的组合使用。例如,在一个事务中,可能会同时使用行锁和间隙锁来确保数据的一致性和并发性。

6.1 综合示例

-- 事务1
START TRANSACTION;
SELECT * FROM my_table WHERE id BETWEEN 1 AND 10 FOR UPDATE;
-- 执行写操作
COMMIT;

-- 事务2
START TRANSACTION;
SELECT * FROM my_table WHERE id = 5 LOCK IN SHARE MODE;
-- 执行读操作
COMMIT;

6.2 思考题

  • 在实际应用中,如何选择合适的锁机制来平衡性能和数据一致性?

7. 锁机制与其他知识的联想

锁机制不仅仅是数据库中的概念,它在操作系统、并发编程等领域也有广泛的应用。例如,操作系统中的互斥锁、信号量等概念与数据库中的锁机制有着异曲同工之妙。

7.1 操作系统中的锁

  • 互斥锁:用于保护临界区,确保同一时间只有一个线程可以访问临界区。
  • 信号量:用于控制多个线程对共享资源的访问。

7.2 并发编程中的锁

  • ReentrantLock:Java中的可重入锁,类似于数据库中的行锁。
  • ReadWriteLock:Java中的读写锁,类似于数据库中的共享锁和排他锁。

7.3 思考题

  • 数据库中的锁机制与操作系统、并发编程中的锁机制有哪些异同点?在实际开发中,如何借鉴这些锁机制的设计思想?

结语

MySQL的锁机制就像是一场精彩的“锁”事大戏,每个锁都有自己独特的角色和职责。通过深入了解这些锁机制,我们不仅能够更好地理解数据库的工作原理,还能够在实际应用中做出更明智的选择。希望这篇文章能够为你带来知识增量,并激发你对数据库锁机制的进一步思考。



Tags:

最近发表
标签列表