MySQL高级讲座篇之:MVCC(多版本并发控制)的内部工作原理:快照读与当前读的协同。

各位观众老爷们,早上好中午好晚上好!欢迎来到今天的MySQL高级讲座!今天咱们要聊的,是MySQL里一个听起来高大上,但实际上也确实挺厉害的技术 – MVCC (Multi-Version Concurrency Control),也就是多版本并发控制。

这玩意儿,说白了,就是让数据库在大家伙儿同时读写的时候,还能保持井然有序,数据不乱套。这就像啥呢?就像你去图书馆借书,有人在你之前借走了,你还能看到书的目录,知道这本书曾经存在过,而且大概讲了啥。MVCC就是让你在数据被修改的时候,还能看到之前的版本,保证你的读操作不被写操作阻塞。

今天咱们就来扒一扒MVCC的内部工作原理,重点说说快照读和当前读是怎么协同工作的。准备好了吗?Let’s go!

一、并发控制的那些事儿

在深入MVCC之前,咱们先简单了解一下并发控制。为啥需要并发控制?因为数据库是多人共享的资源,总有人想同时读写数据。如果没有并发控制,就会出现各种问题,比如:

  • 丢失更新 (Lost Update): 两个事务同时读取同一行数据,然后各自修改后提交,后提交的事务会覆盖先提交的事务的修改,导致数据丢失。
  • 脏读 (Dirty Read): 一个事务读取了另一个事务尚未提交的修改,如果另一个事务回滚了,那么第一个事务读到的就是无效数据。
  • 不可重复读 (Non-Repeatable Read): 在同一个事务中,多次读取同一行数据,由于其他事务的修改,导致每次读取的结果不一致。
  • 幻读 (Phantom Read): 在同一个事务中,多次执行同样的查询,由于其他事务的插入或删除操作,导致每次查询的结果集不同。

为了解决这些问题,数据库需要提供并发控制机制。常见的并发控制方法包括:

  • 锁 (Locking): 悲观锁,认为总是会发生冲突,所以每次读写数据都要加锁,防止其他事务修改。
  • MVCC (Multi-Version Concurrency Control): 乐观锁,认为大部分情况下不会发生冲突,允许多个事务同时读写数据,只有在提交的时候才检查是否有冲突。

锁机制简单粗暴,但效率较低,容易造成阻塞。MVCC则更加灵活高效,是现代数据库常用的并发控制方法。

二、MVCC的基石:版本链

MVCC的核心思想是为每一行数据维护多个版本。每个版本都包含数据的值,以及一些元数据信息,比如创建版本的时间戳、删除版本的时间戳等等。这些版本按照时间顺序组成一个版本链。

咱们来举个例子,假设有一张用户表 users,包含 idname 两个字段。

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(255)
);

INSERT INTO users (id, name) VALUES (1, 'Alice');

现在,users 表中只有一条记录,id = 1, name = 'Alice'。 在MVCC的加持下,这条记录实际上会被存储成一个版本:

id name 创建时间戳 (trx_id) 删除时间戳 (roll_ptr)
1 Alice 100 NULL
  • trx_id:表示创建这个版本的事务ID。
  • roll_ptr:指向前一个版本的指针。这里是NULL,表示这是第一个版本。

现在,假设有一个事务A,将 Alice 的名字修改为 Bob

-- 事务A
START TRANSACTION;
UPDATE users SET name = 'Bob' WHERE id = 1;
COMMIT;

在MVCC的机制下,MySQL不会直接修改原来的数据,而是创建一个新的版本:

id name 创建时间戳 (trx_id) 删除时间戳 (roll_ptr)
1 Bob 200 NULL

同时,原来的版本会被标记为已删除:

id name 创建时间戳 (trx_id) 删除时间戳 (roll_ptr)
1 Alice 100 200

注意,Alice 版本的 roll_ptr 变成了 200,指向了 Bob 版本。这样就形成了一个版本链。

如果又有事务B,将 Bob 的名字修改为 Charlie,那么会再次创建一个新的版本:

id name 创建时间戳 (trx_id) 删除时间戳 (roll_ptr)
1 Charlie 300 NULL

Bob 版本会被标记为已删除:

id name 创建时间戳 (trx_id) 删除时间戳 (roll_ptr)
1 Bob 200 300

现在,版本链就变成了这样:Charlie (300) -> Bob (200) -> Alice (100)

三、快照读 (Snapshot Read)

快照读,也叫做一致性读,是指读取数据时,读取的是某个时间点的快照版本。这种读取方式不会阻塞其他事务的读写操作,保证了并发性。

在MySQL中,SELECT语句默认情况下执行的是快照读(在REPEATABLE READ隔离级别下)。

那么问题来了,快照读到底读哪个版本的数据呢? 这就涉及到一致性视图 (Consistent View) 的概念。

一致性视图 (Consistent View)

一致性视图是指事务启动时,系统为该事务创建的一个快照,记录了当前活跃事务的信息。简单来说,就是事务启动时,它能看到哪些事务已经提交,哪些事务尚未提交。

一致性视图包含以下信息:

  • m_ids: 当前所有活跃事务的ID列表。
  • low_limit_id: 最小的事务ID,通常是数据库启动时的事务ID。
  • up_limit_id: 下一个要分配的事务ID,也就是当前最大的事务ID + 1。

当事务执行快照读时,会根据一致性视图来判断应该读取哪个版本的数据。判断规则如下:

  1. 如果数据的版本号小于 low_limit_id,说明该版本在事务启动之前就已经提交了,可以读取。
  2. 如果数据的版本号大于等于 up_limit_id,说明该版本在事务启动之后才创建,不能读取。
  3. 如果数据的版本号在 low_limit_idup_limit_id 之间,需要判断该版本号是否在 m_ids 列表中。
    • 如果在 m_ids 列表中,说明该版本是由当前活跃的事务创建的,不能读取。
    • 如果不在 m_ids 列表中,说明该版本在事务启动之前就已经提交了,可以读取。

咱们还是用上面的例子来说明。假设现在有三个事务:

  • 事务A:trx_id = 100,已经提交。
  • 事务B:trx_id = 200,正在执行,尚未提交。
  • 事务C:trx_id = 300,刚刚启动。

此时,users 表的版本链是:Charlie (300) -> Bob (200) -> Alice (100)

事务C启动时,会创建一个一致性视图,假设其信息如下:

  • m_ids = [200]
  • low_limit_id = 100
  • up_limit_id = 301

现在,事务C执行以下查询:

-- 事务C
SELECT * FROM users WHERE id = 1;
  1. 首先,事务C会找到最新的版本 Charlie (300)
  2. 判断 300 >= 301,不成立。
  3. 判断 300 >= 100300 < 301,成立。
  4. 判断 300 是否在 m_ids 列表中,不在。
  5. 因此,Charlie (300) 版本不能读取。
  6. 然后,事务C会沿着版本链找到前一个版本 Bob (200)
  7. 判断 200 >= 301,不成立。
  8. 判断 200 >= 100200 < 301,成立。
  9. 判断 200 是否在 m_ids 列表中,在。
  10. 因此,Bob (200) 版本也不能读取。
  11. 然后,事务C会沿着版本链找到前一个版本 Alice (100)
  12. 判断 100 >= 301,不成立。
  13. 判断 100 < 100,不成立。
  14. 因此,Alice (100) 版本可以读取。

最终,事务C会读取到 Alice (100) 版本的数据,也就是 name = 'Alice'

通过一致性视图和版本链,MVCC保证了事务只能读取到在它启动之前就已经提交的数据,避免了脏读、不可重复读等问题。

四、当前读 (Current Read)

当前读,是指读取数据的最新版本。这种读取方式需要加锁,保证读取到的数据是最新的,并且防止其他事务修改该数据。

在MySQL中,以下语句会执行当前读:

  • SELECT ... LOCK IN SHARE MODE (共享锁)
  • SELECT ... FOR UPDATE (排他锁)
  • UPDATE ...
  • DELETE ...
  • INSERT ...

咱们还是用上面的例子来说明。假设现在有三个事务:

  • 事务A:trx_id = 100,已经提交。
  • 事务B:trx_id = 200,正在执行,尚未提交。
  • 事务C:trx_id = 300,刚刚启动。

此时,users 表的版本链是:Charlie (300) -> Bob (200) -> Alice (100)

事务C执行以下查询:

-- 事务C
SELECT * FROM users WHERE id = 1 FOR UPDATE;

由于使用了 FOR UPDATE,因此这是一个当前读操作。

  1. 事务C会尝试获取 id = 1 这一行数据的排他锁。
  2. 如果当前没有其他事务持有该行数据的锁,事务C就可以成功获取锁。
  3. 事务C会读取到最新的版本 Charlie (300),也就是 name = 'Charlie'

注意,如果事务B已经持有 id = 1 这一行数据的锁,那么事务C会被阻塞,直到事务B释放锁为止。

当前读保证了读取到的数据是最新的,但同时也增加了锁的开销,降低了并发性。

五、快照读与当前读的协同

MVCC的强大之处在于,它能够巧妙地将快照读和当前读结合起来,在保证并发性的同时,也能够满足不同的业务需求。

  • 快照读: 适用于对数据一致性要求不高,但对并发性要求高的场景。例如,统计报表、数据分析等。
  • 当前读: 适用于对数据一致性要求高,但对并发性要求不高的场景。例如,银行转账、库存管理等。

在实际应用中,我们可以根据不同的业务场景选择不同的读取方式。

六、总结

咱们今天聊了MVCC的内部工作原理,重点说了快照读和当前读是怎么协同工作的。

  • MVCC通过版本链来维护数据的多个版本。
  • 快照读读取的是某个时间点的快照版本,通过一致性视图来判断应该读取哪个版本的数据。
  • 当前读读取的是数据的最新版本,需要加锁,保证读取到的数据是最新的。
  • MVCC能够巧妙地将快照读和当前读结合起来,在保证并发性的同时,也能够满足不同的业务需求。

希望今天的讲座能够帮助大家更好地理解MVCC,并在实际应用中灵活运用。

七、彩蛋:隔离级别与MVCC

MVCC的实现与MySQL的隔离级别密切相关。不同的隔离级别对MVCC的实现方式和可见性规则有不同的影响。

隔离级别 脏读 不可重复读 幻读 MVCC是否生效
READ UNCOMMITTED 不生效
READ COMMITTED 生效
REPEATABLE READ 生效
SERIALIZABLE 不生效 (强制加锁)
  • READ UNCOMMITTED: 不使用MVCC,直接读取最新的数据,因此会出现脏读。
  • READ COMMITTED: 使用MVCC,每次读取数据时都会创建一个新的快照,因此可以避免脏读,但会出现不可重复读。
  • REPEATABLE READ: 使用MVCC,事务启动时创建一个快照,整个事务期间都使用该快照,因此可以避免脏读和不可重复读,但会出现幻读。
  • SERIALIZABLE: 强制加锁,不使用MVCC,可以避免脏读、不可重复读和幻读。

MySQL默认的隔离级别是REPEATABLE READ,通过MVCC来保证事务的隔离性。

好了,今天的讲座就到这里。 谢谢大家的观看,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注