之前我们最开始的几篇文章就讲过,你除了写redolog日志还必须要写undo log日志,这个undo log日志是至关重要的,没有他,你根本都没办法回滚事务!
1.事务
1.1.多线程并发执行多个事务
对于我们的业务系统去访问数据库而言,他往往都是多个线程并发执行多个事务的,对于数据库而言,他会有多个事务同时执行,可能这多个事务还会同时更新和查询同一条数据,所以这里会有一些问题需要数据库来解决,如下图:
每个事务都会执行各种增删改查的语句,把磁盘上的数据页加载到buffer pool的缓存页里来,然后更新
缓存页,记录redo log和undo log,最终提交事务或者是回滚事务,多个事务会并发干上述一系列事情。
多线程并发执行多个事务带来的问题如下。
1.1.1脏写
- 事务A和事务B同时在更新一条数据,事务A先把他更新为A值,事务B紧接着就把他更新为B值。
- 此时事务A突然回滚了,那么就会用他的undo log日志去回滚。
- 事务B看到的场景,就是自己明明更新了,结果值却没了。
本质就是事务B去修改了事务A修改过的值,但是此时事务A还没提交,所以事务A随时会回滚,
导致事务B修改的值也没了,这就是脏写
1.1.2.脏读
- 事务A更新了一行数据的值为A值,此时事务B去查询了一下这行数据的A值
- 事务B此时拿到刚查出来的A值在做一些业务处理
- 事务A回滚了事务,导致刚才更新的A值没了,此时那行数据的值回滚为NULL值
- 事务B此时再次查询那行数据的值,看到的居然此时是NULL值
本质其实就是事务B去查询了事务A修改过的数据,但是此时事务A还没提交,所以事务A随时会回滚导致事务B再次查询就读不到刚才事务A修改的数据了!这就是脏读。
无论是脏写还是脏读,都是因为一个事务去更新或者查询了另外一个还没提交的事务更新过的数据。因为另外一个事务还没提交,所以他随时可能会反悔会回滚,那么必然导致你更新的数据就没了,或者你之前查询到的数据就没了,这就是脏写和脏读两种坑爹场景。
1.1.3.不可重复读
假设我们有一个事务A开启了,在这个事务A里会多次对一条数据进行查询
假设是事务A只能在事务B提交之后读取到他修改的数据(避免脏读)
- 假设缓存页里一条数据原来的值是A值,此时事务A开启之后,第一次查询这条数据,读取到的就是A值
- 事务B更新了那行数据的值为B值,同时事务B立马提交了,然后事务A此时可是还没提交!
- 事务A执行期间第二次查询数据,此时查到的是事务B修改过的值,B值,因为事务B已经提交了,所以事务A可以读到的了。
其实要说没问题也可以是没问题,毕竟事务B和事务C都提交之后,事务A多次查询查到他们修改的值,是ok的。
但是你要说有问题,也可以是有问题的,就是事务A可能第一次查询到的是A值,那么他可能希望的是在
事务执行期间,如果多次查询数据,都是同样的一个A值,他希望这个A值是他重复读取的时候一直可以
读到的!他希望这行数据的值是可重复读的!
这个问题简单来说,就是一个事务多次查询一条数据,结果每次读到的值都不一样,这个过程中可能别的事务会修改这条数据的值,而且修改值之后事务都提交了,结果导致人家每次查到的值都不一样,都查到了提交事务修改过的值,这就是所谓的不可重复读。
1.1.4.幻读
- 个事务A,先发送一条SQL语句,他一开始查询出来了10条数据
- 事务B往表里插入了几条数据,而且事务B还提交了,此时多了2行数据出来
- 事务A此时第二次查询,还是那条SQL,还是那个查询条件,但是查询出来的数据是12条
幻读指的就是你一个事务用一样的SQL多次查询,结果每次查询都会发现查到了一些之前没看到过的数
据注意,幻读特指的是你查询到了之前查询没看到过的数据!此时就说你是幻读了。
1.2.SQL标准四种事务隔离
这4种级别包括了:read uncommitted(读未提交),read committed(读已提交),repeatable read(可重复读),serializable(串行化)
事务级别 | 可能出现的问题 | 备注 |
---|---|---|
读未提交 | 脏读,不可重复读,幻读 | |
读已提交RC | 不可重复读,幻读 | |
可重复读 RR | 幻读 | |
串行化 | 性能降低 | 根本就不允许你多个事务并发执行 |
1.3.MySQL事务隔离
但是要注意的一点是,MySQL默认设置的事务隔离级别,是RR级别的(一般数据库是RC)。并且MySQL的RR级别的语义跟SQL标准的RR级别不同的,毕竟SQL标准里规定RR级别是可以发生幻
读的,但是MySQL的RR级别就避免了幻读。
MySQL里执行的事务,默认情况下不会发生脏写、脏读、不可重复读和幻读的问题,事务的执行都是并行的,大家互相不会影响,我不会读到你没提交事务修改的值,即使你修改了值还提交了,我也不会读到的,即使你插入了一行值还提交了,我也不会读到的,总之,事务之间互相都完全不影响!
当然,要做到这么神奇和牛叉的效果,MySQL是下了苦功夫的,后续我们接着就要讲解MySQL里的
MVCC机制,就是多版本并发控制隔离机制,依托这个MVCC机制,就能让RR级别避免不可重复读和幻读的问题。但是一般来说,真的其实不用修改这个级别,就用默认的RR其实就特别好,保证你每个事务跑的时候都没人干扰,何乐而不为呢
2.undo log
2.1.undo log 日志结构
- 一条日志必须得有自己的一个开始位置,这个没什么好说的
- 那么主键的各列长度和值是什么意思?大家都知道,你插入一条数据,必然会有一个主键!如果你自己指定了一个主键,那么可能这个主键就是一个列,比如id之类的,也可能是多个列组成的一个主键,比如“id+name+type”三个字段组成的一个联合主键,也是有可能的。所以这个主键的各列长度和值,意思就是你插入的这条数据的主键的每个列,他的长度是多少,具体的值是多少。即使你没有设置主键,MySQL自己也会给你弄一个row_id作为隐藏字段,做你的主键。
- 接着是表id,这个就不用多说了,你插入一条数据必然是往一个表里插入数据的,那当然得有一个表id,记录下来是在哪个表里插入的数据了。
- undo log日志编号,这个意思就是,每个undo log日志都是有自己的编号的。而在一个事务里会有多个SQL语句,就会有多个undo log日志,在每个事务里的undo log日志的编号都是从0开始的,然后依次递增。
- undo log日志类型,就是TRX_UNDO_INSERT_REC,insert语句的undo log日志类型就是这个东西。
- undo log日志的结束位置,这个自然也不用多说了,他就是告诉你undo log日志结束的位置是什么。
2.2.undo log 版本链
每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向你了你更新这个事务之前生成的undo log。
- 假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo
log如下图所示,插入的这条数据的值是值A,因为事务A的id是50,所以这条数据的txr_id就是50,roll_pointer指向一个空的undo log,因为之前这条数据是没有的 - 事务B跑来修改了一下这条数据,把值改成了值B,事务B的id是58,那么此时更新之会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,这个undo log就记录你更新之前的那条数据的值。
- 事务C又来修改了一下这个值为值C,他的事务id是69,此时会把数据行里的txr_id改成69,然后生成一条undo log,记录之前事务B修改的那个值。
多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段txr_id和roll_pointer,同时之前多个数据快照对应的undo log,会通过roll_pinter指针串联起来,形成一个重要的版本链!
2.3.ReadView
生成readview时机
RC隔离级别:每次读取数据前,都生成一个readview;
RR隔离级别:在第一次读取数据前,生成一个readview;
当我们执行一个事务的时候,就给你生成一个ReadView,里面比较关键的东西有4个
readview四个主要元素
m_ids:表示在生成readview时,当前系统中活跃的读写事务id列表;
min_trx_id:表示在生成readview时,当前系统中活跃的读写事务中最小的事务id,也就是m_ids中最小的值;
max_trx_id:表示生成readview时,系统中应该分配给下一个事务的id值;
creator_trx_id:表示生成该readview的事务的事务id;
readview判断版本链
有了readview,在访问某条记录时,按照以下步骤判断记录的某个版本是否可见
- 如果被访问版本的trx_id,与readview中的creator_trx_id值相同,表明当前事务在访问自己修改过的记录,该版本可以被当前事务访问;
- 如果被访问版本的trx_id,小于readview中的min_trx_id值,表明生成该版本的事务在当前事务生成readview前已经提交,该版本可以被当前事务访问;
- 如果被访问版本的trx_id,大于readview中的max_trx_id值,表明生成该版本的事务在当前事务生成readview后才开启,该版本不可以被当前事务访问;
- 如果被访问版本的trx_id,值在readview的min_trx_id和max_trx_id之间,就需要判断trx_id属性值是不是在m_ids列表中?
如果在:说明创建readview时生成该版本的事务还是活跃的,该版本不可以被访问
如果不在:说明创建readview时生成该版本的事务已经被提交,该版本可以被访问;
通过undo log多版本链条,加上你开启事务时候生产的一个ReadView,然后再有一个查询的时候,根
据ReadView进行判断的机制,你就知道你应该读取哪个版本的数据。而且他可以保证你只能读到你事务开启前,别的提交事务更新的值,还有就是你自己事务更新的值。假如说是你事务开启之前,就有别的事务正在运行,然后你事务开启之后 ,别的事务更新了值,你是绝对读不到的!或者是你事务开启之后,比你晚开启的事务更新了值,你也是读不到的!通过这套机制就可以实现多个事务并发执行时候的数据隔离。
2.4.基于ReadView实现RR事务
在MySQL中让多个事务并发运行的时候能够互相隔离,避免同时读写一条数据的时候有影响,是依托undo log版本链条和ReadView机制来实现的。
第一步:首先我们还是假设有一条数据是事务id=50的一个事务插入的,同时此时有事务A和事务B同时在运行,事务A的id是60,事务B的id是70。
第二步:这个时候,事务A发起了一个查询,他就是第一次查询就会生成一个ReadView
第三步:事务A基于这个ReadView去查这条数据,通提供过对比这条数据的事务id和ReadView比较
第四步:事务B此时更新了这条数据的值为值B,此时会修改trx_id为70,同时生成一个undo log版本链,而且关键是事务B此时他还提交了,也就是说此时事务B已经结束了,如下图所示。
这个时候大家思考一个问题,ReadView中的m_ids此时还会是60和70吗?那必然是的,因为ReadView一旦生成了就不会改变了,这个时候虽然事务B已经结束了,但是事务A的ReadView里,还是会有60和70两个事务id。
第五步:事务A继续查询数据,结果发现这条数据的trx_id为70,如果被访问版本的trx_id,值在readview的min_trx_id和max_trx_id之间,就需要判断trx_id属性值是不是在m_ids列表中,而我们发现我们的ReadView中,刚好有70。说明创建Readview时生成该版本的事务B还是活跃的,该版本不可以被访问。
因此这个时候只能顺着指针往历史版本链条上,找到下面一条数据,trx_id为50,是小于ReadView的min_trx_id的,说明在他开启查询之前,就已经提交了这个事务了,所以事务A是可以查询到这个值的,此时事务A查到的是原始值。
你事务A多次读同一个数据,每次读到的都是一样的值,除非是他自己修改了值,否则读到的一直会一
样的值。不管别的事务如何修改数据,事务A的ReadView始终是不变的,他基于这个ReadView始终看到的值是一样的!
2.4.RR事务解决幻读
接着我们来看看幻读的问题他是如何解决的。假设现在事务A先用select * from x where id>10来查
询,此时可能查到的就是10条数据,而且读到的是这条数据的原始值的那个版本。具体原因同上
如果被访问版本的trx_id,值在readview的min_trx_id和max_trx_id之间,就需要判断trx_id属性值是不是在m_ids列表中?
如果在:说明创建readview时生成该版本的事务还是活跃的,该版本不可以被访问
如果不在:说明创建readview时生成该版本的事务已经被提交,该版本可以被访问;
第一步:现在有一个事务C插入了一条数据
第二步:此时事务A再次查询,此时会发现符合条件的有12条数据,10条是原始值那个数据,2条是事务C插入的那条数据,但是事务C插入的那条数据的trx_id是80,这个80是大于自己的ReadView的max_trx_id的,说明是自己发起查询之后,这个事务才启动的,所以此时这条数据是不能查询的。
因此事务A本次查询,还是只能查到原始值10条数据,如下图。
所以大家可以看到,在这里,事务A根本不会发生幻读,他根据条件范围查询的时候,每次读到的数据
都是一样的,不会读到人家插入进去的数据,这都是依托ReadView机制实现的。
3.MVCC机制
multi-version concurrent control,就是多版本并发控制机制。
MySQL实现MVCC机制的时候,是基于undo log多版本链条+ReadView机制来做的,默认的RR隔离级别,就是基于这套机制来实现的,依托这套机制实现了RR级别,除了避免脏写、脏读、不可重复读,还能避免幻读问题。因此一般来说我们都用默认的RR隔离级别就好了。
这就使得别的事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。