MySQL 多版本并发控制 —— MVCC

2023年 9月 4日 8.5k 0

前言

  • MVCC指的是快照读。
  • MySQL中仅在RC读已提交级别、RR可重复读级别才会使用MVCC机制。
  • RC级别中,MVCC机制是会在每次select语句执行前,都会生成一个ReadView
  • RR级别中,一个事务只会在首次执行select语句时生成快照,后续所有的select操作都会基于这个ReadView来判断,这样也就解决了RC级别中存在的不可重复问题。

隔离级别

  • 读未提交(READ UNCOMMITTED):这是事务的最低隔离级别,事务中的读取操作可以看到其他未提交事务的变动。这种隔离级别可能导致脏读不可重复读幻读问题。

  • 读提交(READ COMMITTED):这是大多数数据库系统的默认隔离级别。事务中的读取操作只能看到其他事务已经提交的变动。这种隔离级别解决了脏读问题,但可能会出现不可重复读幻读问题。

  • 可重复读(REPEATABLE READ):这是MySQL的默认隔离级别。事务中多次读取同一行数据,而不会看到其他事务对这一行数据的修改。这种隔离级别解决了脏读不可重复读问题,但仍然可能出现幻读问题。

  • 可串行化(SERIALIZABLE):这是事务的最高隔离级别。在事务执行期间,使用该隔离级别可以保证事务串行执行,避免了脏读不可重复读幻读问题。但是这种隔离级别效率低下,因为事务通常需要等待前一个事务完成,才能继续执行。

读方式

  • 当前读是指在事务执行过程中,直接读取数据,而不是等待事务提交。这种读取方式会阻塞其他事务的写入操作,因此在并发性能方面可能较差。

    • 排他锁(也称为写锁):使用 SELECT ... FOR UPDATE 语句来对选定的数据行添加排他锁。例如:SELECT * FROM table_name WHERE condition_column = 'condition_value' FOR UPDATE; 将返回满足条件的数据行,并对这些行添加排他锁。其他事务需要对这些行进行修改或删除时,必须等待排他锁被释放。

    • 共享锁(也称为读锁):使用 SELECT ... LOCK IN SHARE MODE 语句来对选定的数据行添加共享锁。例如:SELECT * FROM table_name WHERE condition_column = 'condition_value' LOCK IN SHARE MODE; 将返回满足条件的数据行,并对这些行添加共享锁。其他事务可以继续读取这些行,但是不能修改或删除。其他事务必须等待共享锁被释放。

  • 快照读是指在某个时间点上,一个事务读取另一个已经提交的事务的数据。在大多数情况下,读取操作不会阻塞其他事务的写入操作,这样可以提高数据库的并发性能。

    • SELECT * FROM table_name WHERE condition_column = 'condition_value';

隐藏字段

  • DB_ROW_ID:这是一个隐藏的列,它存储了行的唯一标识符。每一行都有一个唯一的ID,这个ID是在创建行时自动生成的。DB_ROW_ID列通常在InnoDB表的物理结构中存在,但在SELECT、INSERT、UPDATE等SQL语句中是隐藏的,不能直接访问或修改。
  • DB_Deleted_Bit:这是一个内部标记位,用于表示该行是否被标记为删除。当一行被删除时,该标记位将被设置为1,表示该行已被删除。这个字段仅在内部使用,对用户来说是隐藏的。
  • DB_TRX_ID:这是一个隐藏的列,它存储了当前事务的ID。当一行被修改或删除时,该事务的ID将被存储在该列中。通过这个字段,InnoDB引擎可以追踪事务的修改操作,并在需要时进行回滚操作。
  • DB_ROLL_PTR:这是一个隐藏的指针,它指向回滚日志中的位置,用于在需要回滚时恢复数据。当事务需要回滚时,InnoDB引擎将使用DB_ROLL_PTR来找到对应的回滚日志,并根据日志中的信息将数据恢复到事务开始之前的状态。

undo-log

SELECT * FROM `users` WHERE user_id = 1;
+---------+-----------+----------+
| user_id | user_name | user_age |
+---------+-----------+----------+
|       1 | xyc       | 18       |
+---------+-----------+----------+

UPDATE `users` SET user_name = "neil" WHERE user_id = 1;
UPDATE `users` SET user_age = 20 WHERE user_id = 1;

比如上述这段SQL隶属于trx_id=1的事务,undo-log日志中存储的数据为:

undo log.png

不同的旧版本数据,会以roll_ptr回滚指针作为链接点,然后将所有的旧版本数据组成一个单向链表。最新的旧版本数据,都会插入到链表头中,而不是追加到链表尾部。

ReadView

ReadView就是一个事务在尝试读取一条数据时,MVCC基于当前MySQL的运行状态生成的快照,也被称之为读视图。当一个事务启动后,首次执行select操作时,MVCC就会生成一个数据库当前的ReadView,通常而言,一个事务与一个ReadView属于一对一的关系,ReadView一般包含四个核心内容:

  • creator_trx_id:代表创建当前这个ReadView的事务ID

  • trx_ids:表示在生成当前ReadView时,系统内活跃的事务ID列表,活跃事务是指还在执行的事务,即未结束(提交/回滚)的事务。

  • up_limit_id:活跃的事务列表中,最小的事务ID

  • low_limit_id:表示在生成当前ReadView时,系统中要给下一个事务分配的ID值,MySQL的事务ID是按序递增的。

例如:

{
    "creator_trx_id" : "0",
    "trx_ids" : "[1,2,4]",
    "up_limit_id" : "1",
    "low_limit_id" : "5"
}

MVCC机制实现原理

  • 当一个事务尝试改动某条数据时,会将原本表中的旧数据放入undo-log中。
  • 当一个事务尝试查询某条数据时,MVCC会生成一个ReadView快照。
  • 其中undo-log主要实现数据的多版本,ReadView则主要实现多版本的并发控制。
-- 事务T1:trx_id=1
UPDATE `users` SET user_name = "neil" WHERE user_id = 1;
UPDATE `users` SET user_sex = 20 WHERE user_id = 1;
-- 事务T2:trx_id=2
SELECT * FROM `users` WHERE user_id = 1;

目前存在T1、T2两个并发事务,T1目前在修改user_id=1的这条数据,而T2则准备查询这条数据,那么T2在执行时具体过程如下:

  • 当事务中出现select语句时,会先根据MySQL的当前情况生成一个ReadView

  • 判断行数据中的隐藏列trx_idReadView.creator_trx_id是否相同:

    • 相同:代表创建ReadView和修改行数据的事务是同一个,自然可以读取最新版数据。
    • 不相同:代表目前要查询的数据,是被其他事务修改过的,继续往下执行。
  • 判断隐藏列trx_id是否小于ReadView.up_limit_id最小活跃事务ID:

    • 小于:代表改动行数据的事务在创建快照前就已结束,可以读取最新版本的数据。
    • 不小于:则代表改动行数据的事务还在执行,因此需要继续往下判断。
  • 判断隐藏列trx_id是否小于ReadView.low_limit_id这个值:

    • 大于或等于:代表改动行数据的事务是生成快照后才开启的,因此不能访问最新版数据。
    • 小于:表示改动行数据的事务IDup_limit_id、low_limit_id之间,需要进一步判断。
  • 判断隐藏列trx_id是否在trx_ids中:

    • 在:表示改动行数据的事务目前依旧在执行,不能访问最新版数据。
    • 不在:表示改动行数据的事务已经结束,可以访问最新版的数据。
  • 就是首先会去获取表中行数据的隐藏列,然后经过上述一系列判断后,可以得知:目前查询数据的事务到底能不能访问最新版的数据。如果能,就直接拿到表中的数据并返回,反之,不能则通过roll_ptrundo-log日志中获取链表头的旧版本数据,然后依次遍历整个链表,从而检索到最合适的一条数据并返回。

    相关文章

    如何删除WordPress中的所有评论
    检查WordPress服务器磁盘使用情况的7种简便方法(查找大文件和数据)
    如何更改WordPress常量FS_METHOD
    如何为区块编辑器、Elementor等构建WordPress文章模板
    如何彻底地删除WordPress主题及相关内容
    如何使用WordPress搭建一个内网

    发布评论