为什么需要MVCC?
MVCC(multi versionong concurrent control:多版本并发控制),主要是发生在分布式事务上。维特事务隔离级别的数据一致性要求。
事务隔离级别
事务是Innodb存储引擎所特有的,它的隔离级别有四种:
读未提交(Read Uncommit):一个事务可以读取到另一个事务修改但未提交的数据;
读已提交(Read Commit,RC):一个事务只能读取另一个事务修改且提交的数据;
可重复读(Repeat Read:RR):一个事务多次读取的数据都是一致的(默认隔离级别)
串行化(Serial Read):所有事务都需要串行化执行(排队执行,类似于synchronized)。
事务隔离级别带来的数据一致性问题
脏读 | 不可重复读 | 幻读 | |
读未提交 | ✔ | ✔ | ✔ |
读已提交 | ❌ | ✔ | ✔ |
可重复读 | ❌ | ❌ | ✔ |
串行化 | ❌ | ❌ | ❌ |
脏读:
原始数据id为1的name为李四;
事务A修改id为1的name属性为张三(未提交);
事务B读取到事务A修改且未提交的数据,name为张三;
事务A在提交之前发生问题,导致事务A回滚,id为1的name属性仍然为李四。
称这种情况为脏读。
原始数据:{id:1, name:李四} | |
事务A:update user set name='张三' where id = 1 uncommit; | |
事务B:select name from user where id = 1 结果:{张三} | |
事务A提交之前中途发生网络问题,事务A回滚,id为1的name为李四 |
不可重复读:
原始数据id为1的name为李四;
事务A修改id为1的name属性为张三(提交);
事务B读取id为1的name属性为张三,读完之后事务A又修改id为1的name属性为王五(提交);
事务B读取id为1的name属性为王五。
事务内数据前后读取不一致的问题称为不可重复读。
原始数据:{id:1, name:李四} | |
事务A:update user set name='张三' where id = 1 commit; | |
事务B:select name from user where id = 1 结果:{张三} 事务B:select name from user where id = 1 结果:{王五} | 事务A:update user set name='王五' where id = 1 commit; |
幻读:
原始数据name为李四只有一条;
事务A读取name为李四有一条id为1的数据,此时事务B插入另一条name为李四且id为2的数据(提交);
事务A再次读取name为李四的数据变成两条。
这种前后读取数据条数不一致的情况称为幻读(幻读针对数据的插入,不可重复读针对数据的修改)
原始数据:{id:1, name:李四} | |
事务A:select * from user where name = 李四 结果:{id:1, name:李四} 事务A:select * from user where name = 李四 结果: {id:1, name:李四} {id:2, name:李四} | 事务B:insert into user (id, name) values (2, 李四); commit; |
如何解决不同隔离级别带来的数据一致性问题
MySQL利用锁和MVCC机制来解决这个问题。
对于脏读的情况,MySQL使用读(也称共享锁)、写(也称排他锁)锁来进行控制。
例如上面表格描述的脏读的情况,在事务A修改数据前加上写锁,在提交后释放锁。其他事务想要读/写都需要等待锁的释放,这样就不会读取到脏数据。
对于不可重复读、幻读的情况,时使用MVCC机制来进行控制的。
MVCC解决不可重复读和避免幻读
MVCC=ReadView+undo_log,MVCC机制其实就是靠的ReadView和undo_log。
ReadView四个字段
m_ids:记录创建ReadView时的活跃事务ID(活跃事务:修改但未提交的事务)
m_low_limit_id:活跃事务ID的最小ID
m_up_limit_id:活跃事务ID的最大ID+1(也就是下一个产生的活跃事务ID)
m_create_trx_id:创建ReadView的事务ID
undo_log两个关键字段
每个数据行都会存在以下两个关键字段(undo_log的具体实现):
DB_TRX_ID:当前数据行所从属的事务ID
DB_ROLL_PTR:上一个版本的事务ID指针(回滚到上一个版本数据)
RC和RR情况下MVCC情况
RC(读已提交):在每次读之前生成ReadView
图中,事务102、104分别对id为1的name属性进行修改,但是事务都未提交;
事务103在事务102和事务104之间执行了一条select操作(查询user表中id为1的name属性),此时103在m_ids(102-104)之间,所以事务102、事务104对事务103的查询的数据都不可见,因此找到m_ids的最小事务版本(m_up_limit_id)进行回滚操作,回滚后到达事务101(这个不在m_ids范围内),对事务103的select操作数据可见,因此事务103查询到的name属性为张三。
下一个select操作也是再次生成ReadView视图。
RR(可重复度):在第一次读之前生成ReadView
RR是在事务第一次读的时候生成ReadView,然后一直复用这个ReadView,而不是每次读都生成新的ReadView,因此它可以保证多次读的结果都相同。
解决幻读
幻读需要锁与MVCC相结合,也即是在创建视图的同时,添加next-key-lock(对影响到的数据区间进行枷锁),因此不可以在该视图影响的数据区域间进行更新或插入操作,可以打到解决幻读的情况。这种情况对性能有一定的要求,一般情况我们只需要到可重复读的隔离级别就可以了。