MySql事务实现的机制:MVCC
这一篇将简单说明一下最近学习了Mysql的事务实现的简单理解。
如果存在一张A表,id=1,name=2,此时,存在多个事务对该表进行处理,那么会怎样呢?
假设我们会存在多个事务同时进行,
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
1 | begin | begin | begin | begin | Begin |
2 | Update A set name='name2' where id=1 | ||||
3 | Update A set name='name3' where id=1 | ||||
4 | Update A set name='name4' where id=1 | ||||
5 | Commit | ||||
6 | Select name from A where id = 1 | ||||
7 | |||||
8 | |||||
9 |
首先运行第2行sql
当程序运行到第2行的时候,由于事务1对A表进行了更新操作,此时会产生一个事务id,我们假设该id的值为2。其中事务id是一个自增长的id,仅当事务中进行了更新操作的时候生成,同时该事务id整个数据库共用。
此时,mysql在一个叫做undo的回滚日志栈中存在着一条旧有的数据,在更新操作执行的时候,会押入当前更新的数据,同时记录下当前的事务id,并且产生一条指向前面押入的旧有数据的回滚指针。
即此时,回滚日志栈的结构如下
同理,我们接下来运行第3行sql
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
1 | begin | begin | begin | begin | Begin |
2 | Update A set name='name2' where id=1 | ||||
3 | Update A set name='name3' where id=1 |
由于事务3运行了新的更新语句,一个新的事务id3生成,并且押入了undo中,即此时结构如下
同理,运行第4,5行
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
1 | begin | begin | begin | begin | Begin |
2 | Update A set name='name2' where id=1 | ||||
3 | Update A set name='name3' where id=1 | ||||
4 | Update A set name='name4' where id=1 | ||||
5 | Commit |
之后结构如下
运行第6行
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
1 | begin | begin | begin | begin | Begin |
2 | Update A set name='name2' where id=1 | ||||
3 | Update A set name='name3' where id=1 | ||||
4 | Update A set name='name4' where id=1 | ||||
5 | Commit | ||||
6 | Select name from A where id = 1 |
我们可以看到,这一行的操作是查询操作。对于该查询操作,我们如何保证该操作的正确性?
当第6行执行查询操作的时候,会生成一个叫做read-view的一致性视图,该视图的结构如下
- [min_id,min_id+1,...]-max_id
如上图所示,该视图的结构由一个id数组和一个id组成;
其中,数组包含了所有未提交的事务的id,而其中min_id是未提交的最小事务id。max_id则是已经创建的最大事务id,该id不关心是否已经提交。
那么,结合我们的事务的执行情况,则第六行生成的read-view为[2,3]-4。
接下来我们看图
根据min_id和max_id的区分,我们将每个事务id(trx_id)区分为;
- 已经提交的事务id;(trx_id
- 未提交或者可能提交了事务id;(min_id<=trx_id<=max_id)
- 当前查询语句之后才开始的事务id(max_id
然后,通过使用生成的read-view,从undo栈的栈顶开始逐行查询,之后通过以下规则鉴定可见性,如果当前查询行的数据可见,将返回对应的结果。
一、 查询行的版本号如果小于min_id,即为已经提交的事务(trx_id
结合上面的规则,我们来看这条查询语句:
- 语句执行后,read-view=[2,3]-4
- 查询栈顶,该行的版本号为4,即属于第三种情况,并且该事务已经提交,不在数组中,可见,返回结果:name4
运行第7,8行
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
1 | begin | begin | begin | begin | Begin |
2 | Update A set name='name2' where id=1 | ||||
3 | Update A set name='name3' where id=1 | ||||
4 | Update A set name='name4' where id=1 | ||||
5 | Commit | ||||
6 | Select name from A where id = 1 | ||||
7 | Update A set name='name2-1' where id=1 | ||||
8 | Update A set name='name2-2' where id=1 |
此时undo结构如下
运行行数9
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
1 | begin | begin | begin | begin | Begin |
2 | Update A set name='name2' where id=1 | ||||
3 | Update A set name='name3' where id=1 | ||||
4 | Update A set name='name4' where id=1 | ||||
5 | Commit | ||||
6 | Select name from A where id = 1 | ||||
7 | Update A set name='name2-1' where id=1 | ||||
8 | Update A set name='name2-2' where id=1 | ||||
9 | Select name from A where id = 1 |
第9行我们在查询1中,运行了新的查询语句,我们来看这条查询语句
- 语句执行后,read-view=[2,3]-4
- 从栈顶开始查询,该行的版本号为2,则属于第三种情况,同时存在于数组中,即该行数据不可见
- 继续查询下一行,结果同2
- 继续查询下一行,版本号为4,不存在于数组中,可见,返回结果name4
运行第10,11,12行
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
7 | Update A set name='name2-1' where id=1 | ||||
8 | Update A set name='name2-2' where id=1 | ||||
9 | Select name from A where id = 1 | ||||
10 | commit; | Update A set name='name3-1' where id=1 | |||
11 | Update A set name='name3-2' where id=1 | ||||
12 |
这行我们提交了事务2,而且事务3进行了两次更新,此时数据格式如下:
运行第11行
0 | 事务2 | 事务3 | 事务4 | 查询1 | 查询2 |
---|---|---|---|---|---|
7 | Update A set name='name2-1' where id=1 | ||||
8 | Update A set name='name2-2' where id=1 | ||||
9 | Select name from A where id = 1 | ||||
10 | commit; | ||||
Select name from A where id = 1 | Select name from A where id = 1 |
这一行我们将查询1和查询2放置在一起进行比较。
首先我们看查询2的语句:
- 查询语句生成read-view=[3]-4
- 从栈顶开始查询,该行的版本号为3,则属于第三种情况,同时存在于数组中,即该行数据不可见
- 继续查询下一行,结果同2
- 继续查询下一行,版本号为2,不存在于数组中,可见,返回结果name2-1
接下来,我们看查询1的查询情况:
如上图所示,mysql的事务分为可重复读,与不可重复读的情况。
如果是重复读的情况,每次查询将使用第一次生成的read-view;而如果是不可重复读的情况,那么每次查询都将使用新的read-view。即如果是可重复,处理情况如下
- 查询语句生成read-view=[2,3]-4
- 从栈顶开始查询,该行的版本号为3,则属于第三种情况,同时存在于数组中,即该行数据不可见
- 继续查询下一行,结果同3
- 继续查询下一行,版本号为2,则属于第三种情况,同时存在于数组中,即该行数据不可见
- 继续查询下一行,结果同4
- 继续查询下一行,版本号为4,即属于第三种情况,并且不存在于数组中,即该行数据可见,返回数据为name4。
由上面的情况即可以发现,两个查询事件就算同一时间查询,结果也是可能不同的。
对于查询的版本号小于min_id和大于min_id的情况,相对简单这里就不做讨论了。
而删除操作,在mysql中视作一种特殊的更新操作,会复制原来的数据,并更新状态为已删除,然后压入undo栈中。