MySQL事务并发问题和MVCC机制
事务并发的几种问题
1.脏读(读未提交数据)
- 脏读指的是事务读取到其他事务没有提交的数据
- 举个栗子
事务A读取表中的name,事务B将表中的张三修改称为了张老三,假设这时候事务A读取到了name为张老三,但是到最后事务B又因为其他原因回滚了数据,数据库表中的name为张三,那这就出现了问题,事务A读到的数据和数据库表中的不一致.而且这个数据变化到底是因为原因造成的事务A是不知道的
2.不可重复读(并发更新的情况下)
- 不可重复读指的是在同一次事务中前后查询不一致的问题
- 举个栗子
事务A先查询了数据库中的人员名字,之后就去处理其他的数据,之后有重复了最开始查询的内容,那么事务A预期的应该是两次查询的结果应该是一致的,因为事务A在处理其他数据的时候,没有对查询的数据做一个修改 但是问题是在事务A处理其他数据的时候,事务B对事务A查询的数据做了更新且提交了,那么事务A和事务B在并发的情况下,两次查询的结果就不一致了 但是实际的理论上我们事务A没有对数据进行更新,那么在事务A在没有提交之前,无论查询多少次数据都应该是一致的才对,这个理论情况叫做可重复的 上面并发出现的这个情况叫做不可重复读 同一个事务前后两次读取的数据不一致,就导致我们的程序不可预期不可控,这也就是不可重复读带来的危害
3.幻读(并发新增,删除这种导致数据总量发生变化的操作)
- 幻读是一次失误当中前后数据量发生变化,用户产生不可预估的问题
- 举个栗子
在这个例子当中,事务A先查询了数据库表发现有一条数据,他在删除了这个数据之后,事务B向数据库表当中插入了一条数据,事务A此时还没有提交,当事务A再次查询数据库表的收,预期结果应该是空数据,但是凭空多出来这个李四是什么鬼,这个就是这两个事务在并发情况下发生的幻读问题
事务隔离级别
其中MySQL默认的事务隔离级别是第三个:可重复读,比较特殊的要记住就是,常常说可重复读事务隔离级别能避免脏读和不可重复读,但是不能避免幻读,但是实际上如果我们的数据库使用的是InnoDB存储引擎是不存在幻读的.
获取和设置事务隔离级别的命令
- 其实一般不用设置,使用默认的就行,因为MySQL5.1之后默认使用的是InnoDB存储引擎,且默认的事务隔离级别是RR,能够避免三种并发事务问题
- 为啥InnoDB存储引擎在RR事务隔离级别下也能够避免幻读呢?这就要说说这个MVCC机制了
MVCC机制
写在前面
在MySQL的InnoDB存储引擎下 RC和RR 事务隔离级别都是基于这个MVCC(多版本并发控制)进行并发事务的控制的 然后这个MVCC是基于这个"数据版本"对并发事务进行访问的
示例分析
有四个事务ABCD其中前三个事务执行的操作都是对表中name字段进行修改 事务D执行的操作是进行数据的查询,但是仔细看事务D查询的位置还特殊的不行,第一次查询是在事务A提交之后事务B提交之前,第二次查询数据是在事务B提交之后在事务C提交之前
- 分别在RR隔离级别和RC隔离级别下分析下两次查询读取到的数据情况
RR隔离级别下
RR隔离级别是可重复读,那么就意味着在整个事务中读取到的数据是一致的那么显而易见第一次查询和第二次查询得到的数据是一致的,即name=张三
RC隔离级别下
由于RC是读已提交,只要别的事务提交数据,我就进行读取,那么实际上最后RC隔离级别下两次读到的数据分别是: 第一次:张三 第二次:张小三 显而易见RC隔离级别下出现的问题就是不可重复读
分析现象
基于UNDO_LOG的版本链
- UNDO_LOG是回滚日志,在我们进行事务处理的时候,只有增加回滚日志,当我们事务执行失败的时候要执行rollback回滚操作的时候才能够进行还原.其原理就是采用了链的方式进行组织.
- 最上面一行的数据就是我们数据库中该字段当前的数据,并且当时执行这个事务update语句的事务编号为TRX_ID = 3
- 额外附加的字段1:TRX_ID执行这个版本的数据是哪个事务执行的结果,就记录下这个事务的id
- 额外添加字段2:DB_ROLL_PTR:指针信息,指向了上一次进行版本变化的时候他的数据是什么
- 按照这个表我们就可以清楚的知道最原始的表中的数据是张三丰(假设),然后经过三次的更新之后更新为了张老三,每个数据都采用事务ID作为版本来进行标识
- UNDO_LOG如果被删除了怎么办?中间数据被删除了UNDO_LOG版本链不就断了吗?
UNDO_LOG版本链不是立即删除的,MySQL确保版本数据链不再被引用之后在进行删除,所以除了一些不可控的因素之外,一般数据都是比较完整的
ReadView
- ReadView是"快照读"SQL执行时MVCC提取数据的依据
- 所以说使用MVCC机制的前提时快照读,这也是InnoDB存储引擎下RR事务隔离级别的一种实现方式
- 如果读取方式不是快照读而是当前读,那么实现的机制就不是MVCC机制了而是使用了加锁的方式,加的是行锁和间隙锁
快照读和当前读
- 快照读:就是最普通的select查询SQL语句就是快照读
- 当前读:当指代执行下列语句时进行数据读取的方式:
insert into ...
update ...
delete ...
select ... for update
select ...lock in share mode
ReadView数据结构
- ReadView就是一个数据接结构,它包含了4个字段
读已提交(RC)场景下
在每一次执行快照读的时候生成全新的ReadView
- 第一次快照读生成的readview从图中可以看出事务2,3,4还没有提交所以当前活跃事务id:m_ids={2,3,4},最小活跃事务id:min_trx_id=2,预分配事务id:max_trx_id = 5 (因为时当前最大活跃事务id + 1所以就是4 + 1 = 5),当前事务创建者id:creator_trx_id = 4
- 第二次快照读生成的readview和第一次分析逻辑相同
版本链数据访问规则
版本链数据访问规则:
- 判断当前事务id等于creator_trx_id(4)吗?成立说明数据就是自己这个事务更改的,可以访问.
- 判断trx_id < min_trx_id(2) ? 成立则说明数据已经提交了,可以访问.
- 判断trx_id > max_trx_id(5) ? 成立则说明该事务是在readview生成以后才开启的,不允许访问.
- 判断min_trx_id(2) <= trx_id <= max_trx_id(5),成立在m_ids数据中进行对比,不存在则代表数据是已经提交的,可以进行访问.
让undo_log版本链中的版本从小到校一次带入到这个规则当中进行判断找到第一个可以访问的数据.
这个快照读带入可以在trx_id = 1这个版本的时候,满足trx_id < min_trx_id(2) 说明数据已经提交可以访问所以第一次查询语句查询出来的数据就是张三.
这个快照读可以在trx_id = 2的时候满足规则中的第二条 trx_id < min_trx_id(3)说明数据已经提交了,读取的数据为张小三 !!!出现了不可重复读的现象
可重复读(RR)的场景
- 仅在第一次执行快照读时生成ReadView,后续快照读复用这个ReadView(但是存在例外)
- 那么在这样的情况下,版本链不变化,ReadView不变,那么两次读的结果肯定是一样的
- RR级别下使用MVCC能避免幻读吗? 能但是不完全能!
连续多次快照读的时候,ReadView会产生复用,没有幻读问题 但是需要注意的是:当两次快照读之间存在当前读,ReadView会重新生成,导致产生幻读
|