mysql中mvcc相关概念

一、什么是mvcc

MVCC(Multiversion Concurrency Control)多版本并发控制。即通过数据行的多个版本管理来实现数据库的并发控制,使得在InnoDB事务隔离级别下执行一致性读操作有了保障。

二、事务隔离级别的演示

mysql> select * from account;       
+----+-------+---------+----------+ 
| id | owner | balance | currency | 
+----+-------+---------+----------+ 
|  1 | one   |     100 | CNY      | 
|  2 | two   |     100 | CNY      | 
|  3 | three |     100 | CNY      | 
+----+-------+---------+----------+ 
3 rows in set (0.06 sec)

ISOLATION_READ_UNCOMMITTED

在这里插入图片描述
我分别设置事务1、事务2隔离级别为read uncommitted,从图中步骤3、4都可以看到,为了方便,后面的展示将不再说明。

  • 分别开始事务1、事务2(步骤5、6)。

  • 在事务1中,进行一个简单的查询,用来对比数据前后变化。

  • 在事务2中,查看 id 为 1 的账户,金额为 100 元。

  • 在事务1中,对 id 为 1 的账户余额减去 10 元,然后查询确认一下余额已经更改为 90 元。

  • 但是,如果在事务2中再次运行相同的 select 语句怎么办?
    你会看到余额被修改为了 90 元,而不是先前的 100 元。请注意,事务1并未提交,但事务2却看到了事务1所做的更改。这就是脏读现象,因为我们使用的事务隔离级别为read uncommitted(读未提交)。

ISOLATION_READ_COMMITTED

read-committed隔离级别只能防止脏读,但是会出现不可重复读和幻读。

在这里插入图片描述
设置隔离级别为 read committed,并开始事务。

  • ③ 在事务1中,进行一个简单的查询,用来对比数据前后变化。

  • ④ 在事务2中,查看 id 为 1 的账户,金额为 90 元。

  • ⑤⑥ 在事务1中,通过更新帐户余额减去 10 元,然后查询确认一下余额已经更改为 80 元,让我们看看此更改是否对事务2可见。

  • ⑦ 事务2中可以看到,其余额仍然与以前一样为 90 元。
    这是因为事务正在使用read-committed隔离级别,并且由于事务1还没有提交,所以它的写入数据不能被其他事务看到。
    因此,读已提交 (read-committed) 隔离级别可以防止脏读现象。那么对于不可重复读和幻读呢?

  • ⑧ 在事务2中,执行另一个操作,查询大于或等于 90 元的账户。

  • ⑨ 事务1进行提交。

  • ⑩ 现在,如果我们再次在事务2中查询帐户1余额,我们可以看到余额已更改为 80元 。所以,获得帐户1余额的同一查询返回了不同的值。 这就叫不可重复读。

  • 另外,在步骤11中,再次运行如⑧中的操作,这次只得到了2条记录,而不是以前的3条,因为事务1提交后,账户1的余额已经减少到 80 元了。

执行了相同的查询,但是返回了不同的行数。由于其他事务的提交,而导致一行数据消失,这种现象叫做幻读。

ISOLATION_REPEATABLE_READ

在这里插入图片描述

  • 设置隔离级别为 Repeatable Read,并开始事务。

  • ③查询事务1中的所有帐户,然后④查询事务2中ID为1的帐户,除此之外,还要⑤查询余额至少为80元的所有帐户。 这将用于验证幻读是否仍然发生。

  • 回到事务1⑥更新账户1余额减去 10 元;可以看到⑦帐户1的余额减少到了 70 元。

我们知道脏读已在较低的隔离级别read-committed不会出现。因此,由于以下规则,我们不需要在此级别进行检查:
在较低隔离级别被阻止的了读现象,不会出现在较高级别。

  • 因此,让我们⑧提交事务1,然后转移到⑨事务2,看看它是否能读取到事务1所做的新更改。

可以看到,该查询返回账户1的余额与先前相同,为 80 元,尽管事务1将账户1的余额更改为 70 元,并成功提交。
.
这是因为Repeatable Read(可重复读)隔离级别确保所有读查询都是可重复的,这意味着即使其他已提交的事务对数据进行了更改,它也始终返回相同的结果。

  • 我们重新运行⑩查询余额至少 80 元的帐户。

它仍然返回与之前相同的3条记录。所以在Repeatable_Read隔离级别中,可以解决幻读问题。解决所有幻读吗?
.
但是,我想知道如果我们还运行步骤 11,从事务1更新过的帐户1的余额中减去10,会发生什么情况? 它将余额更改为70、60还是抛出错误? 试试吧!
.
结果没有报错,该账户余额现在改为了 60 元,这是正确的值,因此事务1早已经提交而将余额修改为了 70 元。
.
但是,从事务2的角度来看,这是没有意义的,因为在上一个查询中,它获取到的是 80 元的余额,但是从帐户中减去 10 元后,现在却得到 60 元。数学运算在这里不起作用,因为此事务仍受到其他事务的并发更新的干扰。

ISOLATION_SERIALIZABLE

在这里插入图片描述

  • 设置隔离级别为 Serializable,并开始事务。
  • ③查询事务1中的所有帐户,然后④查询事务2中ID为1的帐户。
  • 回到⑤事务1更新账户1余额减去 10 元。

有趣的是,这一次更新被阻止了。 事务2的 select 查询语句阻塞了事务1中的 update 更新语句。
.
原因是,在Serializable隔离级别中,MySQL隐式地将所有普通的 SELECT 查询转换为 SELECT FOR SHARE。 持有 SELECT FOR SHARE 锁的事务只允许其他事务读取行,而不能更新或删除行。
.
因此,有了这种锁定机制,我们以前看到的不一致数据场景不再可能出现。
.
但是,这个锁有一个超时持续时间。因此,如果事务2在该持续时间内未提交或回滚以释放锁,我们将看到锁等待超时错误(⑤下面显示错误)。
.
因此,当在应用程序中使用Serializable隔离级别时,请确保实现了一个事务重试策略,以防超时发生。

好的,将事务回滚,现在我将重新测试,看看另一种情况:
在这里插入图片描述

  • 这一次,到步骤⑤的时候,我不会让锁等待超时发生,然后到步骤⑥也进行了跟⑤一样的操作。
  • 到⑥这里,发生了死锁,因为现在事务2也需要等待事务1的 select 查询的锁。

所以请注意,除了锁等待超时之外,还需要处理可能出现的死锁情况。

现在,然我们尝试重启这两个事务:
在这里插入图片描述

  • 这次操作还是跟上面相同,到步骤⑤时,我们知道会阻塞,但如果此时步骤⑥事务2提交了,会怎样呢?
  • 如你所见,在提交了事务2后,事务2的 select 锁立即释放,从而⑤事务1中不再阻塞,更新成功。

三、当前读与快照读

mvcc,也就是多版本并发控制,是为了在读取数据时不加锁来提高读取效率和并发性的一种手段。数据库并发有以下几种场景:

  • 读 - 读:不存在任何问题。
  • 读 - 写:有线程安全问题,可能出现脏读、幻读、不可重复读。
  • 写 - 写:有线程安全问题,可能存在更新丢失等。

mvcc解决的就是读写时的线程安全问题,线程不用去争抢读写锁。

mvcc使用快照读,也就是普通的select语句快照读在读写时不用加锁,不过可能会读到历史数据另一种读取数据的方式是当前读,是一种悲观锁的操作。它会对当前读取的数据进行加锁,所以读到的数据都是最新的。主要包括以下几种操作:

  • select lock in share mode(共享锁)
    • 在所有索引扫描范围的索引记录上加上共享的next key锁;如果是唯一索引,只需要在相应记录上加index record lock。这些被共享lock住的行无法进行update/delete。
    • 允许其它事务对这些记录再加SHARE锁
    • 如果没有使用到索引,则锁住全表(表级的排他锁),无法进行insert/update/delete。
  • select for update(排他锁)
    • 在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。
    • 如果没有利用到索引将锁住全表(表级的排他锁),其它事务无法进行insert/update/delete操作。
  • update(排他锁)
    • 语句在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。
    • 如果没有利用到索引将锁住全表(表级的排他锁),其它事务无法进行其它的insert/update/delete操作。
  • insert(排他锁)
    • 在插入的记录上加一把排他锁,这个锁是一个index-record lock,并不是next-key 锁,因此就没有gap 锁,他将不会阻止其他会话在该条记录之前的gap插入记录。
  • delete(排他锁)
    • 语句在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加index record lock。
    • 如果没有利用到索引将锁住全表(表级的排他锁),其它事务无法进行其它的insert/update/delete操作。

1:Record Lock:单个行记录上的锁。
2:Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
3:Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

四、MVCC的实现

MVCC的实现依赖于:隐藏字段、Undo log、Read View

行隐藏字段

如下图所示 ID~……是表中字段的名称(DATA)。最后三个隐藏字段对应对应行的隐藏ID、事务编号和回滚指针:

  • 隐含ID(DB_ROW_ID): 6字节,当InnoDB自动生成聚集索引时,聚集索引包括这个DB_ROW_ ID的值。

  • 事务编号(DB_TRX_ID): 6字节,它标记了此行最新更新记录的事务ID。每个事务都被处理,其值自动为+1。

  • 回滚指针(DB_ROLL_PT): 7个字节,指向当前记录项的回滚段的撤消日志记录,通过该记录可以找到以前版本的数据。

在这里插入图片描述

版本链 (undo log)

所谓的版本链就是在MVCC中,多个事务对同一行记录进行更新会产生多个历史快照,这些记录保存在Undo Log里,这些undo日志通过回滚指针串联在一起。

  • undo log就是回滚日志,在insert/update/delete变更操作的时候生成的记录方便回滚。
  • 当进行insert操作的时候,产生的undo log只有在事务回滚的时候需要,如果不回滚在事务提交之后就会被删除。
  • 当进行update和delete的时候,产生的undo log不仅仅在事务回滚的时候需要,在快照读的时候也是需要的,所以不会立即删除,只有等不再用到这个日志的时候才会被mysql purge线程统一处理掉(delete操作也只是打一个删除标记,并不是真正的删除)。

在这里插入图片描述
undo log 版本链是基于 undo log 实现的。undo log 中主要保存了数据的基本信息,比如说日志开始的位置、结束的位置,主键的长度、表id,日志编号、日志类型。
在这里插入图片描述
此外,undo log 还包含两个隐藏字段 trx_id 和 roll_pointer。trx_id 表示当前这个事务的 id,MySQL 会为每个事务分配一个 id,这个 id 是递增的。roll_pointer 是一个指针,指向这个事务之前的 undo log。
在这里插入图片描述
如下图所示,现在有一个 id 为 10 的事务 A 正在执行,undo log 日志的信息如下所示:
在这里插入图片描述
紧接着 id 为 18 的事务 B 开始执行,就会再生成一条 undo log 日志,同时新生成的日志的 roll_pointer 指向上一条 undo log 日志。
在这里插入图片描述日志与日志之间通过 roll_pointer 指针连接,就形成了 undo log 版本链

readview

所谓readview顾名思义是一个视图内存结构,·在事务select查询数据时,就会构造一个readview,里面记录了该数据版本链的一些统计值,这样在后续查询处理是就无需遍历所有版本链了;

在这里插入图片描述

  • m_ids: 当前有哪些事务正在执行,且还没有提交,这些事务的 id 就会存在这里;
  • min_trx_id: 是指 m_ids 里最小的值;
  • max_trx_id: 是指下一个要生成的事务 id。下一个要生成的事务 id 肯定比现在所有事务的 id 都大;
  • creator_trx_id: 每开启一个事务都会生成一个 ReadView,而 creator_trx_id 就是这个开启的事务的 id。

当一个事务执行 SELECT 操作时,它会使用自己的读视图来确定应该看到哪个数据版本。具体规则如下:

  • 如果数据版本的 事务ID 等于 creator_trx_id ,表示该版本是当前事务, 可见

  • 如果数据版本的 事务ID小于 min_trx_id ,表示该版本已经提交, 可见

  • 如果数据版本的 事务ID大于 max_trx_id ,表示该版本在事务开始后创建, 不可见

  • 如果数据版本的 事务ID在 min_trx_id max_trx_id 之间,但在 m_ids 集合中 ,表示该版本由尚未提交的事务创建, 不可见

  • 如果数据版本的 事务ID在min_trx_id max_trx_id 之间,并且不在 m_ids 集合中,表示该版本由已提交的事务创建,可见

数据读取过程

事务是可以并发执行的,现在有事务 A、事务 B 这两个事务,且这两个都没有提交。事务 A 将会执行多次读操作,来模拟可重复读中多次读取同一行数据的场景。事务 B 则会修改这一行数据。
在这里插入图片描述

  • 事务 A 开启事务的时候会生成一个 ReadView,所以说这个 ReadView 的创建者就是事务 A,事务 A 的事务 id 是 10,所以 creator_trx_id 就是 10。

  • 此时,总共就只有事务 A、事务 B 这两个事务,而且它们都还没有提交,所以说 m_ids 会把这两个事务 id,10、18 都记录下来。min_trx_id 是 m_ids 里面的最小值,10、18 中最小的显然是 10。当前最大的事务 id 是 18,那么下一个事务的 id 就是 19,max_trx_id 就是 19。

  • ReadView 生成之后,事务 A 就要去 undo log 版本链中读取值了。

  • 现在只有一条 undo log 日志,但这并不意味着事务 A 就能读到这条日志的值 X。它要先判断这行日志的 trx_id 是否小于当前事务的 min_trx_id。看图我们可以很轻松地发现,日志的 trx_id = 8 小于 ReadView 中 min_trx_id = 10。

  • 这就意味着,这个事务 A 开始执行之前,修改这行数据的事务已经提交了,所以事务 A 是可以查到值 X 的。

五、RR级别下的幻读问题

InnoDB默认的隔离级别是RR(可重复读),只解决了快照读情况下的不可重复读和幻读问题当前读情况下解决不可重复读和幻读问题问题得靠next-key锁

快照读操作

在只有快照读的情况下,不存在不可重复读和幻读问题。事务2在事务1的两个select语句之间执行

//事务1,开启事务,操作并提交
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from useinfo;//步骤1:事务1进行读取数据库——>快照读
+------+------+------+
| id   | name | age  |
+------+------+------+
|    1 |    1 |    1 |
|    2 |    2 |    2 |
|    3 |    3 |    3 |
+------+------+------+
3 rows in set (0.00 sec)

mysql> select * from useinfo;//步骤3,事务1再次进行读取数据库——>快照读
+------+------+------+
| id   | name | age  |
+------+------+------+
|    1 |    1 |    1 |
|    2 |    2 |    2 |
|    3 |    3 |    3 |
+------+------+------+
3 rows in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
//事务2,开启事务执行并提交事务-----------步骤2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into useinfo values(4,4,4);//插入数据
Query OK, 1 row affected (0.00 sec)

mysql> update useinfo set age = 20 where id = 1;//更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

当前读操作

在有当前读和快照读的情况下,存在不可重复读和幻读问题。事务2在事务1的select和update语句之间执行

//事务1,开启事务,操作并提交
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from useinfo;//步骤1:事务1进行读取数据库——>快照读
+------+------+------+
| id   | name | age  |
+------+------+------+
|    1 |    1 |    1 |
|    2 |    2 |    2 |
|    3 |    3 |    3 |
|    4 |    4 |    4 |
+------+------+------+
4 rows in set (0.00 sec)

mysql> update useinfo set name = 10;//步骤3:事务1进行更新数据库——>当前读
Query OK, 4 rows affected (0.00 sec)
Rows matched: 5  Changed: 4  Warnings: 0

mysql> select * from useinfo;//步骤4,事务1再次进行读取数据库——>快照读
+------+------+------+
| id   | name | age  |
+------+------+------+
|    1 |   10 |    1 |
|    2 |   10 |   20 |
|    3 |   10 |    3 |
|    4 |   10 |    4 |
|    5 |   10 |    5 |
+------+------+------+
5 rows in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
//事务2,开启事务执行并提交事务-----------步骤2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into useinfo values(5,5,5);//插入数据
Query OK, 1 row affected (0.00 sec)

mysql> update useinfo set age = 20 where id = 2;//更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction.
个人认为应该翻译为: 数据库状态的快照适用于事务中的SELECT语句, 而不一定适用于所有DML语句。 如果您插入或修改某些行, 然后提交该事务, 则从另一个并发REPEATABLE READ事务发出的DELETE或UPDATE语句就可能会影响那些刚刚提交的行, 即使该事务无法查询它们。 如果事务更新或删除由不同事务提交的行, 则这些更改对当前事务变得可见

参考博客

图解 ReadView 机制

深入理解MVCC实现原理以及当前读和快照读存在的问题

  • 14
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: MySQLMVCC(Multi-Version Concurrency Control)机制是通过为每个读操作创建一个版本(Version)并保留旧版本来实现的。这个机制允许多个事务同时访问同一数据行,同时确保它们不会互相干扰或产生冲突。 MVCC在MySQL的实现方式是,对于每一行数据,在表存储一个隐藏的系统版本号(system versioning),并将每个操作(包括SELECT查询)的时间戳与该行的版本号进行比较。当读取一行数据时,MySQL会根据当前的事务时间戳和行的版本号来决定该行是否可见。如果行的版本号早于当前事务的时间戳,则说明该行是旧版本,不可见;如果行的版本号晚于当前事务的时间戳,则说明该行是新版本,可见。 在MVCC机制下,读操作不会阻塞写操作,写操作也不会阻塞读操作。因此,MVCC机制可以提高并发性能和可伸缩性,使得多个事务可以同时访问同一数据库而不会产生锁定和阻塞问题。 但是,MVCC机制也有一些限制。例如,如果事务A在读取某个数据行的同时,事务B修改了该行的值,那么事务A在提交时就会检测到该数据行已经被修改,从而回滚该操作。此外,MVCC机制也会占用更多的存储空间来存储旧版本的数据行。 ### 回答2: MySQLMVCC(多版本并发控制)是一种用于处理并发访问的机制。MVCC是通过在数据库的各种操作(如事务的开启、读取和写入)使用隐藏的时间戳来实现的。 MVCC的主要目标是避免读取和写入操作之间的冲突,从而提高数据库的并发性能和资源利用率。它通过在内部为每个事务提供一个唯一的时间戳来实现。每个事务在开始时都会获得一个时间戳,并且事务的每个操作都使用这个时间戳。 当一个事务读取数据时,它只能读取它开始时间之前的数据版本。这样可以避免读取到其他事务正在写入或修改的数据,从而保证读取操作的一致性和隔离性。 当一个事务写入数据时,它会创建一个新的数据版本,并将其与事务的时间戳关联。这个新版本的数据不会立即覆盖旧的数据,而是以一种类似于快照的方式存在。其他事务在读取数据时仍然可以访问旧版本的数据。 MVCC还使用了回滚段(undo log)来处理事务的回滚操作。当一个事务被回滚时,数据库会使用回滚段将所有该事务做出的修改逆转回去,从而恢复到事务开始之前的状态。 需要注意的是,MVCC机制对于并发性能和资源利用率的提升是有限的。在高并发的情况下,数据库可能会出现锁等待和资源竞争的问题。为了进一步优化并发性能,可以考虑使用其他技术,如乐观并发控制(Optimistic Concurrency Control)和分布式数据库。 ### 回答3: MySQLMVCC(Multi-Version Concurrency Control)机制是一种并发控制技术,用于处理数据库的读写冲突。它允许多个事务同时读取数据库,同时也使得读写冲突被有效地解决。 MVCC机制基于以下两个重要的概念:版本号和快照。 首先,每个表的每个行都有一个版本号。当一个事务对某行进行修改时,会为该事务创建一个新的版本,并将旧版本标记为过期。这样,读取该行的事务会读取到未过期的版本,而不会受到写用户的影响。同时,这也避免了仅读用户被阻塞的情况。 其次,为了实现读取未过期版本的行,MVCC机制通过创建快照来实现。快照是数据库在某个时间点的一个镜像,其包含了未过期的行版本。当一个读取事务开始时,会生成一个当前的数据库快照,并基于这个快照来读取数据行。这样,读取事务不会看到在其开始时(即快照生成时)已提交的写入事务,从而实现了读写并发。 MVCC机制对于提高数据库的并发性能非常重要。它允许多个事务同时进行读操作,提高了数据库的并发处理能力。此外,它也避免了读写冲突和阻塞的情况,提高了数据库的效率和稳定性。 总之,MySQLMVCC机制通过使用版本号和快照来实现读写并发控制和冲突的解决。它是提高数据库并发性能和减少阻塞的关键技术之一,并且在实际的数据库应用扮演着非常重要的角色。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值