数据库多版本并发控制-MVCC

版权声明:未经允许不得转载! https://blog.csdn.net/bingxuesiyang/article/details/89293129

MVCC的原理

MVCC(Multiversion Concurrency Control多版本并发控制):

  • MVCC每次更新操作都会复制一条新的记录,新纪录的创建时间为当前事务id
  • 优势为读不加锁,读写不冲突
  • InnoDb存储引擎中,每行数据包含了一些隐藏字段 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT
  • DATA_TRX_ID 字段记录了数据的创建和删除时间,这个时间指的是对数据进行操作的事务的id
  • DATA_ROLL_PTR 指向当前数据的undo log记录,回滚数据就是通过这个指针
  • DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在mysql进行数据的GC,清理历史版本数据的时候。

MVCC只在可重复读、已提交读两个隔离级别下工作。其它两个隔离界别都和MVCC不兼容,因为未提交读总是读取最新的数据行,而不是符合当前事物版本的数据行,而串行化则会对所有读取的行都加锁。

具体的DML:

  • INSERT:创建一条新数据,DB_TRX_ID中的创建时间为当前事务id,DB_ROLL_PT为NULL
  • UPDATE:将当前行的DB_TRX_ID中的删除时间设置为当前事务id,DELETE BIT设置为1
  • DELETE:复制了一行,新行的DB_TRX_ID中的创建时间为当前事务id,删除时间为空,DB_ROLL_PT指向了上一个版本的记录,事务提交后DB_ROLL_PT置为NULL

下面说到的例子中,事物A、B、C、D、E都是同时并发执行的事物。

1、查询举例

下面举例说明(下表并不代表MySQL真实的表结构就是如此仅仅是为了说明逻辑)。

ID name age DATA_TRX_ID (隐藏的列) DELETE BIT(隐藏) 其它隐藏咧
1 John 23 1 null null
2 Amy 22 1 null null
3 Jack 18 1 null null
           

表名:student假设原来表中有这样3条记录,现在有两个事物同时进行。

事物A(事物id=2)查询:Select * from student;

事物B(事物id=3):insert into student values (null,"Lily",17,);执行后的结果:

ID name age DATA_TRX_ID (隐藏的列) DELETE BIT(隐藏) 其它隐藏列
1 John 23 1 null null
2 Amy 22 1 null null
3 Jack 18 1 null null
4 Lily 17 3 null null

事物A:查询的结果

ID name age DATA_TRX_ID (隐藏的列) DELETE BIT(隐藏) 其它隐藏咧
1 John 23 1 null null
2 Amy 22 1 null null
3 Jack 18 1 null null

此时事物A查询的时候仅仅查询DATA_TRX_ID小与等于其自身事物id的记录,也就是DATA_TRX_ID <= 2的记录。这样即便事物B插入了一条新数据,事物A也查询不到,从而避免了幻读的出现。当然这种情况的避免幻读仅仅是针对普通的Select语句。

如果是修改操作或者select ... for update 是避免不了幻读的,需要使用其他机制来防止幻读。如果你想获取最近的被其它事物修改的记录,可以commit后再次查询或者使用update语句后再查询。(后面详细讲解这种情况)

2、删除举例

假如此时事物C、事物D分别进行查询和删除操作。

事物C(事物id=4)查询:Select * from student;

事物D(事物id=5):delete from student where id = 2;

在事物A、B、C并发操作的基础上,事物D操作后的结果:

ID name age DATA_TRX_ID (隐藏的列) DELETE BIT(隐藏) 其它隐藏列
1 John 23 1 null null
2 Amy 22 1 5 null
3 Jack 18 1 null null
4 Lily 17 3 null null

事物D的操作会在原来id=2的记录的DELETE BIT隐藏列上加上自己的事物id也就是5。

事物C查询到的记录:

ID name age DATA_TRX_ID (隐藏的列) DELETE BIT(隐藏) 其它隐藏列
1 John 23 1 null null
2 Amy 22 1 5 null
3 Jack 18 1 null null

此时事物C查询的时候仅仅查询DATA_TRX_ID <= 4,并且DELETE BIT >= 4的记录。此时事物C即便多次查询,得到的记录也是相同的。因为版本号的存在,事物D的删除操作不会对事物C的查询造成影响

此时事物A并发查询得到的记录也是相同的并不会被事物D的删除操作所影响。

3、更新举例

update原则:拷贝一份原来的记录作为新纪录,在新纪录上执行更新操作,并在原记录的DELETE BIT列上添加本事务id。

假如此时事物E并发进行更新 操作。

事物A(事物id=1)查询:Select * from student;

事物E(事物id=6):update student set age =19 where id = 3;

在原有事物A、B、C、D并发执行的基础上,事物E操作后的结果:

ID name age DATA_TRX_ID (隐藏的列) DELETE BIT(隐藏) 其它隐藏列
1 John 23 1 null null
2 Amy 22 1 5 null
3 Jack 18 1 6 null
3 Jack 19 1 null null
4 Lily 17 3 null null

此时,事物E会先拷贝一份原来主键id=3的记录,将原来的记录加上删除标记,也就是在DELETE BIT隐藏列上加上事物E的事物id也就是6。然后将新拷贝的age设置为19。

事物A再次查询到的结果:

ID name age DATA_TRX_ID (隐藏的列) DELETE BIT(隐藏) 其它隐藏列
1 John 23 1 null null
2 Amy 22 1 5 null
3 Jack 18 1 6 null

此时事物A查询到的结果和第一次查询并没有什么区别。这里不考虑隐藏列,因为我们平时查询到的记录里是不包含隐藏列的。

以上说到的例子中,事物A、B、C、D、E都是同时并发执行的事物。

----------------------------------------------------------------------------

如果事物A、B、C、D、E全都执行完毕并且commit了。那么此时再执行查询事物F就会查询到上面并发操作后的最新记录。

如何在并发中看到最新修改记录

那如果想在多个事物的并发中查询到其它事物的修改该怎么办呢?

使用update或者select ... for update,但是使用这两种操作虽然能看到最新的其他事物修改的记录,但是此时幻读也就出现了。那么怎么又能看到最新的记录,而且又可以避免幻读呢?

下面是笔者翻译的MySQL的官方文档介绍,专门描述这种情况

官网描述
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. For example, you 
might encounter a situation like the following:

上面的意思就是:

数据库的快照被应用于Select语句,对DML语句也就是增删改来说是没有必要的,如果你删除或者修改了一些记录
然后commit,但是此时另一个事物也在并发的执行删除更新操作,那么这个并发执行的事物会影响到刚才修改的
记录,即使当前的事物不能查询到这个并发事务所做的修改。如果一个事物执行更新删除操作,同时这个操作被另一个不同的事物执行,
那么所做这些修改相互之间是可见的。

SELECT COUNT(c1) FROM t1 WHERE c1 = 'xyz';
-- Returns 0: no rows match.
DELETE FROM t1 WHERE c1 = 'xyz';
-- Deletes several rows recently committed by other transaction.

SELECT COUNT(c2) FROM t1 WHERE c2 = 'abc';
-- Returns 0: no rows match.
UPDATE t1 SET c2 = 'cba' WHERE c2 = 'abc';
-- Affects 10 rows: another txn just committed 10 rows with 'abc' values.
SELECT COUNT(c2) FROM t1 WHERE c2 = 'cba';
-- Returns 10: this txn can now see the rows it just updated.

官网直译的话感觉还是有点别扭,我自己的理解是:

如果两个事物都进行修改操作,那么其中一个事物的修改时还有查询操作,不会查询到另一个事物的修改,因为有数据库快照。但是如果都修改时就不是这样了,虽然查询不到但是修改的时候是能同时看到并影响对方的。

如果在查询的时候想并发的看到对方修改的记录,使用下面的语句。

SELECT * FROM child WHERE id > 100 FOR UPDATE;

但是这回带来幻读。

官网的针对幻读的解释:

 下面笔者使用 ‘意译’ 翻译成人话(因为使用直译的话听起来别扭,‘意译’ 更贴合我们汉语的思维方式):

所谓的幻读发生在同一个查询,执行多次查询到的结果集却不一致。例如,如果一个Select执行两次,但是第二次返回了在第一次查询时没有查到的新纪录,这一行新纪录就是幻读纪录。

假设在child表上有一个索引在id的列上,你想要在读取的时候锁定id大于100的记录,这样做的原因是你接下来想更新某些大于100的记录。

这个查询从id大于100的记录开始扫描索引,假设我们查询的记录id在90和102之间,如果锁锁住90~102的记录,但是记录的间隙之间是插入的(笔者的理解:间隙应该就是不能在两条相邻的记录之间插入记录的意思,两条相邻的记录中间的部分就叫间隙,就像夹缝一样可以插入其它东西。间隙锁就是把这个夹缝给锁上,不让插入东西)。如果一个事物此时插入一条新记录id=101,你使用之前同样的查询SQL就会返回刚才新插入的这条记录(称为幻读),这种幻读现象就会违反“一个事物运行期间,已经被读到的数据不应该改变”的原则。

为了阻止幻读,InnoDB使用了一个融合了行锁和间隙锁的next-key。当搜索或者扫描表的索引的时候,InnoDB使用共享锁或者排它锁。行锁实际上就是索引记录锁,一个next-key在索引记录上加锁同时也会锁住这个记录前的间隙(就是间隙锁的意思),因此一个next-key锁=行锁+间隙锁,如果一个事物在一条数据库记录上添加了共享锁或者排它锁,另一个事物在这个事物返回前是不能在间隙处立即插入新纪录的。

当InnoDB扫描一个索引的时候,它也能在最后一条记录的后面加间隙锁(笔者的理解:既然不让在两条记录之间插入,那么最后一条记录后面是不是能插入?答案是最后一条记录的后面也加上了间隙锁不让插入)。还是之前的例子,为了阻止id>100范围内的任何插入,不光会锁住id为100和102这两条记录,同时原来没有的id为101这条记录也会被锁定。

你能使用next-key实现唯一性的查询(笔者的理解:同一时间相关资源只能有一个事物使用,其它事物阻塞等待),如果你以共享锁的模式读取记录,并且期间没有发现和你要插入的记录一模一样的记录,那么你就可以放心的插入了。因为在读取数据期间,next-key会锁住在你操作的记录后所有的其他行,阻止任何其他和你要插入的数据一模一样的插入操作。next-key能锁住一些本来不存在的记录(笔者的理解:假如这张表的最大id=102,那么105这条原来不存在的记录也会被锁住,其他事物不能插入id=105的记录)

参考:

  1. https://www.jianshu.com/p/47e6b959a66e
  2. https://www.cnblogs.com/yxy-ngu/p/8134979.html
  3. https://blog.csdn.net/whoamiyang/article/details/51901888
  4. https://dev.mysql.com/doc/refman/8.0/en/innodb-next-key-locking.html
  5. https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html

猜你喜欢

转载自blog.csdn.net/bingxuesiyang/article/details/89293129