前文
本篇文章记录下关于MySQL老生常谈的事务,相信作为进阶必备知识,事务时绕不开的,而以前的我老是没弄清楚MVCC和锁的关系,没弄清楚脏读、可重复读、幻读等等和MVCC、锁的关系,就比如,RR模式下(repeatable read)的幻读能完全避免吗?如何避免的?再比如RR、RC模式下,一个事务执行update,另一个事务再执行update情况会如何?
如果还不知道当前读和一致性读的区分,以及这两者在MVCC里扮演的角色,那还是看下这篇文章,能带给你不小帮助。
了解事务
ACID和隔离性
我们知道事务分为ACID,分别代表着:Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性,其中隔离性才是事务的重点,又分为:read uncommited、read commited、repeatable read、serializable,即读未提交(RU)、读已提交(RC)、可重复读(RR)、串行化,不同的隔离性对应着不同的问题和处理并发的效果,对应的问题有:脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read),对应的关系如下:
- 读未提交:事务能读到其他事务未提交的部分,此时并发性最好,会产生脏读、不可重复读、幻读等问题;
- 读已提交:事务只能读到其他事务已提交的部分,并发性较好,解决了脏读,会产生可重复读、幻读问题,是当前生产环境普遍使用的隔离级别;
- 可重复读:事务只能读到自己事务的内容,其他事务提交的也不可见,在当前读情况下通过gap锁+行锁来避免不可重复读+幻读;在一致性读的情况下通过MVCC来避免不可重复读+幻读,并发性一般,适用于账单比对、全量备份等场景;
- 串行读:事务操作每一行内容时,读会加读锁,写会加写锁,有锁的话其他事务只能等着,并发性最差,安全性最好!
其中读未提交会产生脏读,所以考虑到安全性会被摒弃;而串行读考虑到并发性实在太差,所以实用性也太差。而最难理解也是最容易被面试到的就是RR和RC即可重复读和读已提交,掌握这两者尤为重要!
可重复读和读已提交
理解这两者得先明白这两者为什么会产生不同的效果。MySQL里这两个不同的级别差异在所产生的快照时间不同,而快照可以理解为是一个视图(read-view),这个视图相当于是当前MySQL的数据的一个存档,所以每次访问仅访问这个视图的内容,就能做到和其他视图(read-view)所读的内容不同。
读已提交,即每次在一致性读的时候(也就是开启事务并执行select语句)都会产生一个read-view,通过这个read-view保存当前数据的内容,这样有什么好处呢?也就是在你执行第一个select和第二个select之间,如果有事务提交了,那么提交的内容就被保存在第二个select的read-view了,即此时可读到其他人提交的内容,当然也会产生可重复读和幻读的问题,不过这两者在真实生产环境中也的确是需要的。
可重复读,则是在开启事务后的第一个select语句就产生read-view,此后任意的select都不再产生read-view,而这就能完美地避开其他read-view所提交的内容!
举例如上图所示,如果在RC级别下,A先于B开启事务,A将1改为2,因为行锁(MDL)的原因此时B如果要改为4则会被阻塞直到A提交,此时B select结果是1,当A提交后此时B select结果是2,实现了读已提交;而在RR级别下,因为行锁的原因也是无法修改,但是无论A是否提交,B读到的都是1,这就是MVCC,也就是所谓的多版本控制。
undo log的概念,知道MVCC后我们需要了解MySQL是如何实现多版本控制的,也就是不同read-view到底是如何掌控同一行内容,而这就要用到undo log(回滚日志),如下图,当该字段从数值1依次被A、B、C修改成2,3,4,那么在RC\RR下是如何保存的呢?
当前读和一致性读
要解决前面这个问题,需要理解当前读和一致性读的概念!
- 当前读:即update、insert、delete等dml语句,select…for update/select … lock in share mode这类加锁读语句
- 一致性读:即普通的select语句
明白了这个就容易理解前面的问题了,如果是当前读的情况下,那么如果read-view A、B、C都不提交事务,则后续的事务都无法修改,因为会被行锁所阻挡,而这也是当前读的定义,每次都要获取当前的最新值!所以当前读的情况下会被阻塞,那如果是提交后能避免幻读吗?如果是RC的话,因为没有gap锁(间隙锁,负责锁住间隙,防止insert语句插入新的记录),则无法阻塞insert语句,所以无法避免幻读;但是RR的话,可以通过gap锁+行锁来避免幻读!
如果是一致性读,那就是要满足MVCC,即多个版本,而这多个版本就是通过undo log实现,也就是在每条记录下面有一个pointer指针,负责指向每个事务所对应的值,那如何区分不同事务所对应的值呢?通过trx_id,即每个事务都拥有对应的事务id,在不同的事务id下所对应的undo log的值自然也就不同。
那这个所谓的undo log总不能一直存在吧?实际上MySQL会在undo log不再用的时候删除它,而不在用就是指当前活跃的最小事务没有比这个回滚日志所对应的read-view更小的事务,此时则说明回滚日志所对应的read-view已经提交,则可以删除该日志!
所以极其不建议长事务的方式,因为长事务则说明从事务启动到现在所产生的undo log都不能删除,这对于维护和空间存储都是很大的浪费,并且在MySQL5.5之前,数据字典和undo log都是存储在ibdata里,这导致即使你数据量很小,因为要维护回滚日志的需求,则ibdata所占的空间还是非常大!
总结
此次分享到这,由于篇幅的原因,关于gap锁、undo log等等没有展开讲,后续会补上对应的文章~