多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,在mysql的innodb中,在读已提交和可重复读取隔离级别会使用mvcc来提升并发。
先说答案,可重复读没有完美的解决幻读,对于select(快照读)不会产生幻读,但对于update(当前读)会产生幻读,下面通过案例进行分析。
测试:
准备工作:
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`book_name` varchar(256) COLLATE utf8_bin NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
insert into book values (1, 'java');
//设置read uncommitted级别:
set session transaction isolation level read uncommitted;
//设置read committed级别:
set session transaction isolation level read committed;
//设置repeatable read级别:
set session transaction isolation level repeatable read;
//设置serializable级别:
set session transaction isolation level serializable;
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set, 1 warning (0.00 sec)
测试1
事务1 | 事务2 |
---|---|
begin | begin |
select * from book; | \ |
\ | insert into book values (2, ‘python’); |
\ | commit |
select * from book; | \ |
commit | \ |
事务1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from book;
+----+-----------+
| id | book_name |
+----+-----------+
| 1 | java |
+----+-----------+
1 row in set (0.00 sec)
mysql> select * from book;
+----+-----------+
| id | book_name |
+----+-----------+
| 1 | java |
+----+-----------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
事务2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into book values (2, 'python');
Query OK, 1 row affected (0.01 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
根据上面的流程执行,预期来说应该是事物1的第一条select查询出一条数据,第二个select查询出两条数据(包含事物2提交的数据)。
但是在实际测试中发现第二条select实际上也只查询处理到一条数据。
从上面的测试结果来看,貌似在MySQL中通过MVCC就解决了幻读的问题,那既然这样串行化读貌似就没啥意义了,带着疑问继续测试。
测试2
测试前数据:
mysql> select * from book;
+----+-----------+
| id | book_name |
+----+-----------+
| 1 | java |
+----+-----------+
事务1 | 事务2 |
---|---|
begin | begin |
select * from book; | \ |
\ | insert into book values (2, “go”); |
\ | commit |
update book set book_name = “python”(工作中如果不想被辞退一定要写where条件) | \ |
commit | \ |
事务1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from book;
+----+-----------+
| id | book_name |
+----+-----------+
| 1 | java |
+----+-----------+
1 row in set (0.00 sec)
mysql> update book set book_name = "python";
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2 Changed: 2 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
事务2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into book values (2, "go");
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
根据上面的结果我们期望的结果是这样的:
+----+-----------+
| id | book_name |
+----+-----------+
| 1 | python |
+----+-----------+
| 2 | go |
+----+-----------+
但是实际上我们的经过是:
+----+-----------+
| id | book_name |
+----+-----------+
| 1 | python |
| 2 | python |
+----+-----------+
本来我们希望得到的结果只是第一条数据的book_name改为python,但是结果却是两条数据都被修改了。这种结果告诉我们其实在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决时不彻底的。
备注
可以通过开启两个mysql client 来模拟以上测试
在学习MVCC多版本并发控制之前,我们必须先了解一下,什么是MySQL InnoDB下的当前读和快照读?
像select lock in share mode(共享锁), select for update(排他锁); update, insert ,delete这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。所以我们先来看看这个三个point的概念
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
undo日志
undo log主要分为两种:
对MVCC有帮助的实质是update undo log
Read View(读视图)
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
很明显可重复读的隔离级别没有办法彻底的解决幻读的问题,如果我们的项目中需要解决幻读的话也有两个办法:
1.使用串行化读的隔离级别
2.MVCC + 间隙锁
mysql的间隙所是基于索引的,对于唯一索引innode会把间隙所降级为行锁,非唯一索引的话就需要用到间隙锁(也叫范围锁)
id | number |
---|---|
1 | 1 |
2 | 3 |
13 | 3 |
23 | 3 |
31 | 11 |
40 | 40 |
事务一:select * from test where number = 3 for update
对于number索引可以分为多个范围
(无穷小,1)(1,3)(3,3)(3,11)(11,40)(40,无穷大)
这时候锁住的是(3,3)区间,对应的临界记录是(id=1,number=1)(id=31,number=11),对于这范围内的数据都是被锁住的。
事务二:insert into test(id, number) value(5, 3) //是会被阻塞
事务三:insert into test(id, number) value(25, 4) //也是会被阻塞
事务四:insert into test(id, number) value(35, 4) //也是会被阻塞
事务五:insert into test(id, number) value(22, 12) //插入成功 (因为12>11所以在锁区间外)
事务六:insert into test(id, number) value(71, 11) //插入成功 (number值一样,但是id71>31所以在锁区间外)
BEGIN 语句并没有开启一个事务,实际上是对数据进行了增删改查等操作后才开启了一个事务。
引用:
1.https://www.cnblogs.com/liyus/p/10556563.html
2.https://zhuanlan.zhihu.com/p/64576887
3.https://blog.csdn.net/SnailMann/article/details/94724197