在讲MVCC之前,当然得知道MVCC解决了什么问题,MVCC(Multi-Version Concurrency Control:多版本并发控制)主要解决的是多个事务并发引起的问题
关于数据库的多事务并发问题,这就是老生常谈的问题了,我这里稍微带一下:
- 脏写:事务B修改了事务A改过的值,此时事务A未提交,如果事务A最终回滚的话,那么事务B修改的值也就没了
- 脏读:事务B查询了事务A修改过的数据,但是此时事务A还没提交,如果事务A回滚事务的话就会造成事务B再次查询就读不到刚才事务A修改的数据了
- 不可重复读:事务A查询一条数据,事务B修改了这条数据并提交事务,此时事务A再次查询的时候发现数据就不一样了
- 幻读:事务A查询了很多条数据,事务B增删了这些数据,事务A再查询的时候,发现查询出来的数据不一样了
其中不可重复读和幻读看起来很相似,他们的区别是,不可重复读主要针对的是update操作,而幻读针对的是insert和delete操作
那么为了解决上面说的四个问题,就有了SQL标准(非MySQL)中规定的四种事务隔离级别
- read uncommitted(读未提交) :解决脏写
- read committed(读已提交) :解决脏读
- repeatable read(可重复读):解决不可重复读
- serializable(串行化):解决幻读
MySQL中的RR(repeatable read)级别就可以解决上面所说的四种问题,这里面依靠的就是MVCC,不过在这之前,还有一个概念,就是undo log版本链(undo log我之前的博客中有写过,点这里)
其实每条数据都有两个隐藏字段,一个是trx_id,代表最近最近一次更新这条数据的事务id,另一个字段是roll_pointer,这个字段指向了这个实际undo log的回滚日志
举个例子,假如有一个事务A(事务id为1)插入了一条数据,此时就会生成一条undo log,但是他的roll_pointer不会指向任何undo log,因为这是第一条数据,接下来有一个事务B(事务id为2),修改了这条数据,就会生成一条新的undo log,然后这个undo log的roll_pointer就会指向事务A生成的那条undo log,接着又有一个事务C(事务id为3)也修改了这条数据,会跟事务B做一样的事情,事务C的undo log的roll_pointer会指向事务B的undo log,这样就形成了一条undo log版本链,如下图所示
现在我们已经了解了undo log版本链,有了这个基础之后,就可以使用undo log版本链来实现ReadView机制,ReadView机制主要包含四个东西:
- m_ids:此时有哪些事务在MySQL中执行还没提交
- min_trx_id:m_ids里最小的值
- max_trx_id:MySQL下一个要生成的事务id,就是最大事务id
- creator_trx_id:当前你这个事务的id
扯了这么多,那么MVCC和ReadView机制又有什么关联呢?之前我们讲过了MySQL的RR隔离级别就可以解决脏写、脏读、不可重复读、幻读这几个问题,那么是如何实现的呢?
其实MVCC就是依赖ReadView机制来解决多事务并发处理问题的,下面举个例子来说:
我们假设有两个事务A(事务id为2)、B(事务id为3)并发执行,现在数据库已经由事务id为1的事务生成了一条数据(值为10),事务A发起一个查询请求,此时生成了一个ReadView,如图所示
那么此时事务A能否看到这条数据呢?答案是可以的,因为在ReadView中的min_trx_id是2,比undo log版本链中的trx_id要大,这说明了此时数据库中的数据是在事务2执行之前就生成了的
现在假设并发并发执行的事务B执行了一条update操作,那么按照我们上面说的undo log版本链机制,此时会新生成一条undo log,指向trx_id为1的这条undo log
现在我们假设事务A再次进行查询,会查询到事务B修改的值吗?答案是不会,这里的关键点在于事务A的ReadView一旦生成,就不会再改变了,所以我们再来看事务A的ReadView,里面的m_ids包含了2和3,说明事务A和事务B是同时运行的,而且事务B修改形成的undo log中的trx_id为3,所以此时事务A就会顺着版本链往下找到trx_id为1的这条数据,也就是不在并发事务中的这条数据
接下来我们假设另外一个场景,事务A使用的是范围查询,只要值大于等于10都会被查出,事务A和事务B还是按照上面的场景运行,接下来有一个事务C(事务id为4),往数据里面添加了一条数据,如下图所示
那么此时事务A在执行一次查询的话,能查询到事务C增加的这条数据吗?答案是不能,因为事务C的事务id大于等于了事务A生成的ReadView中的max_trx_id,这就说明事务C是在事务A执行之后才执行的,所以事务A是不能查询到事务C的执行结果的,只能顺着undo log版本链继续往下寻找
通过undo log版本链和ReadView机制,就能实现MVCC,从而解决事务并发执行造成的各种问题