一文解决MySQL事务并发问题和事务隔离级别及MVCC原理

事务并发产生的一致性问题

脏写

如果一个事务修改了另一个未提交事务的修改过的数据,则称发生了脏写现象

举个例子:
假设数据库中有一个数据项为x=2;
事务A修改了x=1;此时事务A并没有提交,事务B此时也来修改数据项x=0;此时事务A,B提交
事务B修改了事务A未提交修改过的数据,所以发生了脏写的现象

脏读

如果一个事务读取了另一个事务未提交的数据,则称发生了脏读现象
在这里插入图片描述

不可重复读

如果一个事务A读取了x的值,然后事务B又修改了x的值并且提交,事务A再次读取x的值时,则会与第一次x的值不同,则称发生了不可重复读(也就是两次读的结果不一样)
注意:如果事务发生了update更新值,delete删除记录,则可以把这种现象叫做不可重复读
在这里插入图片描述

幻读

当一个事务A读取符合搜索条件的记录数为x,此时另一个事务B往符合条件里插入了一条行数据并提交,此时事务A再去读符合条件的记录数时,则跟原来的不一样,则称发生了幻读
注意:如果事务发生了insert插入符合条件的记录,或则是update修改了记录的键值导致插入符合记录数里,则可以把这种现象叫做幻读
在这里插入图片描述

SQL标准对这里的不可重复读和幻读有点描述不清,我们一般认为:

  • 幻读强调的是在后读取到了之前没有读取到的数据,这个没有读取到的数据,可能是insert或则update插入的
  • 不可重复读的话,则是updata和delete

SQL标准中的四种隔离级别

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPETABLE READ 不可能 不可能 可能
SERIALIZABLE 不可能 不可能 不可能

MySQL支持的四种隔离级别

oralce支持两种隔离级别:read committed和serializable
mysql支持虽然支持四种隔离级别,但是mysql的repeatable read隔离级别不允许幻读

MVCC原理

实现MVCC关键:隐藏字段,undo log,ReadView

版本链

对于聚簇索引记录来讲,每个行格式都包含了两个必须的隐藏列

  • row_id不是必须的,只有在没有主键或者没有非空的unique字段时,才会有row_id;
  • trx_id事务id是必须有的;一个事务在对记录进行修改后,都会将该事务的id写入记录的trx_id中
  • roll_point回滚指针,指向上一个版本的记录,相当于一个指针
    在这里插入图片描述

这里insert log日志在只在事务回滚时起作用,事务提交后该日志文件就被系统回收了(可能是被重用或则释放了)。
虽然insert undo日志被真正回收了,但是回滚指针的值并没有清除,如果roll_point第一个字节是1的话,表示它是一个insert undo日志

现在假设我们的数据库中发生了如下事务:
在这里插入图片描述
那么对应的版本链则如下图:
在这里插入图片描述
每次事务更新该行时,都会将事务id写入trx_id中。将旧记录写入undo 日志中,每条undo日志中也会有roll_point属性,其他字段只包含索引列和更新的列(insert的undo日志中没有该属性,因为insert没有旧版本)。这些undo日志通过roll_point形成一个版本链。

我们后面会通过该记录的版本链来控制并发事务可以访问到的记录,我们把这种机制叫做MVCC多版本并发控制

注意:这里undo日志并不是全部字段都有,只会包含一些索引列和被更新的字段
比如我们上面的class字段,undo日志里是没有记录这个字段的,因为它并没有被更新,如果该版本没有这个字段,说明它是和上一个版本字段值相同

ReadView

使用uncommitted read隔离级别时,由于可以读到未提交的值,所以直接读取最新版本的值即可。使用可串行化隔离级别时,使用加锁来访问记录。

对于读已提交和不可重复读这两个隔离级别时,由于不能读取未提交事务的值,所以我们必须控制它可以读取的记录,故而提出了ReadView的概念

ReadView主要包含以下四个重要的内容:
m_ids:在生成readview时,当前活跃的事务id
min_trx_id:当前活跃事务中,id最小的值
max_trx_id:表示生成该readview时,分配给下一个事务的事务id

注意:max_trx_id并不是m_ids中的最大值,事务id是递增分配的。

  • 比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。
    那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4

creat_trx_id:创建该readview的事务id

只有执行insert,update,delete操作时,才会给它分配事务唯一id,执行select操作不会给它分配事务id,默认都为0

有了这个readview和版本链后,我们就可以根据这些判断该记录是否可见了:

  • 如果记录中的trx_id和readview中的create_trx_id值相同时,则说明这个记录是该事务修改的,则表示可以读取
  • 如果记录中的trx_id小于readview中的min_trx_id值,则说明生成该readview时,修改该记录的事务已经提交,可以被读取
  • 如果记录中的trx_id大于readview中max_trx_id值,则说明生成该readview时,修改该记录的事务还没有创建出来,说明它是不可被读取的
  • 如果记录汇总的trx_id小于readview的最大值事务id,大于readview的最小值事务id。则查看它是否则活跃事务列表里,如果在的话,说明生成该readview时,该事务还没有被提交,则不可读取该记录。否则说明已提交,可以读取

如果该版本对该事务不可见,则顺着版本链往下找,直到记录可见。如果到最后都没有可见的记录,则说明该记录不可见,不能包含到结果集中

读已提交和可重复读:最大的区别在于生成readview的时机不一样
因为读已提交是不能解决不可重复读的,所以一个事务中两次读取的记录会不一样,所以每执行一个select操作都会生成一个readview。

而可重复读在一个事务中两次读取的记录必须是一样的,所以在该隔离级别下只有第一次select操作会生成一个readview,后续的select操作会复用这个readview

二级索引与MVCC

前面我们讲的行记录中都是包含trx_id和roll_point隐藏列的,那么二级索引是没有隐藏列的,那么我们通过二级索引查找记录时,如何判断它的可见性呢???

比如下面这个事务
begin;
select name from user where name=‘刘备’;

此时我们的查询优化器决定了要到二级索引中查找name为‘刘备’的记录,那么该如何判断该记录可见呢??可分为以下两步:

  1. 二级索引页面的Page Header部分有一个名为PAGE_MAX_TRX_ID的属性,每次对该页面做增删改操作时,判断该事务的id是否大于PAGE_MAX_TRX_ID的值,如果大于,则把PAGE_MAX_TRX_ID的值更新为事务id值。那么我们的事务在select查询时,如果readview中的最小事务id大于PAGE_MAX_TRX_ID的值,则说明最后修改该页面的事务早就提交了,此页面全部可以访问。否则的话,就需要进行第二部操作,回表
  2. 通过name找到聚簇索引的记录,然后通过前面的方式找到可见的第一个版本,如果该版本的name符合‘刘备’,则把该记录返回。否则查找下一个记录(不是查找下一个版本哦)

补充:关于purge

我们知道在insert操作时,事务提交后就可以释放掉了,而进行delete操作和更新键值的updata操作时,并不会立即删除记录,而是给该记录打一个删除标记,其主要就是为了给MVCC服务。那么总不可能一直不删除记录吧!!那存储空间不得爆了!!

处理这些记录的操作我们叫做purge操作,主要的问题就是什么时候处理???
简单一句话:当系统中最早产生的readview不再访问他们了,那么就可以清理了。那么什么时候才表示肯定不再访问它们了呢?只要在生成readview时,保证某个事务已经提交,就说明该readview不会访问该事务的updata undo日志了
详见《MySQL是怎样运行的》P398

猜你喜欢

转载自blog.csdn.net/small_engineer/article/details/124252956