文章目录
- 再次理解MySQL事务
- 一、MVCC机制
- 数据库并发的场景有三种:
- 3个记录隐藏列字段
- undo日志——由mysql维护的一段内存空间
- 再次理解隔离性和隔离级别
- Read View 理论部分
- RR 和 RC 的本质区别
再次理解MySQL事务
- 1.每个事务都有自己的事务ID,根据事务的大小,进行排队。
- 2.既然事务能排队,那就不能简单地认为事务就是一堆sql语句的集合,事务在语言看来,就是一个结构体对象/类对象。
- 3.mysql可能会面临处理多个事务的情况,事务也有自己的生命周期,所以mysql要对多个事务进行管理,那就需要先描述,再组织。
一、MVCC机制
数据库并发的场景有三种:
读-读 :不存在任何问题,也不需要并发控制
读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读(重点)
写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失(后面补充)
重点在于第二类问题——读写。
要解决读写问题,就要学习MVCC机制,要学习MVCC机制,就要了解三个知识点:
- 3个记录隐藏字段
- undo日志
- Read View
3个记录隐藏列字段
- 1.DB_TRX_ID:占6字节,记录创建这个记录/最近修改这个记录的事务ID。
- 2.DB_ROLL_PTR:7字节,回滚指针,跟回滚有关的。指向这条记录的上一个版本。
- 3.DB_ROW——ID:6字节,这是一个隐藏的自增主键,如果表中没有创建主键,InnoDB会以该隐藏字段,产生一个聚簇索引。(就是建立一棵B+树,叶子节点与数据一起放的)
举例:
创建一个表,并插入一条数据:
create table if not exists student(
name varchar(11) not null,
age int not null);
mysql> insert into student (name, age) values (‘张三’, 28);
并且查看变量’autocommit’,这是一直开着的,所以,insert 这条插入sql语句,一定会被看成一个事务,自动提交了。
所以,看到的table,不仅仅是name,age两列,还有三个隐藏的字段:
因为insert语句也被看成了一个事务!
undo日志——由mysql维护的一段内存空间
这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。
模拟MVCC的过程:
在上表的基础上,现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成
name(李四)。
具体过程如下:
1.加锁
2.将修改前的整个记录,先拷贝一份到undo日志中,再将相关数据进行修改,结果如下:
回滚指针指向的是默认的上一条历史版本。
事务10的update语句完成后,就会释放锁。
现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)。
注意:这里修改,一定不会是修改历史版本的记录,因为历史版本记录是稳定的,持久的,改的是最新版本的事务。
1.加锁
2.将现存的事务先拷贝一份到undo日志中,再更新相关数据。
3.释放锁。
如上,就完成了数据的更新,同时形成了一份历史版本链。
这就是对历史事务进行管理的提现:先描述再组织,这个组织的过程就用了链表。
注意一些细节问题:
- 1.如果一个事务commit后,就无法回滚了。要知道,undo log只是一块内存缓冲区,会被写满,每次update/delete数据,都会保存一份历史版本,一旦commit后,就会把undo log的空间释放。
- 2.对于比较特殊的insert操作,因为是新插入的操作,那么在此之前不会形成版本链,但是为了回滚操作,insert的数据也会被拷贝到undo log中,所以一旦commit之后,undo log就会被释放,也就是insert也不会回滚了。
再次理解隔离性和隔离级别
正因为有了undo log这样的缓冲区来保存历史版本数据。
才有了读写并发访问事务,不需要加锁。因为读的是历史版本事务,写的是最新数据,读写的版本位置不同就不需要加锁。所以就是隔离性,针对隔离级别,就有RU,RC,RR,串行化等几种隔离级别,本质上就是是否允许同时读写历史版本的不同位置。
那么,如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?
Read View 理论部分
现在的问题就是,当前快照读,应不应该读到当前版本记录,也就是应不应该读到历史版本链的事务。
Read View就是一个视图,在MySQL源码中,这是一个类,当我们查看历史版本时,就会生成这个Read View。具体只需要记住该类的四个成员即可:
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错,注意不是视图里面的最大事务ID+1)
creator_trx_id //创建该ReadView的事务ID
注意:不是创建事务的时候,就形成这个Read View类,而是该事务已经存在的前提下,该事务第一次进行快照读(查看历史版本)的时候,就会形成这个Read View类。
所以,在该视图第一次查看事务时,会产生一个视图,这个视图就能看到当前sql系统正在活跃的事务。
具体如下图:
注意:事务ID是不断递增的,事务ID越小,说明该事务创建的越早。
- 1.首先解析中间的事务,在快照时,当前系统正在活跃的事务,也就是已经begin但未commit的事务。
- 2.已经提交的事务:在当前事务第一次进行快照读产生的Read View视图之前,已经commit的事务。
- 3.快照后来的新事务:这些事务是在进行快照都之后,才开始begin活跃的事务。
- 由于是已经形成Read View视图之后,才开始活跃的事务,所以在Read View列表中看不到。
解析这部分的意思:
- 1.creator_trx_id就是创建该视图的事务ID,当该事务ID(creator_trx_id) == 版本链中的某个事务ID(DB_TRX_ID)时,说明我要看的事务ID,就是我本身。
- 2.DB_TRX_ID < up_limit_id,说明我要查询的历史事务ID,小于当前我这个事务创建出来的视图ID(Read View),说明该事务是已经commit的,我应该要看到。
解析这部分的意思:
说明我当前要查询的事务ID,比我这个视图里面能看到的最大事务ID还要大,说明在我创建这个视图Read View的时候,你那个事务还没出生呢,或者你在我创建视图后才到来的。所以我不应该能看到你。
解析这部分的意思:
快照到的事务不一定连续。
如果要查询的事务ID不在Read View视图列表中,说明该事务已经提交,那就可以看到。
如果要查询的事务ID在Read View视图列表中,说明该事务同样是活跃事务还没commit,那么它的增删查改就不应该看到。
上面所提到的应不应该看到,指的是,应不应该出现在Read View视图里,这就像,假如我是大二学生,我应该要看到大四毕业学长的工作情况,但是当我大四的时候,我不可能也不应该看到大一的工作情况。
源码策略:
RR 和 RC 的本质区别
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见。
而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
简单说:RR和RC,RR:不更新Read View视图。
RC,每次进行快照读都会更新Read View视图。